aboutsummaryrefslogtreecommitdiff
path: root/src/client/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/util')
-rw-r--r--src/client/util/CurrentUserUtils.ts92
-rw-r--r--src/client/util/DictationManager.ts435
-rw-r--r--src/client/util/DocumentManager.ts17
-rw-r--r--src/client/util/DragManager.ts20
-rw-r--r--src/client/util/UndoManager.ts51
5 files changed, 317 insertions, 298 deletions
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index ce32595d4..d19874720 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -97,6 +97,44 @@ export class CurrentUserUtils {
return DocUtils.AssignScripts(DocUtils.AssignOpts(tempDocs, reqdOpts, requiredTypes) ?? Docs.Create.MasonryDocument(requiredTypes, reqdOpts), reqdScripts, reqdFuncs);
}
+ /// Initializes templates for editing click funcs of a document
+ static setupChildClickEditors(doc: Doc, field = "clickFuncs-child") {
+ const tempClicks = DocCast(doc[field]);
+ const reqdClickOpts:DocumentOptions = {_width: 300, _height:200, system: true};
+ const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [
+ { opts: { title: "Open In Target", targetScriptKey: "onChildClick"}, script: "docCast(thisContainer.target).then((target) => target && (target.proto.data = new List([self])))"},
+ { opts: { title: "Open Detail On Right", targetScriptKey: "onChildDoubleClick"}, script: "openOnRight(self.doubleClickView)"}];
+ 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),allOpts);
+ });
+
+ const reqdOpts:DocumentOptions = { title: "child click editors", _height:75, system: true};
+ return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts));
+ }
+
+ /// Initializes templates for editing click funcs of a document
+ static setupClickEditorTemplates(doc: Doc, field = "template-clickFuncs") {
+ const tempClicks = DocCast(doc[field]);
+ const reqdClickOpts:DocumentOptions = { _width: 300, _height:200, system: true};
+ const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [
+ { opts: { title: "onClick"}, script: "console.log( 'click')"},
+ { opts: { title: "onDoubleClick"}, script: "console.log( 'double click')"},
+ { opts: { title: "onChildClick"}, script: "console.log( 'child click')"},
+ { opts: { title: "onChildDoubleClick"}, script: "console.log( 'child double click')"},
+ { opts: { title: "onCheckedClick"}, script: "console.log( heading, checked, containingTreeView)"},
+ ];
+ 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) ?? MakeTemplate(Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script, {heading:Doc.name, checked:"boolean", containingTreeView:Doc.name}), allOpts), true, opts.opts.title);
+ });
+
+ const reqdOpts:DocumentOptions = {title: "click editor templates", _height:75, system: true};
+ return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts));
+ }
+
/// Initializes templates that can be applied to notes
static setupNoteTemplates(doc: Doc, field="template-notes") {
const tempNotes = DocCast(doc[field]);
@@ -105,7 +143,7 @@ export class CurrentUserUtils {
{ noteType: "Idea", backgroundColor: "pink", icon: "lightbulb" },
{ noteType: "Topic", backgroundColor: "lightblue", icon: "book-open" }];
const reqdNoteList = reqdTempOpts.map(opts => {
- const reqdOpts = {...opts, title: "text", system: true};
+ const reqdOpts = {...opts, title: "text", width:200, system: true};
const noteType = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.noteType === opts.noteType): undefined;
return DocUtils.AssignOpts(noteType, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts), true, opts.noteType??"Note");
});
@@ -118,10 +156,10 @@ export class CurrentUserUtils {
static setupDocTemplates(doc: Doc, field="myTemplates") {
DocUtils.AssignDocField(doc, "presElement", opts => Docs.Create.PresElementBoxDocument(opts), { title: "pres element template", type: DocumentType.PRESELEMENT, _fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: "data"});
const templates = [
- DocCast(doc.presElement),
CurrentUserUtils.setupNoteTemplates(doc),
CurrentUserUtils.setupClickEditorTemplates(doc)
];
+ CurrentUserUtils.setupChildClickEditors(doc)
const reqdOpts = { title: "template layouts", _xMargin: 0, system: true, };
const reqdScripts = { dropConverter: "convertToButtons(dragData)" };
return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, templates, reqdScripts);
@@ -216,6 +254,7 @@ export class CurrentUserUtils {
creator:(opts:DocumentOptions)=> any // how to create the empty thing if it doesn't exist
}[] = [
{key: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _autoHeight: true }},
+ {key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200 }},
{key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100 }},
{key: "Equation", creator: opts => Docs.Create.EquationDocument(opts), opts: { _width: 300, _height: 35, _fitWidth:false, _backgroundGridShow: true, }},
{key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, useCors: true, }},
@@ -241,6 +280,7 @@ export class CurrentUserUtils {
return [
{ toolTip: "Tap or drag to create a note", title: "Note", icon: "sticky-note", dragFactory: doc.emptyNote as Doc, },
+ { toolTip: "Tap or drag to create a note board", title: "Notes", icon: "folder", dragFactory: doc.emptyNoteboard as Doc, },
{ toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab), scripts: { onClick: 'openOnRight(copyDragFactory(this.clickFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, },
{ toolTip: "Tap or drag to create an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyEquation as Doc, },
{ toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, },
@@ -593,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_);}'}},
];
}
@@ -630,7 +671,7 @@ export class CurrentUserUtils {
CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Multicolumn,
CollectionViewType.Multirow, CollectionViewType.Time, CollectionViewType.Carousel,
CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map,
- CollectionViewType.Grid]),
+ CollectionViewType.Grid, CollectionViewType.NoteTaking]),
title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}},
{ title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
{ title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
@@ -748,51 +789,6 @@ export class CurrentUserUtils {
DocUtils.AssignDocField(myImports, "buttonMenuDoc", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" });
return myImports;
}
-
- static setupClickEditorTemplates(doc: Doc) {
- if (doc["clickFuncs-child"] === undefined) {
- // to use this function, select it from the context menu of a collection. then edit the onChildClick script. Add two Doc variables: 'target' and 'thisContainer', then assign 'target' to some target collection. After that, clicking on any document in the initial collection will open it in the target
- const openInTarget = Docs.Create.ScriptingDocument(ScriptField.MakeScript(
- "docCast(thisContainer.target).then((target) => target && (target.proto.data = new List([self]))) ",
- { thisContainer: Doc.name }), {
- title: "Click to open in target", _width: 300, _height: 200,
- targetScriptKey: "onChildClick", system: true
- });
-
- const openDetail = Docs.Create.ScriptingDocument(ScriptField.MakeScript( "openOnRight(self.doubleClickView)", {}),
- { title: "Double click to open doubleClickView", _width: 300, _height: 200, targetScriptKey: "onChildDoubleClick", system: true });
-
- doc["clickFuncs-child"] = Docs.Create.TreeDocument([openInTarget, openDetail], { title: "on Child Click function templates", system: true });
- }
-
- if (doc.clickFuncs === undefined) {
- const onClick = Docs.Create.ScriptingDocument(undefined, {
- title: "onClick", "onClick-rawScript": "console.log('click')",
- isTemplateDoc: true, isTemplateForField: "onClick", _width: 300, _height: 200, system: true
- }, "onClick");
- const onChildClick = Docs.Create.ScriptingDocument(undefined, {
- title: "onChildClick", "onChildClick-rawScript": "console.log('child click')",
- isTemplateDoc: true, isTemplateForField: "onChildClick", _width: 300, _height: 200, system: true
- }, "onChildClick");
- const onDoubleClick = Docs.Create.ScriptingDocument(undefined, {
- title: "onDoubleClick", "onDoubleClick-rawScript": "console.log('double click')",
- isTemplateDoc: true, isTemplateForField: "onDoubleClick", _width: 300, _height: 200, system: true
- }, "onDoubleClick");
- const onChildDoubleClick = Docs.Create.ScriptingDocument(undefined, {
- title: "onChildDoubleClick", "onChildDoubleClick-rawScript": "console.log('child double click')",
- isTemplateDoc: true, isTemplateForField: "onChildDoubleClick", _width: 300, _height: 200, system: true
- }, "onChildDoubleClick");
- const onCheckedClick = Docs.Create.ScriptingDocument(undefined, {
- title: "onCheckedClick", "onCheckedClick-rawScript": "console.log(heading + checked + containingTreeView)",
- "onCheckedClick-params": new List<string>(["heading", "checked", "containingTreeView"]), isTemplateDoc: true,
- isTemplateForField: "onCheckedClick", _width: 300, _height: 200, system: true
- }, "onCheckedClick");
- doc.clickFuncs = Docs.Create.TreeDocument([onClick, onChildClick, onDoubleClick, onCheckedClick], { title: "onClick funcs", system: true });
- }
-
- return doc.clickFuncs as Doc;
- }
-
/// Updates the UserDoc to have all required fields, docs, etc. No changes should need to be
/// written to the server if the code hasn't changed. However, choices need to be made for each Doc/field
/// whether to revert to "default" values, or to leave them as the user/system last set them.
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index a6dcda4bc..0a61f3478 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<ListeningOptions>) => {
if (!recognizer) {
- console.log("DictationManager:" + unsupported);
+ console.log('DictationManager:' + unsupported);
return unsupported;
}
if (isListening) {
@@ -146,16 +143,17 @@ 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<string>((resolve, reject) => {
- recognizer.onerror = (e: any) => { // e is SpeechRecognitionError but where is that defined?
- if (!(indefinite && e.error === "no-speech")) {
+ recognizer.onerror = (e: any) => {
+ // e is SpeechRecognitionError but where is that defined?
+ if (!(indefinite && e.error === 'no-speech')) {
recognizer.stop();
resolve(e);
//reject(e);
@@ -165,7 +163,7 @@ export namespace DictationManager {
recognizer.onresult = (e: SpeechRecognitionEvent) => {
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();
@@ -191,7 +189,7 @@ export namespace DictationManager {
current && sessionResults.push(current);
sessionResults.length && resolve(sessionResults.join(inter || interSession));
} else {
- resolve(current || "");
+ resolve(current || '');
}
current = undefined;
sessionResults = [];
@@ -201,7 +199,6 @@ export namespace DictationManager {
recognizer.onerror = null;
recognizer.onend = null;
};
-
});
};
@@ -222,171 +219,173 @@ export namespace DictationManager {
}
return transcripts.join(delimiter || intraSession);
};
-
}
- // 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 () => {
- // 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, CastCtor>([
- // [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<string, IndependentEntry>([
-
- // ["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<DependentEntry>(
-
- // {
- // 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<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 () => {
+ 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, CastCtor>([
+ [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<string, IndependentEntry>([
+ [
+ '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<DependentEntry>(
+ {
+ 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/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index d3ac2f03f..52b643c04 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -12,6 +12,9 @@ import { CollectionFreeFormView } from '../views/collections/collectionFreeForm'
import { CollectionView } from '../views/collections/CollectionView';
import { ScriptingGlobals } from './ScriptingGlobals';
import { SelectionManager } from './SelectionManager';
+import { listSpec } from '../../fields/Schema';
+import { AudioField } from '../../fields/URLField';
+const { Howl } = require('howler');
export class DocumentManager {
//global holds all of the nodes (regardless of which collection they're in)
@@ -186,6 +189,20 @@ export class DocumentManager {
} else {
finalTargetDoc.hidden && (finalTargetDoc.hidden = undefined);
!noSelect && docView?.select(false);
+ if (originatingDoc?.followLinkAudio) {
+ const anno = Cast(finalTargetDoc[Doc.LayoutFieldKey(finalTargetDoc) + '-audioAnnotations'], listSpec(AudioField), null).lastElement();
+ if (anno) {
+ if (anno instanceof AudioField) {
+ new Howl({
+ src: [anno.url.href],
+ format: ['mp3'],
+ autoplay: true,
+ loop: false,
+ volume: 0.5,
+ });
+ }
+ }
+ }
}
finished?.();
};
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 4338072df..ccd94c56e 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,4 +1,4 @@
-import { action } from 'mobx';
+import { action, observable, runInAction } from 'mobx';
import { DateField } from '../../fields/DateField';
import { Doc, Field, Opt } from '../../fields/Doc';
import { List } from '../../fields/List';
@@ -320,7 +320,7 @@ export namespace DragManager {
y: snapVal([yFromTop, yFromBottom], e.pageY, SnappingManager.horizSnapLines()),
};
}
- export let docsBeingDragged: Doc[] = [];
+ export let docsBeingDragged: Doc[] = observable([] as Doc[]);
export let CanEmbed = false;
export let DocDragData: DocumentDragData | undefined;
export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) {
@@ -349,13 +349,13 @@ export namespace DragManager {
xs: number[] = [],
ys: number[] = [];
- docsBeingDragged = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : [];
const elesCont = {
left: Number.MAX_SAFE_INTEGER,
right: Number.MIN_SAFE_INTEGER,
top: Number.MAX_SAFE_INTEGER,
bottom: Number.MIN_SAFE_INTEGER,
};
+ const docsToDrag = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : [];
const dragElements = eles.map(ele => {
if (!ele.parentNode) dragDiv.appendChild(ele);
const dragElement = ele.parentNode === dragDiv ? ele : (ele.cloneNode(true) as HTMLElement);
@@ -405,7 +405,7 @@ export namespace DragManager {
});
dragLabel.style.transform = `translate(${rect.left + (options?.offsetX || 0)}px, ${rect.top + (options?.offsetY || 0) - 20}px)`;
- if (docsBeingDragged.length) {
+ if (docsToDrag.length) {
const pdfBox = dragElement.getElementsByTagName('canvas');
const pdfBoxSrc = ele.getElementsByTagName('canvas');
Array.from(pdfBox)
@@ -429,6 +429,8 @@ export namespace DragManager {
return dragElement;
});
+ runInAction(() => docsBeingDragged.push(...docsToDrag));
+
const hideDragShowOriginalElements = (hide: boolean) => {
dragLabel.style.display = hide ? '' : 'none';
!hide && dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
@@ -446,16 +448,18 @@ export namespace DragManager {
AbortDrag = () => {
options?.dragComplete?.(new DragCompleteEvent(true, dragData));
- cleanupDrag();
+ cleanupDrag(true);
};
- const cleanupDrag = action(() => {
+ const cleanupDrag = action((undo: boolean) => {
hideDragShowOriginalElements(false);
document.removeEventListener('pointermove', moveHandler, true);
document.removeEventListener('pointerup', upHandler, true);
SnappingManager.SetIsDragging(false);
SnappingManager.clearSnapLines();
- batch.end();
+ const ended = batch.end();
+ if (undo && ended) UndoManager.Undo();
+ docsBeingDragged.length = 0;
});
var startWindowDragTimer: any;
const moveHandler = (e: PointerEvent) => {
@@ -545,7 +549,7 @@ export namespace DragManager {
const upHandler = (e: PointerEvent) => {
clearTimeout(startWindowDragTimer);
startWindowDragTimer = undefined;
- dispatchDrag(document.elementFromPoint(e.x, e.y) || document.body, e, new DragCompleteEvent(false, dragData), snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom), finishDrag, options, cleanupDrag);
+ dispatchDrag(document.elementFromPoint(e.x, e.y) || document.body, e, new DragCompleteEvent(false, dragData), snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom), finishDrag, options, () => cleanupDrag(false));
};
document.addEventListener('pointermove', moveHandler, true);
document.addEventListener('pointerup', upHandler, true);
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index d1f1a0099..d0aec45a6 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -1,5 +1,5 @@
-import { observable, action, runInAction } from "mobx";
-import { Without } from "../../Utils";
+import { observable, action, runInAction } from 'mobx';
+import { Without } from '../../Utils';
function getBatchName(target: any, key: string | symbol): string {
const keyName = key.toString();
@@ -28,9 +28,9 @@ function propertyDecorator(target: any, key: string | symbol) {
} finally {
batch.end();
}
- }
+ },
});
- }
+ },
});
}
@@ -39,7 +39,7 @@ export function undoBatch(fn: (...args: any[]) => any): (...args: any[]) => any;
export function undoBatch(target: any, key?: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any {
if (!key) {
return function () {
- const batch = UndoManager.StartBatch("");
+ const batch = UndoManager.StartBatch('');
try {
return target.apply(undefined, arguments);
} finally {
@@ -96,7 +96,7 @@ export namespace UndoManager {
}
export function PrintBatches(): void {
- console.log("Open Undo Batches:");
+ console.log('Open Undo Batches:');
GetOpenBatches().forEach(batch => console.log(batch.batchName));
}
@@ -106,23 +106,25 @@ export namespace UndoManager {
}
export function FilterBatches(fieldTypes: string[]) {
const fieldCounts: { [key: string]: number } = {};
- const lastStack = UndoManager.undoStack.slice(-1)[0];//.lastElement();
+ const lastStack = UndoManager.undoStack.slice(-1)[0]; //.lastElement();
if (lastStack) {
lastStack.forEach(ev => fieldTypes.includes(ev.prop) && (fieldCounts[ev.prop] = (fieldCounts[ev.prop] || 0) + 1));
const fieldCount2: { [key: string]: number } = {};
- runInAction(() =>
- UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => {
- if (fieldTypes.includes(ev.prop)) {
- fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1;
- if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true;
- return false;
- }
- return true;
- }));
+ runInAction(
+ () =>
+ (UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => {
+ if (fieldTypes.includes(ev.prop)) {
+ fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1;
+ if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true;
+ return false;
+ }
+ return true;
+ }))
+ );
}
}
export function TraceOpenBatches() {
- console.log(`Open batches:\n\t${openBatches.map(batch => batch.batchName).join("\n\t")}\n`);
+ console.log(`Open batches:\n\t${openBatches.map(batch => batch.batchName).join('\n\t')}\n`);
}
export class Batch {
private disposed: boolean = false;
@@ -133,15 +135,15 @@ export namespace UndoManager {
private dispose = (cancel: boolean) => {
if (this.disposed) {
- throw new Error("Cannot dispose an already disposed batch");
+ throw new Error('Cannot dispose an already disposed batch');
}
this.disposed = true;
openBatches.splice(openBatches.indexOf(this));
- EndBatch(cancel);
- }
+ return EndBatch(cancel);
+ };
- end = () => { this.dispose(false); };
- cancel = () => { this.dispose(true); };
+ end = () => this.dispose(false);
+ cancel = () => this.dispose(true);
}
export function StartBatch(batchName: string): Batch {
@@ -163,7 +165,9 @@ export namespace UndoManager {
}
redoStack.length = 0;
currentBatch = undefined;
+ return true;
}
+ return false;
});
export function RunInTempBatch<T>(fn: () => T) {
@@ -232,5 +236,4 @@ export namespace UndoManager {
undoStack.push(commands);
});
-
-} \ No newline at end of file
+}