aboutsummaryrefslogtreecommitdiff
path: root/src/client/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/util')
-rw-r--r--src/client/util/CurrentUserUtils.ts26
-rw-r--r--src/client/util/DictationManager.ts435
-rw-r--r--src/client/util/DocumentManager.ts17
-rw-r--r--src/client/util/DragManager.ts14
-rw-r--r--src/client/util/InteractionUtils.tsx240
-rw-r--r--src/client/util/ReportManager.scss88
-rw-r--r--src/client/util/ReportManager.tsx282
7 files changed, 751 insertions, 351 deletions
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 7856c913b..17d58595c 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};
@@ -235,9 +235,9 @@ export class CurrentUserUtils {
const header = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { ...opts, title: "text",
layout:
"<HTMLdiv transformOrigin='top left' width='{100/scale}%' height='{100/scale}%' transform='scale({scale})'>" +
- ` <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._headerHeight||0}px)'/>` +
- " <FormattedTextBox {...props} dontScale='true' fieldKey={'header'} dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._headerFontSize||9}px' height='{(this._headerHeight||0)}px' background='{this._headerColor || MySharedDocs().userColor||`lightGray`}' />" +
- ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' background='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’} >Metadata</HTMLdiv>` +
+ ` <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._headerHeight||0}px)'/>` +
+ " <FormattedTextBox {...props} dontScale='true' fieldKey={'header'} dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._headerFontSize||9}px' height='{(this._headerHeight||0)}px' backgroundColor='{this._headerColor || MySharedDocs().userColor||`lightGray`}' />" +
+ ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' backgroundColor='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’} >Metadata</HTMLdiv>` +
"</HTMLdiv>"
}, "header");
@@ -289,11 +289,12 @@ export class CurrentUserUtils {
{ toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, },
{ toolTip: "Tap or drag to create a screen grabber", title: "Grab", icon: "photo-video", dragFactory: doc.emptyScreengrab as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'},funcs: { hidden: 'IsNoviceMode()'} },
{ toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'},funcs: { hidden: 'IsNoviceMode()'}},
- { toolTip: "Tap or drag to create a button", title: "Button", icon: "bolt", dragFactory: doc.emptyButton as Doc, funcs: { hidden: 'IsNoviceMode()'} },
- { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, funcs: { hidden: 'IsNoviceMode()'}},
+ { toolTip: "Tap or drag to create a button", title: "Button", icon: "bolt", dragFactory: doc.emptyButton as Doc, funcs: { hidden: 'IsNoviceMode()'} },
+ { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, funcs: { hidden: 'IsNoviceMode()'}},
{ toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "file", dragFactory: doc.emptyDataViz as Doc, },
- { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize", dragFactory: doc.emptyHeader as Doc, scripts: {onClick: 'openOnRight(delegateDragFactory(this.dragFactory))', onDragStart: '{ return delegateDragFactory(this.dragFactory);}'}, },
- { toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", scripts: {onClick: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' } },
+ { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "file", dragFactory: doc.emptySlide as Doc, funcs: { hidden: 'IsNoviceMode()'}},
+ { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize", dragFactory: doc.emptyHeader as Doc,scripts: { onClick: 'openOnRight(delegateDragFactory(this.dragFactory))', onDragStart: '{ return delegateDragFactory(this.dragFactory);}'}, },
+ { toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", scripts: { onClick: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' } },
].map(tuple => ({scripts: {onClick: 'openOnRight(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, ...tuple, }))
}
@@ -632,7 +633,8 @@ export class CurrentUserUtils {
{ title: "Left", toolTip: "Left align", btnType: ButtonType.ToggleButton, icon: "align-left", scripts: {onClick:'{ return setAlignment("left", _readOnly_);}' }},
{ 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: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}},
+ { title: "Dictate",toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", scripts: {onClick:'{ return toggleDictation(_readOnly_);}'}},
];
}
@@ -644,8 +646,9 @@ export class CurrentUserUtils {
// { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", scripts:{onClick: 'setActiveTool("highlighter")'} },
{ title: "Circle", toolTip: "Circle (Ctrl+Shift+C)", btnType: ButtonType.ToggleButton, icon: "circle", scripts: {onClick:'{ return setActiveTool("circle", _readOnly_);}'} },
// { title: "Square", toolTip: "Square (Ctrl+Shift+S)", btnType: ButtonType.ToggleButton, icon: "square", click: 'setActiveTool("square")' },
- { title: "Line", toolTip: "Line (Ctrl+Shift+L)", btnType: ButtonType.ToggleButton, icon: "minus", scripts: {onClick: '{ return setActiveTool("line", _readOnly_);}' }},
- { title: "Fill", toolTip: "Fill color", btnType: ButtonType.ColorButton, icon: "fill-drip",ignoreClick: true, scripts: {script: "{ return setFillColor(value, _readOnly_);}"} },
+ { title: "Line", toolTip: "Line (Ctrl+Shift+L)", btnType: ButtonType.ToggleButton, icon: "minus", scripts: {onClick:'{ return setActiveTool("line", _readOnly_);}' }},
+ { title: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle", scripts: {onClick:'{ return setIsInkMask(_readOnly_);}'} },
+ { title: "Fill", toolTip: "Fill color", btnType: ButtonType.ColorButton, icon: "fill-drip",ignoreClick: true, scripts: {script: '{ return setFillColor(value, _readOnly_);}'} },
{ title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberButton, ignoreClick: true, scripts: {script: '{ return setStrokeWidth(value, _readOnly_);}'}, numBtnType: NumButtonType.Slider, numBtnMin: 1},
{ title: "Color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", ignoreClick: true, scripts: {script: '{ return setStrokeColor(value, _readOnly_);}'} },
];
@@ -673,6 +676,7 @@ export class CurrentUserUtils {
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: "Num", icon: "", toolTip: "Frame Number", btnType: ButtonType.TextButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement().currentFrame.toString()'}, width: 20, scripts: {}},
{ 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_)'}},
{ title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, width: 20, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected
{ title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}},
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 eef5b9ce1..6386c87a0 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -377,14 +377,14 @@ export namespace DragManager {
}
const rect = ele.getBoundingClientRect();
const scaleX = rect.width / (ele.offsetWidth || rect.width);
- const scaleY = ele.offsetHeight ? rect.height / (ele.offsetHeight || rect.height) : scaleX;
+ const scaleY = scaleX; //ele.offsetHeight ? rect.height / (ele.offsetHeight || rect.height) : scaleX;
elesCont.left = Math.min(rect.left, elesCont.left);
elesCont.top = Math.min(rect.top, elesCont.top);
elesCont.right = Math.max(rect.right, elesCont.right);
elesCont.bottom = Math.max(rect.bottom, elesCont.bottom);
- xs.push(rect.left);
- ys.push(rect.top);
+ xs.push(rect.left + (options?.offsetX || 0));
+ ys.push(rect.top + (options?.offsetY || 0));
scaleXs.push(scaleX);
scaleYs.push(scaleY);
Object.assign(dragElement.style, {
@@ -401,9 +401,9 @@ export namespace DragManager {
transformOrigin: '0 0',
width: `${rect.width / scaleX}px`,
height: `${rect.height / scaleY}px`,
- transform: `translate(${rect.left + (options?.offsetX || 0)}px, ${rect.top + (options?.offsetY || 0)}px) scale(${scaleX}, ${scaleY})`,
+ transform: `translate(${xs[0]}px, ${ys[0]}px) scale(${scaleX}, ${scaleY})`,
});
- dragLabel.style.transform = `translate(${rect.left + (options?.offsetX || 0)}px, ${rect.top + (options?.offsetY || 0) - 20}px)`;
+ dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
if (docsToDrag.length) {
const pdfBox = dragElement.getElementsByTagName('canvas');
@@ -542,8 +542,8 @@ export namespace DragManager {
const moveVec = { x: x - lastPt.x, y: y - lastPt.y };
lastPt = { x, y };
- dragLabel.style.transform = `translate(${xs[0] + moveVec.x + (options?.offsetX || 0)}px, ${ys[0] + moveVec.y + (options?.offsetY || 0) - 20}px)`;
- dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x) + (options?.offsetX || 0)}px, ${(ys[i] += moveVec.y) + (options?.offsetY || 0)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`));
+ dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`));
+ dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
};
const upHandler = (e: PointerEvent) => {
clearTimeout(startWindowDragTimer);
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 289c5bc51..4af51b9a0 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -1,12 +1,12 @@
-import React = require("react");
-import { Utils } from "../../Utils";
-import "./InteractionUtils.scss";
+import React = require('react');
+import { Utils } from '../../Utils';
+import './InteractionUtils.scss';
export namespace InteractionUtils {
- export const MOUSETYPE = "mouse";
- export const TOUCHTYPE = "touch";
- export const PENTYPE = "pen";
- export const ERASERTYPE = "eraser";
+ export const MOUSETYPE = 'mouse';
+ export const TOUCHTYPE = 'touch';
+ export const PENTYPE = 'pen';
+ export const ERASERTYPE = 'eraser';
const POINTER_PEN_BUTTON = -1;
const REACT_POINTER_PEN_BUTTON = 0;
@@ -19,24 +19,23 @@ export namespace InteractionUtils {
readonly touches: T extends React.TouchEvent ? React.Touch[] : Touch[],
readonly changedTouches: T extends React.TouchEvent ? React.Touch[] : Touch[],
readonly touchEvent: T extends React.TouchEvent ? React.TouchEvent : TouchEvent
- ) { }
+ ) {}
}
- export interface MultiTouchEventDisposer { (): void; }
+ export interface MultiTouchEventDisposer {
+ (): void;
+ }
/**
*
* @param element - element to turn into a touch target
* @param startFunc - event handler, typically Touchable.onTouchStart (classes that inherit touchable can pass in this.onTouchStart)
*/
- export function MakeMultiTouchTarget(
- element: HTMLElement,
- startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void
- ): MultiTouchEventDisposer {
+ export function MakeMultiTouchTarget(element: HTMLElement, startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void): MultiTouchEventDisposer {
const onMultiTouchStartHandler = (e: Event) => startFunc(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail);
// const onMultiTouchMoveHandler = moveFunc ? (e: Event) => moveFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
// const onMultiTouchEndHandler = endFunc ? (e: Event) => endFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
- element.addEventListener("dashOnTouchStart", onMultiTouchStartHandler);
+ element.addEventListener('dashOnTouchStart', onMultiTouchStartHandler);
// if (onMultiTouchMoveHandler) {
// element.addEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
// }
@@ -44,7 +43,7 @@ export namespace InteractionUtils {
// element.addEventListener("dashOnTouchEnd", onMultiTouchEndHandler);
// }
return () => {
- element.removeEventListener("dashOnTouchStart", onMultiTouchStartHandler);
+ element.removeEventListener('dashOnTouchStart', onMultiTouchStartHandler);
// if (onMultiTouchMoveHandler) {
// element.removeEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
// }
@@ -59,14 +58,11 @@ export namespace InteractionUtils {
* @param element - element to add events to
* @param func - function to add to the event
*/
- export function MakeHoldTouchTarget(
- element: HTMLElement,
- func: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void
- ): MultiTouchEventDisposer {
+ export function MakeHoldTouchTarget(element: HTMLElement, func: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void): MultiTouchEventDisposer {
const handler = (e: Event) => func(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail);
- element.addEventListener("dashOnTouchHoldStart", handler);
+ element.addEventListener('dashOnTouchHoldStart', handler);
return () => {
- element.removeEventListener("dashOnTouchHoldStart", handler);
+ element.removeEventListener('dashOnTouchHoldStart', handler);
};
}
@@ -89,71 +85,108 @@ export namespace InteractionUtils {
return myTouches;
}
- export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number,
- color: string, width: number, strokeWidth: number, lineJoin: string, lineCap: string, bezier: string, fill: string, arrowStart: string, arrowEnd: string,
- markerScale: number, dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean,
- downHdlr?: ((e: React.PointerEvent) => void)) {
+ export function CreatePolyline(
+ points: { X: number; Y: number }[],
+ left: number,
+ top: number,
+ color: string,
+ width: number,
+ strokeWidth: number,
+ lineJoin: string,
+ lineCap: string,
+ bezier: string,
+ fill: string,
+ arrowStart: string,
+ arrowEnd: string,
+ markerScale: number,
+ dash: string | undefined,
+ scalex: number,
+ scaley: number,
+ shape: string,
+ pevents: string,
+ opacity: number,
+ nodefs: boolean,
+ downHdlr?: (e: React.PointerEvent) => void
+ ) {
const pts = shape ? makePolygon(shape, points) : points;
if (isNaN(scalex)) scalex = 1;
if (isNaN(scaley)) scaley = 1;
- const toScr = (p: { X: number, Y: number }) => ` ${!p ? 0 : (p.X - left - width / 2) * scalex + width / 2}, ${!p ? 0 : (p.Y - top - width / 2) * scaley + width / 2} `;
- const strpts = bezier ?
- pts.reduce((acc: string, pt, i) => acc + (i % 4 !== 0 ? "" : (i === 0 ? "M" + toScr(pt) : "") + "C" + toScr(pts[i + 1]) + toScr(pts[i + 2]) + toScr(pts[i + 3])), "") :
- pts.reduce((acc: string, pt) => acc + `${toScr(pt)} `, "");
+ const toScr = (p: { X: number; Y: number }) => ` ${!p ? 0 : (p.X - left - width / 2) * scalex + width / 2}, ${!p ? 0 : (p.Y - top - width / 2) * scaley + width / 2} `;
+ const strpts = bezier
+ ? pts.reduce((acc: string, pt, i) => acc + (i % 4 !== 0 ? '' : (i === 0 ? 'M' + toScr(pt) : '') + 'C' + toScr(pts[i + 1]) + toScr(pts[i + 2]) + toScr(pts[i + 3])), '')
+ : pts.reduce((acc: string, pt) => acc + `${toScr(pt)} `, '');
const dashArray = dash && Number(dash) ? String(Number(width) * Number(dash)) : undefined;
const defGuid = Utils.GenerateGuid();
- const Tag = (bezier ? "path" : "polyline") as keyof JSX.IntrinsicElements;
+ const Tag = (bezier ? 'path' : 'polyline') as keyof JSX.IntrinsicElements;
const markerStrokeWidth = strokeWidth / 2;
- const arrowWidthFactor = 3 * (markerScale || 0.5);// used to be 1.5
+ const arrowWidthFactor = 3 * (markerScale || 0.5); // used to be 1.5
const arrowLengthFactor = 5 * (markerScale || 0.5);
const arrowNotchFactor = 2 * (markerScale || 0.5);
- return (<svg fill={color} onPointerDown={downHdlr}> {/* setting the svg fill sets the arrowStart fill */}
- {nodefs ? (null) : <defs>
- {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) :
- <marker id={`dot${defGuid}`} orient="auto" markerUnits="userSpaceOnUse" refX={0} refY="0" overflow="visible">
- <circle r={strokeWidth * arrowWidthFactor} fill="context-stroke" />
- </marker>}
- {arrowStart !== "arrow" ? (null) :
- <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} refY={0} markerWidth="10" markerHeight="7">
- <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={markerStrokeWidth * 2 / 3}
- points={`${arrowLengthFactor * markerStrokeWidth} ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} 0, ${arrowLengthFactor * markerStrokeWidth} ${markerStrokeWidth * arrowWidthFactor}, 0 0`} />
- </marker>}
- {arrowEnd !== "arrow" ? (null) :
- <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * arrowNotchFactor} refY={0} markerWidth="10" markerHeight="7">
- <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={markerStrokeWidth * 2 / 3}
- points={`0 ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * arrowNotchFactor} 0, 0 ${markerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * markerStrokeWidth} 0`} />
- </marker>}
- </defs>}
-
- <Tag
- d={bezier ? strpts : undefined}
- points={bezier ? undefined : strpts}
- style={{
- // filter: drawHalo ? "url(#inkSelectionHalo)" : undefined,
- fill: fill && fill !== "transparent" ? fill : "none",
- opacity: 1.0,
- // opacity: strokeWidth !== width ? 0.5 : undefined,
- pointerEvents: pevents as any,
- stroke: color ?? "rgb(0, 0, 0)",
- strokeWidth: strokeWidth,
- strokeLinecap: lineCap as any,
- strokeDasharray: dashArray
- }}
- markerStart={`url(#${arrowStart === "dot" ? arrowStart + defGuid : arrowStart + "Start" + defGuid})`}
- markerEnd={`url(#${arrowEnd === "dot" ? arrowEnd + defGuid : arrowEnd + "End" + defGuid})`}
- />
-
- </svg>);
+ return (
+ <svg fill={color} style={{ transition: 'inherit' }} onPointerDown={downHdlr}>
+ {' '}
+ {/* setting the svg fill sets the arrowStart fill */}
+ {nodefs ? null : (
+ <defs>
+ {arrowStart !== 'dot' && arrowEnd !== 'dot' ? null : (
+ <marker id={`dot${defGuid}`} orient="auto" markerUnits="userSpaceOnUse" refX={0} refY="0" overflow="visible">
+ <circle r={strokeWidth * arrowWidthFactor} fill="context-stroke" />
+ </marker>
+ )}
+ {arrowStart !== 'arrow' ? null : (
+ <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} refY={0} markerWidth="10" markerHeight="7">
+ <polygon
+ style={{ stroke: color }}
+ strokeLinejoin={lineJoin as any}
+ strokeWidth={(markerStrokeWidth * 2) / 3}
+ points={`${arrowLengthFactor * markerStrokeWidth} ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} 0, ${arrowLengthFactor * markerStrokeWidth} ${
+ markerStrokeWidth * arrowWidthFactor
+ }, 0 0`}
+ />
+ </marker>
+ )}
+ {arrowEnd !== 'arrow' ? null : (
+ <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * arrowNotchFactor} refY={0} markerWidth="10" markerHeight="7">
+ <polygon
+ style={{ stroke: color }}
+ strokeLinejoin={lineJoin as any}
+ strokeWidth={(markerStrokeWidth * 2) / 3}
+ points={`0 ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * arrowNotchFactor} 0, 0 ${markerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * markerStrokeWidth} 0`}
+ />
+ </marker>
+ )}
+ </defs>
+ )}
+ <Tag
+ d={bezier ? strpts : undefined}
+ points={bezier ? undefined : strpts}
+ style={{
+ // filter: drawHalo ? "url(#inkSelectionHalo)" : undefined,
+ fill: fill && fill !== 'transparent' ? fill : 'none',
+ opacity: 1.0,
+ // opacity: strokeWidth !== width ? 0.5 : undefined,
+ pointerEvents: pevents as any,
+ stroke: color ?? 'rgb(0, 0, 0)',
+ strokeWidth: strokeWidth,
+ strokeLinecap: lineCap as any,
+ strokeDasharray: dashArray,
+ transition: 'inherit',
+ }}
+ markerStart={`url(#${arrowStart === 'dot' ? arrowStart + defGuid : arrowStart + 'Start' + defGuid})`}
+ markerEnd={`url(#${arrowEnd === 'dot' ? arrowEnd + defGuid : arrowEnd + 'End' + defGuid})`}
+ />
+ </svg>
+ );
}
- export function makePolygon(shape: string, points: { X: number, Y: number }[]) {
+ export function makePolygon(shape: string, points: { X: number; Y: number }[]) {
if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y + 1 === points[0].Y) {
//pointer is up (first and last points are the same)
- if (shape === "arrow" || shape === "line" || shape === "circle") {
+ if (shape === 'arrow' || shape === 'line' || shape === 'circle') {
//if arrow or line, the two end points should be the starting and the ending point
var left = points[0].X;
var top = points[0].Y;
@@ -175,7 +208,7 @@ export namespace InteractionUtils {
left = points[0].X;
bottom = points[points.length - 1].Y;
top = points[0].Y;
- if (shape !== "arrow" && shape !== "line" && shape !== "circle") {
+ if (shape !== 'arrow' && shape !== 'line' && shape !== 'circle') {
//switch left/right and top/bottom if needed
if (left > right) {
const temp = right;
@@ -191,14 +224,13 @@ export namespace InteractionUtils {
}
points = [];
switch (shape) {
- case "rectangle":
+ case 'rectangle':
points.push({ X: left, Y: top });
points.push({ X: right, Y: top });
points.push({ X: right, Y: bottom });
points.push({ X: left, Y: bottom });
points.push({ X: left, Y: top });
- return points;
- case "triangle":
+ case 'triangle':
// points.push({ X: left, Y: bottom });
// points.push({ X: right, Y: bottom });
// points.push({ X: (right + left) / 2, Y: top });
@@ -219,62 +251,39 @@ export namespace InteractionUtils {
points.push({ X: left, Y: bottom });
points.push({ X: left, Y: bottom });
-
-
- return points;
- case "circle":
+ case 'circle':
const centerX = (Math.max(left, right) + Math.min(left, right)) / 2;
const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2;
const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
if (centerX - Math.min(left, right) < centerY - Math.min(top, bottom)) {
for (var y = Math.min(top, bottom); y < Math.max(top, bottom); y++) {
- const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX;
+ const x = Math.sqrt(Math.pow(radius, 2) - Math.pow(y - centerY, 2)) + centerX;
points.push({ X: x, Y: y });
}
for (var y = Math.max(top, bottom); y > Math.min(top, bottom); y--) {
- const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX;
+ const x = Math.sqrt(Math.pow(radius, 2) - Math.pow(y - centerY, 2)) + centerX;
const newX = centerX - (x - centerX);
points.push({ X: newX, Y: y });
}
- points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(top, bottom) - centerY), 2))) + centerX, Y: Math.min(top, bottom) });
+ points.push({ X: Math.sqrt(Math.pow(radius, 2) - Math.pow(Math.min(top, bottom) - centerY, 2)) + centerX, Y: Math.min(top, bottom) });
} else {
for (var x = Math.min(left, right); x < Math.max(left, right); x++) {
- const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY;
+ const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY;
points.push({ X: x, Y: y });
}
for (var x = Math.max(left, right); x > Math.min(left, right); x--) {
- const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY;
+ const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY;
const newY = centerY - (y - centerY);
points.push({ X: x, Y: newY });
}
- points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(left, right) - centerX), 2))) + centerY });
+ points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - Math.pow(Math.min(left, right) - centerX, 2)) + centerY });
}
- return points;
- // case "arrow":
- // const x1 = left;
- // const y1 = top;
- // const x2 = right;
- // const y2 = bottom;
- // const L1 = Math.sqrt(Math.pow(Math.abs(x1 - x2), 2) + (Math.pow(Math.abs(y1 - y2), 2)));
- // const L2 = L1 / 5;
- // const angle = 0.785398;
- // const x3 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) + (y1 - y2) * Math.sin(angle));
- // const y3 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) - (x1 - x2) * Math.sin(angle));
- // const x4 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle));
- // const y4 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) + (x1 - x2) * Math.sin(angle));
- // points.push({ X: x1, Y: y1 });
- // points.push({ X: x2, Y: y2 });
- // points.push({ X: x3, Y: y3 });
- // points.push({ X: x4, Y: y4 });
- // points.push({ X: x2, Y: y2 });
- // return points;
- case "line":
+ case 'line':
points.push({ X: left, Y: top });
points.push({ X: right, Y: bottom });
return points;
- default:
- return points;
}
+ return points;
}
/**
* Returns whether or not the pointer event passed in is of the type passed in
@@ -284,11 +293,14 @@ export namespace InteractionUtils {
export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean {
switch (type) {
// pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2
- case PENTYPE: return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0);
- case ERASERTYPE: return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON);
+ case PENTYPE:
+ return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0);
+ case ERASERTYPE:
+ return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON);
case TOUCHTYPE:
return e.pointerType === TOUCHTYPE;
- default: return e.pointerType === type;
+ default:
+ return e.pointerType === type;
}
}
@@ -305,7 +317,7 @@ export namespace InteractionUtils {
* Returns the centroid of an n-arbitrary long list of points (takes the average the x and y components of each point)
* @param pts - n-arbitrary long list of points
*/
- export function CenterPoint(pts: React.Touch[]): { X: number, Y: number } {
+ export function CenterPoint(pts: React.Touch[]): { X: number; Y: number } {
const centerX = pts.map(pt => pt.clientX).reduce((a, b) => a + b, 0) / pts.length;
const centerY = pts.map(pt => pt.clientY).reduce((a, b) => a + b, 0) / pts.length;
return { X: centerX, Y: centerY };
@@ -324,9 +336,9 @@ export namespace InteractionUtils {
const newDist = TwoPointEuclidist(pt1, pt2);
/** if they have the same sign, then we are either pinching in or out.
- * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch)
- * so that it can still pan without freaking out
- */
+ * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch)
+ * so that it can still pan without freaking out
+ */
if (Math.sign(oldDist) === Math.sign(newDist) && Math.abs(oldDist - newDist) > threshold) {
return Math.sign(oldDist - newDist);
}
@@ -372,8 +384,6 @@ export namespace InteractionUtils {
// These might not be very useful anymore, but I'll leave them here for now -syip2
{
-
-
/**
* Returns the type of Touch Interaction from a list of points.
* Also returns any data that is associated with a Touch Interaction
diff --git a/src/client/util/ReportManager.scss b/src/client/util/ReportManager.scss
new file mode 100644
index 000000000..5a2f2fcad
--- /dev/null
+++ b/src/client/util/ReportManager.scss
@@ -0,0 +1,88 @@
+@import '../views/global/globalCssVariables';
+
+.issue-list-wrapper {
+ position: relative;
+ min-width: 250px;
+ background-color: $light-blue;
+ overflow-y: scroll;
+}
+
+.issue-list {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 5px;
+ margin: 5px;
+ border-radius: 5px;
+ border: 1px solid grey;
+ background-color: lightgoldenrodyellow;
+}
+
+// issue should pop up when the user hover over the issue
+.issue-list:hover {
+ box-shadow: 2px;
+ cursor: pointer;
+ border: 3px solid #252b33;
+}
+
+.issue-content {
+ background-color: white;
+ padding: 10px;
+ flex: 1 1 auto;
+ overflow-y: scroll;
+}
+
+.issue-title {
+ font-size: 20px;
+ font-weight: 600;
+ color: black;
+}
+
+.issue-body {
+ padding: 0 10px;
+ width: 100%;
+ text-align: left;
+}
+
+.issue-body > * {
+ margin-top: 5px;
+}
+
+.issue-body img,
+.issue-body video {
+ display: block;
+ max-width: 100%;
+}
+
+.report-issue-fab {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+.loading-center {
+ margin: auto 0;
+}
+
+.settings-content label {
+ margin-top: 10px;
+}
+
+.report-disclaimer {
+ font-size: 8px;
+ color: grey;
+ padding-right: 50px;
+ font-style: italic;
+ text-align: left;
+}
+
+.flex-select {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+}
diff --git a/src/client/util/ReportManager.tsx b/src/client/util/ReportManager.tsx
new file mode 100644
index 000000000..55c5ca87f
--- /dev/null
+++ b/src/client/util/ReportManager.tsx
@@ -0,0 +1,282 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { ColorState, SketchPicker } from 'react-color';
+import { Doc } from '../../fields/Doc';
+import { Id } from '../../fields/FieldSymbols';
+import { BoolCast, Cast, StrCast } from '../../fields/Types';
+import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils';
+import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
+import { DocServer } from '../DocServer';
+import { Networking } from '../Network';
+import { MainViewModal } from '../views/MainViewModal';
+import { FontIconBox } from '../views/nodes/button/FontIconBox';
+import { DragManager } from './DragManager';
+import { GroupManager } from './GroupManager';
+import './SettingsManager.scss';
+import './ReportManager.scss';
+import { undoBatch } from './UndoManager';
+import { Octokit } from "@octokit/core";
+import { CheckBox } from '../views/search/CheckBox';
+import ReactLoading from 'react-loading';
+import ReactMarkdown from 'react-markdown';
+import rehypeRaw from 'rehype-raw';
+import remarkGfm from 'remark-gfm';
+const higflyout = require('@hig/flyout');
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
+
+@observer
+export class ReportManager extends React.Component<{}> {
+ public static Instance: ReportManager;
+ @observable private isOpen = false;
+
+ private octokit: Octokit;
+
+ @observable public issues: any[] = [];
+ @action setIssues = action((issues: any[]) => { this.issues = issues; });
+
+ // undefined is the default - null is if the user is making an issue
+ @observable public selectedIssue: any = undefined;
+ @action setSelectedIssue = action((issue: any) => { this.selectedIssue = issue; });
+
+ // only get the open issues
+ @observable public shownIssues = this.issues.filter(issue => issue.state === 'open');
+
+ public updateIssueSearch = action((query: string = '') => {
+ if (query === '') {
+ this.shownIssues = this.issues.filter(issue => issue.state === 'open');
+ return;
+ }
+ this.shownIssues = this.issues.filter(issue => issue.title.toLowerCase().includes(query.toLowerCase()));
+ });
+
+ constructor(props: {}) {
+ super(props);
+ ReportManager.Instance = this;
+
+ this.octokit = new Octokit({
+ auth: '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'
+ });
+ }
+
+ public close = action(() => (this.isOpen = false));
+ public open = action(() => {
+ if (this.issues.length === 0) {
+ // load in the issues if not already loaded
+ this.getAllIssues()
+ .then(issues => {
+ this.setIssues(issues);
+ this.updateIssueSearch();
+ })
+ .catch(err => console.log(err));
+ }
+ (this.isOpen = true)
+ });
+
+ @observable private bugTitle = '';
+ @action setBugTitle = action((title: string) => { this.bugTitle = title; });
+ @observable private bugDescription = '';
+ @action setBugDescription = action((description: string) => { this.bugDescription = description; });
+ @observable private bugType = '';
+ @action setBugType = action((type: string) => { this.bugType = type; });
+ @observable private bugPriority = '';
+ @action setBugPriority = action((priortiy: string) => { this.bugPriority = priortiy; });
+
+ // private toGithub = false;
+ // will always be set to true - no alterntive option yet
+ private toGithub = true;
+
+ private formatTitle = (title: string, userEmail: string) => `${title} - ${userEmail.replace('@brown.edu', '')}`;
+
+ public async getAllIssues() : Promise<any[]> {
+ const res = await this.octokit.request('GET /repos/{owner}/{repo}/issues', {
+ owner: 'brown-dash',
+ repo: 'Dash-Web',
+ });
+
+ // 200 status means success
+ if (res.status === 200) {
+ return res.data;
+ } else {
+ throw new Error('Error getting issues');
+ }
+ }
+
+ public async reportIssue() {
+ if (this.bugTitle === '' || this.bugDescription === ''
+ || this.bugType === '' || this.bugPriority === '') {
+ alert('Please fill out all required fields to report an issue.');
+ return;
+ }
+
+
+ if (this.toGithub) {
+
+ const req = await this.octokit.request('POST /repos/{owner}/{repo}/issues', {
+ owner: 'brown-dash',
+ repo: 'Dash-Web',
+ title: this.formatTitle(this.bugTitle, Doc.CurrentUserEmail),
+ body: `${this.bugDescription} \n\nfiles:\n${(this.fileLinks ?? []).join('\n')}`,
+ labels: [
+ 'from-dash-app',
+ this.bugType,
+ this.bugPriority
+ ]
+ });
+
+ // 201 status means success
+ if (req.status !== 201) {
+ alert('Error creating issue on github.');
+ // on error, don't close the modal
+ return;
+ }
+ }
+ else {
+ // if not going to github issues, not sure what to do yet...
+ }
+
+ // if we're down here, then we're good to go. reset the fields.
+ this.setBugTitle('');
+ this.setBugDescription('');
+ this.toGithub = false;
+ this.setFileLinks([]);
+ this.setBugType('');
+ this.setBugPriority('');
+ this.close();
+ }
+
+ @observable public fileLinks: any = [];
+ @action setFileLinks = action((links: any) => { this.fileLinks = links; });
+
+ private getServerPath = (link: any) => { return link.result.accessPaths.agnostic.server }
+
+ private uploadFiles = (input: any) => {
+ // keep null while uploading
+ this.setFileLinks(null);
+ // upload the files to the server
+ if (input.files && input.files.length !== 0) {
+ const fileArray: File[] = Array.from(input.files);
+ (Networking.UploadFilesToServer(fileArray)).then(links => {
+ console.log('finshed uploading', links.map(this.getServerPath));
+ this.setFileLinks((links ?? []).map(this.getServerPath));
+ })
+ }
+
+ }
+
+
+ private renderIssue = (issue: any) => {
+
+ const isReportingIssue = issue === null;
+
+ return isReportingIssue ?
+ // report issue
+ (<div className="settings-content">
+ <h3 style={{ 'textDecoration': 'underline'}}>Report an Issue</h3>
+ <label>Please leave a title for the bug.</label><br />
+ <input type="text" placeholder='title' onChange={(e) => this.bugTitle = e.target.value} required/>
+ <br />
+ <label>Please leave a description for the bug and how it can be recreated.</label>
+ <textarea placeholder='description' onChange={(e) => this.bugDescription = e.target.value} required/>
+ <br />
+ {/* {<label>Send to github issues? </label>
+ <input type="checkbox" onChange={(e) => this.toGithub = e.target.checked} />
+ <br /> } */}
+
+ <label>Please label the issue</label>
+ <div className='flex-select'>
+ <select name="bugType">
+ <option value="" disabled selected>Type</option>
+ <option value="bug">Bug</option>
+ <option value="cosmetic">Poor Design or Cosmetic</option>
+ <option value="documentation">Poor Documentation</option>
+ </select>
+
+ <select name="bigPriority">
+ <option value="" disabled selected>Priority</option>
+ <option value="priority-low">Low</option>
+ <option value="priority-medium">Medium</option>
+ <option value="priority-high">High</option>
+ </select>
+ </div>
+
+
+ <div>
+ <label>Upload media that shows the bug (optional)</label>
+ <input type="file" name="file" multiple accept='audio/*, video/*' onChange={e => this.uploadFiles(e.target)}/>
+ </div>
+ <br />
+
+ <button onClick={() => this.reportIssue()} disabled={this.fileLinks === null} style={{ backgroundColor: this.fileLinks === null ? 'grey' : '' }}>{this.fileLinks === null ? 'Uploading...' : 'Submit'}</button>
+ </div>)
+ :
+ // view issue
+ (
+ <div className='issue-container'>
+ <h5 style={{'textAlign': "left"}}><a href={issue.html_url} target="_blank">Issue #{issue.number}</a></h5>
+ <div className='issue-title'>
+ {issue.title}
+ </div>
+ <ReactMarkdown children={issue.body} className='issue-body' linkTarget={"_blank"} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} />
+ </div>
+ );
+ }
+
+ private showReportIssueScreen = () => {
+ this.setSelectedIssue(null);
+ }
+
+ private closeReportIssueScreen = () => {
+ this.setSelectedIssue(undefined);
+ }
+
+ private get reportInterface() {
+
+ const isReportingIssue = this.selectedIssue === null;
+
+ return (
+ <div className="settings-interface">
+ <div className='issue-list-wrapper'>
+ <h3>Current Issues</h3>
+ <input type="text" placeholder='search issues' onChange={(e => this.updateIssueSearch(e.target.value))}></input><br />
+ {this.issues.length === 0 ? <ReactLoading className='loading-center'/> : this.shownIssues.map(issue => <div className='issue-list' key={issue.number} onClick={() => this.setSelectedIssue(issue)}>{issue.title}</div>)}
+
+ {/* <div className="settings-user">
+ <button onClick={() => this.getAllIssues().then(issues => this.issues = issues)}>Poll Issues</button>
+ </div> */}
+ </div>
+
+ <div className="close-button" onClick={this.close}>
+ <FontAwesomeIcon icon={'times'} color="black" size={'lg'} />
+ </div>
+
+ <div className="issue-content" style={{'paddingTop' : this.selectedIssue === undefined ? '50px' : 'inherit'}}>
+ {this.selectedIssue === undefined ? "no issue selected" : this.renderIssue(this.selectedIssue)}
+ </div>
+
+ <div className='report-issue-fab'>
+ <span className='report-disclaimer' hidden={!isReportingIssue}>Note: issue reporting is not anonymous.</span>
+ <button
+ onClick={() => isReportingIssue ? this.closeReportIssueScreen() : this.showReportIssueScreen()}
+ >{isReportingIssue ? 'Cancel' : 'Report New Issue'}</button>
+ </div>
+
+
+ </div>
+ );
+ }
+
+ render() {
+ return (
+ <MainViewModal
+ contents={this.reportInterface}
+ isDisplayed={this.isOpen}
+ interactive={true}
+ closeOnExternalClick={this.close}
+ dialogueBoxStyle={{ width: 'auto', height: '500px', background: Cast(Doc.SharingDoc().userColor, 'string', null) }}
+ />
+ );
+ }
+}