aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.vscode/launch.json48
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts24
-rw-r--r--src/client/util/CurrentUserUtils.ts8
-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
-rw-r--r--src/client/views/DocumentButtonBar.tsx41
-rw-r--r--src/client/views/DocumentDecorations.tsx8
-rw-r--r--src/client/views/MarqueeAnnotator.tsx1
-rw-r--r--src/client/views/PropertiesButtons.tsx12
-rw-r--r--src/client/views/PropertiesView.tsx2
-rw-r--r--src/client/views/SidebarAnnos.tsx1
-rw-r--r--src/client/views/StyleProvider.tsx7
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx2
-rw-r--r--src/client/views/collections/CollectionMenu.tsx129
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.scss482
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.tsx730
-rw-r--r--src/client/views/collections/CollectionNoteTakingViewColumn.tsx325
-rw-r--r--src/client/views/collections/CollectionNoteTakingViewDivider.tsx63
-rw-r--r--src/client/views/collections/CollectionStackingView.scss32
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx55
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx337
-rw-r--r--src/client/views/collections/CollectionView.tsx3
-rw-r--r--src/client/views/collections/TabDocView.tsx27
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx5
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx5
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx1
-rw-r--r--src/client/views/linking/LinkEditor.scss65
-rw-r--r--src/client/views/linking/LinkEditor.tsx173
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx93
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx2
-rw-r--r--src/client/views/nodes/DocumentView.scss6
-rw-r--r--src/client/views/nodes/DocumentView.tsx123
-rw-r--r--src/client/views/nodes/FilterBox.tsx2
-rw-r--r--src/client/views/nodes/FunctionPlotBox.tsx76
-rw-r--r--src/client/views/nodes/LinkAnchorBox.tsx2
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx21
-rw-r--r--src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx5
-rw-r--r--src/client/views/nodes/button/FontIconBox.tsx9
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.tsx14
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx137
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx13
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx14
-rw-r--r--src/fields/Doc.ts3
-rw-r--r--src/fields/List.ts72
-rw-r--r--src/fields/documentSchemas.ts182
-rw-r--r--src/fields/util.ts12
-rw-r--r--webpack.config.js162
52 files changed, 3000 insertions, 1065 deletions
diff --git a/.gitignore b/.gitignore
index 7a3c82ee6..7d3a4d214 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,4 @@ src/server/session_manager/logs/**/*.log
debug.log
.vscodeignore
Dockerfile
+.vscode/launch.json
diff --git a/.vscode/launch.json b/.vscode/launch.json
index a7c30ca1e..ce9f50f67 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -12,9 +12,7 @@
"breakOnLoad": true,
"url": "http://localhost:1050/login",
"webRoot": "${workspaceFolder}",
- "runtimeArgs": [
- "--experimental-modules"
- ]
+ "runtimeArgs": ["--experimental-modules"]
},
{
"type": "chrome",
@@ -25,18 +23,21 @@
"url": "http://localhost:1050/login",
"webRoot": "${workspaceFolder}",
"runtimeExecutable": "/usr/bin/chromium",
- "runtimeArgs": [
- "--experimental-modules"
- ]
+ "runtimeArgs": ["--experimental-modules"]
},
{
"type": "firefox",
"request": "launch",
"name": "Launch Firefox against localhost",
- //"sourceMaps": "client",
"reAttach": true,
"url": "http://localhost:1050/login",
"webRoot": "${workspaceFolder}",
+ "pathMappings": [
+ {
+ "url": "webpack://dash/src",
+ "path": "${workspaceFolder}/src"
+ }
+ ]
},
{
"type": "chrome",
@@ -45,8 +46,7 @@
"sourceMaps": true,
"breakOnLoad": true,
"url": "https://browndash.com/login",
- //"url": "http://dash-web.eastus2.cloudapp.azure.com:1050/login",
- "webRoot": "${workspaceFolder}",
+ "webRoot": "${workspaceFolder}"
},
{
"type": "node",
@@ -62,13 +62,7 @@
"request": "launch",
"name": "Current TS File",
"runtimeExecutable": "npx",
- "runtimeArgs": [
- "ts-node-dev",
- "--nolazy",
- "--inspect",
- "--",
- "${relativeFile}"
- ],
+ "runtimeArgs": ["ts-node-dev", "--nolazy", "--inspect", "--", "${relativeFile}"],
"port": 9229
},
{
@@ -76,14 +70,7 @@
"request": "launch",
"name": "Mocha Tests",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
- "args": [
- "-r",
- "ts-node/register",
- "--timeout",
- "999999",
- "--colors",
- "${workspaceFolder}/test/**/*.ts"
- ],
+ "args": ["-r", "ts-node/register", "--timeout", "999999", "--colors", "${workspaceFolder}/test/**/*.ts"],
"console": "integratedTerminal",
"internalConsoleOptions": "openOnSessionStart",
"protocol": "inspector"
@@ -93,17 +80,10 @@
"request": "launch",
"name": "Mocha Current File",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
- "args": [
- "-r",
- "ts-node/register",
- "--timeout",
- "999999",
- "--colors",
- "${file}"
- ],
+ "args": ["-r", "ts-node/register", "--timeout", "999999", "--colors", "${file}"],
"console": "integratedTerminal",
"internalConsoleOptions": "openOnSessionStart",
"protocol": "inspector"
- },
+ }
]
-} \ No newline at end of file
+}
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 4e98a90a3..9dfadf778 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -63,4 +63,5 @@ export enum CollectionViewType {
Grid = 'grid',
Pile = 'pileup',
StackedTimeline = 'stacked timeline',
+ NoteTaking = "notetaking"
}
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 64d26e425..c7ea04839 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1035,6 +1035,17 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Stacking }, id, undefined, protoId);
}
+ export function NoteTakingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
+ return InstanceFromProto(
+ Prototypes.get(DocumentType.COL),
+ new List(documents),
+ { columnHeaders: new List<SchemaHeaderField>([new SchemaHeaderField('Untitled')]), ...options, _viewType: CollectionViewType.NoteTaking },
+ id,
+ undefined,
+ protoId
+ );
+ }
+
export function MulticolumnDocument(documents: Array<Doc>, options: DocumentOptions) {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Multicolumn });
}
@@ -1276,10 +1287,11 @@ export namespace DocUtils {
export let ActiveRecordings: { props: FieldViewProps; getAnchor: () => Doc }[] = [];
- export function MakeLinkToActiveAudio(getSourceDoc: () => Doc, broadcastEvent = true) {
+ export function MakeLinkToActiveAudio(getSourceDoc: () => Doc | undefined, broadcastEvent = true) {
broadcastEvent && runInAction(() => (DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1));
return DocUtils.ActiveRecordings.map(audio => {
- const link = DocUtils.MakeLink({ doc: getSourceDoc() }, { doc: audio.getAnchor() || audio.props.Document }, 'recording annotation:linked recording', 'recording timeline');
+ const sourceDoc = getSourceDoc();
+ const link = sourceDoc && DocUtils.MakeLink({ doc: sourceDoc }, { doc: audio.getAnchor() || audio.props.Document }, 'recording annotation:linked recording', 'recording timeline');
link && (link.followLinkLocation = 'add:right');
return link;
});
@@ -1485,7 +1497,7 @@ export namespace DocUtils {
return ctor ? ctor(path, options) : undefined;
}
- export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false): void {
+ export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false, pivotField?: string, pivotValue?: string): void {
!simpleMenu &&
ContextMenu.Instance.addItem({
description: 'Quick Notes',
@@ -1501,6 +1513,9 @@ export namespace DocUtils {
});
textDoc.layoutKey = 'layout_' + note.title;
textDoc[textDoc.layoutKey] = note;
+ if (pivotField) {
+ textDoc[pivotField] = pivotValue;
+ }
docTextAdder(textDoc);
}),
icon: StrCast(note.icon) as IconProp,
@@ -1521,6 +1536,9 @@ export namespace DocUtils {
newDoc.y = y;
EquationBox.SelectOnLoad = newDoc[Id];
if (newDoc.type === DocumentType.RTF) FormattedTextBox.SelectOnLoad = newDoc[Id];
+ if (pivotField) {
+ newDoc[pivotField] = pivotValue;
+ }
docAdder?.(newDoc);
}
}),
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index f55ed6e72..d19874720 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -107,7 +107,7 @@ export class CurrentUserUtils {
const reqdClickList = reqdTempOpts.map(opts => {
const allOpts = {...reqdClickOpts, ...opts.opts};
const clickDoc = tempClicks ? DocListCast(tempClicks.data).find(doc => doc.title === opts.opts.title): undefined;
- return DocUtils.AssignOpts(clickDoc, allOpts) ?? Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script,allOpts));
+ return DocUtils.AssignOpts(clickDoc, allOpts) ?? Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script, allOpts),allOpts);
});
const reqdOpts:DocumentOptions = { title: "child click editors", _height:75, system: true};
@@ -135,7 +135,6 @@ export class CurrentUserUtils {
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]);
@@ -255,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, }},
@@ -280,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, },
@@ -632,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_);}'}},
];
}
@@ -669,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_)'}},
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
+}
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 1d4056759..1f8550ad6 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -6,7 +6,7 @@ import { observer } from 'mobx-react';
import { Doc } from '../../fields/Doc';
import { RichTextField } from '../../fields/RichTextField';
import { Cast, NumCast } from '../../fields/Types';
-import { emptyFunction, setupMoveUpEvents, simulateMouseClick } from '../../Utils';
+import { emptyFunction, returnFalse, setupMoveUpEvents, simulateMouseClick } from '../../Utils';
import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
import { Docs } from '../documents/Documents';
@@ -21,7 +21,7 @@ import './DocumentButtonBar.scss';
import { Colors } from './global/globalEnums';
import { MetadataEntryMenu } from './MetadataEntryMenu';
import { DocumentLinksButton } from './nodes/DocumentLinksButton';
-import { DocumentView } from './nodes/DocumentView';
+import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
import { DashFieldView } from './nodes/formattedText/DashFieldView';
import { GoogleRef } from './nodes/formattedText/FormattedTextBox';
import { TemplateMenu } from './TemplateMenu';
@@ -274,12 +274,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
get menuButton() {
const targetDoc = this.view0?.props.Document;
return !targetDoc ? null : (
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{`Open Context Menu`}</div>
- </>
- }>
+ <Tooltip title={<div className="dash-tooltip">{`Open Context Menu`}</div>}>
<div className="documentButtonBar-icon" style={{ color: 'white', cursor: 'pointer' }} onClick={e => this.openContextMenu(e)}>
<FontAwesomeIcon className="documentdecorations-icon" icon="bars" />
</div>
@@ -334,6 +329,35 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
</Tooltip>
);
}
+
+ @observable _isRecording = false;
+ @computed
+ get recordButton() {
+ const targetDoc = this.view0?.props.Document;
+ return !targetDoc ? null : (
+ <Tooltip title={<div className="dash-tooltip">{'Click to record 5 second annotation'}</div>}>
+ <div
+ className="documentButtonBar-icon"
+ style={{ backgroundColor: this._isRecording ? 'red' : targetDoc.isLinkButton ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: targetDoc.isLinkButton ? Colors.BLACK : Colors.WHITE }}
+ onClick={undoBatch(
+ action(e => {
+ this._isRecording = true;
+ this.props.views().map(
+ view =>
+ view &&
+ DocumentViewInternal.recordAudioAnnotation(
+ view.dataDoc,
+ view.LayoutFieldKey,
+ action(() => (this._isRecording = false))
+ )
+ );
+ })
+ )}>
+ <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="microphone" />
+ </div>
+ </Tooltip>
+ );
+ }
@observable _aliasDown = false;
onTemplateButton = action((e: React.PointerEvent): void => {
this._tooltipOpen = false;
@@ -411,6 +435,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
<DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} />
</div>
) : null}
+ <div className="documentButtonBar-button">{this.recordButton}</div>
{
Doc.noviceMode ? null : <div className="documentButtonBar-button">{this.templateButton}</div>
/*<div className="documentButtonBar-button">
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index c53e61699..3544f74b4 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -201,7 +201,10 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
const finished = action((force?: boolean) => {
if ((force || --iconifyingCount === 0) && this._iconifyBatch) {
if (this._deleteAfterIconify) {
- views.forEach(iconView => iconView.props.removeDocument?.(iconView.props.Document));
+ views.forEach(iconView => {
+ Doc.setNativeView(iconView.props.Document);
+ iconView.props.removeDocument?.(iconView.props.Document);
+ });
SelectionManager.DeselectAll();
}
this._iconifyBatch?.end();
@@ -351,7 +354,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
@action
onPointerDown = (e: React.PointerEvent): void => {
- DragManager.docsBeingDragged = SelectionManager.Views().map(dv => dv.rootDoc);
+ DragManager.docsBeingDragged.push(...SelectionManager.Views().map(dv => dv.rootDoc));
this._inkDragDocs = DragManager.docsBeingDragged
.filter(doc => doc.type === DocumentType.INK)
.map(doc => {
@@ -491,7 +494,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
let actualdW = Math.max(width + dW * scale, 20);
let actualdH = Math.max(height + dH * scale, 20);
const fixedAspect = nwidth && nheight && (!doc._fitWidth || e.ctrlKey || doc.nativeHeightUnfrozen);
- console.log(fixedAspect);
if (fixedAspect) {
if ((Math.abs(dW) > Math.abs(dH) && ((!dragBottom && !dragTop) || !modifyNativeDim)) || dragRight) {
if (dragRight && modifyNativeDim) {
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index b01ee5f42..f90ad8bb5 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -65,6 +65,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
AnchorMenu.Instance.OnCrop = (e: PointerEvent) => this.props.anchorMenuCrop?.(this.highlight('rgba(173, 216, 230, 0.75)', true), true);
AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight('rgba(173, 216, 230, 0.75)', true));
+ AnchorMenu.Instance.OnAudio = unimplementedFunction;
AnchorMenu.Instance.Highlight = this.highlight;
AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations);
AnchorMenu.Instance.onMakeAnchor = AnchorMenu.Instance.GetAnchor;
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 8c4c1d00b..80c2c7705 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -68,14 +68,6 @@ export class PropertiesButtons extends React.Component<{}, {}> {
on => 'thumbtack'
);
}
- @computed get dictationButton() {
- return this.propertyToggleBtn(
- 'Dictate',
- '_showAudio',
- on => `${on ? 'Hide' : 'Show'} dictation/recording controls`,
- on => 'microphone'
- );
- }
@computed get maskButton() {
return this.propertyToggleBtn(
'Mask',
@@ -348,7 +340,8 @@ export class PropertiesButtons extends React.Component<{}, {}> {
const isInk = layoutField instanceof InkField;
const isMap = this.selectedDoc?.type === DocumentType.MAP;
const isCollection = this.selectedDoc?.type === DocumentType.COL;
- const isStacking = this.selectedDoc?._viewType === CollectionViewType.Stacking;
+ //TODO: will likely need to create separate note-taking view type here
+ const isStacking = this.selectedDoc?._viewType === CollectionViewType.Stacking || this.selectedDoc?._viewType === CollectionViewType.NoteTaking;
const isFreeForm = this.selectedDoc?._viewType === CollectionViewType.Freeform;
const isTree = this.selectedDoc?._viewType === CollectionViewType.Tree;
const isTabView = this.selectedTabView;
@@ -364,7 +357,6 @@ export class PropertiesButtons extends React.Component<{}, {}> {
{toggle(this.titleButton)}
{toggle(this.captionButton)}
{toggle(this.lockButton)}
- {toggle(this.dictationButton, { display: isNovice ? 'none' : '' })}
{toggle(this.onClickButton)}
{toggle(this.fitWidthButton)}
{toggle(this.freezeThumb)}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index aecbc4255..ef0e057dc 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1616,7 +1616,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
const type = PresBox.Instance.activeItem?.type;
const viewType = PresBox.Instance.activeItem?._viewType;
const pannable: boolean = (type === DocumentType.COL && viewType === CollectionViewType.Freeform) || type === DocumentType.IMG;
- const scrollable: boolean = type === DocumentType.PDF || type === DocumentType.WEB || type === DocumentType.RTF || viewType === CollectionViewType.Stacking;
+ const scrollable: boolean = type === DocumentType.PDF || type === DocumentType.WEB || type === DocumentType.RTF || viewType === CollectionViewType.Stacking || viewType === CollectionViewType.NoteTaking;
return (
<div className="propertiesView" style={{ width: this.props.width }}>
<div className="propertiesView-title" style={{ width: this.props.width }}>
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 1c14c7cd5..90d9c3c43 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -74,6 +74,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on');
this.addDocument(target);
this._stackRef.current?.focusDocument(target);
+ return target;
};
makeDocUnfiltered = (doc: Doc) => {
if (DocListCast(this.props.rootDoc[this.sidebarKey]).includes(doc)) {
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 334f381be..3bd4f5152 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -110,6 +110,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
return undefined;
case StyleProp.WidgetColor:
return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? 'lightgrey' : 'dimgrey';
+ // return doc = dragManager.dragDocument ? props.dragEffects.opacity??CastofOpacityonline94 : Cast())
+ // same idea for background Color
case StyleProp.Opacity:
return Cast(doc?._opacity, 'number', Cast(doc?.opacity, 'number', null));
case StyleProp.HideLinkButton:
@@ -122,6 +124,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
return (
(doc &&
!doc.presentationTargetDoc &&
+ props?.showTitle?.() !== '' &&
StrCast(
doc._showTitle,
props?.showTitle?.() ||
@@ -154,7 +157,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
case StyleProp.JitterRotation:
return comicStyle() ? random(-1, 1, NumCast(doc?.x), NumCast(doc?.y)) * ((props?.PanelWidth() || 0) > (props?.PanelHeight() || 0) ? 5 : 10) : 0;
case StyleProp.HeaderMargin:
- return ([CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._viewType as any) || (doc?.type === DocumentType.RTF && !showTitle()?.includes('noMargin')) || doc?.type === DocumentType.LABEL) &&
+ return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._viewType as any) ||
+ (doc?.type === DocumentType.RTF && !showTitle()?.includes('noMargin')) ||
+ doc?.type === DocumentType.LABEL) &&
showTitle() &&
!StrCast(doc?.showTitle).includes(':hover')
? 15
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 39e2cc17d..6d70cc0d2 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -175,7 +175,7 @@ export class CollectionDockingView extends CollectionSubView() {
@undoBatch
@action
public static AddSplit(document: Doc, pullSide: string, stack?: any, panelName?: string) {
- if (document._viewType === CollectionViewType.Docking) return DashboardView.openDashboard(document);
+ if (document?._viewType === CollectionViewType.Docking) return DashboardView.openDashboard(document);
if (!CollectionDockingView.Instance) return false;
const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document);
if (tab) {
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 2c0e44bc3..cfbcec2d6 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -317,6 +317,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
@computed get _stacking_commands() {
return Doc.noviceMode ? undefined : [this._contentCommand, this._templateCommand];
}
+ @computed get _notetaking_commands() {
+ return Doc.noviceMode ? undefined : [this._contentCommand, this._templateCommand];
+ }
@computed get _masonry_commands() {
return Doc.noviceMode ? undefined : [this._contentCommand, this._templateCommand];
}
@@ -341,6 +344,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
return this._schema_commands;
case CollectionViewType.Stacking:
return this._stacking_commands;
+ case CollectionViewType.NoteTaking:
+ return this._notetaking_commands;
case CollectionViewType.Masonry:
return this._stacking_commands;
case CollectionViewType.Time:
@@ -386,6 +391,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
return <CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={this.props.type === CollectionViewType.Invalid} />;
case CollectionViewType.Stacking:
return <CollectionStackingViewChrome key="collchrome" {...this.props} />;
+ case CollectionViewType.NoteTaking:
+ return <CollectionNoteTakingViewChrome key="collchrome" {...this.props} />;
case CollectionViewType.Schema:
return <CollectionSchemaViewChrome key="collchrome" {...this.props} />;
case CollectionViewType.Tree:
@@ -588,7 +595,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
const size: number = PresBox.Instance?._selectedArray.size;
const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined;
const activeDoc = presSelected ? PresBox.Instance?.childDocs[PresBox.Instance?.childDocs.indexOf(presSelected) + 1] : PresBox.Instance?.childDocs[PresBox.Instance?.childDocs.length - 1];
- if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB || targetDoc._viewType === CollectionViewType.Stacking) {
+ if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB || targetDoc._viewType === CollectionViewType.Stacking || targetDoc._viewType === CollectionViewType.NoteTaking) {
const scroll = targetDoc._scrollTop;
activeDoc.presPinView = true;
activeDoc.presPinViewScroll = scroll;
@@ -1146,6 +1153,126 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
}
@observer
+export class CollectionNoteTakingViewChrome extends React.Component<CollectionViewMenuProps> {
+ @observable private _currentKey: string = '';
+ @observable private suggestions: string[] = [];
+
+ get document() {
+ return this.props.docView.props.Document;
+ }
+
+ @computed private get descending() {
+ return StrCast(this.document._columnsSort) === 'descending';
+ }
+ @computed get pivotField() {
+ return StrCast(this.document._pivotField);
+ }
+
+ getKeySuggestions = async (value: string): Promise<string[]> => {
+ const val = value.toLowerCase();
+ const docs = DocListCast(this.document[this.props.fieldKey]);
+
+ if (Doc.UserDoc().noviceMode) {
+ if (docs instanceof Doc) {
+ const keys = Object.keys(docs).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('creationDate') >= 0 || key.indexOf('lastModified') >= 0 || (key[0].toUpperCase() === key[0] && key[0] !== '_'));
+ return keys.filter(key => key.toLowerCase().indexOf(val) > -1);
+ } else {
+ const keys = new Set<string>();
+ docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key)));
+ const noviceKeys = Array.from(keys).filter(
+ key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('creationDate') >= 0 || key.indexOf('lastModified') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_')
+ );
+ return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1);
+ }
+ }
+
+ if (docs instanceof Doc) {
+ return Object.keys(docs).filter(key => key.toLowerCase().indexOf(val) > -1);
+ } else {
+ const keys = new Set<string>();
+ docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key)));
+ return Array.from(keys).filter(key => key.toLowerCase().indexOf(val) > -1);
+ }
+ };
+
+ @action
+ onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => {
+ this._currentKey = newValue;
+ };
+
+ getSuggestionValue = (suggestion: string) => suggestion;
+
+ renderSuggestion = (suggestion: string) => {
+ return <p>{suggestion}</p>;
+ };
+
+ onSuggestionFetch = async ({ value }: { value: string }) => {
+ const sugg = await this.getKeySuggestions(value);
+ runInAction(() => {
+ this.suggestions = sugg;
+ });
+ };
+
+ @action
+ onSuggestionClear = () => {
+ this.suggestions = [];
+ };
+
+ @action
+ setValue = (value: string) => {
+ this.document._pivotField = value;
+ return true;
+ };
+
+ @action toggleSort = () => {
+ this.document._columnsSort = this.document._columnsSort === 'descending' ? 'ascending' : this.document._columnsSort === 'ascending' ? undefined : 'descending';
+ };
+ @action resetValue = () => {
+ this._currentKey = this.pivotField;
+ };
+
+ render() {
+ const doctype = this.props.docView.Document.type;
+ const isPres: boolean = doctype === DocumentType.PRES;
+ return isPres ? null : (
+ <div className="collectionStackingViewChrome-cont">
+ <div className="collectionStackingViewChrome-pivotField-cont">
+ <div className="collectionStackingViewChrome-pivotField-label">GROUP BY:</div>
+ <div className="collectionStackingViewChrome-sortIcon" onClick={this.toggleSort} style={{ transform: `rotate(${this.descending ? '180' : '0'}deg)` }}>
+ <FontAwesomeIcon icon="caret-up" size="2x" color="white" />
+ </div>
+ <div className="collectionStackingViewChrome-pivotField">
+ <EditableView
+ GetValue={() => this.pivotField}
+ autosuggestProps={{
+ resetValue: this.resetValue,
+ value: this._currentKey,
+ onChange: this.onKeyChange,
+ autosuggestProps: {
+ inputProps: {
+ value: this._currentKey,
+ onChange: this.onKeyChange,
+ },
+ getSuggestionValue: this.getSuggestionValue,
+ suggestions: this.suggestions,
+ alwaysRenderSuggestions: true,
+ renderSuggestion: this.renderSuggestion,
+ onSuggestionsFetchRequested: this.onSuggestionFetch,
+ onSuggestionsClearRequested: this.onSuggestionClear,
+ },
+ }}
+ oneLine
+ SetValue={this.setValue}
+ contents={this.pivotField ? this.pivotField : 'N/A'}
+ />
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+@observer
export class CollectionSchemaViewChrome extends React.Component<CollectionViewMenuProps> {
// private _textwrapAllRows: boolean = Cast(this.document.textwrappedSchemaRows, listSpec("string"), []).length > 0;
get document() {
diff --git a/src/client/views/collections/CollectionNoteTakingView.scss b/src/client/views/collections/CollectionNoteTakingView.scss
new file mode 100644
index 000000000..fe98f307e
--- /dev/null
+++ b/src/client/views/collections/CollectionNoteTakingView.scss
@@ -0,0 +1,482 @@
+@import '../global/globalCssVariables';
+
+.collectionNoteTakingView-DocumentButtons {
+ display: flex;
+ justify-content: space-between;
+ margin: auto;
+}
+
+.collectionNoteTakingView-addDocumentButton {
+ display: flex;
+ overflow: hidden;
+ margin: auto;
+ width: 100%;
+ overflow: ellipses;
+
+ .editableView-container-editing-oneLine,
+ .editableView-container-editing {
+ color: grey;
+ padding: 10px;
+ width: 100%;
+ }
+
+ .editableView-input:hover,
+ .editableView-container-editing:hover,
+ .editableView-container-editing-oneLine:hover {
+ cursor: text;
+ }
+
+ .editableView-input {
+ outline-color: black;
+ letter-spacing: 2px;
+ color: grey;
+ border: 0px;
+ padding: 12px 10px 11px 10px;
+ }
+
+ font-size: 75%;
+ letter-spacing: 2px;
+ cursor: pointer;
+
+ .editableView-input {
+ outline-color: black;
+ letter-spacing: 2px;
+ color: grey;
+ border: 0px;
+ padding: 12px 10px 11px 10px;
+ }
+}
+
+.collectionNoteTakingView {
+ display: flex;
+}
+
+// TODO:glr Turn this into a seperate class
+.documentButtonMenu {
+ position: relative;
+ height: fit-content;
+ border-bottom: $standard-border;
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
+ align-content: center;
+ padding: 5px 0 5px 0;
+
+ .documentExplanation {
+ width: 90%;
+ margin: 5px;
+ font-size: 11px;
+ background-color: $light-blue;
+ color: $medium-blue;
+ padding: 10px;
+ border-radius: 10px;
+ border: solid 2px $medium-blue;
+ }
+}
+
+.collectionNoteTakingView {
+ height: 100%;
+ width: 100%;
+ position: absolute;
+ top: 0;
+ overflow-y: auto;
+ overflow-x: hidden;
+ flex-wrap: wrap;
+ transition: top 0.5s;
+
+ > div {
+ position: relative;
+ display: block;
+ }
+
+ .collectionSchemaView-previewDoc {
+ height: 100%;
+ position: absolute;
+ }
+
+ .collectionNoteTakingView-docView-container {
+ width: 45%;
+ margin: 5% 2.5%;
+ padding-left: 2.5%;
+ height: auto;
+ }
+
+ .collectionNoteTakingView-Nodes {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ top: 0;
+ left: 0;
+ width: 100%;
+ position: absolute;
+ }
+
+ .collectionNoteTakingView-description {
+ font-size: 100%;
+ margin-bottom: 1vw;
+ padding: 10px;
+ height: 2vw;
+ width: 100%;
+ font-family: $sans-serif;
+ background: $dark-gray;
+ color: $white;
+ }
+
+ .collectionNoteTakingView-columnDragger {
+ width: 15;
+ height: 15;
+ position: absolute;
+ margin-left: -5;
+ }
+
+ // Documents in NoteTaking view
+ .collectionNoteTakingView-columnDoc {
+ display: flex;
+ // margin: auto; // Removed auto so that it is no longer center aligned - this could be something we change
+ position: relative;
+
+ &:hover {
+ .hoverButtons {
+ opacity: 1;
+ }
+ }
+
+ .hoverButtons {
+ display: flex;
+ opacity: 0;
+ position: absolute;
+ height: 100%;
+ left: -35px;
+ justify-content: center;
+ align-items: center;
+ padding: 0px 10px;
+
+ .buttonWrapper {
+ padding: 3px;
+ border-radius: 3px;
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.26);
+ }
+ }
+ }
+ }
+
+ .collectionNoteTakingView-collapseBar {
+ margin-left: 2px;
+ margin-right: 2px;
+ margin-top: 2px;
+ background: $medium-gray;
+ height: 5px;
+
+ &.active {
+ margin-left: 0;
+ margin-right: 0;
+ background: red;
+ }
+ }
+
+ .collectionNoteTakingView-miniHeader {
+ width: 100%;
+
+ .editableView-container-editing-oneLine {
+ min-height: 20px;
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ }
+
+ span::before,
+ span::after {
+ content: '';
+ width: 50%;
+ border-top: dashed gray 1px;
+ position: relative;
+ display: inline-block;
+ }
+
+ span::before {
+ margin-right: 10px;
+ }
+
+ span::after {
+ margin-left: 10px;
+ }
+
+ span {
+ position: relative;
+ text-align: center;
+ white-space: nowrap;
+ overflow: visible;
+ width: 100%;
+ display: flex;
+ color: gray;
+ align-items: center;
+ }
+ }
+
+ .collectionNoteTakingView-sectionHeader {
+ text-align: center;
+ margin: auto;
+ margin-bottom: 10px;
+ background: $medium-gray;
+ // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong
+
+ .editableView-input {
+ color: $dark-gray;
+ }
+
+ .editableView-input:hover,
+ .editableView-container-editing:hover,
+ .editableView-container-editing-oneLine:hover {
+ cursor: text;
+ }
+
+ .collectionNoteTakingView-sectionHeader-subCont {
+ outline: none;
+ border: 0px;
+ width: 100%;
+ letter-spacing: 2px;
+ font-size: 75%;
+ transition: transform 0.2s;
+ position: relative;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: $dark-gray;
+
+ .editableView-container-editing-oneLine,
+ .editableView-container-editing {
+ color: grey;
+ padding: 10px;
+ }
+
+ .editableView-input:hover,
+ .editableView-container-editing:hover,
+ .editableView-container-editing-oneLine:hover {
+ cursor: text;
+ }
+
+ .editableView-input {
+ padding: 12px 10px 11px 10px;
+ border: 0px;
+ color: grey;
+ text-align: center;
+ letter-spacing: 2px;
+ outline-color: black;
+ }
+ }
+
+ .collectionNoteTakingView-sectionColor {
+ position: absolute;
+ left: 0;
+ top: 0;
+ height: 100%;
+ display: none;
+
+ [class*='css'] {
+ max-width: 102px;
+ }
+
+ .collectionNoteTakingView-sectionColorButton {
+ height: 30px;
+ display: inherit;
+ }
+
+ .collectionNoteTakingView-colorPicker {
+ width: 78px;
+ z-index: 10;
+ position: relative;
+ background: white;
+
+ .colorOptions {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ .colorPicker {
+ cursor: pointer;
+ width: 20px;
+ height: 20px;
+ border-radius: 10px;
+ margin: 3px;
+
+ &.active {
+ border: 2px solid white;
+ box-shadow: 0 0 0 2px lightgray;
+ }
+ }
+ }
+ }
+
+ .collectionNoteTakingView-sectionOptions {
+ position: absolute;
+ right: 0;
+ top: 0;
+ height: 100%;
+ display: none;
+
+ [class*='css'] {
+ max-width: 102px;
+ }
+
+ .collectionNoteTakingView-sectionOptionButton {
+ height: 30px;
+ }
+
+ .collectionNoteTakingView-optionPicker {
+ .optionOptions {
+ display: inline;
+ }
+
+ .optionPicker {
+ cursor: pointer;
+ height: 20px;
+ border-radius: 10px;
+ margin: 3px;
+ width: max-content;
+
+ &.active {
+ color: red;
+ }
+ }
+ }
+ }
+
+ .collectionNoteTakingView-sectionDelete {
+ position: absolute;
+ right: 25px;
+ top: 0;
+ height: 100%;
+ display: none;
+ }
+ }
+
+ .collectionNoteTakingView-sectionHeader:hover {
+ .collectionNoteTakingView-sectionColor {
+ display: unset;
+ }
+
+ .collectionNoteTakingView-sectionOptions {
+ display: unset;
+ }
+
+ .collectionNoteTakingView-sectionDelete {
+ display: unset;
+ }
+ }
+
+ .collectionNoteTakingView-addDocumentButton,
+ .collectionNoteTakingView-addGroupButton {
+ display: flex;
+ overflow: hidden;
+ margin: auto;
+ width: 90%;
+ overflow: ellipses;
+
+ .editableView-container-editing-oneLine,
+ .editableView-container-editing {
+ color: grey;
+ padding: 10px;
+ width: 100%;
+ }
+
+ .editableView-input:hover,
+ .editableView-container-editing:hover,
+ .editableView-container-editing-oneLine:hover {
+ cursor: text;
+ }
+
+ .editableView-input {
+ outline-color: black;
+ letter-spacing: 2px;
+ color: grey;
+ border: 0px;
+ padding: 12px 10px 11px 10px;
+ }
+ }
+
+ .collectionNoteTakingView-addDocumentButton {
+ font-size: 75%;
+ letter-spacing: 2px;
+ cursor: pointer;
+
+ .editableView-input {
+ outline-color: black;
+ letter-spacing: 2px;
+ color: grey;
+ border: 0px;
+ padding: 12px 10px 11px 10px;
+ }
+ }
+
+ .collectionNoteTakingView-addGroupButton {
+ background: rgb(238, 238, 238);
+ font-size: 75%;
+ text-align: center;
+ letter-spacing: 2px;
+ height: fit-content;
+ }
+
+ .rc-switch {
+ position: absolute;
+ display: inline-block;
+ bottom: 4px;
+ right: 4px;
+ width: 70px;
+ height: 30px;
+ border-radius: 40px 40px;
+ background-color: lightslategrey;
+ }
+
+ .rc-switch:after {
+ position: absolute;
+ width: 22px;
+ height: 22px;
+ left: 3px;
+ top: 4px;
+ border-radius: 50% 50%;
+ background-color: #fff;
+ content: ' ';
+ cursor: pointer;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26);
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ transition: left 0.3s cubic-bezier(0.35, 0, 0.25, 1);
+ -webkit-animation-timing-function: cubic-bezier(0.35, 0, 0.25, 1);
+ animation-timing-function: cubic-bezier(0.35, 0, 0.25, 1);
+ -webkit-animation-duration: 0.3s;
+ animation-duration: 0.3s;
+ }
+
+ .rc-switch-checked:after {
+ left: 44px;
+ }
+
+ .rc-switch-inner {
+ color: #fff;
+ font-size: 12px;
+ position: absolute;
+ left: 28px;
+ top: 8px;
+ }
+
+ .rc-switch-checked .rc-switch-inner {
+ left: 8px;
+ }
+}
+
+@media only screen and (max-device-width: 480px) {
+ .collectionNoteTakingView .collectionNoteTakingView-columnDragger,
+ .collectionNoteTakingView-columnDragger {
+ width: 0.1;
+ height: 0.1;
+ opacity: 0;
+ font-size: 0;
+ }
+}
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
new file mode 100644
index 000000000..5c8b10ae1
--- /dev/null
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -0,0 +1,730 @@
+import React = require('react');
+import { CursorProperty } from 'csstype';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { DataSym, Doc, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { Id } from '../../../fields/FieldSymbols';
+import { List } from '../../../fields/List';
+import { listSpec } from '../../../fields/Schema';
+import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
+import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { TraceMobx } from '../../../fields/util';
+import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, returnZero, smoothScroll, Utils } from '../../../Utils';
+import { Docs, DocUtils } from '../../documents/Documents';
+import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
+import { DragManager, dropActionType } from '../../util/DragManager';
+import { SnappingManager } from '../../util/SnappingManager';
+import { Transform } from '../../util/Transform';
+import { undoBatch } from '../../util/UndoManager';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { LightboxView } from '../LightboxView';
+import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from '../nodes/DocumentView';
+import { FieldViewProps } from '../nodes/FieldView';
+import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import { StyleProp } from '../StyleProvider';
+import './CollectionNoteTakingView.scss';
+import { CollectionNoteTakingViewColumn } from './CollectionNoteTakingViewColumn';
+import { CollectionNoteTakingViewDivider } from './CollectionNoteTakingViewDivider';
+import { CollectionSubView } from './CollectionSubView';
+const _global = (window /* browser */ || global) /* node */ as any;
+
+export type collectionNoteTakingViewProps = {
+ chromeHidden?: boolean;
+ viewType?: CollectionViewType;
+ NativeWidth?: () => number;
+ NativeHeight?: () => number;
+};
+
+//TODO: somehow need to update the mapping and then have everything else rerender. Maybe with a refresh boolean like
+// in Hypermedia?
+
+@observer
+export class CollectionNoteTakingView extends CollectionSubView<Partial<collectionNoteTakingViewProps>>() {
+ _disposers: { [key: string]: IReactionDisposer } = {};
+ _masonryGridRef: HTMLDivElement | null = null;
+ _draggerRef = React.createRef<HTMLDivElement>(); // change to relative widths for deleting. change storage from columnStartXCoords to columnHeaders (schemaHeaderFields has a widgth alrady)
+ @observable columnStartXCoords: number[] = []; // columnHeaders -- SchemaHeaderField -- widht
+ @observable docsDraggedRowCol: number[] = [];
+ @observable _cursor: CursorProperty = 'grab';
+ @observable _scroll = 0; // used to force the document decoration to update when scrolling
+ @computed get chromeHidden() {
+ return this.props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden);
+ }
+ @computed get columnHeaders() {
+ const columnHeaders = Array.from(Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null));
+ const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders.find(sh => sh.heading === 'unset'));
+
+ // @#Oberable draggedColIndex = ...
+ //@observable cloneDivXYcoords
+ // @observable clonedDiv...
+
+ // render() {
+ // { this.clonedDiv ? <div clone styule={{transform: clonedDivXYCoords}} : null}
+ // }
+
+ // in NoteatakinView Column code, add a poinerDown handler that calls setupMoveUpEvents() which will clone the column div
+ // and re-render it under the cursor during move events.
+ // that move move event will update 2 observales -- the draggedColIndex up above, and the location of the clonedDiv so that the render in this view will know where to render the cloned div
+ // add observable variable that tells drag column to rnder in a different location than where the schemaHeaderFiel sa y ot.
+ // if (col 1 is where col 3) {
+ // return 3 2 1 4 56
+ // }
+ if (needsUnsetCategory) {
+ columnHeaders.push(new SchemaHeaderField('unset'));
+ }
+ return columnHeaders;
+ }
+ @computed get notetakingCategoryField() {
+ return 'NotetakingCategory';
+ }
+ @computed get filteredChildren() {
+ return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout);
+ }
+ @computed get headerMargin() {
+ return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin);
+ }
+ @computed get xMargin() {
+ return NumCast(this.layoutDoc._xMargin, 2 * Math.min(this.gridGap, 0.05 * this.props.PanelWidth()));
+ }
+ @computed get yMargin() {
+ return this.props.yPadding || NumCast(this.layoutDoc._yMargin, 5);
+ } // 2 * this.gridGap)); }
+ @computed get gridGap() {
+ return NumCast(this.layoutDoc._gridGap, 10);
+ }
+ @computed get numGroupColumns() {
+ return this.columnHeaders.length;
+ }
+ @computed get PanelWidth() {
+ return this.props.PanelWidth();
+ }
+ @computed get maxColWdith() {
+ return this.props.PanelWidth() - 2 * this.xMargin;
+ }
+
+ // If the user has not yet created any docs (in another view), this will create a single column. Otherwise,
+ // it will adjust according to the
+ constructor(props: any) {
+ super(props);
+ if (this.columnHeaders === undefined) {
+ this.dataDoc.columnHeaders = new List<SchemaHeaderField>([new SchemaHeaderField('New Column')]);
+ // add all of the docs that have not been added to a column to this new column
+ }
+ }
+
+ // passed as a prop to the NoteTakingField, which uses this function
+ // to render the docs you see within an individual column.
+ children = (docs: Doc[]) => {
+ TraceMobx();
+ return docs.map((d, i) => {
+ const height = () => this.getDocHeight(d);
+ const width = () => this.getDocWidth(d);
+ const style = { width: width(), marginTop: this.gridGap, height: height() };
+ return (
+ <div className={`collectionNoteTakingView-columnDoc`} key={d[Id]} style={style}>
+ {this.getDisplayDoc(d, width)}
+ </div>
+ );
+ });
+ };
+
+ // [CAVEATS] (1) keep track of the offsetting
+ // (2) documentView gets unmounted as you remove it from the list
+ @computed get Sections() {
+ TraceMobx();
+ const columnHeaders = this.columnHeaders;
+ let docs = this.childDocs;
+ const sections = new Map<SchemaHeaderField, Doc[]>(columnHeaders.map(sh => [sh, []] as [SchemaHeaderField, []]));
+ const rowCol = this.docsDraggedRowCol;
+
+ // filter out the currently dragged docs from the child docs, since we will insert them later
+ if (rowCol.length && DragManager.docsBeingDragged.length) {
+ const docIdsToRemove = new Set();
+ DragManager.docsBeingDragged.forEach(d => docIdsToRemove.add(d[Id]));
+ docs = docs.filter(d => !docIdsToRemove.has(d[Id]));
+ }
+
+ // this will sort the docs into the correct columns (minus the ones you're currently dragging)
+ docs.map(d => {
+ const sectionValue = (d[this.notetakingCategoryField] as object) ?? `unset`;
+
+ // look for if header exists already
+ const existingHeader = columnHeaders.find(sh => sh.heading === sectionValue.toString());
+ if (existingHeader) {
+ sections.get(existingHeader)!.push(d);
+ }
+ });
+
+ // now we add back in the docs that we're dragging
+ if (rowCol.length && DragManager.docsBeingDragged.length) {
+ const colHeader = columnHeaders[rowCol[1]];
+ // TODO: get the actual offset that occurs if the docs were in that column
+ const offset = 0;
+ sections.get(colHeader)?.splice(rowCol[0] - offset, 0, ...DragManager.docsBeingDragged);
+ }
+ return sections;
+ }
+
+ removeDocDragHighlight = () => {
+ setTimeout(
+ action(() => (this.docsDraggedRowCol.length = 0)),
+ 100
+ );
+ };
+ componentDidMount() {
+ super.componentDidMount?.();
+ document.addEventListener('pointerup', this.removeDocDragHighlight, true);
+ this._disposers.autoHeight = reaction(
+ () => this.layoutDoc._autoHeight,
+ autoHeight => autoHeight && this.props.setHeight?.(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), this.headerMargin + Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', ''))))))
+ );
+ this._disposers.headers = reaction(
+ () => this.columnHeaders.slice(),
+ headers => this.resizeColumns(headers.length),
+ { fireImmediately: true }
+ );
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('pointerup', this.removeDocDragHighlight, true);
+ super.componentWillUnmount();
+ Object.keys(this._disposers).forEach(key => this._disposers[key]());
+ }
+
+ @action
+ moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean): boolean => {
+ return this.props.removeDocument?.(doc) && addDocument?.(doc) ? true : false;
+ };
+
+ createRef = (ele: HTMLDivElement | null) => {
+ this._masonryGridRef = ele;
+ this.createDashEventsTarget(ele!); //so the whole grid is the drop target?
+ };
+
+ @computed get onChildClickHandler() {
+ return () => this.props.childClickScript || ScriptCast(this.Document.onChildClick);
+ }
+ @computed get onChildDoubleClickHandler() {
+ return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
+ }
+
+ addDocTab = (doc: Doc, where: string) => {
+ if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) {
+ this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]);
+ return true;
+ }
+ return this.props.addDocTab(doc, where);
+ };
+
+ scrollToBottom = () => {
+ smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight);
+ };
+
+ // let's dive in and get the actual document we want to drag/move around
+ focusDocument = (doc: Doc, options?: DocFocusOptions) => {
+ Doc.BrushDoc(doc);
+
+ let focusSpeed = 0;
+ const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]);
+ if (found) {
+ const top = found.getBoundingClientRect().top;
+ const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top);
+ if (Math.floor(localTop[1]) !== 0) {
+ smoothScroll((focusSpeed = doc.presTransition || doc.presTransition === 0 ? NumCast(doc.presTransition) : 500), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
+ }
+ }
+ const endFocus = async (moved: boolean) => (options?.afterFocus ? options?.afterFocus(moved) : ViewAdjustment.doNothing);
+ this.props.focus(this.rootDoc, {
+ willZoom: options?.willZoom,
+ scale: options?.scale,
+ afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)),
+ });
+ };
+
+ styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => {
+ if (property === StyleProp.BoxShadow && doc && DragManager.docsBeingDragged.includes(doc)) {
+ return `#9c9396 ${StrCast(doc?.boxShadow, '10px 10px 0.9vw')}`;
+ }
+ if (property === StyleProp.Opacity && doc) {
+ if (this.props.childOpacity) {
+ return this.props.childOpacity();
+ }
+ if (this.Document._currentFrame !== undefined) {
+ return CollectionFreeFormDocumentView.getValues(doc, NumCast(this.Document._currentFrame))?.opacity;
+ }
+ }
+ return this.props.styleProvider?.(doc, props, property);
+ };
+
+ isContentActive = () => this.props.isSelected() || this.props.isContentActive();
+
+ // rules for displaying the documents
+ getDisplayDoc(doc: Doc, width: () => number) {
+ const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.DataDoc;
+ const height = () => this.getDocHeight(doc);
+ let dref: Opt<DocumentView>;
+ const noteTakingDocTransform = () => this.getDocTransform(doc, dref);
+ return (
+ <DocumentView
+ ref={r => (dref = r || undefined)}
+ Document={doc}
+ DataDoc={dataDoc || (!Doc.AreProtosEqual(doc[DataSym], doc) && doc[DataSym])}
+ renderDepth={this.props.renderDepth + 1}
+ PanelWidth={width}
+ PanelHeight={height}
+ styleProvider={this.styleProvider}
+ docViewPath={this.props.docViewPath}
+ fitWidth={this.props.childFitWidth}
+ isContentActive={emptyFunction}
+ onKey={this.onKeyDown}
+ //TODO: change this from a prop to a parameter passed into a function
+ dontHideOnDrag={true}
+ isDocumentActive={this.isContentActive}
+ LayoutTemplate={this.props.childLayoutTemplate}
+ LayoutTemplateString={this.props.childLayoutString}
+ NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox
+ NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeHeight(doc)) ? height : undefined}
+ dontCenter={this.props.childIgnoreNativeSize ? 'xy' : undefined}
+ dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)}
+ rootSelected={this.rootSelected}
+ showTitle={this.props.childShowTitle}
+ dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType}
+ onClick={this.onChildClickHandler}
+ onDoubleClick={this.onChildDoubleClickHandler}
+ ScreenToLocalTransform={noteTakingDocTransform}
+ focus={this.focusDocument}
+ docFilters={this.childDocFilters}
+ hideDecorationTitle={this.props.childHideDecorationTitle?.()}
+ hideResizeHandles={this.props.childHideResizeHandles?.()}
+ hideTitle={this.props.childHideTitle?.()}
+ docRangeFilters={this.childDocRangeFilters}
+ searchFilterDocs={this.searchFilterDocs}
+ ContainingCollectionDoc={this.props.CollectionView?.props.Document}
+ ContainingCollectionView={this.props.CollectionView}
+ addDocument={this.props.addDocument}
+ moveDocument={this.props.moveDocument}
+ removeDocument={this.props.removeDocument}
+ contentPointerEvents={StrCast(this.layoutDoc.contentPointerEvents)}
+ whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
+ addDocTab={this.addDocTab}
+ bringToFront={returnFalse}
+ scriptContext={this.props.scriptContext}
+ pinToPres={this.props.pinToPres}
+ />
+ );
+ }
+
+ // This is used to get the coordinates of a document when we go from a view like freeform to columns
+ getDocTransform(doc: Doc, dref?: DocumentView) {
+ const y = this._scroll; // required for document decorations to update when the text box container is scrolled
+ const { scale, translateX, translateY } = Utils.GetScreenTransform(dref?.ContentDiv || undefined);
+ // the document view may center its contents and if so, will prepend that onto the screenToLocalTansform. so we have to subtract that off
+ return new Transform(-translateX + (dref?.centeringX || 0), -translateY + (dref?.centeringY || 0), 1).scale(this.props.ScreenToLocalTransform().Scale);
+ }
+
+ // how to get the width of a document. Currently returns the width of the column (minus margins)
+ // if a note doc. Otherwise, returns the normal width (for graphs, images, etc...)
+ getDocWidth(d: Doc) {
+ const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as Field);
+ const existingHeader = this.columnHeaders.find(sh => sh.heading === heading);
+ const index = existingHeader ? this.columnHeaders.indexOf(existingHeader) : 0;
+ const endColValue = index === this.columnHeaders.length - 1 || index > this.columnStartXCoords.length - 1 ? this.PanelWidth : this.columnStartXCoords[index + 1];
+ const maxWidth = index > this.columnStartXCoords.length - 1 ? this.PanelWidth : endColValue - this.columnStartXCoords[index] - 3 * this.xMargin;
+ if (d.type === DocumentType.RTF) {
+ return maxWidth;
+ }
+ const width = d[WidthSym]();
+ return width < maxWidth ? width : maxWidth;
+ }
+
+ // how to get the height of a document. Nothing special here.
+ getDocHeight(d?: Doc) {
+ if (!d || d.hidden) return 0;
+ const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
+ const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS ? undefined : this.props.DataDoc;
+ const maxHeight = (lim => (lim === 0 ? this.props.PanelWidth() : lim === -1 ? 10000 : lim))(NumCast(this.layoutDoc.childLimitHeight, -1));
+ const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[WidthSym]() : 0);
+ const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[HeightSym]() : 0);
+ if (nw && nh) {
+ // const colWid = this.columnWidth / this.numGroupColumns;
+ // const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d), colWid);
+ const docWid = this.getDocWidth(d);
+ return Math.min(maxHeight, (docWid * nh) / nw);
+ }
+ const childHeight = NumCast(childLayoutDoc._height);
+ const panelHeight = childLayoutDoc._fitWidth || this.props.childFitWidth?.(d) ? Number.MAX_SAFE_INTEGER : this.props.PanelHeight() - 2 * this.yMargin;
+ return Math.min(childHeight, maxHeight, panelHeight);
+ }
+
+ // called when a column is either added or deleted. This function creates n evenly spaced columns
+ @action
+ resizeColumns = (n: number) => {
+ const totalWidth = this.PanelWidth;
+ const dividerWidth = 32;
+ const totaldividerWidth = (n - 1) * dividerWidth;
+ const colWidth = (totalWidth - totaldividerWidth) / n;
+ const newColXCoords: number[] = [];
+ let colStart = 0;
+ for (let i = 0; i < n; i++) {
+ newColXCoords.push(colStart);
+ colStart += colWidth + dividerWidth;
+ }
+ this.columnStartXCoords = newColXCoords;
+ };
+
+ // This function is used to preview where a document will drop in a column once a drag is complete.
+ @action
+ onPointerMove = (force: boolean, ex: number, ey: number) => {
+ if (this.childDocList && (this.childDocList.includes(DragManager.DocDragData?.draggedDocuments.lastElement()!) || force || this.isContentActive())) {
+ // get the current docs for the column based on the mouse's x coordinate
+ // will use again later, which is why we're saving as local
+ const xCoord = this.props.ScreenToLocalTransform().transformPoint(ex, ey)[0] - 2 * this.gridGap;
+ const colDocs = this.getDocsFromXCoord(xCoord);
+ // get the index for where you need to insert the doc you are currently dragging
+ const clientY = this.props.ScreenToLocalTransform().transformPoint(ex, ey)[1];
+ let dropInd = -1;
+ let pos0 = (this.refList.lastElement() as HTMLDivElement).children[0].getBoundingClientRect().height + this.yMargin * 2;
+ colDocs.forEach((doc, i) => {
+ let pos1 = this.getDocHeight(doc) + 2 * this.gridGap;
+ pos1 += pos0;
+ // updating drop position based on y coordinates
+ const yCoordInBetween = clientY > pos0 && clientY < pos1;
+ if (yCoordInBetween || (clientY < pos0 && i === 0)) {
+ dropInd = i;
+ } else if (i === colDocs.length - 1 && dropInd === -1) {
+ dropInd = !colDocs.includes(DragManager.docsBeingDragged.lastElement()) ? i + 1 : i;
+ }
+ pos0 = pos1;
+ });
+ // we alter the pivot fields of the docs in case they are moved to a new column.
+ const colIndex = this.getColumnFromXCoord(xCoord);
+ const colHeader = StrCast(this.columnHeaders[colIndex].heading);
+ DragManager.docsBeingDragged.forEach(d => (d[this.notetakingCategoryField] = colHeader));
+ // used to notify sections to re-render
+ this.docsDraggedRowCol.length = 0;
+ this.docsDraggedRowCol.push(dropInd, this.getColumnFromXCoord(xCoord));
+ }
+ };
+
+ // returns the column index for a given x-coordinate
+ getColumnFromXCoord = (xCoord: number): number => {
+ const numColumns = this.columnHeaders.length;
+ const coords = this.columnStartXCoords.slice();
+ coords.push(this.PanelWidth);
+ let colIndex = 0;
+ for (let i = 0; i < numColumns; i++) {
+ if (xCoord > coords[i] && xCoord < coords[i + 1]) {
+ colIndex = i;
+ break;
+ }
+ }
+ return colIndex;
+ };
+
+ // returns the docs of a column based on the x-coordinate provided.
+ getDocsFromXCoord = (xCoord: number): Doc[] => {
+ const colIndex = this.getColumnFromXCoord(xCoord);
+ const colHeader = StrCast(this.columnHeaders[colIndex].heading);
+ // const docs = this.childDocList
+ const docs = this.childDocs;
+ const docsMatchingHeader: Doc[] = [];
+ if (docs) {
+ docs.map(d => {
+ if (d instanceof Promise) return;
+ const sectionValue = (d[this.notetakingCategoryField] as object) ?? 'unset';
+ if (sectionValue.toString() == colHeader) {
+ docsMatchingHeader.push(d);
+ }
+ });
+ }
+ return docsMatchingHeader;
+ };
+
+ @undoBatch
+ @action
+ onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => {
+ const docView = fieldProps.DocumentView?.();
+ if (docView && (e.ctrlKey || docView.rootDoc._singleLine) && ['Enter'].includes(e.key)) {
+ e.stopPropagation?.();
+ const newDoc = Doc.MakeCopy(docView.rootDoc, true);
+ Doc.GetProto(newDoc).text = undefined;
+ FormattedTextBox.SelectOnLoad = newDoc[Id];
+ return this.addDocument?.(newDoc);
+ }
+ };
+
+ @undoBatch
+ @action
+ onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
+ if (de.complete.docDragData) {
+ if (super.onInternalDrop(e, de)) {
+ // filter out the currently dragged docs from the child docs, since we will insert them later
+ const rowCol = this.docsDraggedRowCol;
+ const droppedDocs = this.childDocs.slice().filter((d: Doc, ind: number) => ind >= this.childDocs.length); // if the drop operation adds something to the end of the list, then use that as the new document (may be different than what was dropped e.g., in the case of a button which is dropped but which creates say, a note).
+ const newDocs = droppedDocs.length ? droppedDocs : de.complete.docDragData.droppedDocuments;
+
+ // const docs = this.childDocs
+ const docs = this.childDocList;
+ if (docs && newDocs.length) {
+ // remove the dragged documents from the childDocList
+ newDocs.filter(d => docs.indexOf(d) !== -1).forEach(d => docs.splice(docs.indexOf(d), 1));
+ // if the doc starts a columnm (or the drop index is undefined), we can just push it to the front. Otherwise we need to add it to the column properly
+ //TODO: you need to update childDocList instead. It seems that childDocs is a copy of the actual array we want to modify
+ if (rowCol[0] <= 0) {
+ docs.splice(0, 0, ...newDocs);
+ } else {
+ const colDocs = this.getDocsFromXCoord(this.props.ScreenToLocalTransform().transformPoint(de.x, de.y)[0]);
+ const previousDoc = colDocs[rowCol[0] - 1];
+ const previousDocIndex = docs.indexOf(previousDoc);
+ docs.splice(previousDocIndex + 1, 0, ...newDocs);
+ }
+ }
+ }
+ } // it seems like we're creating a link here. Weird. I didn't know that you could establish links by dragging
+ else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) {
+ const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' });
+ this.props.addDocument?.(source);
+ de.complete.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: de.complete.linkDragData.linkSourceGetAnchor() }, 'doc annotation', ''); // TODODO this is where in text links get passed
+ e.stopPropagation();
+ } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData);
+ return false;
+ };
+
+ @undoBatch
+ internalAnchorAnnoDrop(e: Event, annoDragData: DragManager.AnchorAnnoDragData) {
+ const dropCreator = annoDragData.dropDocCreator;
+ annoDragData.dropDocCreator = (annotationOn: Doc | undefined) => {
+ const dropDoc = dropCreator(annotationOn);
+ return dropDoc || this.rootDoc;
+ };
+ return true;
+ }
+
+ // when dropping outside of the current noteTaking context (like a new tab, freeform view, etc...)
+ onExternalDrop = async (e: React.DragEvent): Promise<void> => {
+ const targInd = this.docsDraggedRowCol?.[0] || 0;
+ const colInd = this.docsDraggedRowCol?.[1] || 0;
+ super.onExternalDrop(
+ e,
+ {},
+ undoBatch(
+ action(docus => {
+ this.onPointerMove(true, e.clientX, e.clientY);
+ docus?.map((doc: Doc) => this.addDocument(doc));
+ const newDoc = this.childDocs.lastElement();
+ const colHeader = StrCast(this.columnHeaders[colInd].heading);
+ newDoc[this.notetakingCategoryField] = colHeader;
+ const docs = this.childDocList;
+ if (docs && targInd !== -1) {
+ docs.splice(docs.length - 1, 1);
+ docs.splice(targInd, 0, newDoc);
+ }
+ this.removeDocDragHighlight();
+ })
+ )
+ );
+ };
+
+ headings = () => Array.from(this.Sections);
+
+ refList: any[] = [];
+ editableViewProps = () => ({
+ GetValue: () => '',
+ SetValue: this.addGroup,
+ contents: '+ New Column',
+ });
+
+ sectionNoteTaking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
+ const type = 'number';
+ return (
+ <CollectionNoteTakingViewColumn
+ unobserveHeight={ref => this.refList.splice(this.refList.indexOf(ref), 1)}
+ observeHeight={ref => {
+ if (ref) {
+ this.refList.push(ref);
+ this.observer = new _global.ResizeObserver(
+ action((entries: any) => {
+ if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) {
+ const height = this.headerMargin + Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', '')))));
+ if (!LightboxView.IsLightboxDocView(this.props.docViewPath())) {
+ this.props.setHeight?.(height);
+ }
+ }
+ })
+ );
+ this.observer.observe(ref);
+ }
+ }}
+ addDocument={this.addDocument}
+ // docsByColumnHeader={this._docsByColumnHeader}
+ // setDocsForColHeader={this.setDocsForColHeader}
+ chromeHidden={this.chromeHidden}
+ columnHeaders={this.columnHeaders}
+ Document={this.props.Document}
+ DataDoc={this.props.DataDoc}
+ resizeColumns={this.resizeColumns}
+ renderChildren={this.children}
+ numGroupColumns={this.numGroupColumns}
+ gridGap={this.gridGap}
+ pivotField={this.notetakingCategoryField}
+ columnStartXCoords={this.columnStartXCoords}
+ maxColWidth={this.maxColWdith}
+ PanelWidth={this.PanelWidth}
+ key={heading?.heading ?? ''}
+ headings={this.headings}
+ heading={heading?.heading ?? ''}
+ headingObject={heading}
+ docList={docList}
+ yMargin={this.yMargin}
+ type={type}
+ createDropTarget={this.createDashEventsTarget}
+ screenToLocalTransform={this.props.ScreenToLocalTransform}
+ editableViewProps={this.editableViewProps}
+ />
+ );
+ };
+
+ // called when adding a new columnHeader
+ @undoBatch
+ @action
+ addGroup = (value: string) => {
+ const columnHeaders = Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null);
+ return value && columnHeaders?.push(new SchemaHeaderField(value)) ? true : false;
+ };
+
+ sortFunc = (a: [SchemaHeaderField, Doc[]], b: [SchemaHeaderField, Doc[]]): 1 | -1 => {
+ const descending = StrCast(this.layoutDoc._columnsSort) === 'descending';
+ const firstEntry = descending ? b : a;
+ const secondEntry = descending ? a : b;
+ return firstEntry[0].heading > secondEntry[0].heading ? 1 : -1;
+ };
+
+ onContextMenu = (e: React.MouseEvent): void => {
+ // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
+ if (!e.isPropagationStopped()) {
+ const subItems: ContextMenuProps[] = [];
+ subItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' });
+ subItems.push({ description: `${this.layoutDoc._autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: 'plus' });
+ subItems.push({ description: 'Clear All', event: () => (this.dataDoc.data = new List([])), icon: 'times' });
+ ContextMenu.Instance.addItem({ description: 'Options...', subitems: subItems, icon: 'eye' });
+ }
+ };
+
+ // used to reset column sizes when using the drag handlers
+ @action
+ setColumnStartXCoords = (movementX: number, colIndex: number) => {
+ const coords = [...this.columnStartXCoords];
+ coords[colIndex] += movementX;
+ this.columnStartXCoords = coords;
+ };
+
+ @computed get renderedSections() {
+ TraceMobx();
+ // let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]];
+ // if (this.pivotField) {
+ // const entries = Array.from(this.Sections.entries());
+ // sections = this.layoutDoc._columnsSort ? entries.sort(this.sortFunc) : entries;
+ // }
+ const entries = Array.from(this.Sections.entries());
+ const sections = entries; //.sort(this.sortFunc);
+ const eles: JSX.Element[] = [];
+ for (let i = 0; i < sections.length; i++) {
+ const col = this.sectionNoteTaking(sections[i][0], sections[i][1]);
+ eles.push(col);
+ if (i < sections.length - 1) {
+ eles.push(<CollectionNoteTakingViewDivider key={`divider${i}`} index={i + 1} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />);
+ }
+ }
+ return eles;
+ }
+
+ @computed get buttonMenu() {
+ const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null);
+ // TODO:glr Allow support for multiple buttons
+ if (menuDoc) {
+ const width: number = NumCast(menuDoc._width, 30);
+ const height: number = NumCast(menuDoc._height, 30);
+ return (
+ <div className="buttonMenu-docBtn" style={{ width: width, height: height }}>
+ <DocumentView
+ Document={menuDoc}
+ DataDoc={menuDoc}
+ isContentActive={this.props.isContentActive}
+ isDocumentActive={returnTrue}
+ addDocument={this.props.addDocument}
+ moveDocument={this.props.moveDocument}
+ addDocTab={this.props.addDocTab}
+ pinToPres={emptyFunction}
+ rootSelected={this.props.isSelected}
+ removeDocument={this.props.removeDocument}
+ ScreenToLocalTransform={Transform.Identity}
+ PanelWidth={() => 35}
+ PanelHeight={() => 35}
+ renderDepth={this.props.renderDepth}
+ focus={emptyFunction}
+ styleProvider={this.props.styleProvider}
+ docViewPath={returnEmptyDoclist}
+ whenChildContentsActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ docFilters={this.props.docFilters}
+ docRangeFilters={this.props.docRangeFilters}
+ searchFilterDocs={this.props.searchFilterDocs}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ />
+ </div>
+ );
+ }
+ }
+
+ @computed get nativeWidth() {
+ return this.props.NativeWidth?.() ?? Doc.NativeWidth(this.layoutDoc);
+ }
+ @computed get nativeHeight() {
+ return this.props.NativeHeight?.() ?? Doc.NativeHeight(this.layoutDoc);
+ }
+
+ @computed get scaling() {
+ return !this.nativeWidth ? 1 : this.props.PanelHeight() / this.nativeHeight;
+ }
+
+ @computed get backgroundEvents() {
+ return SnappingManager.GetIsDragging();
+ }
+ observer: any;
+ render() {
+ TraceMobx();
+ const buttonMenu = this.rootDoc.buttonMenu;
+ const noviceExplainer = StrCast(this.rootDoc.explainer);
+ return (
+ <>
+ {buttonMenu || noviceExplainer ? (
+ <div className="documentButtonMenu" key="buttons">
+ {buttonMenu ? this.buttonMenu : null}
+ {Doc.UserDoc().noviceMode && noviceExplainer ? <div className="documentExplanation">{noviceExplainer}</div> : null}
+ </div>
+ ) : null}
+ <div
+ className="collectionNoteTakingView"
+ ref={this.createRef}
+ key="notes"
+ style={{
+ overflowY: this.props.isContentActive() ? 'auto' : 'hidden',
+ background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor),
+ pointerEvents: this.backgroundEvents ? 'all' : undefined,
+ }}
+ onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))}
+ onPointerLeave={action(e => (this.docsDraggedRowCol.length = 0))}
+ onPointerMove={e => e.buttons && this.onPointerMove(false, e.clientX, e.clientY)}
+ onDragOver={e => this.onPointerMove(true, e.clientX, e.clientY)}
+ onDrop={this.onExternalDrop.bind(this)}
+ onContextMenu={this.onContextMenu}
+ onWheel={e => this.props.isContentActive(true) && e.stopPropagation()}>
+ {this.renderedSections}
+ </div>
+ </>
+ );
+ }
+}
diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
new file mode 100644
index 000000000..624beca96
--- /dev/null
+++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
@@ -0,0 +1,325 @@
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { Id } from '../../../fields/FieldSymbols';
+import { RichTextField } from '../../../fields/RichTextField';
+import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
+import { ScriptField } from '../../../fields/ScriptField';
+import { ImageField } from '../../../fields/URLField';
+import { TraceMobx } from '../../../fields/util';
+import { emptyFunction, returnEmptyString, setupMoveUpEvents } from '../../../Utils';
+import { Docs, DocUtils } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { DragManager } from '../../util/DragManager';
+import { SnappingManager } from '../../util/SnappingManager';
+import { Transform } from '../../util/Transform';
+import { undoBatch } from '../../util/UndoManager';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { EditableView } from '../EditableView';
+import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import './CollectionNoteTakingView.scss';
+import { listSpec } from '../../../fields/Schema';
+import { Cast } from '../../../fields/Types';
+const higflyout = require('@hig/flyout');
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
+
+// So this is how we are storing a column
+interface CSVFieldColumnProps {
+ Document: Doc;
+ DataDoc: Opt<Doc>;
+ docList: Doc[];
+ heading: string;
+ pivotField: string;
+ chromeHidden?: boolean;
+ columnHeaders: SchemaHeaderField[] | undefined;
+ headingObject: SchemaHeaderField | undefined;
+ yMargin: number;
+ // columnWidth: number;
+ numGroupColumns: number;
+ gridGap: number;
+ type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined;
+ headings: () => object[];
+ renderChildren: (docs: Doc[]) => JSX.Element[];
+ addDocument: (doc: Doc | Doc[]) => boolean;
+ createDropTarget: (ele: HTMLDivElement) => void;
+ screenToLocalTransform: () => Transform;
+ observeHeight: (myref: any) => void;
+ unobserveHeight: (myref: any) => void;
+ //setDraggedCol:(clonedDiv:any, header:SchemaHeaderField, xycoors: )
+ editableViewProps: () => any;
+ resizeColumns: (n: number) => void;
+ columnStartXCoords: number[];
+ PanelWidth: number;
+ maxColWidth: number;
+ // docsByColumnHeader: Map<string, Doc[]>
+ // setDocsForColHeader: (key: string, docs: Doc[]) => void
+}
+
+@observer
+export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColumnProps> {
+ @observable private _background = 'inherit';
+
+ @computed get columnWidth() {
+ // base cases
+ if (!this.props.columnHeaders || !this.props.headingObject || this.props.columnHeaders.length == 1) {
+ return this.props.maxColWidth;
+ }
+ const i = this.props.columnHeaders.indexOf(this.props.headingObject);
+ if (i < 0 || i > this.props.columnStartXCoords.length - 1) {
+ return this.props.maxColWidth;
+ }
+ const endColValue = i == this.props.numGroupColumns - 1 ? this.props.PanelWidth : this.props.columnStartXCoords[i + 1];
+ // TODO make the math work here. 35 is half of 70, which is the current width of the divider
+ return endColValue - this.props.columnStartXCoords[i] - 30;
+ }
+
+ private dropDisposer?: DragManager.DragDropDisposer;
+ private _headerRef: React.RefObject<HTMLDivElement> = React.createRef();
+
+ @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading;
+ @observable _color = this.props.headingObject ? this.props.headingObject.color : '#f1efeb';
+ _ele: HTMLElement | null = null;
+
+ // This is likely similar to what we will be doing. Why do we need to make these refs?
+ // is that the only way to have drop targets?
+ createColumnDropRef = (ele: HTMLDivElement | null) => {
+ this.dropDisposer?.();
+ if (ele) {
+ this._ele = ele;
+ this.props.observeHeight(ele);
+ this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this));
+ }
+ };
+
+ componentWillUnmount() {
+ this.props.unobserveHeight(this._ele);
+ }
+
+ @undoBatch
+ columnDrop = action((e: Event, de: DragManager.DropEvent) => {
+ const drop = { docs: de.complete.docDragData?.droppedDocuments, val: this.getValue(this._heading) };
+ drop.docs?.forEach(d => Doc.SetInPlace(d, this.props.pivotField, drop.val, false));
+ });
+
+ getValue = (value: string): any => {
+ const parsed = parseInt(value);
+ if (!isNaN(parsed)) return parsed;
+ if (value.toLowerCase().indexOf('true') > -1) return true;
+ if (value.toLowerCase().indexOf('false') > -1) return false;
+ return value;
+ };
+
+ @action
+ headingChanged = (value: string, shiftDown?: boolean) => {
+ const castedValue = this.getValue(value);
+ if (castedValue) {
+ if (this.props.columnHeaders?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) {
+ return false;
+ }
+ this.props.docList.forEach(d => (d[this.props.pivotField] = castedValue));
+ if (this.props.headingObject) {
+ this.props.headingObject.setHeading(castedValue.toString());
+ this._heading = this.props.headingObject.heading;
+ }
+ return true;
+ }
+ return false;
+ };
+
+ @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = '#b4b4b4');
+ @action pointerLeave = () => (this._background = 'inherit');
+ textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true);
+
+ @action
+ addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => {
+ if (!value && !forceEmptyNote) return false;
+ const key = this.props.pivotField;
+ const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _fitWidth: true, title: value, _autoHeight: true });
+ const colValue = this.getValue(this.props.heading);
+ newDoc[key] = colValue;
+ FormattedTextBox.SelectOnLoad = newDoc[Id];
+ FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' ';
+ return this.props.addDocument?.(newDoc) || false;
+ };
+
+ @undoBatch
+ @action
+ deleteColumn = () => {
+ const columnHeaders = Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null);
+ if (columnHeaders && this.props.headingObject) {
+ const index = columnHeaders.indexOf(this.props.headingObject);
+ this.props.docList.forEach(d => (d[this.props.pivotField] = 'unset'));
+ columnHeaders.splice(index, 1);
+ }
+ };
+
+ menuCallback = (x: number, y: number) => {
+ ContextMenu.Instance.clearItems();
+ const layoutItems: ContextMenuProps[] = [];
+ const docItems: ContextMenuProps[] = [];
+ const dataDoc = this.props.DataDoc || this.props.Document;
+ const pivotValue = this.getValue(this.props.heading);
+
+ DocUtils.addDocumentCreatorMenuItems(
+ doc => {
+ const key = this.props.pivotField;
+ doc[key] = this.getValue(this.props.heading);
+ FormattedTextBox.SelectOnLoad = doc[Id];
+ return this.props.addDocument?.(doc);
+ },
+ this.props.addDocument,
+ x,
+ y,
+ true,
+ this.props.pivotField,
+ pivotValue
+ );
+
+ Array.from(Object.keys(Doc.GetProto(dataDoc)))
+ .filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof dataDoc[fieldKey] === 'string')
+ .map(fieldKey =>
+ docItems.push({
+ description: ':' + fieldKey,
+ event: () => {
+ const created = DocUtils.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.Document));
+ if (created) {
+ if (this.props.Document.isTemplateDoc) {
+ Doc.MakeMetadataFieldTemplate(created, this.props.Document);
+ }
+ return this.props.addDocument?.(created);
+ }
+ },
+ icon: 'compress-arrows-alt',
+ })
+ );
+ Array.from(Object.keys(Doc.GetProto(dataDoc)))
+ .filter(fieldKey => DocListCast(dataDoc[fieldKey]).length)
+ .map(fieldKey =>
+ docItems.push({
+ description: ':' + fieldKey,
+ event: () => {
+ const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey });
+ if (created) {
+ const container = this.props.Document.resolvedDataDoc ? Doc.GetProto(this.props.Document) : this.props.Document;
+ if (container.isTemplateDoc) {
+ Doc.MakeMetadataFieldTemplate(created, container);
+ return Doc.AddDocToList(container, Doc.LayoutFieldKey(container), created);
+ }
+ return this.props.addDocument?.(created) || false;
+ }
+ },
+ icon: 'compress-arrows-alt',
+ })
+ );
+ !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: 'Doc Fields ...', subitems: docItems, icon: 'eye' });
+ !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: 'Containers ...', subitems: layoutItems, icon: 'eye' });
+ ContextMenu.Instance.setDefaultItem('::', (name: string): void => {
+ Doc.GetProto(this.props.Document)[name] = '';
+ const created = Docs.Create.TextDocument('', { title: name, _width: 250, _autoHeight: true });
+ if (created) {
+ if (this.props.Document.isTemplateDoc) {
+ Doc.MakeMetadataFieldTemplate(created, this.props.Document);
+ }
+ this.props.addDocument?.(created);
+ }
+ });
+ ContextMenu.Instance.displayMenu(x, y, undefined, true);
+ };
+
+ @computed get innards() {
+ TraceMobx();
+ const key = this.props.pivotField;
+ const heading = this._heading;
+ const columnYMargin = this.props.headingObject ? 0 : this.props.yMargin;
+ const evContents = heading ? heading : '25';
+ const headingView = this.props.headingObject ? (
+ <div
+ key={heading}
+ className="collectionNoteTakingView-sectionHeader"
+ ref={this._headerRef}
+ style={{
+ marginTop: 2 * this.props.yMargin,
+ // width: (this.props.columnWidth) /
+ // ((uniqueHeadings.length) || 1)
+ width: this.columnWidth - 20,
+ }}>
+ <div
+ className="collectionNoteTakingView-sectionHeader-subCont"
+ title={evContents === `No Value` ? `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ''}
+ style={{ background: evContents !== `No Value` ? this._color : 'inherit' }}>
+ <EditableView GetValue={() => evContents} SetValue={this.headingChanged} contents={evContents} oneLine={true} />
+ </div>
+ </div>
+ ) : null;
+ // const templatecols = `${this.props.columnWidth / this.props.numGroupColumns}px `;
+ const templatecols = `${this.columnWidth}px `;
+ const type = this.props.Document.type;
+ return (
+ <>
+ {headingView}
+ {
+ <div style={{ height: '100%' }}>
+ <div
+ key={`${heading}-stack`}
+ className={`collectionNoteTakingView-Nodes`}
+ style={{
+ padding: `${columnYMargin}px ${0}px ${this.props.yMargin}px ${0}px`,
+ margin: 'auto',
+ width: 'max-content', //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
+ height: 'max-content',
+ position: 'relative',
+ gridGap: this.props.gridGap,
+ gridTemplateColumns: templatecols,
+ gridAutoRows: '0px',
+ }}>
+ {this.props.renderChildren(this.props.docList)}
+ </div>
+
+ {!this.props.chromeHidden && type !== DocumentType.PRES ? (
+ <div
+ className="collectionNoteTakingView-DocumentButtons"
+ // style={{ width: this.props.columnWidth / this.props.numGroupColumns, marginBottom: 10 }}>
+ style={{ width: this.columnWidth - 20, marginBottom: 10 }}>
+ <div key={`${heading}-add-document`} className="collectionNoteTakingView-addDocumentButton">
+ <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.textCallback} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} />
+ </div>
+ <div key={`${this.props.Document[Id]}-addGroup`} className="collectionNoteTakingView-addDocumentButton">
+ <EditableView {...this.props.editableViewProps()} />
+ </div>
+ {this.props.columnHeaders?.length && this.props.columnHeaders.length > 1 && (
+ <button className="collectionNoteTakingView-sectionDelete" onClick={this.deleteColumn}>
+ <FontAwesomeIcon icon="trash" size="lg" />
+ </button>
+ )}
+ </div>
+ ) : null}
+ </div>
+ }
+ </>
+ );
+ }
+
+ render() {
+ TraceMobx();
+ const heading = this._heading;
+ return (
+ <div
+ className={'collectionNoteTakingViewFieldColumn' + (SnappingManager.GetIsDragging() ? 'Dragging' : '')}
+ key={heading}
+ style={{
+ //TODO: change this so that it's based on the column width
+ width: this.columnWidth,
+ background: this._background,
+ }}
+ ref={this.createColumnDropRef}
+ onPointerEnter={this.pointerEntered}
+ onPointerLeave={this.pointerLeave}>
+ {this.innards}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx
new file mode 100644
index 000000000..7d31b3193
--- /dev/null
+++ b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx
@@ -0,0 +1,63 @@
+import { action, observable } from 'mobx';
+import * as React from 'react';
+
+interface DividerProps {
+ index: number;
+ xMargin: number;
+ setColumnStartXCoords: (movementX: number, colIndex: number) => void;
+}
+
+export class CollectionNoteTakingViewDivider extends React.Component<DividerProps> {
+ @observable private isHoverActive = false;
+ @observable private isResizingActive = false;
+
+ @action
+ private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => {
+ e.stopPropagation();
+ e.preventDefault();
+ window.removeEventListener('pointermove', this.onPointerMove);
+ window.removeEventListener('pointerup', this.onPointerUp);
+ window.addEventListener('pointermove', this.onPointerMove);
+ window.addEventListener('pointerup', this.onPointerUp);
+ this.isResizingActive = true;
+ };
+
+ @action
+ private onPointerUp = () => {
+ this.isResizingActive = false;
+ this.isHoverActive = false;
+ window.removeEventListener('pointermove', this.onPointerMove);
+ window.removeEventListener('pointerup', this.onPointerUp);
+ };
+
+ @action
+ onPointerMove = ({ movementX }: PointerEvent) => {
+ this.props.setColumnStartXCoords(movementX, this.props.index);
+ };
+
+ render() {
+ return (
+ <div
+ className="columnResizer"
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ cursor: 'col-resize',
+ }}
+ onPointerEnter={action(() => (this.isHoverActive = true))}
+ onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))}>
+ <div
+ className="columnResizer-handler"
+ onPointerDown={e => this.registerResizing(e)}
+ style={{
+ height: '95%',
+ width: 12,
+ borderRight: '4px solid #282828',
+ borderLeft: '4px solid #282828',
+ margin: '0px 10px',
+ }}
+ />
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 73572299a..7385f933b 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -1,4 +1,4 @@
-@import "../global/globalCssVariables";
+@import '../global/globalCssVariables';
.collectionMasonryView {
display: inline;
@@ -33,7 +33,7 @@
color: $medium-blue;
padding: 10px;
border-radius: 5px;
- border: solid .5px $medium-blue;
+ border: solid 0.5px $medium-blue;
}
}
@@ -46,9 +46,9 @@
overflow-y: auto;
overflow-x: hidden;
flex-wrap: wrap;
- transition: top .5s;
+ transition: top 0.5s;
- >div {
+ > div {
position: relative;
display: block;
}
@@ -130,9 +130,11 @@
margin-left: -5;
}
+ // Documents in stacking view
.collectionStackingView-columnDoc {
- display: inline-block;
- margin: auto;
+ display: flex;
+ // margin: auto; // Removed auto so that it is no longer center aligned - this could be something we change
+ position: relative;
}
.collectionStackingView-masonryDoc {
@@ -173,7 +175,7 @@
span::before,
span::after {
- content: "";
+ content: '';
width: 50%;
border-top: dashed gray 1px;
position: relative;
@@ -203,6 +205,7 @@
.collectionStackingView-sectionHeader {
text-align: center;
margin: auto;
+ margin-bottom: 10px;
background: $medium-gray;
// overflow: hidden; overflow is visible so the color menu isn't hidden -ftong
@@ -213,7 +216,7 @@
.editableView-input:hover,
.editableView-container-editing:hover,
.editableView-container-editing-oneLine:hover {
- cursor: text
+ cursor: text;
}
.collectionStackingView-sectionHeader-subCont {
@@ -259,7 +262,7 @@
height: 100%;
display: none;
- [class*="css"] {
+ [class*='css'] {
max-width: 102px;
}
@@ -301,7 +304,7 @@
height: 100%;
display: none;
- [class*="css"] {
+ [class*='css'] {
max-width: 102px;
}
@@ -310,7 +313,6 @@
}
.collectionStackingView-optionPicker {
-
.optionOptions {
display: inline;
}
@@ -370,7 +372,7 @@
.editableView-input:hover,
.editableView-container-editing:hover,
.editableView-container-editing-oneLine:hover {
- cursor: text
+ cursor: text;
}
.editableView-input {
@@ -385,6 +387,7 @@
.collectionStackingView-addDocumentButton {
font-size: 75%;
letter-spacing: 2px;
+ cursor: pointer;
.editableView-input {
outline-color: black;
@@ -422,7 +425,7 @@
top: 4px;
border-radius: 50% 50%;
background-color: #fff;
- content: " ";
+ content: ' ';
cursor: pointer;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26);
-webkit-transform: scale(1);
@@ -452,7 +455,6 @@
}
@media only screen and (max-device-width: 480px) {
-
.collectionStackingView .collectionStackingView-columnDragger,
.collectionMasonryView .collectionStackingView-columnDragger {
width: 0.1;
@@ -460,4 +462,4 @@
opacity: 0;
font-size: 0;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 7e66aa844..d4efef47a 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -34,6 +34,7 @@ const _global = (window /* browser */ || global) /* node */ as any;
export type collectionStackingViewProps = {
chromeHidden?: boolean;
+ // view type is stacking
viewType?: CollectionViewType;
NativeWidth?: () => number;
NativeHeight?: () => number;
@@ -42,26 +43,39 @@ export type collectionStackingViewProps = {
@observer
export class CollectionStackingView extends CollectionSubView<Partial<collectionStackingViewProps>>() {
_masonryGridRef: HTMLDivElement | null = null;
+ // used in a column dragger, likely due for the masonry grid view. We want to use this
_draggerRef = React.createRef<HTMLDivElement>();
+ // Not sure what a pivot field is. Seems like we cause reaction in MobX get rid of it once we exit this view
_pivotFieldDisposer?: IReactionDisposer;
+ // Seems like we cause reaction in MobX get rid of our height once we exit this view
_autoHeightDisposer?: IReactionDisposer;
+ // keeping track of documents. Updated on internal and external drops. What's the difference?
_docXfs: { height: () => number; width: () => number; stackedDocTransform: () => Transform }[] = [];
+ // Doesn't look like this field is being used anywhere. Obsolete?
_columnStart: number = 0;
+ // map of node headers to their heights. Used in Masonry
@observable _heightMap = new Map<string, number>();
+ // Assuming that this is the current css cursor style
@observable _cursor: CursorProperty = 'grab';
+ // gets reset whenever we scroll. Not sure what it is
@observable _scroll = 0; // used to force the document decoration to update when scrolling
+ // does this mean whether the browser is hidden? Or is chrome something else entirely?
@computed get chromeHidden() {
return this.props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden);
}
+ // it looks like this gets the column headers that Mehek was showing just now
@computed get columnHeaders() {
return Cast(this.layoutDoc._columnHeaders, listSpec(SchemaHeaderField), null);
}
+ // Still not sure what a pivot is, but it appears that we can actually filter docs somehow?
@computed get pivotField() {
return StrCast(this.layoutDoc._pivotField);
}
+ // filteredChildren is what you want to work with. It's the list of things that you're currently displaying
@computed get filteredChildren() {
return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout);
}
+ // how much margin we give the header
@computed get headerMargin() {
return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin);
}
@@ -74,18 +88,23 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
@computed get gridGap() {
return NumCast(this.layoutDoc._gridGap, 10);
}
+ // are we stacking or masonry?
@computed get isStackingView() {
return (this.props.viewType ?? this.layoutDoc._viewType) === CollectionViewType.Stacking;
}
+ // this is the number of StackingViewFieldColumns that we have
@computed get numGroupColumns() {
return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1;
}
+ // reveals a button to add a group in masonry view
@computed get showAddAGroup() {
return this.pivotField && !this.chromeHidden;
}
+ // columnWidth handles the margin on the left and right side of the documents
@computed get columnWidth() {
return Math.min(this.props.PanelWidth() - 2 * this.xMargin, this.isStackingView ? Number.MAX_VALUE : this.layoutDoc._columnWidth === -1 ? this.props.PanelWidth() - 2 * this.xMargin : NumCast(this.layoutDoc._columnWidth, 250));
}
+
@computed get NodeWidth() {
return this.props.PanelWidth() - this.gridGap;
}
@@ -94,18 +113,26 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
super(props);
if (this.columnHeaders === undefined) {
+ // TODO: what is a layout doc? Is it literally how this document is supposed to be layed out?
+ // here we're making an empty list of column headers (again, what Mehek showed us)
this.layoutDoc._columnHeaders = new List<SchemaHeaderField>();
}
}
+ // TODO: plj - these are the children
children = (docs: Doc[]) => {
+ //TODO: can somebody explain me to what exactly TraceMobX is?
TraceMobx();
+ // appears that we are going to reset the _docXfs. TODO: what is Xfs?
this._docXfs.length = 0;
return docs.map((d, i) => {
const height = () => this.getDocHeight(d);
const width = () => this.getDocWidth(d);
+ // assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns
const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap);
+ // just getting the style
const style = this.isStackingView ? { width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` };
+ // So we're choosing whether we're going to render a column or a masonry doc
return (
<div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}>
{this.getDisplayDoc(d, width)}
@@ -118,7 +145,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
this._heightMap.set(key, sectionHeight);
};
+ // is sections that all collections inherit? I think this is how we show the masonry/columns
+ //TODO: this seems important
get Sections() {
+ // appears that pivot field IS actually for sorting
if (!this.pivotField || this.columnHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>();
if (this.columnHeaders === undefined) {
@@ -146,6 +176,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
}
});
// remove all empty columns if hideHeadings is set
+ // we will want to have something like this, so that we can hide columns and add them back in
if (this.layoutDoc._columnsHideIfEmpty) {
Array.from(fields.keys())
.filter(key => !fields.get(key)!.length)
@@ -218,6 +249,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight);
};
+ // let's dive in and get the actual document we want to drag/move around
focusDocument = (doc: Doc, options?: DocFocusOptions) => {
Doc.BrushDoc(doc);
@@ -268,8 +300,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
return this.addDocument?.(newDoc);
}
};
- isContentActive = () => this.props.isSelected() || this.props.isContentActive();
- isChildContentActive = () => (this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : undefined);
+ isContentActive = () => (this.props.isSelected() || this.props.isContentActive() ? true : this.props.isSelected() === false || this.props.isContentActive() === false ? false : undefined);
+
+ isChildContentActive = () =>
+ this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined;
+ // this is what renders the document that you see on the screen
+ // called in Children: this actually adds a document to our children list
getDisplayDoc(doc: Doc, width: () => number) {
const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.DataDoc;
const height = () => this.getDocHeight(doc);
@@ -277,6 +313,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
let dref: Opt<DocumentView>;
const stackedDocTransform = () => this.getDocTransform(doc, dref);
this._docXfs.push({ stackedDocTransform, width, height });
+ //DocumentView is how the node will be rendered
return (
<DocumentView
ref={r => (dref = r || undefined)}
@@ -357,6 +394,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
return Math.min(childHeight, maxHeight, panelHeight);
}
+ // This following three functions must be from the view Mehek showed
columnDividerDown = (e: React.PointerEvent) => {
runInAction(() => (this._cursor = 'grabbing'));
setupMoveUpEvents(
@@ -384,10 +422,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
@undoBatch
@action
onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
+ // Fairly confident that this is where the swapping of nodes in the various arrays happens
const where = [de.x, de.y];
+ // start at -1 until we're sure we want to add it to the column
let dropInd = -1;
let dropAfter = 0;
if (de.complete.docDragData) {
+ // going to re-add the docs to the _docXFs based on position of where we just dropped
this._docXfs.map((cd, i) => {
const pos = cd
.stackedDocTransform()
@@ -402,17 +443,20 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
});
const oldDocs = this.childDocs.length;
if (super.onInternalDrop(e, de)) {
+ // check to see if we actually need anything to the new column of nodes (if droppedDocs != empty)
const droppedDocs = this.childDocs.slice().filter((d: Doc, ind: number) => ind >= oldDocs); // if the drop operation adds something to the end of the list, then use that as the new document (may be different than what was dropped e.g., in the case of a button which is dropped but which creates say, a note).
const newDocs = droppedDocs.length ? droppedDocs : de.complete.docDragData.droppedDocuments; // if nothing was added to the end of the list, then presumably the dropped documents were already in the list, but possibly got reordered so we use them.
const docs = this.childDocList;
- DragManager.docsBeingDragged = [];
+ // still figuring out where to add the document
if (docs && newDocs.length) {
const insertInd = dropInd === -1 ? docs.length : dropInd + dropAfter;
const offset = newDocs.reduce((off, ndoc) => (this.filteredChildren.find((fdoc, i) => ndoc === fdoc && i < insertInd) ? off + 1 : off), 0);
newDocs.filter(ndoc => docs.indexOf(ndoc) !== -1).forEach(ndoc => docs.splice(docs.indexOf(ndoc), 1));
docs.splice(insertInd - offset, 0, ...newDocs);
}
+ // reset drag manager docs, because we just dropped
+ DragManager.docsBeingDragged.length = 0;
}
} else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) {
const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' });
@@ -433,6 +477,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
return true;
}
+ /// an item from outside of Dash is being dropped onto this stacking view (e.g, a document from the file system)
@undoBatch
@action
onExternalDrop = async (e: React.DragEvent): Promise<void> => {
@@ -461,6 +506,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
};
headings = () => Array.from(this.Sections);
refList: any[] = [];
+ // what a section looks like if we're in stacking view
sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
const key = this.pivotField;
let type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined = undefined;
@@ -512,6 +558,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
);
};
+ // what a section looks like if we're in masonry. Shouldn't actually need to use this.
sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[], first: boolean) => {
const key = this.pivotField;
let type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined = undefined;
@@ -556,6 +603,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
);
};
+ /// add a new group category (column) to the active set of note categories. (e.g., if the pivot field is 'transportation', groups might be 'car', 'plane', 'bike', etc)
@action
addGroup = (value: string) => {
if (value && this.columnHeaders) {
@@ -584,6 +632,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
}
};
+ //
@computed get renderedSections() {
TraceMobx();
let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]];
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index f3a798143..7b268cd49 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -1,31 +1,32 @@
-import React = require("react");
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import { Doc, DocListCast, Opt } from "../../../fields/Doc";
-import { RichTextField } from "../../../fields/RichTextField";
-import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField";
-import { ScriptField } from "../../../fields/ScriptField";
-import { Cast, NumCast, StrCast } from "../../../fields/Types";
-import { ImageField } from "../../../fields/URLField";
-import { TraceMobx } from "../../../fields/util";
-import { emptyFunction, setupMoveUpEvents, returnFalse, returnEmptyString } from "../../../Utils";
-import { Docs, DocUtils } from "../../documents/Documents";
-import { DocumentType } from "../../documents/DocumentTypes";
-import { DragManager } from "../../util/DragManager";
-import { SnappingManager } from "../../util/SnappingManager";
-import { Transform } from "../../util/Transform";
-import { undoBatch } from "../../util/UndoManager";
-import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import { EditableView } from "../EditableView";
-import "./CollectionStackingView.scss";
-import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox";
-import { Id } from "../../../fields/FieldSymbols";
-const higflyout = require("@hig/flyout");
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { RichTextField } from '../../../fields/RichTextField';
+import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField';
+import { ScriptField } from '../../../fields/ScriptField';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { ImageField } from '../../../fields/URLField';
+import { TraceMobx } from '../../../fields/util';
+import { emptyFunction, setupMoveUpEvents, returnFalse, returnEmptyString } from '../../../Utils';
+import { Docs, DocUtils } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { DragManager } from '../../util/DragManager';
+import { SnappingManager } from '../../util/SnappingManager';
+import { Transform } from '../../util/Transform';
+import { undoBatch } from '../../util/UndoManager';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { EditableView } from '../EditableView';
+import './CollectionStackingView.scss';
+import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import { Id } from '../../../fields/FieldSymbols';
+const higflyout = require('@hig/flyout');
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
+// So this is how we are storing a column
interface CSVFieldColumnProps {
Document: Doc;
DataDoc: Opt<Doc>;
@@ -39,8 +40,9 @@ interface CSVFieldColumnProps {
columnWidth: number;
numGroupColumns: number;
gridGap: number;
- type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined;
+ type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined;
headings: () => object[];
+ // I think that stacking view actually has a single column and then supposedly you can add more columns? Unsure
renderChildren: (docs: Doc[]) => JSX.Element[];
addDocument: (doc: Doc | Doc[]) => boolean;
createDropTarget: (ele: HTMLDivElement) => void;
@@ -51,17 +53,19 @@ interface CSVFieldColumnProps {
@observer
export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldColumnProps> {
- @observable private _background = "inherit";
+ @observable private _background = 'inherit';
private dropDisposer?: DragManager.DragDropDisposer;
private _headerRef: React.RefObject<HTMLDivElement> = React.createRef();
@observable _paletteOn = false;
@observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading;
- @observable _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
+ @observable _color = this.props.headingObject ? this.props.headingObject.color : '#f1efeb';
_ele: HTMLElement | null = null;
+ // This is likely similar to what we will be doing. Why do we need to make these refs?
+ // is that the only way to have drop targets?
createColumnDropRef = (ele: HTMLDivElement | null) => {
this.dropDisposer?.();
if (ele) {
@@ -69,12 +73,13 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
this.props.observeHeight(ele);
this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this));
}
- }
+ };
componentWillUnmount() {
this.props.unobserveHeight(this._ele);
}
+ //TODO: what is scripting? I found it in SetInPlace def but don't know what that is
@undoBatch
columnDrop = action((e: Event, de: DragManager.DropEvent) => {
const drop = { docs: de.complete.docDragData?.droppedDocuments, val: this.getValue(this._heading) };
@@ -83,10 +88,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
getValue = (value: string): any => {
const parsed = parseInt(value);
if (!isNaN(parsed)) return parsed;
- if (value.toLowerCase().indexOf("true") > -1) return true;
- if (value.toLowerCase().indexOf("false") > -1) return false;
+ if (value.toLowerCase().indexOf('true') > -1) return true;
+ if (value.toLowerCase().indexOf('false') > -1) return false;
return value;
- }
+ };
@action
headingChanged = (value: string, shiftDown?: boolean) => {
@@ -95,7 +100,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
if (this.props.columnHeaders?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) {
return false;
}
- this.props.docList.forEach(d => d[this.props.pivotField] = castedValue);
+ this.props.docList.forEach(d => (d[this.props.pivotField] = castedValue));
if (this.props.headingObject) {
this.props.headingObject.setHeading(castedValue.toString());
this._heading = this.props.headingObject.heading;
@@ -103,17 +108,17 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
return true;
}
return false;
- }
+ };
@action
changeColumnColor = (color: string) => {
this.props.headingObject?.setColor(color);
this._color = color;
- }
+ };
- @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = "#b4b4b4");
- @action pointerLeave = () => this._background = "inherit";
- textCallback = (char: string) => this.addNewTextDoc("-typed text-", false, true);
+ @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = '#b4b4b4');
+ @action pointerLeave = () => (this._background = 'inherit');
+ textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true);
@action
addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => {
@@ -121,71 +126,78 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
const key = this.props.pivotField;
const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _fitWidth: true, title: value, _autoHeight: true });
newDoc[key] = this.getValue(this.props.heading);
- const maxHeading = this.props.docList.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0);
+ const maxHeading = this.props.docList.reduce((maxHeading, doc) => (NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading), 0);
const heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3;
newDoc.heading = heading;
FormattedTextBox.SelectOnLoad = newDoc[Id];
- FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? "" : " ";
+ FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' ';
return this.props.addDocument?.(newDoc) || false;
- }
+ };
@action
deleteColumn = () => {
- this.props.docList.forEach(d => d[this.props.pivotField] = undefined);
+ this.props.docList.forEach(d => (d[this.props.pivotField] = undefined));
if (this.props.columnHeaders && this.props.headingObject) {
const index = this.props.columnHeaders.indexOf(this.props.headingObject);
this.props.columnHeaders.splice(index, 1);
}
- }
+ };
@action
collapseSection = () => {
this.props.headingObject?.setCollapsed(!this.props.headingObject.collapsed);
this.toggleVisibility();
- }
+ };
headerDown = (e: React.PointerEvent<HTMLDivElement>) => setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction);
+ //TODO: I think this is where I'm supposed to edit stuff
startDrag = (e: PointerEvent, down: number[], delta: number[]) => {
+ // is MakeAlias a way to make a copy of a doc without rendering it?
const alias = Doc.MakeAlias(this.props.Document);
alias._width = this.props.columnWidth / (this.props.columnHeaders?.length || 1);
alias._pivotField = undefined;
let value = this.getValue(this._heading);
- value = typeof value === "string" ? `"${value}"` : value;
+ value = typeof value === 'string' ? `"${value}"` : value;
alias.viewSpecScript = ScriptField.MakeFunction(`doc.${this.props.pivotField} === ${value}`, { doc: Doc.name });
if (alias.viewSpecScript) {
DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY);
return true;
}
return false;
- }
+ };
renderColorPicker = () => {
- const gray = "#f1efeb";
+ const gray = '#f1efeb';
const selected = this.props.headingObject ? this.props.headingObject.color : gray;
- const colors = ["pink2", "purple4", "bluegreen1", "yellow4", "gray", "red2", "bluegreen7", "bluegreen5", "orange1"];
- return <div className="collectionStackingView-colorPicker">
- <div className="colorOptions">
- {colors.map(col => {
- const palette = PastelSchemaPalette.get(col);
- return <div className={"colorPicker" + (selected === palette ? " active" : "")}
- style={{ backgroundColor: palette }} onClick={() => this.changeColumnColor(palette!)} />;
- })}
+ const colors = ['pink2', 'purple4', 'bluegreen1', 'yellow4', 'gray', 'red2', 'bluegreen7', 'bluegreen5', 'orange1'];
+ return (
+ <div className="collectionStackingView-colorPicker">
+ <div className="colorOptions">
+ {colors.map(col => {
+ const palette = PastelSchemaPalette.get(col);
+ return <div className={'colorPicker' + (selected === palette ? ' active' : '')} style={{ backgroundColor: palette }} onClick={() => this.changeColumnColor(palette!)} />;
+ })}
+ </div>
</div>
- </div>;
- }
+ );
+ };
renderMenu = () => {
- return <div className="collectionStackingView-optionPicker">
- <div className="optionOptions">
- <div className={"optionPicker" + (true ? " active" : "")} onClick={action(() => { })}>Add options here</div>
+ return (
+ <div className="collectionStackingView-optionPicker">
+ <div className="optionOptions">
+ <div className={'optionPicker' + (true ? ' active' : '')} onClick={action(() => {})}>
+ Add options here
+ </div>
+ </div>
</div>
- </div >;
- }
+ );
+ };
@observable private collapsed: boolean = false;
- private toggleVisibility = action(() => this.collapsed = !this.collapsed);
+ private toggleVisibility = action(() => (this.collapsed = !this.collapsed));
menuCallback = (x: number, y: number) => {
ContextMenu.Instance.clearItems();
@@ -193,42 +205,58 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
const docItems: ContextMenuProps[] = [];
const dataDoc = this.props.DataDoc || this.props.Document;
- DocUtils.addDocumentCreatorMenuItems((doc) => {
- FormattedTextBox.SelectOnLoad = doc[Id];
- return this.props.addDocument?.(doc);
- }, this.props.addDocument, x, y, true);
+ DocUtils.addDocumentCreatorMenuItems(
+ doc => {
+ FormattedTextBox.SelectOnLoad = doc[Id];
+ return this.props.addDocument?.(doc);
+ },
+ this.props.addDocument,
+ x,
+ y,
+ true
+ );
- Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof (dataDoc[fieldKey]) === "string").map(fieldKey =>
- docItems.push({
- description: ":" + fieldKey, event: () => {
- const created = DocUtils.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.Document));
- if (created) {
- if (this.props.Document.isTemplateDoc) {
- Doc.MakeMetadataFieldTemplate(created, this.props.Document);
+ Array.from(Object.keys(Doc.GetProto(dataDoc)))
+ .filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof dataDoc[fieldKey] === 'string')
+ .map(fieldKey =>
+ docItems.push({
+ description: ':' + fieldKey,
+ event: () => {
+ const created = DocUtils.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.Document));
+ if (created) {
+ if (this.props.Document.isTemplateDoc) {
+ Doc.MakeMetadataFieldTemplate(created, this.props.Document);
+ }
+ return this.props.addDocument?.(created);
}
- return this.props.addDocument?.(created);
- }
- }, icon: "compress-arrows-alt"
- }));
- Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => DocListCast(dataDoc[fieldKey]).length).map(fieldKey =>
- docItems.push({
- description: ":" + fieldKey, event: () => {
- const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey });
- if (created) {
- const container = this.props.Document.resolvedDataDoc ? Doc.GetProto(this.props.Document) : this.props.Document;
- if (container.isTemplateDoc) {
- Doc.MakeMetadataFieldTemplate(created, container);
- return Doc.AddDocToList(container, Doc.LayoutFieldKey(container), created);
+ },
+ icon: 'compress-arrows-alt',
+ })
+ );
+ Array.from(Object.keys(Doc.GetProto(dataDoc)))
+ .filter(fieldKey => DocListCast(dataDoc[fieldKey]).length)
+ .map(fieldKey =>
+ docItems.push({
+ description: ':' + fieldKey,
+ event: () => {
+ const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey });
+ if (created) {
+ const container = this.props.Document.resolvedDataDoc ? Doc.GetProto(this.props.Document) : this.props.Document;
+ if (container.isTemplateDoc) {
+ Doc.MakeMetadataFieldTemplate(created, container);
+ return Doc.AddDocToList(container, Doc.LayoutFieldKey(container), created);
+ }
+ return this.props.addDocument?.(created) || false;
}
- return this.props.addDocument?.(created) || false;
- }
- }, icon: "compress-arrows-alt"
- }));
- !Doc.noviceMode && ContextMenu.Instance.addItem({ description: "Doc Fields ...", subitems: docItems, icon: "eye" });
- !Doc.noviceMode && ContextMenu.Instance.addItem({ description: "Containers ...", subitems: layoutItems, icon: "eye" });
- ContextMenu.Instance.setDefaultItem("::", (name: string): void => {
- Doc.GetProto(this.props.Document)[name] = "";
- const created = Docs.Create.TextDocument("", { title: name, _width: 250, _autoHeight: true });
+ },
+ icon: 'compress-arrows-alt',
+ })
+ );
+ !Doc.noviceMode && ContextMenu.Instance.addItem({ description: 'Doc Fields ...', subitems: docItems, icon: 'eye' });
+ !Doc.noviceMode && ContextMenu.Instance.addItem({ description: 'Containers ...', subitems: layoutItems, icon: 'eye' });
+ ContextMenu.Instance.setDefaultItem('::', (name: string): void => {
+ Doc.GetProto(this.props.Document)[name] = '';
+ const created = Docs.Create.TextDocument('', { title: name, _width: 250, _autoHeight: true });
if (created) {
if (this.props.Document.isTemplateDoc) {
Doc.MakeMetadataFieldTemplate(created, this.props.Document);
@@ -238,9 +266,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
});
const pt = this.props.screenToLocalTransform().inverse().transformPoint(x, y);
ContextMenu.Instance.displayMenu(x, y, undefined, true);
- }
-
-
+ };
@computed get innards() {
TraceMobx();
@@ -249,39 +275,39 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
const heading = this._heading;
const columnYMargin = this.props.headingObject ? 0 : this.props.yMargin;
const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
- const evContents = heading ? heading : this.props?.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
- const headingView = this.props.headingObject ?
- <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef}
+ const evContents = heading ? heading : this.props?.type === 'number' ? '0' : `NO ${key.toUpperCase()} VALUE`;
+ const headingView = this.props.headingObject ? (
+ <div
+ key={heading}
+ className="collectionStackingView-sectionHeader"
+ ref={this._headerRef}
style={{
marginTop: this.props.yMargin,
- width: (this.props.columnWidth) /
- ((uniqueHeadings.length + (this.props.chromeHidden ? 0 : 1)) || 1)
+ width: this.props.columnWidth / (uniqueHeadings.length + (this.props.chromeHidden ? 0 : 1) || 1),
}}>
- <div className={"collectionStackingView-collapseBar" + (this.props.headingObject.collapsed === true ? " active" : "")} onClick={this.collapseSection}></div>
+ <div className={'collectionStackingView-collapseBar' + (this.props.headingObject.collapsed === true ? ' active' : '')} onClick={this.collapseSection}></div>
{/* the default bucket (no key value) has a tooltip that describes what it is.
Further, it does not have a color and cannot be deleted. */}
- <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown}
- title={evContents === `NO ${key.toUpperCase()} VALUE` ?
- `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""}
- style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "inherit" }}>
- <EditableView
- GetValue={() => evContents}
- SetValue={this.headingChanged}
- contents={evContents}
- oneLine={true}
- toggle={this.toggleVisibility} />
- {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
+ <div
+ className="collectionStackingView-sectionHeader-subCont"
+ onPointerDown={this.headerDown}
+ title={evContents === `NO ${key.toUpperCase()} VALUE` ? `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ''}
+ style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : 'inherit' }}>
+ <EditableView GetValue={() => evContents} SetValue={this.headingChanged} contents={evContents} oneLine={true} toggle={this.toggleVisibility} />
+ {evContents === `NO ${key.toUpperCase()} VALUE` ? null : (
<div className="collectionStackingView-sectionColor">
- <button className="collectionStackingView-sectionColorButton" onClick={action(e => this._paletteOn = !this._paletteOn)}>
+ <button className="collectionStackingView-sectionColorButton" onClick={action(e => (this._paletteOn = !this._paletteOn))}>
<FontAwesomeIcon icon="palette" size="lg" />
</button>
- {this._paletteOn ? this.renderColorPicker() : (null)}
+ {this._paletteOn ? this.renderColorPicker() : null}
</div>
+ )}
+ {
+ <button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}>
+ <FontAwesomeIcon icon="trash" size="lg" />
+ </button>
}
- {<button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}>
- <FontAwesomeIcon icon="trash" size="lg" />
- </button>}
- {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
+ {evContents === `NO ${key.toUpperCase()} VALUE` ? null : (
<div className="collectionStackingView-sectionOptions">
<Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}>
<button className="collectionStackingView-sectionOptionButton">
@@ -289,65 +315,76 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
</button>
</Flyout>
</div>
- }
+ )}
</div>
- </div> : (null);
+ </div>
+ ) : null;
const templatecols = `${this.props.columnWidth / this.props.numGroupColumns}px `;
const type = this.props.Document.type;
- return <>
- {this.props.Document._columnsHideIfEmpty ? (null) : headingView}
- {
- this.collapsed ? (null) :
+ return (
+ <>
+ {this.props.Document._columnsHideIfEmpty ? null : headingView}
+ {this.collapsed ? null : (
<div>
- <div key={`${heading}-stack`} className={`collectionStackingView-masonrySingle`}
+ <div
+ key={`${heading}-stack`}
+ className={`collectionStackingView-masonrySingle`}
style={{
padding: `${columnYMargin}px ${0}px ${this.props.yMargin}px ${0}px`,
- margin: "auto",
- width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
+ margin: 'auto',
+ width: 'max-content', //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
height: 'max-content',
- position: "relative",
+ position: 'relative',
gridGap: this.props.gridGap,
gridTemplateColumns: templatecols,
- gridAutoRows: "0px"
+ gridAutoRows: '0px',
}}>
{this.props.renderChildren(this.props.docList)}
</div>
- {!this.props.chromeHidden ?
- <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
- style={{ width: this.props.columnWidth / this.props.numGroupColumns, marginBottom: 10 }}>
+ {!this.props.chromeHidden && type !== DocumentType.PRES ? (
+ // TODO: this is the "new" button: see what you can work with here
+ // change cursor to pointer for this, and update dragging cursor
+ //TODO: there is a bug that occurs when adding a freeform document and trying to move it around
+ //TODO: would be great if there was additional space beyond the frame, so that you can actually see your
+ // bottom note
+ //TODO: ok, so we are using a single column, and this is it!
+ <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" style={{ width: this.props.columnWidth / this.props.numGroupColumns, marginBottom: 10, marginLeft: 25 }}>
<EditableView
GetValue={returnEmptyString}
SetValue={this.addNewTextDoc}
textCallback={this.textCallback}
- contents={"+ NEW"}
+ placeholder={"Type ':' for commands"}
+ contents={<FontAwesomeIcon icon={'plus'} />}
toggle={this.toggleVisibility}
- menuCallback={this.menuCallback} />
+ menuCallback={this.menuCallback}
+ />
</div>
- : null
- }
-
-
+ ) : null}
</div>
- }
- </>;
+ )}
+ </>
+ );
}
-
render() {
TraceMobx();
const headings = this.props.headings();
const heading = this._heading;
const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
return (
- <div className={"collectionStackingViewFieldColumn" + (SnappingManager.GetIsDragging() ? "Dragging" : "")} key={heading}
+ <div
+ className={'collectionStackingViewFieldColumn' + (SnappingManager.GetIsDragging() ? 'Dragging' : '')}
+ key={heading}
style={{
width: `${100 / (uniqueHeadings.length + (this.props.chromeHidden ? 0 : 1) || 1)}%`,
height: undefined, // DraggingManager.GetIsDragging() ? "100%" : undefined,
- background: this._background
+ background: this._background,
}}
- ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}>
+ ref={this.createColumnDropRef}
+ onPointerEnter={this.pointerEntered}
+ onPointerLeave={this.pointerLeave}>
{this.innards}
- </div >
+ </div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index f0cb23eab..1ee77d4ce 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -26,6 +26,7 @@ import { CollectionGridView } from './collectionGrid/CollectionGridView';
import { CollectionLinearView } from './collectionLinear';
import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView';
import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView';
+import { CollectionNoteTakingView } from './CollectionNoteTakingView';
import { CollectionPileView } from './CollectionPileView';
import { CollectionSchemaView } from './collectionSchema/CollectionSchemaView';
import { CollectionStackingView } from './CollectionStackingView';
@@ -125,6 +126,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
case CollectionViewType.Carousel: return <CollectionCarouselView key="collview" {...props} />;
case CollectionViewType.Carousel3D: return <CollectionCarousel3DView key="collview" {...props} />;
case CollectionViewType.Stacking: return <CollectionStackingView key="collview" {...props} />;
+ case CollectionViewType.NoteTaking: return <CollectionNoteTakingView key="collview" {...props} />;
case CollectionViewType.Masonry: return <CollectionStackingView key="collview" {...props} />;
case CollectionViewType.Time: return <CollectionTimeView key="collview" {...props} />;
case CollectionViewType.Grid: return <CollectionGridView key="collview" {...props} />;
@@ -141,6 +143,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
subItems.push({ description: 'Schema', event: () => func(CollectionViewType.Schema), icon: 'th-list' });
subItems.push({ description: 'Tree', event: () => func(CollectionViewType.Tree), icon: 'tree' });
subItems.push({ description: 'Stacking', event: () => (func(CollectionViewType.Stacking)._autoHeight = true), icon: 'ellipsis-v' });
+ subItems.push({ description: 'Notetaking', event: () => (func(CollectionViewType.NoteTaking)._autoHeight = true), icon: 'ellipsis-v' });
subItems.push({ description: 'Multicolumn', event: () => func(CollectionViewType.Multicolumn), icon: 'columns' });
subItems.push({ description: 'Multirow', event: () => func(CollectionViewType.Multirow), icon: 'columns' });
subItems.push({ description: 'Masonry', event: () => func(CollectionViewType.Masonry), icon: 'columns' });
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 2d08b1c09..ec291e72c 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -212,11 +212,11 @@ export class TabDocView extends React.Component<TabDocViewProps> {
@action
public static PinDoc(docs: Doc | Doc[], pinProps?: PinProps) {
const docList = docs instanceof Doc ? [docs] : docs;
- const batch = UndoManager.StartBatch('pinning doc');
// all docs will be added to the ActivePresentation as stored on CurrentUserUtils
const curPres = Doc.ActivePresentation;
- curPres &&
+ if (curPres) {
+ const batch = UndoManager.StartBatch('pinning doc');
docList.forEach(doc => {
// Edge Case 1: Cannot pin document to itself
if (doc === curPres) {
@@ -301,18 +301,19 @@ export class TabDocView extends React.Component<TabDocViewProps> {
PresBox.Instance?._selectedArray.clear();
pinDoc && PresBox.Instance?._selectedArray.set(pinDoc, undefined); //Update selected array
});
- if (
- CollectionDockingView.Instance &&
- !Array.from(CollectionDockingView.Instance.tabMap)
- .map(d => d.DashDoc)
- .includes(curPres)
- ) {
- const docs = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), []);
- if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1);
- CollectionDockingView.AddSplit(curPres, 'right');
- setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), false, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things
+ if (
+ CollectionDockingView.Instance &&
+ !Array.from(CollectionDockingView.Instance.tabMap)
+ .map(d => d.DashDoc)
+ .includes(curPres)
+ ) {
+ const docs = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), []);
+ if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1);
+ CollectionDockingView.AddSplit(curPres, 'right');
+ setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), false, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things
+ }
+ setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs
}
- setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs
}
componentDidMount() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index a0ebe4cdc..3d85d32a0 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -393,8 +393,10 @@ function normalizeResults(
0
);
aggBounds.r = aggBounds.x + Math.max(minWidth, aggBounds.r - aggBounds.x);
- const wscale = panelDim[0] / (aggBounds.r - aggBounds.x);
- let scale = wscale * (aggBounds.b - aggBounds.y) > panelDim[1] ? panelDim[1] / (aggBounds.b - aggBounds.y) : wscale;
+ const width = aggBounds.r - aggBounds.x === 0 ? 1 : aggBounds.r - aggBounds.x;
+ const height = aggBounds.b - aggBounds.y === 0 ? 1 : aggBounds.b - aggBounds.y;
+ const wscale = panelDim[0] / width;
+ let scale = wscale * height > panelDim[1] ? panelDim[1] / height : wscale;
if (Number.isNaN(scale)) scale = 1;
Array.from(docMap.entries())
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index d979ef961..bf9de6760 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -195,7 +195,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
const bDocBounds = (B.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 };
const aleft = this.visibleX(adiv);
const bleft = this.visibleX(bdiv);
- const clipped = aleft !== a.left || atop !== a.top || bleft !== b.left || btop !== b.top;
+ const aclipped = aleft !== a.left || atop !== a.top;
+ const bclipped = bleft !== b.left || btop !== b.top;
+ if (aclipped && bclipped) return undefined;
+ const clipped = aclipped || bclipped;
const pt1 = [aleft + a.width / 2, atop + a.height / 2];
const pt2 = [bleft + b.width / 2, btop + b.width / 2];
const pt1vec = [(bDocBounds.left + bDocBounds.right) / 2 - pt1[0], (bDocBounds.top + bDocBounds.bottom) / 2 - pt1[1]];
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 8720c9097..b8344dc0c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -5,11 +5,14 @@ import { DocumentManager } from '../../../util/DocumentManager';
import './CollectionFreeFormLinksView.scss';
import { CollectionFreeFormLinkView } from './CollectionFreeFormLinkView';
import React = require('react');
+import { LightboxView } from '../../LightboxView';
@observer
export class CollectionFreeFormLinksView extends React.Component<React.PropsWithChildren<{}>> {
@computed get uniqueConnections() {
- return Array.from(new Set(DocumentManager.Instance.LinkedDocumentViews)).map(c => <CollectionFreeFormLinkView key={c.l[Id]} A={c.a} B={c.b} LinkDocs={[c.l]} />);
+ return Array.from(new Set(DocumentManager.Instance.LinkedDocumentViews))
+ .filter(c => !LightboxView.LightboxDoc || (LightboxView.IsLightboxDocView(c.a.docViewPath) && LightboxView.IsLightboxDocView(c.b.docViewPath)))
+ .map(c => <CollectionFreeFormLinkView key={c.l[Id]} A={c.a} B={c.b} LinkDocs={[c.l]} />);
}
render() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3e938ec1c..82b377dfa 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -496,7 +496,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const currentCol = DocListCast(this.rootDoc.currentInkDoc);
const rootDocList = DocListCast(this.rootDoc.data);
currentCol.push(rootDocList[rootDocList.length - 1]);
- console.log(currentCol);
this._batch?.end();
}
diff --git a/src/client/views/linking/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss
index 1d6496d3c..b0ee4e46d 100644
--- a/src/client/views/linking/LinkEditor.scss
+++ b/src/client/views/linking/LinkEditor.scss
@@ -1,10 +1,11 @@
-@import "../global/globalCssVariables";
+@import '../global/globalCssVariables';
.linkEditor {
width: 100%;
height: auto;
font-size: 13px; // TODO
user-select: none;
+ max-width: 280px;
}
.linkEditor-button-back {
@@ -20,19 +21,20 @@
}
.linkEditor-info {
- //border-bottom: 0.5px solid $light-gray;
- //padding-bottom: 1px;
- padding: 12px;
+ padding-top: 12px;
padding-left: 5px;
+ padding-bottom: 3px;
//margin-bottom: 6px;
display: flex;
justify-content: space-between;
color: black;
.linkEditor-linkedTo {
- width: calc(100% - 26px);
- padding-left: 5px;
- padding-right: 5px;
+ width: calc(100% - 46px);
+ overflow: hidden;
+ position: relative;
+ text-overflow: ellipsis;
+ white-space: pre;
.linkEditor-downArrow {
&:hover {
@@ -77,11 +79,14 @@
width: 20px;
}
}
+.linkEditor-deleteBtn {
+ padding-left: 3px;
+}
.linkEditor-description {
padding-left: 26px;
- padding-right: 6.5px;
padding-bottom: 3.5px;
+ display: flex;
.linkEditor-description-label {
text-decoration-color: black;
@@ -96,7 +101,6 @@
//border: 1px solid grey;
//border-radius: 4px;
padding-left: 2px;
- padding-right: 2px;
//margin-right: 4px;
color: black;
text-decoration-color: grey;
@@ -104,17 +108,13 @@
.linkEditor-description-add-button {
display: inline;
- /* float: right; */
border-radius: 7px;
font-size: 9px;
background: black;
- /* padding: 3px; */
- padding-top: 4px;
- padding-left: 7px;
- padding-bottom: 4px;
- padding-right: 8px;
height: 80%;
color: white;
+ padding: 3px;
+ margin-left: 3px;
&:hover {
cursor: pointer;
@@ -146,6 +146,7 @@
padding-left: 26px;
padding-right: 6.5px;
padding-bottom: 15px;
+ display: flex;
&:hover {
cursor: pointer;
@@ -153,12 +154,11 @@
.linkEditor-followingDropdown-label {
color: black;
+ padding-right: 3px;
}
.linkEditor-followingDropdown-dropdown {
-
.linkEditor-followingDropdown-header {
-
border: 1px solid grey;
border-radius: 4px;
//background-color: rgb(236, 236, 236);
@@ -195,27 +195,25 @@
background-color: rgb(187, 220, 231);
}
}
-
}
}
-
-
}
-
-
-
-
-
.linkEditor-button,
.linkEditor-addbutton {
- width: 18px;
- height: 18px;
- padding: 0;
- // font-size: 12px;
- border-radius: 10px;
-
-
+ width: 15%;
+ border-radius: 7px;
+ font-size: 9px;
+ background: black;
+ padding: 3px;
+ height: 80%;
+ color: white;
+ text-align: center;
+ margin: auto;
+ margin-left: 3px;
+ > svg {
+ margin: auto;
+ }
&:disabled {
background-color: gray;
}
@@ -270,7 +268,6 @@
}
}
-
.linkEditor-dropdown {
width: 100%;
position: relative;
@@ -334,4 +331,4 @@
.linkEditor-button {
margin-left: 3px;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index ba301962b..1697062f4 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -3,32 +3,27 @@ import { Tooltip } from '@material-ui/core';
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, NumListCast, StrListCast, Field } from '../../../fields/Doc';
-import { DateCast, StrCast, Cast } from '../../../fields/Types';
+import { DateCast, StrCast, Cast, BoolCast } from '../../../fields/Types';
import { LinkManager } from '../../util/LinkManager';
import { undoBatch } from '../../util/UndoManager';
import './LinkEditor.scss';
import { LinkRelationshipSearch } from './LinkRelationshipSearch';
import React = require('react');
+import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
interface LinkEditorProps {
sourceDoc: Doc;
linkDoc: Doc;
- showLinks: () => void;
+ showLinks?: () => void;
hideback?: boolean;
}
@observer
export class LinkEditor extends React.Component<LinkEditorProps> {
@observable description = Field.toString(LinkManager.currentLink?.description as any as Field);
@observable relationship = StrCast(LinkManager.currentLink?.linkRelationship);
- @observable zoomFollow = StrCast(this.props.sourceDoc.followLinkZoom);
+ @observable zoomFollow = BoolCast(this.props.sourceDoc.followLinkZoom);
+ @observable audioFollow = BoolCast(this.props.sourceDoc.followLinkAudio);
@observable openDropdown: boolean = false;
- @observable showInfo: boolean = false;
- @computed get infoIcon() {
- if (this.showInfo) {
- return 'chevron-up';
- }
- return 'chevron-down';
- }
@observable private buttonColor: string = '';
@observable private relationshipButtonColor: string = '';
@observable private relationshipSearchVisibility: string = 'none';
@@ -37,12 +32,6 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
//@observable description = this.props.linkDoc.description ? StrCast(this.props.linkDoc.description) : "DESCRIPTION";
@undoBatch
- deleteLink = (): void => {
- LinkManager.Instance.deleteLink(this.props.linkDoc);
- this.props.showLinks();
- };
-
- @undoBatch
setRelationshipValue = action((value: string) => {
if (LinkManager.currentLink) {
const prevRelationship = LinkManager.currentLink.linkRelationship as string;
@@ -159,8 +148,12 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
this.relationship = e.target.value;
};
@action
- handleZoomFollowChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- this.props.sourceDoc.followLinkZoom = e.target.checked;
+ handleZoomFollowChange = () => {
+ this.props.sourceDoc.followLinkZoom = !this.props.sourceDoc.followLinkZoom;
+ };
+ @action
+ handleAudioFollowChange = () => {
+ this.props.sourceDoc.followLinkAudio = !this.props.sourceDoc.followLinkAudio;
};
@action
handleRelationshipSearchChange = (result: string) => {
@@ -173,7 +166,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
//NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS
return (
<div className="linkEditor-description">
- <div className="linkEditor-description-label">Link Relationship:</div>
+ <div className="linkEditor-description-label">Relationship:</div>
<div className="linkEditor-description-input">
<div className="linkEditor-description-editing">
<input
@@ -203,7 +196,23 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
<div className="linkEditor-zoomFollow-label">Zoom To Link Target:</div>
<div className="linkEditor-zoomFollow-input">
<div className="linkEditor-zoomFollow-editing">
- <input style={{ width: '100%' }} type="checkbox" value={this.zoomFollow} onChange={this.handleZoomFollowChange} />
+ <input type="checkbox" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.handleZoomFollowChange)} defaultChecked={this.zoomFollow} />
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ @computed
+ get editAudioFollow() {
+ //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS
+ console.log('AudioFollow:' + this.audioFollow);
+ return (
+ <div className="linkEditor-zoomFollow">
+ <div className="linkEditor-zoomFollow-label">Play Target Audio:</div>
+ <div className="linkEditor-zoomFollow-input">
+ <div className="linkEditor-zoomFollow-editing">
+ <input type="checkbox" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.handleAudioFollowChange)} defaultChecked={this.audioFollow} />
</div>
</div>
</div>
@@ -214,7 +223,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
get editDescription() {
return (
<div className="linkEditor-description">
- <div className="linkEditor-description-label">Link Description:</div>
+ <div className="linkEditor-description-label">Description:</div>
<div className="linkEditor-description-input">
<div className="linkEditor-description-editing">
<input style={{ width: '100%' }} autoComplete={'off'} id="input" value={this.description} placeholder={'Enter link description'} onKeyDown={this.onDescriptionKey} onChange={this.handleDescriptionChange}></input>
@@ -284,68 +293,100 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
);
}
- @action
- changeInfo = () => {
- this.showInfo = !this.showInfo;
+ autoMove = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkAutoMove = !this.props.linkDoc.linkAutoMove))));
+ };
+
+ showAnchor = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.hidden = !this.props.linkDoc.hidden))));
+ };
+
+ showLink = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkDisplay = !this.props.linkDoc.linkDisplay))));
+ };
+
+ deleteLink = (e: React.PointerEvent): void => {
+ setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this.props.linkDoc))));
};
render() {
const destination = LinkManager.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
return !destination ? null : (
- <div className="linkEditor" tabIndex={0} onKeyDown={e => e.stopPropagation()}>
+ <div className="linkEditor" tabIndex={0} onKeyDown={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
<div className="linkEditor-info">
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">Return to link menu</div>
- </>
- }
- placement="top">
- <button className="linkEditor-button-back" style={{ display: this.props.hideback ? 'none' : '' }} onClick={this.props.showLinks}>
- <FontAwesomeIcon icon="arrow-left" size="sm" />{' '}
- </button>
- </Tooltip>
+ {!this.props.showLinks ? null : (
+ <Tooltip title={<div className="dash-tooltip">Return to link menu</div>} placement="top">
+ <button className="linkEditor-button-back" style={{ display: this.props.hideback ? 'none' : '' }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.props.showLinks?.())}>
+ <FontAwesomeIcon icon="arrow-left" size="sm" />{' '}
+ </button>
+ </Tooltip>
+ )}
<p className="linkEditor-linkedTo">
Editing Link to: <b>{StrCast(destination.proto?.title, StrCast(destination.title, 'untitled'))}</b>
</p>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">Show more link information</div>
- </>
- }
- placement="top">
- <div className="linkEditor-downArrow">
- <FontAwesomeIcon className="button" icon={this.infoIcon} size="lg" onPointerDown={this.changeInfo} />
+ <Tooltip title={<div className="dash-tooltip">Delete Link</div>}>
+ <div className="linkEditor-deleteBtn" onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}>
+ <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" />
</div>
</Tooltip>
</div>
- {this.showInfo ? (
- <div className="linkEditor-moreInfo">
- <div>
- {this.props.linkDoc.author ? (
- <div>
- {' '}
- <b>Author:</b> {StrCast(this.props.linkDoc.author)}
- </div>
- ) : null}
- </div>
- <div>
- {this.props.linkDoc.creationDate ? (
- <div>
- {' '}
- <b>Creation Date:</b>
- {DateCast(this.props.linkDoc.creationDate).toString()}
- </div>
- ) : null}
- </div>
- </div>
- ) : null}
+ <div className="linkEditor-moreInfo">
+ {this.props.linkDoc.author ? (
+ <>
+ {' '}
+ <b>Author:</b> {StrCast(this.props.linkDoc.author)}
+ </>
+ ) : null}
+ {this.props.linkDoc.creationDate ? (
+ <>
+ {' '}
+ <b>Creation Date:</b>
+ {DateCast(this.props.linkDoc.creationDate).toString()}
+ </>
+ ) : null}
+ </div>
{this.editDescription}
{this.editRelationship}
{this.editZoomFollow}
+ {this.editAudioFollow}
+ <div className="linkEditor-description">
+ Show Anchor:
+ <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.hidden ? 'Show Link Anchor' : 'Hide Link Anchor'}</div>}>
+ <div
+ className="linkEditor-button"
+ style={{ background: this.props.linkDoc.hidden ? 'gray' : '#4476f7' /* $medium-blue */ }}
+ onPointerDown={this.showAnchor}
+ onClick={e => e.stopPropagation()}>
+ <FontAwesomeIcon className="fa-icon" icon={'eye'} size="sm" />
+ </div>
+ </Tooltip>
+ </div>
+ <div className="linkEditor-description">
+ Show Link Line:
+ <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.linkDisplay ? 'Hide Link Line' : 'Show Link Line'}</div>}>
+ <div
+ className="linkEditor-button"
+ style={{ background: this.props.linkDoc.hidden ? 'gray' : this.props.linkDoc.linkDisplay ? '#4476f7' /* $medium-blue */ : '' }}
+ onPointerDown={this.showLink}
+ onClick={e => e.stopPropagation()}>
+ <FontAwesomeIcon className="fa-icon" icon={'project-diagram'} size="sm" />
+ </div>
+ </Tooltip>
+ </div>
+ <div className="linkEditor-description">
+ Freeze Anchor:
+ <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.linkAutoMove ? 'Click to freeze link anchor position' : 'Click to auto move link anchor'}</div>}>
+ <div
+ className="linkEditor-button"
+ style={{ background: this.props.linkDoc.hidden ? 'gray' : this.props.linkDoc.linkAutoMove ? '' : '#4476f7' /* $medium-blue */ }}
+ onPointerDown={this.autoMove}
+ onClick={e => e.stopPropagation()}>
+ <FontAwesomeIcon className="fa-icon" icon={'play'} size="sm" />
+ </div>
+ </Tooltip>
+ </div>
{this.followingDropdown}
</div>
);
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index ed856a4ab..3f9db2612 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -6,14 +6,12 @@ import { observer } from 'mobx-react';
import { Doc, DocListCast } from '../../../fields/Doc';
import { Cast, StrCast } from '../../../fields/Types';
import { WebField } from '../../../fields/URLField';
-import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, setupMoveUpEvents } from '../../../Utils';
import { DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from '../../util/DragManager';
-import { Hypothesis } from '../../util/HypothesisUtils';
import { LinkFollower } from '../../util/LinkFollower';
import { LinkManager } from '../../util/LinkManager';
-import { undoBatch } from '../../util/UndoManager';
import { DocumentView } from '../nodes/DocumentView';
import { LinkDocPreview } from '../nodes/LinkDocPreview';
import './LinkMenuItem.scss';
@@ -125,33 +123,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
);
};
- deleteLink = (e: React.PointerEvent): void => {
- setupMoveUpEvents(
- this,
- e,
- returnFalse,
- emptyFunction,
- undoBatch(
- action(() => {
- this.props.linkDoc.linksToAnnotation && Hypothesis.deleteLink(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
- LinkManager.Instance.deleteLink(this.props.linkDoc);
- })
- )
- );
- };
-
- autoMove = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkAutoMove = !this.props.linkDoc.linkAutoMove))));
- };
-
- showLink = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkDisplay = !this.props.linkDoc.linkDisplay))));
- };
-
- showAnchor = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.hidden = !this.props.linkDoc.hidden))));
- };
-
render() {
const destinationIcon = Doc.toIcon(this.props.destinationDoc) as any as IconProp;
@@ -183,7 +154,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
linkSrc: this.props.sourceDoc,
linkDoc: this.props.linkDoc,
showHeader: false,
- location: [e.clientX, e.clientY + 20],
+ location: [this._drag.current?.getBoundingClientRect().right ?? 100, this._drag.current?.getBoundingClientRect().top ?? e.clientY],
})
}
onPointerDown={this.onLinkButtonDown}>
@@ -206,69 +177,11 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
</div>
<div className="linkMenu-item-buttons" ref={this._buttonRef}>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{this.props.linkDoc.hidden ? 'Show Link Anchor' : 'Hide Link Anchor'}</div>
- </>
- }>
- <div className="button" ref={this._editRef} style={{ background: this.props.linkDoc.hidden ? '' : '#4476f7' /* $medium-blue */ }} onPointerDown={this.showAnchor} onClick={e => e.stopPropagation()}>
- <FontAwesomeIcon className="fa-icon" icon={'eye'} size="sm" />
- </div>
- </Tooltip>
-
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{this.props.linkDoc.linkDisplay ? 'Hide Link Line' : 'Show Link Line'}</div>
- </>
- }>
- <div
- className="button"
- ref={this._editRef}
- style={{ background: this.props.linkDoc.hidden ? 'gray' : this.props.linkDoc.linkDisplay ? '#4476f7' /* $medium-blue */ : '' }}
- onPointerDown={this.showLink}
- onClick={e => e.stopPropagation()}>
- <FontAwesomeIcon className="fa-icon" icon={'project-diagram'} size="sm" />
- </div>
- </Tooltip>
-
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{this.props.linkDoc.linkAutoMove ? 'Click to freeze link anchor position' : 'Click to auto move link anchor'}</div>
- </>
- }>
- <div
- className="button"
- ref={this._editRef}
- style={{ background: this.props.linkDoc.hidden ? 'gray' : !this.props.linkDoc.linkAutoMove ? '' : '#4476f7' /* $medium-blue */ }}
- onPointerDown={this.autoMove}
- onClick={e => e.stopPropagation()}>
- <FontAwesomeIcon className="fa-icon" icon={'play'} size="sm" />
- </div>
- </Tooltip>
-
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">Edit Link</div>
- </>
- }>
+ <Tooltip title={<div className="dash-tooltip">Edit Link</div>}>
<div className="button" ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}>
<FontAwesomeIcon className="fa-icon" icon="edit" size="sm" />
</div>
</Tooltip>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">Delete Link</div>
- </>
- }>
- <div className="button" onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}>
- <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" />
- </div>
- </Tooltip>
</div>
</div>
</div>
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 5939d1680..a37de7f69 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -307,7 +307,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
<div
className="documentLinksButton-wrapper"
style={{
- transform: this.props.InMenu ? undefined : `scale(${this.props.scaling})`,
+ transform: `scale(${this.props.scaling?.() || 1})`,
}}>
{(this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink)) || (!DocumentLinksButton.LinkEditorDocView && !this.props.InMenu) ? (
<Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip>
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index c02692bfb..6ea697a2f 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -65,11 +65,13 @@
width: 25px;
height: 25;
position: absolute;
- top: 10px;
- left: 10px;
+ top: 0;
+ left: 50%;
border-radius: 25px;
background: white;
opacity: 0.3;
+ pointer-events: all;
+ cursor: default;
svg {
width: 90% !important;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index edaa40bad..f87581875 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -10,7 +10,7 @@ import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { AudioField } from '../../../fields/URLField';
import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util';
import { MobileInterface } from '../../../mobile/MobileInterface';
@@ -52,6 +52,8 @@ import { RadialMenu } from './RadialMenu';
import { ScriptingBox } from './ScriptingBox';
import { PresBox } from './trails/PresBox';
import React = require('react');
+import { DictationManager } from '../../util/DictationManager';
+import { Tooltip } from '@material-ui/core';
const { Howl } = require('howler');
interface Window {
@@ -156,6 +158,7 @@ export interface DocumentViewSharedProps {
scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document
createNewFilterDoc?: () => void;
updateFilterDoc?: (doc: Doc) => void;
+ dontHideOnDrag?: boolean;
}
// these props are specific to DocuentViews
@@ -203,7 +206,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered.
_animateScaleTime = 300; // milliseconds;
@observable _animateScalingTo = 0;
- @observable _mediaState = 0;
@observable _pendingDoubleClick = false;
private _disposers: { [name: string]: IReactionDisposer } = {};
private _downX: number = 0;
@@ -502,9 +504,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
dragData.treeViewDoc = this.props.treeViewDoc;
dragData.removeDocument = this.props.removeDocument;
dragData.moveDocument = this.props.moveDocument;
+ //dragData.dimSource :
+ // dragEffects field, set dim
+ // add kv pairs to a doc, swap properties with the node while dragging, and then swap when dropping
+ // add a dragEffects prop to DocumentView as a function that sets up. Each view has its own prop, when you start dragging:
+ // in Draganager, figure out which doc(s) you're dragging and change what opacity function returns
const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView()));
- DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart) }, () => setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))); // this needs to happen after the drop event is processed.
+ DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) }, () =>
+ setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))
+ ); // this needs to happen after the drop event is processed.
ffview?.setupDragLines(false);
}
}
@@ -869,7 +878,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
!appearance && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' });
if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) {
- !Doc.noviceMode && appearanceItems.splice(0, 0, { description: `${!this.layoutDoc._showAudio ? 'Show' : 'Hide'} Audio Button`, event: action(() => (this.layoutDoc._showAudio = !this.layoutDoc._showAudio)), icon: 'microphone' });
const existingOnClick = cm.findByDescription('OnClick...');
const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : [];
@@ -997,16 +1005,21 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
linkButtonInverseScaling = () => (this.props.NativeDimScaling?.() || 1) * this.props.DocumentView().screenToLocalTransform().Scale;
@computed get contents() {
TraceMobx();
- const audioView = !this.layoutDoc._showAudio ? null : (
- <div className="documentView-audioBackground" onPointerDown={this.recordAudioAnnotation} onPointerEnter={this.onPointerEnter}>
- <FontAwesomeIcon
- className="documentView-audioFont"
- style={{ color: [DocListCast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations']).length ? 'blue' : 'gray', 'green', 'red'][this._mediaState] }}
- icon={!DocListCast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations']).length ? 'microphone' : 'file-audio'}
- size="sm"
- />
- </div>
- );
+ const audioAnnosCount = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null)?.length;
+ const audioTextAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations-text'], listSpec('string'), null);
+ const audioView =
+ (!this.props.isSelected() && !this._isHovering && this.dataDoc.audioAnnoState !== 2) || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (!audioAnnosCount && !this.dataDoc.audioAnnoState) ? null : (
+ <Tooltip title={<div>{audioTextAnnos?.lastElement()}</div>}>
+ <div className="documentView-audioBackground" onPointerDown={this.playAnnotation}>
+ <FontAwesomeIcon
+ className="documentView-audioFont"
+ style={{ color: [audioAnnosCount ? 'blue' : 'gray', 'green', 'red'][NumCast(this.dataDoc.audioAnnoState)] }}
+ icon={!audioAnnosCount ? 'microphone' : 'file-audio'}
+ size="sm"
+ />
+ </div>
+ </Tooltip>
+ );
return (
<div
@@ -1062,7 +1075,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
<DocumentLinksButton
View={this.props.DocumentView()}
scaling={this.linkButtonInverseScaling}
- Offset={[this.topMost ? 0 : !this.props.isSelected() ? -15 : -30, undefined, undefined, this.topMost ? 10 : !this.props.isSelected() ? -15 : -30]}
+ Offset={[this.topMost ? 0 : !this.props.isSelected() ? -15 : -36, undefined, undefined, this.topMost ? 10 : !this.props.isSelected() ? -15 : -28]}
/>
)}
{audioView}
@@ -1140,58 +1153,72 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
@action
- onPointerEnter = () => {
+ playAnnotation = () => {
const self = this;
- const audioAnnos = DocListCast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations']);
- if (audioAnnos && audioAnnos.length && this._mediaState === 0) {
- const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)];
- anno.data instanceof AudioField &&
- new Howl({
- src: [anno.data.url.href],
- format: ['mp3'],
- autoplay: true,
- loop: false,
- volume: 0.5,
- onend: function () {
- runInAction(() => (self._mediaState = 0));
- },
- });
- this._mediaState = 1;
+ const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null);
+ const anno = audioAnnos.lastElement();
+ if (anno instanceof AudioField && this.dataDoc.audioAnnoState === 0) {
+ new Howl({
+ src: [anno.url.href],
+ format: ['mp3'],
+ autoplay: true,
+ loop: false,
+ volume: 0.5,
+ onend: function () {
+ runInAction(() => {
+ self.dataDoc.audioAnnoState = 0;
+ });
+ },
+ });
+ this.dataDoc.audioAnnoState = 1;
}
};
- recordAudioAnnotation = () => {
+
+ static recordAudioAnnotation(dataDoc: Doc, field: string, onEnd?: () => void) {
let gumStream: any;
let recorder: any;
- const self = this;
navigator.mediaDevices
.getUserMedia({
audio: true,
})
.then(function (stream) {
+ let audioTextAnnos = Cast(dataDoc[field + '-audioAnnotations-text'], listSpec('string'), null);
+ if (audioTextAnnos) audioTextAnnos.push('');
+ else audioTextAnnos = dataDoc[field + '-audioAnnotations-text'] = new List<string>(['']);
+ DictationManager.Controls.listen({
+ interimHandler: value => (audioTextAnnos[audioTextAnnos.length - 1] = value),
+ continuous: { indefinite: false },
+ }).then(results => {
+ if (results && [DictationManager.Controls.Infringed].includes(results)) {
+ DictationManager.Controls.stop();
+ }
+ onEnd?.();
+ });
+
gumStream = stream;
recorder = new MediaRecorder(stream);
recorder.ondataavailable = async (e: any) => {
const [{ result }] = await Networking.UploadFilesToServer(e.data);
if (!(result instanceof Error)) {
- const audioDoc = Docs.Create.AudioDocument(result.accessPaths.agnostic.client, { title: 'audio test', _width: 200, _height: 32 });
- audioDoc.treeViewExpandedView = 'layout';
- const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'], listSpec(Doc));
+ const audioField = new AudioField(result.accessPaths.agnostic.client);
+ const audioAnnos = Cast(dataDoc[field + '-audioAnnotations'], listSpec(AudioField), null);
if (audioAnnos === undefined) {
- self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'] = new List([audioDoc]);
+ dataDoc[field + '-audioAnnotations'] = new List([audioField]);
} else {
- audioAnnos.push(audioDoc);
+ audioAnnos.push(audioField);
}
}
};
- runInAction(() => (self._mediaState = 2));
+ runInAction(() => (dataDoc.audioAnnoState = 2));
recorder.start();
setTimeout(() => {
recorder.stop();
- runInAction(() => (self._mediaState = 0));
+ DictationManager.Controls.stop(false);
+ runInAction(() => (dataDoc.audioAnnoState = 0));
gumStream.getAudioTracks()[0].stop();
}, 5000);
});
- };
+ }
captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption');
@computed get innards() {
@@ -1290,6 +1317,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
isHovering = () => this._isHovering;
@observable _isHovering = false;
@observable _: string = '';
+ _hoverTimeout: any = undefined;
@computed get renderDoc() {
TraceMobx();
const thumb = ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png');
@@ -1300,8 +1328,17 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
<div
className={`documentView-node${this.topMost ? '-topmost' : ''}`}
id={this.props.Document[Id]}
- onPointerEnter={action(() => (this._isHovering = true))}
- onPointerLeave={action(() => (this._isHovering = false))}
+ onPointerEnter={action(() => {
+ clearTimeout(this._hoverTimeout);
+ this._isHovering = true;
+ })}
+ onPointerLeave={action(() => {
+ clearTimeout(this._hoverTimeout);
+ this._hoverTimeout = setTimeout(
+ action(() => (this._isHovering = false)),
+ 500
+ );
+ })}
style={{
background: isButton || thumb ? undefined : this.backgroundColor,
opacity: this.opacity,
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
index ff04a293c..dc3fc0396 100644
--- a/src/client/views/nodes/FilterBox.tsx
+++ b/src/client/views/nodes/FilterBox.tsx
@@ -116,7 +116,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
@observable _loaded = false;
componentDidMount() {
reaction(
- () => DocListCastAsync(this.layoutDoc.data),
+ () => DocListCastAsync(this.layoutDoc[this.fieldKey]),
async activeTabsAsync => {
const activeTabs = await activeTabsAsync;
activeTabs && (await SearchBox.foreachRecursiveDocAsync(activeTabs, emptyFunction));
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 3ab0a3ff2..15d0f88f6 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -1,4 +1,4 @@
-import functionPlot from "function-plot";
+import functionPlot from 'function-plot';
import { action, computed, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -12,7 +12,6 @@ import { Docs } from '../../documents/Documents';
import { ViewBoxBaseComponent } from '../DocComponent';
import { FieldView, FieldViewProps } from './FieldView';
-
const EquationSchema = createSchema({});
type EquationDocument = makeInterface<[typeof EquationSchema, typeof documentSchema]>;
@@ -20,74 +19,83 @@ const EquationDocument = makeInterface(EquationSchema, documentSchema);
@observer
export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FunctionPlotBox, fieldKey); }
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(FunctionPlotBox, fieldKey);
+ }
public static GraphCount = 0;
_plot: any;
- _plotId = "";
+ _plotId = '';
_plotEle: any;
constructor(props: any) {
super(props);
- this._plotId = "graph" + FunctionPlotBox.GraphCount++;
+ this._plotId = 'graph' + FunctionPlotBox.GraphCount++;
}
componentDidMount() {
this.props.setContentView?.(this);
- reaction(() => [DocListCast(this.dataDoc.data).lastElement()?.text, this.layoutDoc.width, this.layoutDoc.height, this.dataDoc.xRange, this.dataDoc.yRange],
- () => this.createGraph());
+ reaction(
+ () => [DocListCast(this.dataDoc[this.fieldKey]).lastElement()?.text, this.layoutDoc.width, this.layoutDoc.height, this.dataDoc.xRange, this.dataDoc.yRange],
+ () => this.createGraph()
+ );
}
getAnchor = () => {
const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc });
anchor.xRange = new List<number>(Array.from(this._plot.options.xAxis.domain));
anchor.yRange = new List<number>(Array.from(this._plot.options.yAxis.domain));
return anchor;
- }
+ };
@action
scrollFocus = (doc: Doc, smooth: boolean) => {
- this.dataDoc.xRange = new List<number>(Array.from(Cast(doc.xRange, listSpec("number"), Cast(this.dataDoc.xRange, listSpec("number"), [-10, 10]))));
- this.dataDoc.yRange = new List<number>(Array.from(Cast(doc.yRange, listSpec("number"), Cast(this.dataDoc.xRange, listSpec("number"), [-1, 9]))));
+ this.dataDoc.xRange = new List<number>(Array.from(Cast(doc.xRange, listSpec('number'), Cast(this.dataDoc.xRange, listSpec('number'), [-10, 10]))));
+ this.dataDoc.yRange = new List<number>(Array.from(Cast(doc.yRange, listSpec('number'), Cast(this.dataDoc.xRange, listSpec('number'), [-1, 9]))));
return 0;
- }
+ };
createGraph = (ele?: HTMLDivElement) => {
this._plotEle = ele || this._plotEle;
const width = this.props.PanelWidth();
const height = this.props.PanelHeight();
- const fn = StrCast(DocListCast(this.dataDoc.data).lastElement()?.text, "x^2").replace(/\\frac\{(.*)\}\{(.*)\}/, "($1/$2)");
+ const fn = StrCast(DocListCast(this.dataDoc.data).lastElement()?.text, 'x^2').replace(/\\frac\{(.*)\}\{(.*)\}/, '($1/$2)');
try {
this._plot = functionPlot({
- target: "#" + this._plotEle.id,
+ target: '#' + this._plotEle.id,
width,
height,
- xAxis: { domain: Cast(this.dataDoc.xRange, listSpec("number"), [-10, 10]) },
- yAxis: { domain: Cast(this.dataDoc.xRange, listSpec("number"), [-1, 9]) },
+ xAxis: { domain: Cast(this.dataDoc.xRange, listSpec('number'), [-10, 10]) },
+ yAxis: { domain: Cast(this.dataDoc.xRange, listSpec('number'), [-1, 9]) },
grid: true,
data: [
{
fn,
// derivative: { fn: "2 * x", updateOnMouseMove: true }
- }
- ]
+ },
+ ],
});
} catch (e) {
console.log(e);
}
- }
+ };
@computed get theGraph() {
- return <div id={`${this._plotId}`} ref={r => r && this.createGraph(r)} style={{ position: "absolute", width: "100%", height: "100%" }}
- onPointerDown={e => e.stopPropagation()} />;
+ return <div id={`${this._plotId}`} ref={r => r && this.createGraph(r)} style={{ position: 'absolute', width: '100%', height: '100%' }} onPointerDown={e => e.stopPropagation()} />;
}
render() {
TraceMobx();
- return (<div
- style={{
- pointerEvents: !this.isContentActive() ? "all" : undefined,
- width: this.props.PanelWidth(),
- height: this.props.PanelHeight()
- }}
- >
- {this.theGraph}
- <div style={{
- display: this.props.isSelected() ? "none" : undefined, position: "absolute", width: "100%", height: "100%",
- pointerEvents: "all"
- }} />
- </div>);
+ return (
+ <div
+ style={{
+ pointerEvents: !this.isContentActive() ? 'all' : undefined,
+ width: this.props.PanelWidth(),
+ height: this.props.PanelHeight(),
+ }}>
+ {this.theGraph}
+ <div
+ style={{
+ display: this.props.isSelected() ? 'none' : undefined,
+ position: 'absolute',
+ width: '100%',
+ height: '100%',
+ pointerEvents: 'all',
+ }}
+ />
+ </div>
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index 85a8622ec..5102eae51 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -136,7 +136,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
return (
<div
className={`linkAnchorBox-cont${small ? '-small' : ''}`}
- onPointerLeave={LinkDocPreview.Clear}
+ //onPointerLeave={} //LinkDocPreview.Clear}
onPointerEnter={e =>
LinkDocPreview.SetLinkInfo({
docProps: this.props,
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index a12564839..93ca22d5d 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react';
import wiki from 'wikijs';
import { Doc, DocCastAsync, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils';
import { DocServer } from '../../DocServer';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
@@ -17,6 +17,7 @@ import { undoBatch } from '../../util/UndoManager';
import { DocumentView, DocumentViewSharedProps } from './DocumentView';
import './LinkDocPreview.scss';
import React = require('react');
+import { LinkEditor } from '../linking/LinkEditor';
interface LinkDocPreviewProps {
linkDoc?: Doc;
@@ -118,13 +119,15 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
}
return undefined;
}
- deleteLink = (e: React.PointerEvent) => {
+ @observable _showEditor = false;
+ editLink = (e: React.PointerEvent): void => {
+ LinkManager.currentLink = this.props.linkDoc;
setupMoveUpEvents(
this,
e,
returnFalse,
emptyFunction,
- undoBatch(() => this._linkDoc && LinkManager.Instance.deleteLink(this._linkDoc))
+ action(() => (this._showEditor = !this._showEditor))
);
};
nextHref = (e: React.PointerEvent) => {
@@ -183,9 +186,9 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
</div>
</Tooltip>
- <Tooltip title={<div className="dash-tooltip">Delete Link</div>} placement="top">
- <div className="linkDocPreview-button" onPointerDown={this.deleteLink}>
- <FontAwesomeIcon className="linkDocPreview-fa-icon" icon="trash" color="white" size="sm" />
+ <Tooltip title={<div className="dash-tooltip">Edit Link</div>} placement="top">
+ <div className="linkDocPreview-button" onPointerDown={this.editLink}>
+ <FontAwesomeIcon className="linkDocPreview-fa-icon" icon="edit" color="white" size="sm" />
</div>
</Tooltip>
</div>
@@ -236,6 +239,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
isDocumentActive={returnFalse}
isContentActive={returnFalse}
addDocument={returnFalse}
+ showTitle={returnEmptyString}
removeDocument={returnFalse}
addDocTab={returnFalse}
pinToPres={returnFalse}
@@ -270,8 +274,9 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
className="linkDocPreview"
ref={this._linkDocRef}
onPointerDown={this.followLinkPointerDown}
- style={{ left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}>
- {this.docPreview}
+ style={{ left: this.props.location[0], top: this.props.location[1], width: this._showEditor ? 'auto' : this.width() + borders, height: this._showEditor ? 'max-content' : this.height() + borders + (this.props.showHeader ? 37 : 0) }}>
+ {this._showEditor ? null : this.docPreview}
+ {!this._showEditor || !this._linkSrc || !this._linkDoc ? null : <LinkEditor sourceDoc={this._linkSrc} linkDoc={this._linkDoc} showLinks={action(() => (this._showEditor = !this._showEditor))} />}
</div>
);
}
diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
index 630ae18f5..00bedafbe 100644
--- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
+++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
@@ -1,5 +1,5 @@
import { InfoWindow } from '@react-google-maps/api';
-import { action, computed } from 'mobx';
+import { action } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc } from '../../../../fields/Doc';
@@ -7,6 +7,7 @@ import { Id } from '../../../../fields/FieldSymbols';
import { emptyFunction, OmitKeys, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
+import { CollectionNoteTakingView } from '../../collections/CollectionNoteTakingView';
import { CollectionStackingView } from '../../collections/CollectionStackingView';
import { ViewBoxAnnotatableProps } from '../../DocComponent';
import { FieldViewProps } from '../FieldView';
@@ -40,7 +41,7 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
});
};
- _stack: CollectionStackingView | null | undefined;
+ _stack: CollectionStackingView | CollectionNoteTakingView | null | undefined;
childFitWidth = (doc: Doc) => doc.type === DocumentType.RTF;
addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, 'data', d), true as boolean);
removeDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, 'data', d), true as boolean);
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 78ef85ec2..d3b95e25a 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -1,7 +1,7 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
-import { action, computed, observable } from 'mobx';
+import { action, computed, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ColorState, SketchPicker } from 'react-color';
@@ -710,6 +710,13 @@ ScriptingGlobals.add(function toggleNoAutoLinkAnchor(checkResult?: boolean) {
}
if (editorView) RichTextMenu.Instance?.toggleNoAutoLinkAnchor();
});
+ScriptingGlobals.add(function toggleDictation(checkResult?: boolean) {
+ const textView = RichTextMenu.Instance?.TextView;
+ if (checkResult) {
+ return textView?._recording ? Colors.MEDIUM_BLUE : 'transparent';
+ }
+ if (textView) runInAction(() => (textView._recording = !textView._recording));
+});
ScriptingGlobals.add(function toggleBold(checkResult?: boolean) {
const editorView = RichTextMenu.Instance?.TextView?.EditorView;
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 8c8b74560..d2f7b5677 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -216,7 +216,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
onPointerDownLabelSpan = (e: any) => {
setupMoveUpEvents(this, e, returnFalse, returnFalse, e => {
DashFieldViewMenu.createFieldView = this.createPivotForField;
- DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16);
+ DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16, this._fieldKey);
});
};
@@ -253,17 +253,21 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> {
DashFieldViewMenu.Instance.fadeOut(true);
};
- public show = (x: number, y: number) => {
+ @observable _fieldKey = '';
+
+ @action
+ public show = (x: number, y: number, fieldKey: string) => {
+ this._fieldKey = fieldKey;
this.jumpTo(x, y, true);
const hideMenu = () => {
this.fadeOut(true);
- document.removeEventListener('pointerdown', hideMenu);
+ document.removeEventListener('pointerdown', hideMenu, true);
};
- document.addEventListener('pointerdown', hideMenu);
+ document.addEventListener('pointerdown', hideMenu, true);
};
render() {
const buttons = [
- <Tooltip key="trash" title={<div className="dash-tooltip">{'Remove Link Anchor'}</div>}>
+ <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}>
<button className="antimodeMenu-button" onPointerDown={this.showFields}>
<FontAwesomeIcon icon="eye" size="lg" />
</button>
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index b8ee89ef2..f61533619 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -24,7 +24,7 @@ import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction,
import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils';
import { DocServer } from '../../../DocServer';
import { Docs, DocUtils } from '../../../documents/Documents';
-import { DocumentType } from '../../../documents/DocumentTypes';
+import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
import { DictationManager } from '../../../util/DictationManager';
import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager } from '../../../util/DragManager';
@@ -63,6 +63,7 @@ import applyDevTools = require('prosemirror-dev-tools');
import React = require('react');
import { text } from 'body-parser';
import { CollectionTreeView } from '../../collections/CollectionTreeView';
+import { DocumentViewInternal } from '../DocumentView';
const translateGoogleApi = require('translate-google-api');
export interface FormattedTextBoxProps {
@@ -249,6 +250,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
!this.layoutDoc.showSidebar && this.toggleSidebar();
this._sidebarRef.current?.anchorMenuClick(this.getAnchor());
};
+ AnchorMenu.Instance.OnAudio = (e: PointerEvent) => {
+ !this.layoutDoc.showSidebar && this.toggleSidebar();
+ const anchor = this.getAnchor();
+ const target = this._sidebarRef.current?.anchorMenuClick(anchor);
+ if (target) {
+ anchor.followLinkAudio = true;
+ DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target));
+ target.title = ComputedField.MakeFunction(`self["text-audioAnnotations-text"].lastElement()`);
+ }
+ };
AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => {
this._editorView?.state && RichTextMenu.Instance.setHighlight(color, this._editorView, this._editorView?.dispatch);
return undefined;
@@ -382,9 +393,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
};
autoLink = () => {
+ const newAutoLinks = new Set<Doc>();
+ const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords);
if (this._editorView?.state.doc.textContent) {
- const newAutoLinks = new Set<Doc>();
- const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords);
const f = this._editorView.state.selection.from;
const t = this._editorView.state.selection.to;
var tr = this._editorView.state.tr as any;
@@ -393,8 +404,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
DocListCast(Doc.MyPublishedDocs.data).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks)));
tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t)));
this._editorView?.dispatch(tr);
- oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.anchor2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink);
}
+ oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.anchor2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink);
};
updateTitle = () => {
@@ -426,28 +437,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const editorView = this._editorView;
if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.rootDoc)) {
const autoLinkTerm = StrCast(target.title).replace(/^@/, '');
- const flattened1 = this.findInNode(editorView, editorView.state.doc, autoLinkTerm);
var alink: Doc | undefined;
- flattened1.forEach((flat, i) => {
- const flattened = this.findInNode(this._editorView!, this._editorView!.state.doc, autoLinkTerm);
- this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
+ this.findInNode(editorView, editorView.state.doc, autoLinkTerm).forEach(sel => {
const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
- const sel = flattened[i];
- tr = tr.addMark(sel.from, sel.to, splitter);
- tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
- if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
- alink =
- alink ??
- (DocListCast(this.Document.links).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) ||
- DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, LinkManager.AutoKeywords)!);
- newAutoLinks.add(alink);
- const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }];
- allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? []));
- const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: 'auto term', location: 'add:right' });
- tr = tr.addMark(pos, pos + node.nodeSize, link);
- }
- });
- tr = tr.removeMark(sel.from, sel.to, splitter);
+ if (!sel.$anchor.pos || editorView.state.doc.textBetween(sel.$anchor.pos - 1, sel.$to.pos).trim() === autoLinkTerm) {
+ tr = tr.addMark(sel.from, sel.to, splitter);
+ tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
+ if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
+ alink =
+ alink ??
+ (DocListCast(this.Document.links).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) ||
+ DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, LinkManager.AutoKeywords)!);
+ newAutoLinks.add(alink);
+ const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }];
+ allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? []));
+ const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: 'auto term', location: 'add:right' });
+ tr = tr.addMark(pos, pos + node.nodeSize, link);
+ }
+ });
+ tr = tr.removeMark(sel.from, sel.to, splitter);
+ }
});
}
return tr;
@@ -694,9 +703,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const editor = this._editorView!;
const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY });
- let target = (e.target as any).parentElement; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span>
+ let target = e.target as any; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span>
while (target && !target.dataset?.targethrefs) target = target.parentElement;
- if (target) {
+ if (target && !(e.nativeEvent as any).dash) {
const hrefs = (target.dataset?.targethrefs as string)
?.trim()
.split(' ')
@@ -715,6 +724,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true);
})
);
+ e.preventDefault();
e.stopPropagation();
return;
}
@@ -773,7 +783,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const uicontrols: ContextMenuProps[] = [];
!Doc.noviceMode && uicontrols.push({ description: `${FormattedTextBox._canAnnotate ? "Don't" : ''} Show Menu on Selections`, event: () => (FormattedTextBox._canAnnotate = !FormattedTextBox._canAnnotate), icon: 'expand-arrows-alt' });
uicontrols.push({ description: !this.Document._noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', event: () => (this.layoutDoc._noSidebar = !this.layoutDoc._noSidebar), icon: 'expand-arrows-alt' });
- uicontrols.push({ description: `${this.layoutDoc._showAudio ? 'Hide' : 'Show'} Dictation Icon`, event: () => (this.layoutDoc._showAudio = !this.layoutDoc._showAudio), icon: 'expand-arrows-alt' });
uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' });
!Doc.noviceMode &&
uicontrols.push({
@@ -855,23 +864,28 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
setDictationContent = (value: string) => {
if (this._editorView && this._recordingStart) {
if (this._break) {
- const textanchor = Docs.Create.TextanchorDocument({ title: 'dictation anchor' });
- this.addDocument(textanchor);
- const link = DocUtils.MakeLinkToActiveAudio(() => textanchor, false).lastElement();
- link && (Doc.GetProto(link).isDictation = true);
- if (!link) return;
- const audioanchor = Cast(link.anchor2, Doc, null);
- if (!audioanchor) return;
- audioanchor.backgroundColor = 'tan';
- const audiotag = this._editorView.state.schema.nodes.audiotag.create({
- timeCode: NumCast(audioanchor._timecodeToShow),
- audioId: audioanchor[Id],
- textId: textanchor[Id],
- });
- Doc.GetProto(textanchor).title = 'dictation:' + audiotag.attrs.timeCode;
- const tr = this._editorView.state.tr.insert(this._editorView.state.doc.content.size, audiotag);
- const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size));
- this._editorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size)));
+ const textanchorFunc = () => {
+ const tanch = Docs.Create.TextanchorDocument({ title: 'dictation anchor' });
+ return this.addDocument(tanch) ? tanch : undefined;
+ };
+ const link = DocUtils.MakeLinkToActiveAudio(textanchorFunc, false).lastElement();
+ if (link) {
+ Doc.GetProto(link).isDictation = true;
+ const audioanchor = Cast(link.anchor2, Doc, null);
+ const textanchor = Cast(link.anchor1, Doc, null);
+ if (audioanchor) {
+ audioanchor.backgroundColor = 'tan';
+ const audiotag = this._editorView.state.schema.nodes.audiotag.create({
+ timeCode: NumCast(audioanchor._timecodeToShow),
+ audioId: audioanchor[Id],
+ textId: textanchor[Id],
+ });
+ Doc.GetProto(textanchor).title = 'dictation:' + audiotag.attrs.timeCode;
+ const tr = this._editorView.state.tr.insert(this._editorView.state.doc.content.size, audiotag);
+ const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size));
+ this._editorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size)));
+ }
+ }
}
const from = this._editorView.state.selection.from;
this._break = false;
@@ -1001,7 +1015,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
() => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, autoHeight: this.autoHeight, marginsHeight: this.autoHeightMargins }),
({ sidebarHeight, textHeight, autoHeight, marginsHeight }) => {
const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight));
- autoHeight && newHeight && this.props.setHeight?.(newHeight);
+ if (autoHeight && newHeight && newHeight !== this.rootDoc.height) {
+ this.props.setHeight?.(newHeight);
+ }
},
{ fireImmediately: true }
);
@@ -1459,14 +1475,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
document.removeEventListener('pointermove', this.onSelectMove);
};
onPointerUp = (e: React.PointerEvent): void => {
- if (!this._editorView?.state.selection.empty && FormattedTextBox._canAnnotate) this.setupAnchorMenu();
+ if (!this._editorView?.state.selection.empty && FormattedTextBox._canAnnotate && !(e.nativeEvent as any).dash) this.setupAnchorMenu();
if (!this._downEvent) return;
this._downEvent = false;
- if (this.props.isContentActive(true)) {
+ if (this.props.isContentActive(true) && !(e.nativeEvent as any).dash) {
const editor = this._editorView!;
const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY });
!this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0))));
- let target = (e.target as any).parentElement; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span>
+ let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span>
while (target && !target.dataset?.targethrefs) target = target.parentElement;
FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc);
}
@@ -1695,7 +1711,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
if (this.props.setHeight && scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) {
// if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
const setScrollHeight = () => (this.rootDoc[this.fieldKey + '-scrollHeight'] = scrollHeight);
- if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) {
+ if (this.rootDoc === this.layoutDoc || this.layoutDoc.resolvedDataDoc) {
setScrollHeight();
} else {
setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived...
@@ -1722,9 +1738,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
.scale(1 / NumCast(this.layoutDoc._viewScale, 1) / (this.props.NativeDimScaling?.() || 1));
@computed get audioHandle() {
- return (
- <div className="formattedTextBox-dictation" onClick={action(e => (this._recording = !this._recording))}>
- <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: this._recording ? 'red' : 'blue', transitionDelay: '0.6s', opacity: this._recording ? 1 : 0.25 }} icon={'microphone'} size="sm" />
+ return !this._recording ? null : (
+ <div
+ className="formattedTextBox-dictation"
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ action(e => (this._recording = !this._recording))
+ )
+ }>
+ <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: 'red' }} icon={'microphone'} size="sm" />
</div>
);
}
@@ -1749,7 +1775,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
@computed get sidebarCollection() {
const renderComponent = (tag: string) => {
- const ComponentTag = tag === 'freeform' ? CollectionFreeFormView : tag === 'tree' ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView;
+ const ComponentTag = tag === CollectionViewType.Freeform ? CollectionFreeFormView : tag === CollectionViewType.Tree ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView;
return ComponentTag === CollectionStackingView ? (
<SidebarAnnos
ref={this._sidebarRef}
@@ -1770,7 +1796,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
/>
) : (
<div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}>
- //@ts-ignore
<ComponentTag
{...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
NativeWidth={returnZero}
@@ -1859,7 +1884,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
style={{
width: this.props.dontSelectOnLoad ? '100%' : `calc(100% - ${this.sidebarWidthPercent})`,
pointerEvents: !active && !SnappingManager.GetIsDragging() ? 'none' : undefined,
- overflow: this.layoutDoc._singleLine ? 'hidden' : undefined,
+ overflow: this.layoutDoc._singleLine ? 'hidden' : this.layoutDoc._autoHeight ? 'visible' : undefined,
}}
onScroll={this.onScroll}
onDrop={this.ondrop}>
@@ -1878,7 +1903,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
</div>
{this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.sidebarWidthPercent === '0%' ? null : this.sidebarCollection}
{this.noSidebar || this.Document._noSidebar || this.props.dontSelectOnLoad || this.Document._singleLine ? null : this.sidebarHandle}
- {!this.layoutDoc._showAudio ? null : this.audioHandle}
+ {this.audioHandle}
</div>
</div>
);
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 21326efaa..2a77210ae 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -60,7 +60,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable private showLinkDropdown: boolean = false;
_reaction: IReactionDisposer | undefined;
- _delayHide = false;
constructor(props: Readonly<{}>) {
super(props);
runInAction(() => {
@@ -70,16 +69,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
});
}
- componentDidMount() {
- this._reaction = reaction(
- () => SelectionManager.Views(),
- () => this._delayHide && !(this._delayHide = false) && this.fadeOut(true)
- );
- }
- componentWillUnmount() {
- this._reaction?.();
- }
-
@computed get noAutoLink() {
return this._noLinkActive;
}
@@ -108,8 +97,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
return this._activeAlignment;
}
- public delayHide = () => (this._delayHide = true);
-
@action
public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: any) {
if (this._linkToRef.current?.getBoundingClientRect().width) {
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 1a1120b6c..ee2ae10a7 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -49,6 +49,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
public OnCrop: (e: PointerEvent) => void = unimplementedFunction;
public OnClick: (e: PointerEvent) => void = unimplementedFunction;
+ public OnAudio: (e: PointerEvent) => void = unimplementedFunction;
public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
public Highlight: (color: string, isPushpin: boolean) => Opt<Doc> = (color: string, isPushpin: boolean) => undefined;
@@ -92,6 +93,10 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
);
};
+ audioDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, returnFalse, returnFalse, e => this.OnAudio?.(e));
+ };
+
cropDown = (e: React.PointerEvent) => {
setupMoveUpEvents(
this,
@@ -196,6 +201,15 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
<FontAwesomeIcon icon="comment-alt" size="lg" />
</button>
</Tooltip>,
+ AnchorMenu.Instance.OnAudio === unimplementedFunction ? (
+ <></>
+ ) : (
+ <Tooltip key="annoaudiotate" title={<div className="dash-tooltip">{'Click to Record Annotation'}</div>}>
+ <button className="antimodeMenu-button annotate" onPointerDown={this.audioDown} style={{ cursor: 'grab' }}>
+ <FontAwesomeIcon icon="microphone" size="lg" />
+ </button>
+ </Tooltip>
+ ),
//NOTE: link popup is currently in progress
<Tooltip key="link" title={<div className="dash-tooltip">{'Find document to link to selected text'}</div>}>
<button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup} style={{}}>
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 0c7504913..8d56ebf8c 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -211,6 +211,7 @@ export class Doc extends RefField {
}
public static set ActivePage(val) {
Doc.UserDoc().activePage = val;
+ DocServer.UPDATE_SERVER_CACHE();
}
public static get ActiveDashboard() {
return DocCast(Doc.UserDoc().activeDashboard);
@@ -228,7 +229,7 @@ export class Doc extends RefField {
return DocCast(Doc.UserDoc().activePresentation);
}
public static set ActivePresentation(val) {
- Doc.UserDoc().activePresentation = val;
+ Doc.UserDoc().activePresentation = new PrefetchProxy(val);
}
constructor(id?: FieldId, forceSave?: boolean) {
super(id);
diff --git a/src/fields/List.ts b/src/fields/List.ts
index b15548327..5cc4ca543 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -1,25 +1,25 @@
-import { action, observable } from "mobx";
-import { alias, list, serializable } from "serializr";
-import { DocServer } from "../client/DocServer";
-import { ScriptingGlobals } from "../client/util/ScriptingGlobals";
-import { afterDocDeserialize, autoObject, Deserializable } from "../client/util/SerializationHelper";
-import { Field } from "./Doc";
-import { Copy, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from "./FieldSymbols";
-import { ObjectField } from "./ObjectField";
-import { ProxyField } from "./Proxy";
-import { RefField } from "./RefField";
-import { listSpec } from "./Schema";
-import { Cast } from "./Types";
-import { deleteProperty, getter, setter, updateFunction } from "./util";
+import { action, observable } from 'mobx';
+import { alias, list, serializable } from 'serializr';
+import { DocServer } from '../client/DocServer';
+import { ScriptingGlobals } from '../client/util/ScriptingGlobals';
+import { afterDocDeserialize, autoObject, Deserializable } from '../client/util/SerializationHelper';
+import { Field } from './Doc';
+import { Copy, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from './FieldSymbols';
+import { ObjectField } from './ObjectField';
+import { ProxyField } from './Proxy';
+import { RefField } from './RefField';
+import { listSpec } from './Schema';
+import { Cast } from './Types';
+import { deleteProperty, getter, setter, updateFunction } from './util';
const listHandlers: any = {
/// Mutator methods
copyWithin() {
- throw new Error("copyWithin not supported yet");
+ throw new Error('copyWithin not supported yet');
},
fill(value: any, start?: number, end?: number) {
if (value instanceof RefField) {
- throw new Error("fill with RefFields not supported yet");
+ throw new Error('fill with RefFields not supported yet');
}
const res = this[Self].__fields.fill(value, start, end);
this[Update]();
@@ -44,7 +44,7 @@ const listHandlers: any = {
}
}
const res = list.__fields.push(...items);
- this[Update]({ op: "$addToSet", items, length: length + items.length });
+ this[Update]({ op: '$addToSet', items, length: length + items.length });
return res;
}),
reverse() {
@@ -78,8 +78,13 @@ const listHandlers: any = {
}
}
const res = list.__fields.splice(start, deleteCount, ...items);
- this[Update](items.length === 0 && deleteCount ? { op: "$remFromSet", items: removed, length: list.__fields.length } :
- items.length && !deleteCount && start === list.__fields.length ? { op: "$addToSet", items, length: list.__fields.length } : undefined);
+ this[Update](
+ items.length === 0 && deleteCount
+ ? { op: '$remFromSet', items: removed, length: list.__fields.length }
+ : items.length && !deleteCount && start === list.__fields.length
+ ? { op: '$addToSet', items, length: list.__fields.length }
+ : undefined
+ );
return res.map(toRealField);
}),
unshift(...items: any[]) {
@@ -98,7 +103,6 @@ const listHandlers: any = {
const res = this[Self].__fields.unshift(...items);
this[Update]();
return res;
-
},
/// Accessor methods
concat: action(function (this: any, ...items: any[]) {
@@ -198,7 +202,7 @@ const listHandlers: any = {
},
[Symbol.iterator]() {
return this[Self].__realFields().values();
- }
+ },
};
function toObjectField(field: Field) {
@@ -217,14 +221,14 @@ function listGetter(target: any, prop: string | number | symbol, receiver: any):
}
interface ListSpliceUpdate<T> {
- type: "splice";
+ type: 'splice';
index: number;
added: T[];
removedCount: number;
}
interface ListIndexUpdate<T> {
- type: "update";
+ type: 'update';
index: number;
newValue: T;
}
@@ -233,7 +237,7 @@ type ListUpdate<T> = ListSpliceUpdate<T> | ListIndexUpdate<T>;
type StoredType<T extends Field> = T extends RefField ? ProxyField<T> : T;
-@Deserializable("list")
+@Deserializable('list')
class ListImpl<T extends Field> extends ObjectField {
constructor(fields?: T[]) {
super();
@@ -244,14 +248,16 @@ class ListImpl<T extends Field> extends ObjectField {
getOwnPropertyDescriptor: (target, prop) => {
if (prop in target.__fields) {
return {
- configurable: true,//TODO Should configurable be true?
+ configurable: true, //TODO Should configurable be true?
enumerable: true,
};
}
return Reflect.getOwnPropertyDescriptor(target, prop);
},
deleteProperty: deleteProperty,
- defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); },
+ defineProperty: () => {
+ throw new Error("Currently properties can't be defined on documents using Object.defineProperty");
+ },
});
this[SelfProxy] = list;
if (fields) {
@@ -265,7 +271,7 @@ class ListImpl<T extends Field> extends ObjectField {
// this requests all ProxyFields at the same time to avoid the overhead
// of separate network requests and separate updates to the React dom.
private __realFields() {
- const promised = this.__fields.filter(f => f instanceof ProxyField && f.promisedValue()).map(f => ({ field: f as any, promisedFieldId: (f instanceof ProxyField) ? f.promisedValue() : "" }));
+ const promised = this.__fields.filter(f => f instanceof ProxyField && f.promisedValue()).map(f => ({ field: f as any, promisedFieldId: f instanceof ProxyField ? f.promisedValue() : '' }));
// if we find any ProxyFields that don't have a current value, then
// start the server request for all of them
if (promised.length) {
@@ -282,7 +288,7 @@ class ListImpl<T extends Field> extends ObjectField {
return this.__fields.map(toRealField);
}
- @serializable(alias("fields", list(autoObject(), { afterDeserialize: afterDocDeserialize })))
+ @serializable(alias('fields', list(autoObject(), { afterDeserialize: afterDocDeserialize })))
private get __fields() {
return this.___fields;
}
@@ -299,7 +305,7 @@ class ListImpl<T extends Field> extends ObjectField {
}
[Copy]() {
- const copiedData = this[Self].__fields.map(f => f instanceof ObjectField ? f[Copy]() : f);
+ const copiedData = this[Self].__fields.map(f => (f instanceof ObjectField ? f[Copy]() : f));
const deepCopy = new ListImpl<T>(copiedData as any);
return deepCopy;
}
@@ -313,7 +319,7 @@ class ListImpl<T extends Field> extends ObjectField {
const update = this[OnUpdate];
// update && update(diff);
update?.(diff);
- }
+ };
private [Self] = this;
private [SelfProxy]: any;
@@ -328,9 +334,9 @@ class ListImpl<T extends Field> extends ObjectField {
export type List<T extends Field> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[];
export const List: { new <T extends Field>(fields?: T[]): List<T> } = ListImpl as any;
-ScriptingGlobals.add("List", List);
+ScriptingGlobals.add('List', List);
ScriptingGlobals.add(function compareLists(l1: any, l2: any) {
- const L1 = Cast(l1, listSpec("string"), []);
- const L2 = Cast(l2, listSpec("string"), []);
+ const L1 = Cast(l1, listSpec('string'), []);
+ const L2 = Cast(l2, listSpec('string'), []);
return !L1 && !L2 ? true : L1 && L2 && L1.length === L2.length && L2.reduce((p, v) => p && L1.includes(v), true);
-}, "compare two lists"); \ No newline at end of file
+}, 'compare two lists');
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index be39e0709..24b5a359d 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -1,116 +1,114 @@
-import { makeInterface, createSchema, listSpec } from "./Schema";
-import { ScriptField } from "./ScriptField";
-import { Doc } from "./Doc";
-import { DateField } from "./DateField";
-import { SchemaHeaderField } from "./SchemaHeaderField";
+import { makeInterface, createSchema, listSpec } from './Schema';
+import { ScriptField } from './ScriptField';
+import { Doc } from './Doc';
+import { DateField } from './DateField';
+import { SchemaHeaderField } from './SchemaHeaderField';
export const documentSchema = createSchema({
// content properties
- type: "string", // enumerated type of document -- should be template-specific (ie, start with an '_')
- title: "string", // document title (can be on either data document or layout)
- isTemplateForField: "string",// if specified, it indicates the document is a template that renders the specified field
- creationDate: DateField, // when the document was created
- links: listSpec(Doc), // computed (readonly) list of links associated with this document
+ type: 'string', // enumerated type of document -- should be template-specific (ie, start with an '_')
+ title: 'string', // document title (can be on either data document or layout)
+ isTemplateForField: 'string', // if specified, it indicates the document is a template that renders the specified field
+ creationDate: DateField, // when the document was created
+ links: listSpec(Doc), // computed (readonly) list of links associated with this document
// "Location" properties in a very general sense
- _curPage: "number", // current page of a page based document
- _currentFrame: "number", // current frame of a frame based collection (e.g., a progressive slide)
- lastFrame: "number", // last frame of a frame based collection (e.g., a progressive slide)
- activeFrame: "number", // the active frame of a frame based animated document
- _currentTimecode: "number", // current play back time of a temporal document (video / audio)
- _timecodeToShow: "number", // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
- _timecodeToHIde: "number", // the time that a document should be hidden
- markers: listSpec(Doc), // list of markers for audio / video
- x: "number", // x coordinate when in a freeform view
- y: "number", // y coordinate when in a freeform view
- z: "number", // z "coordinate" - non-zero specifies the overlay layer of a freeformview
- zIndex: "number", // zIndex of a document in a freeform view
- _scrollTop: "number", // scroll position of a scrollable document (pdf, text, web)
- lat: "number",
- lng: "number",
+ _curPage: 'number', // current page of a page based document
+ _currentFrame: 'number', // current frame of a frame based collection (e.g., a progressive slide)
+ lastFrame: 'number', // last frame of a frame based collection (e.g., a progressive slide)
+ activeFrame: 'number', // the active frame of a frame based animated document
+ _currentTimecode: 'number', // current play back time of a temporal document (video / audio)
+ _timecodeToShow: 'number', // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
+ _timecodeToHIde: 'number', // the time that a document should be hidden
+ markers: listSpec(Doc), // list of markers for audio / video
+ x: 'number', // x coordinate when in a freeform view
+ y: 'number', // y coordinate when in a freeform view
+ z: 'number', // z "coordinate" - non-zero specifies the overlay layer of a freeformview
+ zIndex: 'number', // zIndex of a document in a freeform view
+ _scrollTop: 'number', // scroll position of a scrollable document (pdf, text, web)
+ lat: 'number',
+ lng: 'number',
// appearance properties on the layout
- "_backgroundGrid-spacing": "number", // the size of the grid for collection views
- _autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents
- _nativeWidth: "number", // native width of document which determines how much document contents are scaled when the document's width is set
- _nativeHeight: "number", // "
- _width: "number", // width of document in its container's coordinate system
- _height: "number", // "
- _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitContentsToBox is set
- _yPadding: "number", // pixels of padding on top/bottom of collectionfreeformview contents when fitContentsToBox is set
- _xMargin: "number", // margin added on left/right of most documents to add separation from their container
- _yMargin: "number", // margin added on top/bottom of most documents to add separation from their container
- _overflow: "string", // sets overflow behvavior for CollectionFreeForm views
- _showCaption: "string", // whether editable caption text is overlayed at the bottom of the document
- _showTitle: "string", // the fieldkey(s) whose contents should be displayed at the top of the document. separate multiple keys with ";". Use :hover suffix to indicate title should be shown on hover
- _showAudio: "boolean", // whether to show the audio record icon on documents
- _pivotField: "string", // specifies which field key should be used as the timeline/pivot axis
- _columnsFill: "boolean", // whether documents in a stacking view column should be sized to fill the column
- _columnsSort: "string", // how a document should be sorted "ascending", "descending", undefined (none)
- _columnsHideIfEmpty: "boolean", // whether empty stacking view column headings should be hidden
+ '_backgroundGrid-spacing': 'number', // the size of the grid for collection views
+ _autoHeight: 'boolean', // whether the height of the document should be computed automatically based on its contents
+ _nativeWidth: 'number', // native width of document which determines how much document contents are scaled when the document's width is set
+ _nativeHeight: 'number', // "
+ _width: 'number', // width of document in its container's coordinate system
+ _height: 'number', // "
+ _xPadding: 'number', // pixels of padding on left/right of collectionfreeformview contents when fitContentsToBox is set
+ _yPadding: 'number', // pixels of padding on top/bottom of collectionfreeformview contents when fitContentsToBox is set
+ _xMargin: 'number', // margin added on left/right of most documents to add separation from their container
+ _yMargin: 'number', // margin added on top/bottom of most documents to add separation from their container
+ _overflow: 'string', // sets overflow behvavior for CollectionFreeForm views
+ _showCaption: 'string', // whether editable caption text is overlayed at the bottom of the document
+ _showTitle: 'string', // the fieldkey(s) whose contents should be displayed at the top of the document. separate multiple keys with ";". Use :hover suffix to indicate title should be shown on hover
+ _pivotField: 'string', // specifies which field key should be used as the timeline/pivot axis
+ _columnsFill: 'boolean', // whether documents in a stacking view column should be sized to fill the column
+ _columnsSort: 'string', // how a document should be sorted "ascending", "descending", undefined (none)
+ _columnsHideIfEmpty: 'boolean', // whether empty stacking view column headings should be hidden
_columnHeaders: listSpec(SchemaHeaderField), // header descriptions for stacking/masonry
_schemaHeaders: listSpec(SchemaHeaderField), // header descriptions for schema views
- _fontSize: "string",
- _fontFamily: "string",
- _sidebarWidthPercent: "string", // percent of text window width taken up by sidebar
+ _fontSize: 'string',
+ _fontFamily: 'string',
+ _sidebarWidthPercent: 'string', // percent of text window width taken up by sidebar
// appearance properties on the data document
- backgroundColor: "string", // background color of document
- borderRounding: "string", // border radius rounding of document
- boxShadow: "string", // the amount of shadow around the perimeter of a document
- color: "string", // foreground color of document
- fitContentsToBox: "boolean",// whether freeform view contents should be zoomed/panned to fill the area of the document view box
- fontSize: "string",
- hidden: "boolean", // whether a document should not be displayed
- isInkMask: "boolean", // is the document a mask (ie, sits on top of other documents, has an unbounded width/height that is dark, and content uses 'hard-light' mix-blend-mode to let other documents pop through)
- layout: "string", // this is the native layout string for the document. templates can be added using other fields and setting layoutKey below
- layoutKey: "string", // holds the field key for the field that actually holds the current lyoat
- letterSpacing: "string",
- opacity: "number", // opacity of document
- strokeWidth: "number",
- strokeBezier: "number",
- strokeStartMarker: "string",
- strokeEndMarker: "string",
- strokeDash: "string",
- textTransform: "string",
- treeViewOpen: "boolean", // flag denoting whether the documents sub-tree (contents) is visible or hidden
- treeViewExpandedView: "string", // name of field whose contents are being displayed as the document's subtree
- treeViewExpandedViewLock: "boolean", // whether the expanded view can be changed
- treeViewOpenIsTransient: "boolean", // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
- treeViewType: "string", // whether tree view is an outline, file syste or (default) hierarchy. For outline, clicks edit document titles immediately since double-click opening is turned off
+ backgroundColor: 'string', // background color of document
+ borderRounding: 'string', // border radius rounding of document
+ boxShadow: 'string', // the amount of shadow around the perimeter of a document
+ color: 'string', // foreground color of document
+ fitContentsToBox: 'boolean', // whether freeform view contents should be zoomed/panned to fill the area of the document view box
+ fontSize: 'string',
+ hidden: 'boolean', // whether a document should not be displayed
+ isInkMask: 'boolean', // is the document a mask (ie, sits on top of other documents, has an unbounded width/height that is dark, and content uses 'hard-light' mix-blend-mode to let other documents pop through)
+ layout: 'string', // this is the native layout string for the document. templates can be added using other fields and setting layoutKey below
+ layoutKey: 'string', // holds the field key for the field that actually holds the current lyoat
+ letterSpacing: 'string',
+ opacity: 'number', // opacity of document
+ strokeWidth: 'number',
+ strokeBezier: 'number',
+ strokeStartMarker: 'string',
+ strokeEndMarker: 'string',
+ strokeDash: 'string',
+ textTransform: 'string',
+ treeViewOpen: 'boolean', // flag denoting whether the documents sub-tree (contents) is visible or hidden
+ treeViewExpandedView: 'string', // name of field whose contents are being displayed as the document's subtree
+ treeViewExpandedViewLock: 'boolean', // whether the expanded view can be changed
+ treeViewOpenIsTransient: 'boolean', // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
+ treeViewType: 'string', // whether tree view is an outline, file syste or (default) hierarchy. For outline, clicks edit document titles immediately since double-click opening is turned off
// interaction and linking properties
- ignoreClick: "boolean", // whether documents ignores input clicks (but does not ignore manipulation and other events)
- onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
+ ignoreClick: 'boolean', // whether documents ignores input clicks (but does not ignore manipulation and other events)
+ onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
onPointerDown: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
- onPointerUp: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
- onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return the Doc to be dropped.
- followLinkLocation: "string",// flag for where to place content when following a click interaction (e.g., add:right, inPlace, default, )
- hideLinkButton: "boolean", // whether the blue link counter button should be hidden
- hideAllLinks: "boolean", // whether all individual blue anchor dots should be hidden
- linkDisplay: "boolean", // whether a link connection should be shown between link anchor endpoints.
- isInPlaceContainer: "boolean",// whether the marked object will display addDocTab() calls that target "inPlace" destinations
- isLinkButton: "boolean", // whether document functions as a link follow button to follow the first link on the document when clicked
- layers: listSpec("string"), // which layers the document is part of
- _lockedPosition: "boolean", // whether the document can be moved (dragged)
- _lockedTransform: "boolean",// whether a freeformview can pan/zoom
- displayArrow: "boolean", // toggles directed arrows
+ onPointerUp: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
+ onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return the Doc to be dropped.
+ followLinkLocation: 'string', // flag for where to place content when following a click interaction (e.g., add:right, inPlace, default, )
+ hideLinkButton: 'boolean', // whether the blue link counter button should be hidden
+ hideAllLinks: 'boolean', // whether all individual blue anchor dots should be hidden
+ linkDisplay: 'boolean', // whether a link connection should be shown between link anchor endpoints.
+ isInPlaceContainer: 'boolean', // whether the marked object will display addDocTab() calls that target "inPlace" destinations
+ isLinkButton: 'boolean', // whether document functions as a link follow button to follow the first link on the document when clicked
+ layers: listSpec('string'), // which layers the document is part of
+ _lockedPosition: 'boolean', // whether the document can be moved (dragged)
+ _lockedTransform: 'boolean', // whether a freeformview can pan/zoom
+ displayArrow: 'boolean', // toggles directed arrows
// drag drop properties
- _stayInCollection: "boolean",// whether document can be dropped into a different collection
- dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document.
- dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias", "copy", "move")
- targetDropAction: "string", // allows the target of a drop event to specify the dropAction ("alias", "copy", "move") NOTE: if the document is dropped within the same collection, the dropAction is coerced to 'move'
- childDropAction: "string", // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "alias" or "copy")
- removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
+ _stayInCollection: 'boolean', // whether document can be dropped into a different collection
+ dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document.
+ dropAction: 'string', // override specifying what should happen when this document is dropped (can be "alias", "copy", "move")
+ targetDropAction: 'string', // allows the target of a drop event to specify the dropAction ("alias", "copy", "move") NOTE: if the document is dropped within the same collection, the dropAction is coerced to 'move'
+ childDropAction: 'string', // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "alias" or "copy")
+ removeDropProperties: listSpec('string'), // properties that should be removed from the alias/copy/etc of this document when it is dropped
});
-
export const collectionSchema = createSchema({
childLayoutTemplate: Doc, // layout template to use to render children of a collecion
- childLayoutString: "string", //layout string to use to render children of a collection
+ childLayoutString: 'string', //layout string to use to render children of a collection
childClickedOpenTemplateView: Doc, // layout template to apply to a child when its clicked on in a collection and opened (requires onChildClick or other script to read this value and apply template)
- childDontRegisterViews: "boolean", // whether views made of this document are registered so that they can be found when drawing links
+ childDontRegisterViews: 'boolean', // whether views made of this document are registered so that they can be found when drawing links
onChildClick: ScriptField, // script to run for each child when its clicked
onChildDoubleClick: ScriptField, // script to run for each child when its clicked
onCheckedClick: ScriptField, // script to run when a checkbox is clicked next to a child in a tree view
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 41e723119..d87bb6656 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -33,6 +33,7 @@ import { ObjectField } from './ObjectField';
import { PrefetchProxy, ProxyField } from './Proxy';
import { RefField } from './RefField';
import { RichTextField } from './RichTextField';
+import { SchemaHeaderField } from './SchemaHeaderField';
import { ComputedField } from './ScriptField';
import { ScriptCast, StrCast } from './Types';
@@ -455,7 +456,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
? {
redo: action(() => {
diff.items.forEach((item: any) => {
- const ind = receiver[prop].indexOf(item.value ? item.value() : item);
+ const ind = item instanceof SchemaHeaderField ? receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) : receiver[prop].indexOf(item.value ? item.value() : item);
ind !== -1 && receiver[prop].splice(ind, 1);
});
lastValue = ObjectField.MakeCopy(receiver[prop]);
@@ -463,8 +464,13 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
undo: () => {
// console.log("undo $rem: " + prop, diff.items) // bcz: uncomment to log undo
diff.items.forEach((item: any) => {
- const ind = (prevValue as List<any>).indexOf(item.value ? item.value() : item);
- ind !== -1 && receiver[prop].indexOf(item.value ? item.value() : item) === -1 && receiver[prop].splice(ind, 0, item);
+ if (item instanceof SchemaHeaderField) {
+ const ind = (prevValue as List<any>).findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading);
+ ind !== -1 && receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) === -1 && receiver[prop].splice(ind, 0, item);
+ } else {
+ const ind = (prevValue as List<any>).indexOf(item.value ? item.value() : item);
+ ind !== -1 && receiver[prop].indexOf(item.value ? item.value() : item) === -1 && receiver[prop].splice(ind, 0, item);
+ }
});
lastValue = ObjectField.MakeCopy(receiver[prop]);
},
diff --git a/webpack.config.js b/webpack.config.js
index 5a954db19..01625988c 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,38 +1,38 @@
-var path = require('path');
-var webpack = require('webpack');
-const CopyWebpackPlugin = require("copy-webpack-plugin");
+const path = require('path');
+const webpack = require('webpack');
+const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
-const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
+const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const plugins = [
- new CopyWebpackPlugin([{
- from: "deploy",
- to: path.join(__dirname, "build")
- }]),
+ new CopyWebpackPlugin([
+ {
+ from: 'deploy',
+ to: path.join(__dirname, 'build'),
+ },
+ ]),
new HtmlWebpackPlugin({
title: 'Caching',
}),
new ForkTsCheckerWebpackPlugin({
tslint: true,
// memoryLimit: 4096,
- useTypescriptIncrementalApi: true
+ useTypescriptIncrementalApi: true,
}),
- new webpack.ProvidePlugin({ Buffer: ['buffer', 'Buffer'], }),
- new webpack.ProvidePlugin({ process: 'process/browser', }),
+ new webpack.ProvidePlugin({ Buffer: ['buffer', 'Buffer'] }),
+ new webpack.ProvidePlugin({ process: 'process/browser' }),
new webpack.HotModuleReplacementPlugin(),
];
function transferEnvironmentVariables() {
- const prefix = "_CLIENT_";
- const {
- parsed
- } = require('dotenv').config();
+ const prefix = '_CLIENT_';
+ const { parsed } = require('dotenv').config();
if (!parsed) {
return;
}
const resolvedClientSide = Object.keys(parsed).reduce((mapping, envKey) => {
if (envKey.startsWith(prefix)) {
- mapping[`process.env.${envKey.replace(prefix, "")}`] = JSON.stringify(parsed[envKey]);
+ mapping[`process.env.${envKey.replace(prefix, '')}`] = JSON.stringify(parsed[envKey]);
}
return mapping;
}, {});
@@ -44,18 +44,18 @@ transferEnvironmentVariables();
module.exports = {
mode: 'development',
entry: {
- bundle: ["./src/client/views/Main.tsx", 'webpack-hot-middleware/client?reload=true'],
- viewer: ["./src/debug/Viewer.tsx", 'webpack-hot-middleware/client?reload=true'],
- repl: ["./src/debug/Repl.tsx", 'webpack-hot-middleware/client?reload=true'],
- test: ["./src/debug/Test.tsx", 'webpack-hot-middleware/client?reload=true'],
- inkControls: ["./src/mobile/InkControls.tsx", 'webpack-hot-middleware/client?reload=true'],
- mobileInterface: ["./src/client/views/Main.tsx", 'webpack-hot-middleware/client?reload=true'],
+ bundle: ['./src/client/views/Main.tsx', 'webpack-hot-middleware/client?reload=true'],
+ viewer: ['./src/debug/Viewer.tsx', 'webpack-hot-middleware/client?reload=true'],
+ repl: ['./src/debug/Repl.tsx', 'webpack-hot-middleware/client?reload=true'],
+ test: ['./src/debug/Test.tsx', 'webpack-hot-middleware/client?reload=true'],
+ inkControls: ['./src/mobile/InkControls.tsx', 'webpack-hot-middleware/client?reload=true'],
+ mobileInterface: ['./src/client/views/Main.tsx', 'webpack-hot-middleware/client?reload=true'],
},
- devtool: "source-map",
+ devtool: 'source-map',
output: {
- filename: "[name].js",
- path: path.resolve(__dirname, "build"),
- publicPath: "/",
+ filename: '[name].js',
+ path: path.resolve(__dirname, 'build'),
+ publicPath: '/',
},
resolve: {
extensions: ['.js', '.ts', '.tsx'],
@@ -68,67 +68,73 @@ module.exports = {
crypto: false,
assert: false,
os: false,
- path: require.resolve("path-browserify"),
- http: require.resolve("http-browserify"),
- https: require.resolve("https-browserify"),
- stream: require.resolve("stream-browserify"),
- buffer: require.resolve("buffer")
- }
+ path: require.resolve('path-browserify'),
+ http: require.resolve('http-browserify'),
+ https: require.resolve('https-browserify'),
+ stream: require.resolve('stream-browserify'),
+ buffer: require.resolve('buffer'),
+ },
},
module: {
- rules: [{
- test: [/\.tsx?$/],
- use: [{
- loader: 'ts-loader',
- options: {
- transpileOnly: true
- }
- }]
- },
- {
- test: /\.m?js/,
- resolve: {
- fullySpecified: false
- }
- },
- {
- test: /\.(woff|woff2|ttf|eot|otf|svg)$/,
- use: 'file-loader?name=fonts/[name].[ext]!static'
- },
- {
- test: /\.scss|css$/,
- use: [{
- loader: "style-loader"
+ rules: [
+ {
+ test: [/\.tsx?$/],
+ use: [
+ {
+ loader: 'ts-loader',
+ options: {
+ transpileOnly: true,
+ },
+ },
+ ],
},
{
- loader: "css-loader"
+ test: /\.m?js/,
+ resolve: {
+ fullySpecified: false,
+ },
},
{
- loader: "sass-loader"
+ test: /\.(woff|woff2|ttf|eot|otf|svg)$/,
+ use: 'file-loader?name=fonts/[name].[ext]!static',
+ },
+ {
+ test: /\.scss|css$/,
+ use: [
+ {
+ loader: 'style-loader',
+ },
+ {
+ loader: 'css-loader',
+ },
+ {
+ loader: 'sass-loader',
+ },
+ ],
+ },
+ {
+ test: /\.(jpg|png|pdf)$/,
+ use: [
+ {
+ loader: 'file-loader',
+ },
+ ],
+ },
+ {
+ test: /\.(png|jpg|gif)$/i,
+ use: [
+ {
+ loader: 'url-loader',
+ options: {
+ limit: 8192,
+ },
+ },
+ ],
},
- ]
- },
- {
- test: /\.(jpg|png|pdf)$/,
- use: [{
- loader: 'file-loader'
- }]
- },
- {
- test: /\.(png|jpg|gif)$/i,
- use: [{
- loader: 'url-loader',
- options: {
- limit: 8192
- }
- }]
- }
],
noParse: [require.resolve('typescript/lib/typescript.js')],
},
plugins,
- externals: [
- 'child_process'
- ],
-}; \ No newline at end of file
+ externals: ['child_process'],
+};