From 2a1d6793d33d9c72295d23965aefd23f2004bbcb Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 8 Aug 2022 11:48:27 -0400 Subject: cleaning up link ui. linkPreviews (not from linkMenu) have an edit button. link lines work when in lightboxview. --- src/client/views/nodes/formattedText/FormattedTextBox.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx') diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index a018f51c2..40b0d285c 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -24,7 +24,7 @@ import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; -import { DocumentType } from '../../../documents/DocumentTypes'; +import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { DictationManager } from '../../../util/DictationManager'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; @@ -1751,7 +1751,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - const ComponentTag = tag === 'freeform' ? CollectionFreeFormView : tag === 'tree' ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView; + const ComponentTag = tag === CollectionViewType.Freeform ? CollectionFreeFormView : tag === CollectionViewType.Tree ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView; return ComponentTag === CollectionStackingView ? ( Date: Mon, 8 Aug 2022 12:54:06 -0400 Subject: fixed autolinking to not match partial word substrings. restored textbox dictation. --- src/client/documents/Documents.ts | 5 +- src/client/util/DictationManager.ts | 439 +++++++++++---------- src/client/views/DocumentDecorations.tsx | 1 - src/client/views/linking/LinkMenuItem.tsx | 4 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 96 +++-- 5 files changed, 282 insertions(+), 263 deletions(-) (limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 9d00664ad..c7ea04839 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1287,10 +1287,11 @@ export namespace DocUtils { export let ActiveRecordings: { props: FieldViewProps; getAnchor: () => Doc }[] = []; - export function MakeLinkToActiveAudio(getSourceDoc: () => Doc, broadcastEvent = true) { + export function MakeLinkToActiveAudio(getSourceDoc: () => Doc | undefined, broadcastEvent = true) { broadcastEvent && runInAction(() => (DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1)); return DocUtils.ActiveRecordings.map(audio => { - const link = DocUtils.MakeLink({ doc: getSourceDoc() }, { doc: audio.getAnchor() || audio.props.Document }, 'recording annotation:linked recording', 'recording timeline'); + const sourceDoc = getSourceDoc(); + const link = sourceDoc && DocUtils.MakeLink({ doc: sourceDoc }, { doc: audio.getAnchor() || audio.props.Document }, 'recording annotation:linked recording', 'recording timeline'); link && (link.followLinkLocation = 'add:right'); return link; }); diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index a6dcda4bc..13f036838 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -1,37 +1,35 @@ -import * as interpreter from "words-to-numbers"; +import * as interpreter from 'words-to-numbers'; // @ts-ignore bcz: how are you supposed to include these definitions since dom-speech-recognition isn't a module? -import type { } from "@types/dom-speech-recognition"; -import { Doc, Opt } from "../../fields/Doc"; -import { List } from "../../fields/List"; -import { RichTextField } from "../../fields/RichTextField"; -import { listSpec } from "../../fields/Schema"; -import { Cast, CastCtor } from "../../fields/Types"; -import { AudioField, ImageField } from "../../fields/URLField"; -import { Utils } from "../../Utils"; -import { Docs } from "../documents/Documents"; -import { DocumentType } from "../documents/DocumentTypes"; -import { DictationOverlay } from "../views/DictationOverlay"; -import { DocumentView } from "../views/nodes/DocumentView"; -import { SelectionManager } from "./SelectionManager"; -import { UndoManager } from "./UndoManager"; - +import type {} from '@types/dom-speech-recognition'; +import { Doc, Opt } from '../../fields/Doc'; +import { List } from '../../fields/List'; +import { RichTextField } from '../../fields/RichTextField'; +import { listSpec } from '../../fields/Schema'; +import { Cast, CastCtor } from '../../fields/Types'; +import { AudioField, ImageField } from '../../fields/URLField'; +import { Utils } from '../../Utils'; +import { Docs } from '../documents/Documents'; +import { DocumentType } from '../documents/DocumentTypes'; +import { DictationOverlay } from '../views/DictationOverlay'; +import { DocumentView } from '../views/nodes/DocumentView'; +import { SelectionManager } from './SelectionManager'; +import { UndoManager } from './UndoManager'; /** * 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. @@ -42,27 +40,26 @@ export namespace DictationManager { } } const { webkitSpeechRecognition }: CORE.IWindow = window as any as CORE.IWindow; - export const placeholder = "Listening..."; + export const placeholder = 'Listening...'; export namespace Controls { - - export const Infringed = "unable to process: dictation manager still involved in previous session"; + export const Infringed = 'unable to process: dictation manager still involved in previous session'; const browser = (() => { const identifier = navigator.userAgent.toLowerCase(); - if (identifier.indexOf("safari") >= 0) { - return "Safari"; + if (identifier.indexOf('safari') >= 0) { + return 'Safari'; } - if (identifier.indexOf("chrome") >= 0) { - return "Chrome"; + if (identifier.indexOf('chrome') >= 0) { + return 'Chrome'; } - if (identifier.indexOf("firefox") >= 0) { - return "Firefox"; + if (identifier.indexOf('firefox') >= 0) { + return 'Firefox'; } - return "Unidentified Browser"; + return 'Unidentified Browser'; })(); const unsupported = `listening is not supported in ${browser}`; - const intraSession = ". "; - const interSession = " ... "; + const intraSession = '. '; + const interSession = ' ... '; export let isListening = false; let isManuallyStopped = false; @@ -74,7 +71,7 @@ export namespace DictationManager { export type InterimResultHandler = (results: string) => any; export type ContinuityArgs = { indefinite: boolean } | false; - export type DelimiterArgs = { inter: string, intra: string }; + export type DelimiterArgs = { inter: string; intra: string }; export type ListeningUIStatus = { interim: boolean } | false; export interface ListeningOptions { @@ -105,21 +102,21 @@ export namespace DictationManager { try { results = await (pendingListen = listenImpl(options)); pendingListen = undefined; - // if (results) { - // Utils.CopyText(results); - // if (overlay) { - // DictationOverlay.Instance.isListening = false; - // const execute = options?.tryExecute; - // DictationOverlay.Instance.dictatedPhrase = execute ? results.toLowerCase() : results; - // DictationOverlay.Instance.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true; - // } - // options?.tryExecute && await DictationManager.Commands.execute(results); - // } + if (results) { + Utils.CopyText(results); + if (overlay) { + DictationOverlay.Instance.isListening = false; + const execute = options?.tryExecute; + DictationOverlay.Instance.dictatedPhrase = execute ? results.toLowerCase() : results; + DictationOverlay.Instance.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true; + } + options?.tryExecute && (await DictationManager.Commands.execute(results)); + } } catch (e: any) { console.log(e); if (overlay) { DictationOverlay.Instance.isListening = false; - DictationOverlay.Instance.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`; + DictationOverlay.Instance.dictatedPhrase = results = `dictation error: ${'error' in e ? e.error : 'unknown error'}`; DictationOverlay.Instance.dictationSuccess = false; } } finally { @@ -131,7 +128,7 @@ export namespace DictationManager { const listenImpl = (options?: Partial) => { if (!recognizer) { - console.log("DictationManager:" + unsupported); + console.log('DictationManager:' + unsupported); return unsupported; } if (isListening) { @@ -146,16 +143,18 @@ export namespace DictationManager { const intra = options?.delimiters?.intra; const inter = options?.delimiters?.inter; - recognizer.onstart = () => console.log("initiating speech recognition session..."); + recognizer.onstart = () => console.log('initiating speech recognition session...'); recognizer.interimResults = handler !== undefined; recognizer.continuous = continuous === undefined ? false : continuous !== false; - recognizer.lang = language === undefined ? "en-US" : language; + recognizer.lang = language === undefined ? 'en-US' : language; recognizer.start(); return new Promise((resolve, reject) => { - recognizer.onerror = (e: any) => { // e is SpeechRecognitionError but where is that defined? - if (!(indefinite && e.error === "no-speech")) { + recognizer.onerror = (e: any) => { + console.log('SPEECH error:', encodeURIComponent); + // e is SpeechRecognitionError but where is that defined? + if (!(indefinite && e.error === 'no-speech')) { recognizer.stop(); resolve(e); //reject(e); @@ -163,9 +162,10 @@ export namespace DictationManager { }; recognizer.onresult = (e: SpeechRecognitionEvent) => { + console.log('RESULT: ', e); current = synthesize(e, intra); let matchedTerminator: string | undefined; - if (options?.terminators && (matchedTerminator = options.terminators.find(end => current ? current.trim().toLowerCase().endsWith(end.toLowerCase()) : false))) { + if (options?.terminators && (matchedTerminator = options.terminators.find(end => (current ? current.trim().toLowerCase().endsWith(end.toLowerCase()) : false)))) { current = matchedTerminator; recognizer.abort(); return complete(); @@ -175,6 +175,7 @@ export namespace DictationManager { }; recognizer.onend = (e: Event) => { + console.log('END: ', e); if (!indefinite || isManuallyStopped) { return complete(); } @@ -187,11 +188,12 @@ export namespace DictationManager { }; const complete = () => { + console.log('COMPLETE:'); if (indefinite) { current && sessionResults.push(current); sessionResults.length && resolve(sessionResults.join(inter || interSession)); } else { - resolve(current || ""); + resolve(current || ''); } current = undefined; sessionResults = []; @@ -201,7 +203,6 @@ export namespace DictationManager { recognizer.onerror = null; recognizer.onend = null; }; - }); }; @@ -222,171 +223,173 @@ export namespace DictationManager { } return transcripts.join(delimiter || intraSession); }; - } - // export namespace Commands { - - // export const dictationFadeDuration = 2000; - - // export type IndependentAction = (target: DocumentView) => any | Promise; - // export type IndependentEntry = { action: IndependentAction, restrictTo?: DocumentType[] }; - - // export type DependentAction = (target: DocumentView, matches: RegExpExecArray) => any | Promise; - // 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 () => { - // const targets = SelectionManager.Views(); - // if (!targets || !targets.length) { - // return; - // } - - // phrase = phrase.toLowerCase(); - // const entry = Independent.get(phrase); - - // if (entry) { - // let success = false; - // const restrictTo = entry.restrictTo; - // for (const target of targets) { - // if (!restrictTo || validate(target, restrictTo)) { - // await entry.action(target); - // success = true; - // } - // } - // return success; - // } - - // for (const entry of Dependent) { - // const regex = entry.expression; - // const matches = regex.exec(phrase); - // regex.lastIndex = 0; - // if (matches !== null) { - // let success = false; - // const restrictTo = entry.restrictTo; - // for (const 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.COL, listSpec(Doc)], - // [DocumentType.AUDIO, AudioField], - // [DocumentType.IMG, ImageField], - // [DocumentType.IMPORT, listSpec(Doc)], - // [DocumentType.RTF, "string"] - // ]); - - // const tryCast = (view: DocumentView, type: DocumentType) => { - // const 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 (const type of types) { - // if (tryCast(target, type)) { - // return true; - // } - // } - // return false; - // }; - - // const interpretNumber = (number: string) => { - // const initial = parseInt(number); - // if (!isNaN(initial)) { - // return initial; - // } - // const converted = interpreter.wordsToNumbers(number, { fuzzy: true }); - // if (converted === null) { - // return NaN; - // } - // return typeof converted === "string" ? parseInt(converted) : converted; - // }; - - // const Independent = new Map([ - - // ["clear", { - // action: (target: DocumentView) => Doc.GetProto(target.props.Document).data = new List(), - // restrictTo: [DocumentType.COL] - // }], - - // ["open fields", { - // action: (target: DocumentView) => { - // const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 }); - // target.props.addDocTab(kvp, "add:right"); - // } - // }], - - // ["new outline", { - // action: (target: DocumentView) => { - // const newBox = Docs.Create.TextDocument("", { _width: 400, _height: 200, title: "My Outline", _autoHeight: true }); - // const proto = newBox.proto!; - // const prompt = "Press alt + r to start dictating here..."; - // const head = 3; - // const anchor = head + prompt.length; - // const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"ordered_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`; - // proto.data = new RichTextField(proseMirrorState); - // proto.backgroundColor = "#eeffff"; - // target.props.addDocTab(newBox, "add:right"); - // } - // }] - - // ]); - - // const Dependent = new Array( - - // { - // expression: /create (\w+) documents of type (image|nested collection)/g, - // action: (target: DocumentView, matches: RegExpExecArray) => { - // const count = interpretNumber(matches[1]); - // const what = matches[2]; - // const dataDoc = Doc.GetProto(target.props.Document); - // const fieldKey = "data"; - // if (isNaN(count)) { - // return; - // } - // 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) => { - // const mode = matches[1]; - // mode && (target.props.Document._viewType = mode); - // }, - // restrictTo: [DocumentType.COL] - // } - - // ); - - // } - -} \ No newline at end of file + export namespace Commands { + export const dictationFadeDuration = 2000; + + export type IndependentAction = (target: DocumentView) => any | Promise; + export type IndependentEntry = { action: IndependentAction; restrictTo?: DocumentType[] }; + + export type DependentAction = (target: DocumentView, matches: RegExpExecArray) => any | Promise; + 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 () => { + console.log('PHRASE: ' + phrase); + const targets = SelectionManager.Views(); + if (!targets || !targets.length) { + return; + } + + phrase = phrase.toLowerCase(); + const entry = Independent.get(phrase); + + if (entry) { + let success = false; + const restrictTo = entry.restrictTo; + for (const target of targets) { + if (!restrictTo || validate(target, restrictTo)) { + await entry.action(target); + success = true; + } + } + return success; + } + + for (const entry of Dependent) { + const regex = entry.expression; + const matches = regex.exec(phrase); + regex.lastIndex = 0; + if (matches !== null) { + let success = false; + const restrictTo = entry.restrictTo; + for (const 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.COL, listSpec(Doc)], + [DocumentType.AUDIO, AudioField], + [DocumentType.IMG, ImageField], + [DocumentType.IMPORT, listSpec(Doc)], + [DocumentType.RTF, 'string'], + ]); + + const tryCast = (view: DocumentView, type: DocumentType) => { + const 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 (const type of types) { + if (tryCast(target, type)) { + return true; + } + } + return false; + }; + + const interpretNumber = (number: string) => { + const initial = parseInt(number); + if (!isNaN(initial)) { + return initial; + } + const converted = interpreter.wordsToNumbers(number, { fuzzy: true }); + if (converted === null) { + return NaN; + } + return typeof converted === 'string' ? parseInt(converted) : converted; + }; + + const Independent = new Map([ + [ + 'clear', + { + action: (target: DocumentView) => (Doc.GetProto(target.props.Document).data = new List()), + restrictTo: [DocumentType.COL], + }, + ], + + [ + 'open fields', + { + action: (target: DocumentView) => { + const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 }); + target.props.addDocTab(kvp, 'add:right'); + }, + }, + ], + + [ + 'new outline', + { + action: (target: DocumentView) => { + const newBox = Docs.Create.TextDocument('', { _width: 400, _height: 200, title: 'My Outline', _autoHeight: true }); + const proto = newBox.proto!; + const prompt = 'Press alt + r to start dictating here...'; + const head = 3; + const anchor = head + prompt.length; + const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"ordered_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`; + proto.data = new RichTextField(proseMirrorState); + proto.backgroundColor = '#eeffff'; + target.props.addDocTab(newBox, 'add:right'); + }, + }, + ], + ]); + + const Dependent = new Array( + { + expression: /create (\w+) documents of type (image|nested collection)/g, + action: (target: DocumentView, matches: RegExpExecArray) => { + const count = interpretNumber(matches[1]); + const what = matches[2]; + const dataDoc = Doc.GetProto(target.props.Document); + const fieldKey = 'data'; + if (isNaN(count)) { + return; + } + 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) => { + const mode = matches[1]; + mode && (target.props.Document._viewType = mode); + }, + restrictTo: [DocumentType.COL], + } + ); + } +} diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 0db9eab69..7628beef2 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -491,7 +491,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P let actualdW = Math.max(width + dW * scale, 20); let actualdH = Math.max(height + dH * scale, 20); const fixedAspect = nwidth && nheight && (!doc._fitWidth || e.ctrlKey || doc.nativeHeightUnfrozen); - console.log(fixedAspect); if (fixedAspect) { if ((Math.abs(dW) > Math.abs(dH) && ((!dragBottom && !dragTop) || !modifyNativeDim)) || dragRight) { if (dragRight && modifyNativeDim) { diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 60428fc98..3f9db2612 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -6,14 +6,12 @@ import { observer } from 'mobx-react'; import { Doc, DocListCast } from '../../../fields/Doc'; import { Cast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; -import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils'; +import { emptyFunction, setupMoveUpEvents } from '../../../Utils'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; -import { Hypothesis } from '../../util/HypothesisUtils'; import { LinkFollower } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; -import { undoBatch } from '../../util/UndoManager'; import { DocumentView } from '../nodes/DocumentView'; import { LinkDocPreview } from '../nodes/LinkDocPreview'; import './LinkMenuItem.scss'; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 40b0d285c..849deb04e 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -426,28 +426,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - const flattened = this.findInNode(this._editorView!, this._editorView!.state.doc, autoLinkTerm); - this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; + this.findInNode(editorView, editorView.state.doc, autoLinkTerm).forEach(sel => { const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); - const sel = flattened[i]; - tr = tr.addMark(sel.from, sel.to, splitter); - tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { - if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { - alink = - alink ?? - (DocListCast(this.Document.links).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) || - DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, LinkManager.AutoKeywords)!); - newAutoLinks.add(alink); - const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }]; - allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? [])); - const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: 'auto term', location: 'add:right' }); - tr = tr.addMark(pos, pos + node.nodeSize, link); - } - }); - tr = tr.removeMark(sel.from, sel.to, splitter); + if (!sel.$anchor.pos || editorView.state.doc.textBetween(sel.$anchor.pos - 1, sel.$to.pos).trim() === autoLinkTerm) { + tr = tr.addMark(sel.from, sel.to, splitter); + tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { + if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { + alink = + alink ?? + (DocListCast(this.Document.links).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) || + DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, LinkManager.AutoKeywords)!); + newAutoLinks.add(alink); + const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }]; + allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? [])); + const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: 'auto term', location: 'add:right' }); + tr = tr.addMark(pos, pos + node.nodeSize, link); + } + }); + tr = tr.removeMark(sel.from, sel.to, splitter); + } }); } return tr; @@ -841,6 +839,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { + console.log('RECORD DICTATIN:'); DictationManager.Controls.listen({ interimHandler: this.setDictationContent, continuous: { indefinite: false }, @@ -853,26 +852,35 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent DictationManager.Controls.stop(!abort); setDictationContent = (value: string) => { + console.log('DICTATION CONETNT: ' + value); if (this._editorView && this._recordingStart) { + console.log('STEP 1'); if (this._break) { - const textanchor = Docs.Create.TextanchorDocument({ title: 'dictation anchor' }); - this.addDocument(textanchor); - const link = DocUtils.MakeLinkToActiveAudio(() => textanchor, false).lastElement(); - link && (Doc.GetProto(link).isDictation = true); - if (!link) return; - const audioanchor = Cast(link.anchor2, Doc, null); - if (!audioanchor) return; - audioanchor.backgroundColor = 'tan'; - const audiotag = this._editorView.state.schema.nodes.audiotag.create({ - timeCode: NumCast(audioanchor._timecodeToShow), - audioId: audioanchor[Id], - textId: textanchor[Id], - }); - Doc.GetProto(textanchor).title = 'dictation:' + audiotag.attrs.timeCode; - const tr = this._editorView.state.tr.insert(this._editorView.state.doc.content.size, audiotag); - const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size)); - this._editorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size))); + console.log('BREAK'); + const textanchorFunc = () => { + const tanch = Docs.Create.TextanchorDocument({ title: 'dictation anchor' }); + return this.addDocument(tanch) ? tanch : undefined; + }; + const link = DocUtils.MakeLinkToActiveAudio(textanchorFunc, false).lastElement(); + if (link) { + Doc.GetProto(link).isDictation = true; + const audioanchor = Cast(link.anchor2, Doc, null); + const textanchor = Cast(link.anchor1, Doc, null); + if (audioanchor) { + audioanchor.backgroundColor = 'tan'; + const audiotag = this._editorView.state.schema.nodes.audiotag.create({ + timeCode: NumCast(audioanchor._timecodeToShow), + audioId: audioanchor[Id], + textId: textanchor[Id], + }); + Doc.GetProto(textanchor).title = 'dictation:' + audiotag.attrs.timeCode; + const tr = this._editorView.state.tr.insert(this._editorView.state.doc.content.size, audiotag); + const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size)); + this._editorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size))); + } + } } + console.log('FINALIziNG'); const from = this._editorView.state.selection.from; this._break = false; const tr = this._editorView.state.tr.insertText(value); @@ -1468,7 +1476,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent node that wraps the hyerlink + let target = e.target as any; // hrefs are stored on the dataset of the node that wraps the hyerlink while (target && !target.dataset?.targethrefs) target = target.parentElement; FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc); } @@ -1725,7 +1733,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent (this._recording = !this._recording))}> +
+ setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + action(e => (this._recording = !this._recording)) + ) + }>
); -- cgit v1.2.3-70-g09d2 From f9cded6755ee236d88e21b3b2d4c6da357d059b2 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 8 Aug 2022 15:47:35 -0400 Subject: moved 5 sec anno recording button to documentButtonsBar. made recording button appear only when there is one & click to play recording. --- src/client/util/CurrentUserUtils.ts | 3 +- src/client/util/DictationManager.ts | 4 -- src/client/views/DocumentButtonBar.tsx | 24 ++++++- src/client/views/PropertiesButtons.tsx | 9 --- src/client/views/nodes/DocumentView.scss | 6 +- src/client/views/nodes/DocumentView.tsx | 82 +++++++++++++--------- src/client/views/nodes/button/FontIconBox.tsx | 9 ++- .../views/nodes/formattedText/FormattedTextBox.tsx | 12 +--- 8 files changed, 90 insertions(+), 59 deletions(-) (limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 7856c913b..d19874720 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -107,7 +107,7 @@ export class CurrentUserUtils { const reqdClickList = reqdTempOpts.map(opts => { const allOpts = {...reqdClickOpts, ...opts.opts}; const clickDoc = tempClicks ? DocListCast(tempClicks.data).find(doc => doc.title === opts.opts.title): undefined; - return DocUtils.AssignOpts(clickDoc, allOpts) ?? Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script,allOpts)); + return DocUtils.AssignOpts(clickDoc, allOpts) ?? Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script, allOpts),allOpts); }); const reqdOpts:DocumentOptions = { title: "child click editors", _height:75, system: true}; @@ -633,6 +633,7 @@ export class CurrentUserUtils { { title: "Center", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", scripts: {onClick:'{ return setAlignment("center", _readOnly_);}'} }, { title: "Right", toolTip: "Right align", btnType: ButtonType.ToggleButton, icon: "align-right", scripts: {onClick:'{ return setAlignment("right", _readOnly_);}'} }, { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}}, + { title: "Dictate",toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", scripts: {onClick:'{ return toggleDictation(_readOnly_);}'}}, ]; } diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index 13f036838..0a61f3478 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -152,7 +152,6 @@ export namespace DictationManager { return new Promise((resolve, reject) => { recognizer.onerror = (e: any) => { - console.log('SPEECH error:', encodeURIComponent); // e is SpeechRecognitionError but where is that defined? if (!(indefinite && e.error === 'no-speech')) { recognizer.stop(); @@ -162,7 +161,6 @@ export namespace DictationManager { }; recognizer.onresult = (e: SpeechRecognitionEvent) => { - console.log('RESULT: ', e); current = synthesize(e, intra); let matchedTerminator: string | undefined; if (options?.terminators && (matchedTerminator = options.terminators.find(end => (current ? current.trim().toLowerCase().endsWith(end.toLowerCase()) : false)))) { @@ -175,7 +173,6 @@ export namespace DictationManager { }; recognizer.onend = (e: Event) => { - console.log('END: ', e); if (!indefinite || isManuallyStopped) { return complete(); } @@ -188,7 +185,6 @@ export namespace DictationManager { }; const complete = () => { - console.log('COMPLETE:'); if (indefinite) { current && sessionResults.push(current); sessionResults.length && resolve(sessionResults.join(inter || interSession)); diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 1d4056759..265df3abc 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -6,7 +6,7 @@ import { observer } from 'mobx-react'; import { Doc } from '../../fields/Doc'; import { RichTextField } from '../../fields/RichTextField'; import { Cast, NumCast } from '../../fields/Types'; -import { emptyFunction, setupMoveUpEvents, simulateMouseClick } from '../../Utils'; +import { emptyFunction, returnFalse, setupMoveUpEvents, simulateMouseClick } from '../../Utils'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; import { Docs } from '../documents/Documents'; @@ -334,6 +334,27 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV ); } + + @observable _isRecording = false; + @computed + get recordButton() { + const targetDoc = this.view0?.props.Document; + return !targetDoc ? null : ( + {'Click to record 5 second annotation'}}> +
{ + this._isRecording = true; + this.props.views().map(view => view?.docView?.recordAudioAnnotation(action(() => (this._isRecording = false)))); + }) + )}> + +
+
+ ); + } @observable _aliasDown = false; onTemplateButton = action((e: React.PointerEvent): void => { this._tooltipOpen = false; @@ -411,6 +432,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV ) : null} +
{this.recordButton}
{ Doc.noviceMode ? null :
{this.templateButton}
/*
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 39e7b89c1..80c2c7705 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -68,14 +68,6 @@ export class PropertiesButtons extends React.Component<{}, {}> { on => 'thumbtack' ); } - @computed get dictationButton() { - return this.propertyToggleBtn( - 'Dictate', - '_showAudio', - on => `${on ? 'Hide' : 'Show'} dictation/recording controls`, - on => 'microphone' - ); - } @computed get maskButton() { return this.propertyToggleBtn( 'Mask', @@ -365,7 +357,6 @@ export class PropertiesButtons extends React.Component<{}, {}> { {toggle(this.titleButton)} {toggle(this.captionButton)} {toggle(this.lockButton)} - {toggle(this.dictationButton, { display: isNovice ? 'none' : '' })} {toggle(this.onClickButton)} {toggle(this.fitWidthButton)} {toggle(this.freezeThumb)} diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index c02692bfb..6ea697a2f 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -65,11 +65,13 @@ width: 25px; height: 25; position: absolute; - top: 10px; - left: 10px; + top: 0; + left: 50%; border-radius: 25px; background: white; opacity: 0.3; + pointer-events: all; + cursor: default; svg { width: 90% !important; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 2524d66dd..4b2bd07ef 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -10,7 +10,7 @@ import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { AudioField } from '../../../fields/URLField'; import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util'; import { MobileInterface } from '../../../mobile/MobileInterface'; @@ -52,6 +52,8 @@ import { RadialMenu } from './RadialMenu'; import { ScriptingBox } from './ScriptingBox'; import { PresBox } from './trails/PresBox'; import React = require('react'); +import { DictationManager } from '../../util/DictationManager'; +import { Tooltip } from '@material-ui/core'; const { Howl } = require('howler'); interface Window { @@ -1005,16 +1007,16 @@ export class DocumentViewInternal extends DocComponent (this.props.NativeDimScaling?.() || 1) * this.props.DocumentView().screenToLocalTransform().Scale; @computed get contents() { TraceMobx(); - const audioView = !this.layoutDoc._showAudio ? null : ( -
- -
- ); + const audioAnnosCount = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null)?.length; + const audioTextAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations-text'], listSpec('string'), null); + const audioView = + (!this.props.isSelected() && !this._isHovering) || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (!audioAnnosCount && !this._mediaState) ? null : ( + {audioTextAnnos?.lastElement()}
}> +
+ +
+ + ); return (
{ + playAnnotation = () => { const self = this; - const audioAnnos = DocListCast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations']); - if (audioAnnos && audioAnnos.length && this._mediaState === 0) { - const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)]; - anno.data instanceof AudioField && - new Howl({ - src: [anno.data.url.href], - format: ['mp3'], - autoplay: true, - loop: false, - volume: 0.5, - onend: function () { - runInAction(() => (self._mediaState = 0)); - }, - }); + const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null); + const anno = audioAnnos.lastElement(); + if (anno instanceof AudioField && this._mediaState === 0) { + new Howl({ + src: [anno.url.href], + format: ['mp3'], + autoplay: true, + loop: false, + volume: 0.5, + onend: function () { + runInAction(() => { + console.log('PLAYED'); + self._mediaState = 0; + }); + }, + }); this._mediaState = 1; } }; - recordAudioAnnotation = () => { + + recordAudioAnnotation = (onEnd?: () => void) => { let gumStream: any; let recorder: any; const self = this; @@ -1176,18 +1181,30 @@ export class DocumentViewInternal extends DocComponent(['']); + DictationManager.Controls.listen({ + interimHandler: value => (audioTextAnnos[audioTextAnnos.length - 1] = value), + continuous: { indefinite: false }, + }).then(results => { + if (results && [DictationManager.Controls.Infringed].includes(results)) { + DictationManager.Controls.stop(); + } + onEnd?.(); + }); + gumStream = stream; recorder = new MediaRecorder(stream); recorder.ondataavailable = async (e: any) => { const [{ result }] = await Networking.UploadFilesToServer(e.data); if (!(result instanceof Error)) { - const audioDoc = Docs.Create.AudioDocument(result.accessPaths.agnostic.client, { title: 'audio test', _width: 200, _height: 32 }); - audioDoc.treeViewExpandedView = 'layout'; - const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'], listSpec(Doc)); + const audioField = new AudioField(result.accessPaths.agnostic.client); + const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null); if (audioAnnos === undefined) { - self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'] = new List([audioDoc]); + self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'] = new List([audioField]); } else { - audioAnnos.push(audioDoc); + audioAnnos.push(audioField); } } }; @@ -1195,6 +1212,7 @@ export class DocumentViewInternal extends DocComponent { recorder.stop(); + DictationManager.Controls.stop(false); runInAction(() => (self._mediaState = 0)); gumStream.getAudioTracks()[0].stop(); }, 5000); diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index 78ef85ec2..d3b95e25a 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -1,7 +1,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { action, computed, observable } from 'mobx'; +import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ColorState, SketchPicker } from 'react-color'; @@ -710,6 +710,13 @@ ScriptingGlobals.add(function toggleNoAutoLinkAnchor(checkResult?: boolean) { } if (editorView) RichTextMenu.Instance?.toggleNoAutoLinkAnchor(); }); +ScriptingGlobals.add(function toggleDictation(checkResult?: boolean) { + const textView = RichTextMenu.Instance?.TextView; + if (checkResult) { + return textView?._recording ? Colors.MEDIUM_BLUE : 'transparent'; + } + if (textView) runInAction(() => (textView._recording = !textView._recording)); +}); ScriptingGlobals.add(function toggleBold(checkResult?: boolean) { const editorView = RichTextMenu.Instance?.TextView?.EditorView; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 849deb04e..929cca1ea 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -771,7 +771,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent (FormattedTextBox._canAnnotate = !FormattedTextBox._canAnnotate), icon: 'expand-arrows-alt' }); uicontrols.push({ description: !this.Document._noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', event: () => (this.layoutDoc._noSidebar = !this.layoutDoc._noSidebar), icon: 'expand-arrows-alt' }); - uicontrols.push({ description: `${this.layoutDoc._showAudio ? 'Hide' : 'Show'} Dictation Icon`, event: () => (this.layoutDoc._showAudio = !this.layoutDoc._showAudio), icon: 'expand-arrows-alt' }); uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' }); !Doc.noviceMode && uicontrols.push({ @@ -839,7 +838,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - console.log('RECORD DICTATIN:'); DictationManager.Controls.listen({ interimHandler: this.setDictationContent, continuous: { indefinite: false }, @@ -852,11 +850,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent DictationManager.Controls.stop(!abort); setDictationContent = (value: string) => { - console.log('DICTATION CONETNT: ' + value); if (this._editorView && this._recordingStart) { - console.log('STEP 1'); if (this._break) { - console.log('BREAK'); const textanchorFunc = () => { const tanch = Docs.Create.TextanchorDocument({ title: 'dictation anchor' }); return this.addDocument(tanch) ? tanch : undefined; @@ -880,7 +875,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent @@ -1744,7 +1738,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent (this._recording = !this._recording)) ) }> - +
); } @@ -1898,7 +1892,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent {this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.sidebarWidthPercent === '0%' ? null : this.sidebarCollection} {this.noSidebar || this.Document._noSidebar || this.props.dontSelectOnLoad || this.Document._singleLine ? null : this.sidebarHandle} - {!this.layoutDoc._showAudio ? null : this.audioHandle} + {this.audioHandle} ); -- cgit v1.2.3-70-g09d2 From bca0026764fe554e1066b2f432e749371450d239 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 9 Aug 2022 11:35:00 -0400 Subject: added an audio annotation button to the anchor menu bar. --- src/client/views/DocumentButtonBar.tsx | 12 +- src/client/views/MarqueeAnnotator.tsx | 1 + src/client/views/SidebarAnnos.tsx | 1 + src/client/views/nodes/DocumentView.tsx | 35 ++-- .../views/nodes/formattedText/FormattedTextBox.tsx | 11 ++ src/client/views/pdf/AnchorMenu.tsx | 14 ++ src/fields/documentSchemas.ts | 182 ++++++++++----------- 7 files changed, 145 insertions(+), 111 deletions(-) (limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx') diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 265df3abc..81e417fca 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -21,7 +21,7 @@ import './DocumentButtonBar.scss'; import { Colors } from './global/globalEnums'; import { MetadataEntryMenu } from './MetadataEntryMenu'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; -import { DocumentView } from './nodes/DocumentView'; +import { DocumentView, DocumentViewInternal } from './nodes/DocumentView'; import { DashFieldView } from './nodes/formattedText/DashFieldView'; import { GoogleRef } from './nodes/formattedText/FormattedTextBox'; import { TemplateMenu } from './TemplateMenu'; @@ -347,7 +347,15 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV onClick={undoBatch( action(e => { this._isRecording = true; - this.props.views().map(view => view?.docView?.recordAudioAnnotation(action(() => (this._isRecording = false)))); + this.props.views().map( + view => + view && + DocumentViewInternal.recordAudioAnnotation( + view.dataDoc, + view.LayoutFieldKey, + action(() => (this._isRecording = false)) + ) + ); }) )}> diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index b01ee5f42..f90ad8bb5 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -65,6 +65,7 @@ export class MarqueeAnnotator extends React.Component { AnchorMenu.Instance.OnCrop = (e: PointerEvent) => this.props.anchorMenuCrop?.(this.highlight('rgba(173, 216, 230, 0.75)', true), true); AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight('rgba(173, 216, 230, 0.75)', true)); + AnchorMenu.Instance.OnAudio = unimplementedFunction; AnchorMenu.Instance.Highlight = this.highlight; AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations); AnchorMenu.Instance.onMakeAnchor = AnchorMenu.Instance.GetAnchor; diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 1c14c7cd5..90d9c3c43 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -74,6 +74,7 @@ export class SidebarAnnos extends React.Component { DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on'); this.addDocument(target); this._stackRef.current?.focusDocument(target); + return target; }; makeDocUnfiltered = (doc: Doc) => { if (DocListCast(this.props.rootDoc[this.sidebarKey]).includes(doc)) { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 4b2bd07ef..f87581875 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -206,7 +206,6 @@ export class DocumentViewInternal extends DocComponent (this.layoutDoc._showAudio = !this.layoutDoc._showAudio)), icon: 'microphone' }); const existingOnClick = cm.findByDescription('OnClick...'); const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : []; @@ -1010,10 +1008,15 @@ export class DocumentViewInternal extends DocComponent{audioTextAnnos?.lastElement()}}>
- +
); @@ -1154,7 +1157,7 @@ export class DocumentViewInternal extends DocComponent { - console.log('PLAYED'); - self._mediaState = 0; + self.dataDoc.audioAnnoState = 0; }); }, }); - this._mediaState = 1; + this.dataDoc.audioAnnoState = 1; } }; - recordAudioAnnotation = (onEnd?: () => void) => { + static recordAudioAnnotation(dataDoc: Doc, field: string, onEnd?: () => void) { let gumStream: any; let recorder: any; - const self = this; navigator.mediaDevices .getUserMedia({ audio: true, }) .then(function (stream) { - let audioTextAnnos = Cast(self.dataDoc[self.LayoutFieldKey + '-audioAnnotations-text'], listSpec('string'), null); + let audioTextAnnos = Cast(dataDoc[field + '-audioAnnotations-text'], listSpec('string'), null); if (audioTextAnnos) audioTextAnnos.push(''); - else audioTextAnnos = self.dataDoc[self.LayoutFieldKey + '-audioAnnotations-text'] = new List(['']); + else audioTextAnnos = dataDoc[field + '-audioAnnotations-text'] = new List(['']); DictationManager.Controls.listen({ interimHandler: value => (audioTextAnnos[audioTextAnnos.length - 1] = value), continuous: { indefinite: false }, @@ -1200,24 +1201,24 @@ export class DocumentViewInternal extends DocComponent (self._mediaState = 2)); + runInAction(() => (dataDoc.audioAnnoState = 2)); recorder.start(); setTimeout(() => { recorder.stop(); DictationManager.Controls.stop(false); - runInAction(() => (self._mediaState = 0)); + runInAction(() => (dataDoc.audioAnnoState = 0)); gumStream.getAudioTracks()[0].stop(); }, 5000); }); - }; + } captionStyleProvider = (doc: Opt, props: Opt, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption'); @computed get innards() { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 929cca1ea..223441b3b 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -63,6 +63,7 @@ import applyDevTools = require('prosemirror-dev-tools'); import React = require('react'); import { text } from 'body-parser'; import { CollectionTreeView } from '../../collections/CollectionTreeView'; +import { DocumentViewInternal } from '../DocumentView'; const translateGoogleApi = require('translate-google-api'); export interface FormattedTextBoxProps { @@ -249,6 +250,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { + !this.layoutDoc.showSidebar && this.toggleSidebar(); + const anchor = this.getAnchor(); + const target = this._sidebarRef.current?.anchorMenuClick(anchor); + if (target) { + anchor.followLinkAudio = true; + DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target)); + target.title = ComputedField.MakeFunction(`self["text-audioAnnotations-text"].lastElement()`); + } + }; AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => { this._editorView?.state && RichTextMenu.Instance.setHighlight(color, this._editorView, this._editorView?.dispatch); return undefined; diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 1a1120b6c..ee2ae10a7 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -49,6 +49,7 @@ export class AnchorMenu extends AntimodeMenu { public OnCrop: (e: PointerEvent) => void = unimplementedFunction; public OnClick: (e: PointerEvent) => void = unimplementedFunction; + public OnAudio: (e: PointerEvent) => void = unimplementedFunction; public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; public Highlight: (color: string, isPushpin: boolean) => Opt = (color: string, isPushpin: boolean) => undefined; @@ -92,6 +93,10 @@ export class AnchorMenu extends AntimodeMenu { ); }; + audioDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, returnFalse, returnFalse, e => this.OnAudio?.(e)); + }; + cropDown = (e: React.PointerEvent) => { setupMoveUpEvents( this, @@ -196,6 +201,15 @@ export class AnchorMenu extends AntimodeMenu { , + AnchorMenu.Instance.OnAudio === unimplementedFunction ? ( + <> + ) : ( + {'Click to Record Annotation'}}> + + + ), //NOTE: link popup is currently in progress {'Find document to link to selected text'}}> diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 223441b3b..e027ab0bd 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1884,7 +1884,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent -- cgit v1.2.3-70-g09d2 From a63f017c213563728f45f2caa7415843f50f3559 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 9 Aug 2022 19:50:33 -0400 Subject: fixed click on menu button for text box with hyperlink to not bring up buttonBar. fixed right click on hyperlink to just bring up button bar, but not native context menu. fixe stacking view child pointer events so that sidebar documents in lightbox are editable. --- src/client/views/DocumentButtonBar.tsx | 7 +------ src/client/views/collections/CollectionStackingView.tsx | 5 +++-- src/client/views/nodes/formattedText/FormattedTextBox.tsx | 10 +++++----- 3 files changed, 9 insertions(+), 13 deletions(-) (limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx') diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 81e417fca..1f8550ad6 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -274,12 +274,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV get menuButton() { const targetDoc = this.view0?.props.Document; return !targetDoc ? null : ( - -
{`Open Context Menu`}
- - }> + {`Open Context Menu`}}>
this.openContextMenu(e)}>
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index ef68cadd7..d4efef47a 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -300,9 +300,10 @@ export class CollectionStackingView extends CollectionSubView this.props.isSelected() || this.props.isContentActive(); + isContentActive = () => (this.props.isSelected() || this.props.isContentActive() ? true : this.props.isSelected() === false || this.props.isContentActive() === false ? false : undefined); - isChildContentActive = () => this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)); + isChildContentActive = () => + this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined; // this is what renders the document that you see on the screen // called in Children: this actually adds a document to our children list getDisplayDoc(doc: Doc, width: () => number) { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index e027ab0bd..f61533619 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -703,9 +703,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent node that wraps the hyerlink + let target = e.target as any; // hrefs are stored on the database of the
node that wraps the hyerlink while (target && !target.dataset?.targethrefs) target = target.parentElement; - if (target) { + if (target && !(e.nativeEvent as any).dash) { const hrefs = (target.dataset?.targethrefs as string) ?.trim() .split(' ') @@ -724,6 +724,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - if (!this._editorView?.state.selection.empty && FormattedTextBox._canAnnotate) this.setupAnchorMenu(); + if (!this._editorView?.state.selection.empty && FormattedTextBox._canAnnotate && !(e.nativeEvent as any).dash) this.setupAnchorMenu(); if (!this._downEvent) return; this._downEvent = false; - if (this.props.isContentActive(true)) { + if (this.props.isContentActive(true) && !(e.nativeEvent as any).dash) { const editor = this._editorView!; const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0)))); @@ -1795,7 +1796,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent ) : (
setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}> - //@ts-ignore Date: Mon, 22 Aug 2022 17:18:10 -0400 Subject: added animation of background color, and streamlined/extended a variety of props-related things. --- src/client/util/CurrentUserUtils.ts | 6 +-- src/client/views/DocComponent.tsx | 14 ------- src/client/views/StyleProvider.tsx | 9 +++-- .../CollectionFreeFormLayoutEngines.tsx | 2 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 33 ++++++++-------- .../views/nodes/CollectionFreeFormDocumentView.tsx | 30 ++++++++++++--- src/client/views/nodes/FieldView.tsx | 11 ++++-- .../views/nodes/formattedText/FormattedTextBox.tsx | 45 ++++++++++------------ src/fields/Doc.ts | 14 +++++++ src/fields/ScriptField.ts | 12 +++++- 10 files changed, 102 insertions(+), 74 deletions(-) (limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 20c57c617..17d58595c 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -235,9 +235,9 @@ export class CurrentUserUtils { const header = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { ...opts, title: "text", layout: "" + - ` ` + - " " + - ` Metadata` + + ` ` + + " " + + ` Metadata` + "" }, "header"); diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 280ca8a8c..886dd974b 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -132,20 +132,6 @@ export function ViewBoxAnnotatableComponent

() lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result; - styleFromLayoutString = (scale: number) => { - const style: { [key: string]: any } = {}; - const divKeys = ['width', 'height', 'fontSize', 'transform', 'left', 'background', 'left', 'right', 'top', 'bottom', 'pointerEvents', 'position']; - const replacer = (match: any, expr: string, offset: any, string: any) => { - // bcz: this executes a script to convert a property expression string: { script } into a value - return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: this.rootDoc, this: this.layoutDoc, scale }).result?.toString() ?? ''; - }; - divKeys.map((prop: string) => { - const p = (this.props as any)[prop]; - typeof p === 'string' && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer)); - }); - return style; - }; - protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; @computed public get annotationKey() { diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 3bd4f5152..c0ba170c6 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -40,7 +40,8 @@ export enum StyleProp { JitterRotation = 'jitterRotation', // whether documents should be randomly rotated BorderPath = 'customBorder', // border path for document view FontSize = 'fontSize', // size of text font - FontFamily = 'fontFamily', // size of text font + FontFamily = 'fontFamily', // font family of text + FontWeight = 'fontWeight', // font weight of text } function darkScheme() { @@ -117,9 +118,11 @@ export function DefaultStyleProvider(doc: Opt, props: Opt(); @observable _marqueeRef = React.createRef(); @observable _marqueeViewRef = React.createRef(); - @observable _keyframeEditing = false; @observable ChildDrag: DocumentView | undefined; // child document view being dragged. needed to update drop areas of groups when a group item is dragged. @computed get views() { @@ -185,8 +184,6 @@ export class CollectionFreeFormView extends CollectionSubView (this._keyframeEditing = set); - getKeyFrameEditing = () => this._keyframeEditing; onBrowseClickHandler = () => this.props.onBrowseClick?.() || ScriptCast(this.layoutDoc.onBrowseClick); onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); @@ -272,15 +269,15 @@ export class CollectionFreeFormView extends CollectionSubView NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); zsorted.forEach((doc, index) => (doc.zIndex = doc.isInkMask ? 5000 : index + 1)); const dvals = CollectionFreeFormDocumentView.getValues(refDoc, NumCast(refDoc.activeFrame, 1000)); - const dropPos = this.Document._currentFrame !== undefined ? [dvals.x || 0, dvals.y || 0] : [NumCast(refDoc.x), NumCast(refDoc.y)]; + const dropPos = this.Document._currentFrame !== undefined ? [NumCast(dvals.x), NumCast(dvals.y)] : [NumCast(refDoc.x), NumCast(refDoc.y)]; for (let i = 0; i < docDragData.droppedDocuments.length; i++) { const d = docDragData.droppedDocuments[i]; const layoutDoc = Doc.Layout(d); if (this.Document._currentFrame !== undefined) { CollectionFreeFormDocumentView.setupKeyframes([d], NumCast(this.Document._currentFrame), false); const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000)); - vals.x = x + (vals.x || 0) - dropPos[0]; - vals.y = y + (vals.y || 0) - dropPos[1]; + vals.x = x + NumCast(vals.x) - dropPos[0]; + vals.y = y + NumCast(vals.y) - dropPos[1]; vals._scrollTop = this.Document.editScrollProgressivize ? vals._scrollTop : undefined; CollectionFreeFormDocumentView.setValues(NumCast(this.Document._currentFrame), d, vals); } else { @@ -1317,22 +1314,22 @@ export class CollectionFreeFormView extends CollectionSubView { x: number; y: number; zIndex?: number; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined; + dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined; sizeProvider?: (doc: Doc, replica: string) => { width: number; height: number } | undefined; renderCutoffProvider: (doc: Doc) => boolean; zIndex?: number; @@ -32,6 +32,7 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { @observer export class CollectionFreeFormDocumentView extends DocComponent() { public static animFields = ['_height', '_width', 'x', 'y', '_scrollTop', 'opacity']; // fields that are configured to be animatable using animation frames + public static animStringFields = ['backgroundColor']; // fields that are configured to be animatable using animation frames @observable _animPos: number[] | undefined = undefined; @observable _contentView: DocumentView | undefined | null; get displayName() { @@ -41,16 +42,19 @@ export class CollectionFreeFormDocumentView extends DocComponent, property: string) => { if (property === StyleProp.Opacity && doc === this.layoutDoc) return this.Opacity; // only change the opacity for this specific document, not its children + if (property === StyleProp.BackgroundColor) { + return this.BackgroundColor; // only change the opacity for this specific document, not its children + } return this.props.styleProvider?.(doc, props, property); }; @@ -77,6 +84,13 @@ export class CollectionFreeFormDocumentView extends DocComponent }); } + public static getStringValues(doc: Doc, time: number) { + return CollectionFreeFormDocumentView.animStringFields.reduce((p, val) => { + p[val] = Cast(`${val}-indexed`, listSpec('string'), [StrCast(doc[val])]).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as string); + return p; + }, {} as { [val: string]: Opt }); + } + public static setValues(time: number, d: Doc, vals: { [val: string]: Opt }) { const timecode = Math.round(time); Object.keys(vals).forEach(val => { @@ -95,6 +109,10 @@ export class CollectionFreeFormDocumentView extends DocComponent { + const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null); + findexed?.length <= timecode + 1 && findexed.push(undefined as any as string); + }); }) ); setTimeout( @@ -142,6 +160,7 @@ export class CollectionFreeFormDocumentView extends DocComponent(numberRange(currTimecode + 1).map(t => (!doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1))); } CollectionFreeFormDocumentView.animFields.forEach(val => (doc[val] = ComputedField.MakeInterpolated(val, 'activeFrame', doc, currTimecode))); + CollectionFreeFormDocumentView.animStringFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode))); doc.activeFrame = ComputedField.MakeFunction('self.context?._currentFrame||0'); doc.dataTransition = 'inherit'; }); @@ -189,7 +208,6 @@ export class CollectionFreeFormDocumentView extends DocComponent number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to DocumentViewInternalsProps onBrowseClick?: () => ScriptField | undefined; onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => boolean | undefined; + pointerEvents?: () => Opt; // properties intended to be used from within layout strings (otherwise use the function equivalents that work more efficiently with React) - pointerEvents?: () => Opt; + // See currentUserUtils headerTemplate for examples of creating text boxes from html which set some of these fields + // Also, see InkingStroke for examples of creating text boxes from render() methods which set some of these fields + backgroundColor?: string; + color?: string; fontSize?: number; height?: number; width?: number; - background?: string; - color?: string; xPadding?: number; yPadding?: number; + noSidebar?: boolean; + dontScale?: boolean; + dontSelectOnLoad?: boolean; // suppress selecting (e.g.,. text box) when loaded (and mark as not being associated with scrollTop document field) } @observer diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index f61533619..81ac45521 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -11,7 +11,7 @@ import { Fragment, Mark, Node, Slice } from 'prosemirror-model'; import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { DateField } from '../../../../fields/DateField'; -import { AclAdmin, AclAugment, AclEdit, AclReadonly, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { PrefetchProxy } from '../../../../fields/Proxy'; @@ -35,6 +35,7 @@ import { SnappingManager } from '../../../util/SnappingManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView'; import { CollectionStackingView } from '../../collections/CollectionStackingView'; +import { CollectionTreeView } from '../../collections/CollectionTreeView'; import { ContextMenu } from '../../ContextMenu'; import { ContextMenuProps } from '../../ContextMenuItem'; import { ViewBoxAnnotatableComponent } from '../../DocComponent'; @@ -44,6 +45,7 @@ import { LightboxView } from '../../LightboxView'; import { AnchorMenu } from '../../pdf/AnchorMenu'; import { SidebarAnnos } from '../../SidebarAnnos'; import { StyleProp } from '../../StyleProvider'; +import { DocumentViewInternal } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { LinkDocPreview } from '../LinkDocPreview'; import { DashDocCommentView } from './DashDocCommentView'; @@ -61,25 +63,14 @@ import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; import applyDevTools = require('prosemirror-dev-tools'); import React = require('react'); -import { text } from 'body-parser'; -import { CollectionTreeView } from '../../collections/CollectionTreeView'; -import { DocumentViewInternal } from '../DocumentView'; const translateGoogleApi = require('translate-google-api'); -export interface FormattedTextBoxProps { - makeLink?: () => Opt; // bcz: hack: notifies the text document when the container has made a link. allows the text doc to react and setup a hyeprlink for any selected text - xPadding?: number; // used to override document's settings for xMargin --- see CollectionCarouselView - yPadding?: number; - noSidebar?: boolean; - dontScale?: boolean; - dontSelectOnLoad?: boolean; // suppress selecting the text box when loaded (and mark as not being associated with scrollTop document field) -} export const GoogleRef = 'googleDocId'; type PullHandler = (exportState: Opt, dataDoc: Doc) => void; @observer -export class FormattedTextBox extends ViewBoxAnnotatableComponent() { +export class FormattedTextBox extends ViewBoxAnnotatableComponent() { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } @@ -1843,18 +1834,27 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent= 10 ? '-selected' : ''; - const styleFromString = this.styleFromLayoutString(scale); // this converts any expressions in the format string to style props. e.g., - return styleFromString?.height === '0px' ? null : ( + const styleFromLayoutString = Doc.styleFromLayoutString(this.rootDoc, this.layoutDoc, this.props, scale); // this converts any expressions in the format string to style props. e.g., + return styleFromLayoutString?.height === '0px' ? null : (

this.props.isContentActive() && e.stopPropagation()} style={{ - transform: this.props.dontScale ? undefined : `scale(${scale})`, - transformOrigin: this.props.dontScale ? undefined : 'top left', - width: this.props.dontScale ? undefined : `${100 / scale}%`, - height: this.props.dontScale ? undefined : `${100 / scale}%`, + ...(this.props.dontScale + ? {} + : { + transform: `scale(${scale})`, + transformOrigin: 'top left', + width: `${100 / scale}%`, + height: `${100 / scale}%`, + }), + transition: 'inherit', // overflowY: this.layoutDoc._autoHeight ? "hidden" : undefined, - ...styleFromString, + color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color), + fontSize: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize), + fontFamily: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily), + fontWeight: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontWeight), + ...styleFromLayoutString, }}>
Doc.AreProtosEqual(val, doc)) !== -1; } + export function styleFromLayoutString(rootDoc: Doc, layoutDoc: Doc, props: any, scale: number) { + const style: { [key: string]: any } = {}; + const divKeys = ['width', 'height', 'fontSize', 'transform', 'left', 'backgroundColor', 'left', 'right', 'top', 'bottom', 'pointerEvents', 'position']; + const replacer = (match: any, expr: string, offset: any, string: any) => { + // bcz: this executes a script to convert a property expression string: { script } into a value + return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: rootDoc, this: layoutDoc, scale }).result?.toString() ?? ''; + }; + divKeys.map((prop: string) => { + const p = props[prop]; + typeof p === 'string' && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer)); + }); + return style; + } + // prettier-ignore export function toIcon(doc?: Doc, isOpen?: boolean) { switch (StrCast(doc?.type)) { diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 68fb45987..0fd992d3b 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -9,7 +9,7 @@ import { Doc, Field, Opt } from './Doc'; import { Copy, Id, ToScriptString, ToString } from './FieldSymbols'; import { List } from './List'; import { ObjectField } from './ObjectField'; -import { Cast, NumCast } from './Types'; +import { Cast, NumCast, StrCast } from './Types'; import { Plugins } from './util'; function optional(propSchema: PropSchema) { @@ -199,6 +199,16 @@ export class ComputedField extends ScriptField { const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {}); return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; } + public static MakeInterpolatedString(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) { + if (!doc[`${fieldKey}-indexed`]) { + const flist = new List(numberRange(curTimecode + 1).map(i => undefined) as any as string[]); + flist[curTimecode] = StrCast(doc[fieldKey]); + doc[`${fieldKey}-indexed`] = flist; + } + const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {}); + const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {}); + return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; + } } export namespace ComputedField { let useComputed = true; -- cgit v1.2.3-70-g09d2