aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/DocComponent.tsx24
-rw-r--r--src/client/views/DocumentDecorations.tsx6
-rw-r--r--src/client/views/GlobalKeyHandler.ts17
-rw-r--r--src/client/views/MainView.tsx4
-rw-r--r--src/client/views/collections/CollectionSubView.tsx4
-rw-r--r--src/client/views/collections/CollectionView.tsx20
-rw-r--r--src/client/views/collections/SchemaTable.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx60
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx13
-rw-r--r--src/client/views/collections/collectionFreeForm/PropertiesView.scss27
-rw-r--r--src/client/views/collections/collectionFreeForm/PropertiesView.tsx103
-rw-r--r--src/client/views/linking/LinkMenu.scss1
-rw-r--r--src/client/views/linking/LinkMenu.tsx26
-rw-r--r--src/client/views/nodes/AudioBox.scss154
-rw-r--r--src/client/views/nodes/AudioBox.tsx599
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx4
-rw-r--r--src/client/views/nodes/DocumentView.tsx34
-rw-r--r--src/client/views/nodes/LinkAnchorBox.tsx2
-rw-r--r--src/client/views/nodes/PresBox.tsx4
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx87
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts6
22 files changed, 1009 insertions, 194 deletions
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 804c7a8d4..831c246d1 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,4 +1,4 @@
-import { Doc, Opt, DataSym, AclReadonly, AclAddonly, AclPrivate, AclEdit, AclSym, DocListCastAsync, DocListCast } from '../../fields/Doc';
+import { Doc, Opt, DataSym, AclReadonly, AclAddonly, AclPrivate, AclEdit, AclSym, DocListCastAsync, DocListCast, AclAdmin } from '../../fields/Doc';
import { Touchable } from './Touchable';
import { computed, action, observable } from 'mobx';
import { Cast, BoolCast, ScriptCast } from '../../fields/Types';
@@ -7,7 +7,7 @@ import { InteractionUtils } from '../util/InteractionUtils';
import { List } from '../../fields/List';
import { DateField } from '../../fields/DateField';
import { ScriptField } from '../../fields/ScriptField';
-import { GetEffectiveAcl, SharingPermissions } from '../../fields/util';
+import { GetEffectiveAcl, SharingPermissions, distributeAcls } from '../../fields/util';
/// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView)
@@ -96,7 +96,8 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
[AclPrivate, SharingPermissions.None],
[AclReadonly, SharingPermissions.View],
[AclAddonly, SharingPermissions.Add],
- [AclEdit, SharingPermissions.Edit]
+ [AclEdit, SharingPermissions.Edit],
+ [AclAdmin, SharingPermissions.Admin]
]);
lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result;
@@ -154,15 +155,14 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
return false;
}
else {
- // if (this.props.Document[AclSym]) {
- // added.forEach(d => {
- // const dataDoc = d[DataSym];
- // dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym];
- // for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
- // dataDoc[key] = d[key] = this.AclMap.get(value);
- // }
- // });
- // }
+ if (this.props.Document[AclSym]) {
+ added.forEach(d => {
+ for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
+ distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true);
+ }
+ });
+ }
+
if (effectiveAcl === AclAddonly) {
added.map(doc => Doc.AddDocToList(targetDataDoc, this.annotationKey, doc));
}
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 005df6fe0..f1169763e 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -611,7 +611,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 2 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
return (null);
}
- const canDelete = SelectionManager.SelectedDocuments().map(docView => GetEffectiveAcl(docView.props.ContainingCollectionDoc)).some(permission => permission === AclAdmin || permission === AclEdit);
+ const canDelete = SelectionManager.SelectedDocuments().some(docView => {
+ const docAcl = GetEffectiveAcl(docView.props.Document);
+ const collectionAcl = GetEffectiveAcl(docView.props.ContainingCollectionDoc);
+ return [docAcl, collectionAcl].some(acl => [AclAdmin, AclEdit].includes(acl));
+ });
const minimal = bounds.r - bounds.x < 100 ? true : false;
const maximizeIcon = minimal ? (
<Tooltip title={<><div className="dash-tooltip">Show context menu</div></>} placement="top">
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index c9f95a538..3a61e89ce 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -1,6 +1,6 @@
import { action } from "mobx";
import { DateField } from "../../fields/DateField";
-import { Doc, DocListCast } from "../../fields/Doc";
+import { Doc, DocListCast, AclEdit, AclAdmin } from "../../fields/Doc";
import { Id } from "../../fields/FieldSymbols";
import { InkTool } from "../../fields/InkField";
import { List } from "../../fields/List";
@@ -24,6 +24,7 @@ import PDFMenu from "./pdf/PDFMenu";
import { ContextMenu } from "./ContextMenu";
import GroupManager from "../util/GroupManager";
import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu";
+import { GetEffectiveAcl } from "../../fields/util";
const modifiers = ["control", "meta", "shift", "alt"];
type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>;
@@ -118,8 +119,18 @@ export default class KeyManager {
return { stopPropagation: false, preventDefault: false };
}
}
- UndoManager.RunInBatch(() =>
- SelectionManager.SelectedDocuments().map(dv => dv.props.removeDocument?.(dv.props.Document)), "delete");
+
+ const recent = Cast(Doc.UserDoc().myRecentlyClosed, Doc) as Doc;
+ const selected = SelectionManager.SelectedDocuments().slice();
+ UndoManager.RunInBatch(() => {
+ selected.map(dv => {
+ const effectiveAcl = GetEffectiveAcl(dv.props.Document);
+ if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) { // deletes whatever you have the right to delete
+ recent && Doc.AddDocToList(recent, "data", dv.props.Document, undefined, true, true);
+ dv.props.removeDocument?.(dv.props.Document);
+ }
+ });
+ }, "delete");
SelectionManager.DeselectAll();
break;
case "arrowleft":
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 51137a080..b6058db7a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -437,7 +437,7 @@ export class MainView extends React.Component {
}
sidebarScreenToLocal = () => new Transform(0, (CollectionMenu.Instance.Pinned ? -35 : 0), 1);
//sidebarScreenToLocal = () => new Transform(0, (RichTextMenu.Instance.Pinned ? -35 : 0) + (CollectionMenu.Instance.Pinned ? -35 : 0), 1);
- mainContainerXf = () => this.sidebarScreenToLocal().translate(0, -this._buttonBarHeight);
+ mainContainerXf = () => this.sidebarScreenToLocal().translate(-55, 0);
@computed get closePosition() { return 55 + this.flyoutWidth; }
@computed get flyout() {
@@ -788,7 +788,7 @@ export class MainView extends React.Component {
<FormatShapePane />
<div style={{ display: "none" }}><RichTextMenu key="rich" /></div>
{LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null}
- {DocumentLinksButton.EditLink ? <LinkMenu location={DocumentLinksButton.EditLinkLoc} docView={DocumentLinksButton.EditLink} addDocTab={DocumentLinksButton.EditLink.props.addDocTab} changeFlyout={emptyFunction} /> : (null)}
+ {DocumentLinksButton.EditLink ? <LinkMenu docView={DocumentLinksButton.EditLink} addDocTab={DocumentLinksButton.EditLink.props.addDocTab} changeFlyout={emptyFunction} /> : (null)}
{LinkDocPreview.LinkInfo ? <LinkDocPreview location={LinkDocPreview.LinkInfo.Location} backgroundColor={this.defaultBackgroundColors}
linkDoc={LinkDocPreview.LinkInfo.linkDoc} linkSrc={LinkDocPreview.LinkInfo.linkSrc} href={LinkDocPreview.LinkInfo.href}
addDocTab={LinkDocPreview.LinkInfo.addDocTab} /> : (null)}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 4025e25f9..0e40cd21c 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -407,8 +407,8 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
title: uriList,
_width: 400,
_height: 315,
- _nativeWidth: 600,
- _nativeHeight: 472.5
+ _nativeWidth: 850,
+ _nativeHeight: 962
}));
return;
}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 6e15cb887..4d1cb670c 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -17,7 +17,7 @@ import { listSpec } from '../../../fields/Schema';
import { ComputedField, ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
-import { TraceMobx, GetEffectiveAcl, SharingPermissions } from '../../../fields/util';
+import { TraceMobx, GetEffectiveAcl, SharingPermissions, distributeAcls } from '../../../fields/util';
import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
@@ -148,16 +148,14 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
return false;
}
else {
- // if (this.props.Document[AclSym]) {
- // // change so it only adds if more restrictive
- // added.forEach(d => {
- // // const dataDoc = d[DataSym];
- // for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
- // // key.substring(4).replace("_", ".") !== Doc.CurrentUserEmail && distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true);
- // distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true);
- // }
- // });
- // }
+ if (this.props.Document[AclSym]) {
+ added.forEach(d => {
+ for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
+ if (d.author === Doc.CurrentUserEmail && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true);
+ else distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true);
+ }
+ });
+ }
if (effectiveAcl === AclAddonly) {
added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc));
diff --git a/src/client/views/collections/SchemaTable.tsx b/src/client/views/collections/SchemaTable.tsx
index 75d484cbe..a974c5496 100644
--- a/src/client/views/collections/SchemaTable.tsx
+++ b/src/client/views/collections/SchemaTable.tsx
@@ -177,7 +177,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
}
);
}
- console.log(columns);
const cols = this.props.columns.map(col => {
@@ -315,7 +314,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
width: 28,
resizable: false
});
- console.log(columns);
return columns;
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index bfe569853..3a2979696 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -54,15 +54,15 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
const bfield = afield === "anchor1" ? "anchor2" : "anchor1";
// really hacky stuff to make the LinkAnchorBox display where we want it to:
- // if there's an element in the DOM with a classname containing the link's id and a targetids attribute containing the other end of the link,
+ // if there's an element in the DOM with a classname containing the link's id and a data-targetids attribute containing the other end of the link,
// then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right
// otherwise, we just use the computed nearest point on the document boundary to the target Document
const linkId = this.props.LinkDocs[0][Id]; // this link's Id
const AanchorId = (this.props.LinkDocs[0][afield] as Doc)[Id]; // anchor a's id
const BanchorId = (this.props.LinkDocs[0][bfield] as Doc)[Id]; // anchor b's id
const linkEles = Array.from(window.document.getElementsByClassName(linkId));
- const targetAhyperlink = linkEles.find((ele: any) => ele.getAttribute("targetids")?.includes(AanchorId));
- const targetBhyperlink = linkEles.find((ele: any) => ele.getAttribute("targetids")?.includes(BanchorId));
+ const targetAhyperlink = linkEles.find((ele: any) => ele.dataset.targetids?.includes(AanchorId));
+ const targetBhyperlink = linkEles.find((ele: any) => ele.dataset.targetids?.includes(BanchorId));
if (!targetBhyperlink) {
this.props.A.rootDoc[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100;
this.props.A.rootDoc[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 034d32e0a..d9acc3376 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1706,49 +1706,39 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
}
@computed get zoomProgressivize() {
- if (PresBox.Instance) return (
- <>
- {this.props.zoomProgressivize ? this.zoomProgressivizeContainer : (null)}
- </>
- );
+ return PresBox.Instance && this.props.zoomProgressivize ? PresBox.Instance.zoomProgressivizeContainer : (null);
}
@computed get progressivize() {
- if (PresBox.Instance) return (
- <>
- {this.props.progressivize ? PresBox.Instance.progressivizeChildDocs : (null)}
- </>
- );
+ return PresBox.Instance && this.props.progressivize ? PresBox.Instance.progressivizeChildDocs : (null);
}
@computed get presPaths() {
const presPaths = "presPaths" + (this.props.presPaths ? "" : "-hidden");
- if (PresBox.Instance) return (
- <>
- {!this.props.presPaths ? (null) : <><div>{PresBox.Instance.order}</div>
- <svg className={presPaths}>
- <defs>
- <marker id="arrow" markerWidth="3" overflow="visible" markerHeight="3" refX="5" refY="5" orient="auto" markerUnits="strokeWidth">
- <path d="M0,0 L0,6 L9,3 z" fill="#69a6db" />
- </marker>
- <marker id="square" markerWidth="3" markerHeight="3" overflow="visible"
- refX="5" refY="5" orient="auto" markerUnits="strokeWidth">
- <path d="M 5,1 L 9,5 5,9 1,5 z" fill="#69a6db" />
- </marker>
- <marker id="markerSquare" markerWidth="7" markerHeight="7" refX="4" refY="4"
- orient="auto" overflow="visible">
- <rect x="1" y="1" width="5" height="5" fill="#69a6db" />
- </marker>
-
- <marker id="markerArrow" markerWidth="5" markerHeight="5" refX="2" refY="7"
- orient="auto" overflow="visible">
- <path d="M2,2 L2,13 L8,7 L2,2" fill="#69a6db" />
- </marker>
- </defs>;
+ return !(PresBox.Instance) ? (null) : (<>
+ {!this.props.presPaths ? (null) : <><div>{PresBox.Instance.order}</div>
+ <svg className={presPaths}>
+ <defs>
+ <marker id="arrow" markerWidth="3" overflow="visible" markerHeight="3" refX="5" refY="5" orient="auto" markerUnits="strokeWidth">
+ <path d="M0,0 L0,6 L9,3 z" fill="#69a6db" />
+ </marker>
+ <marker id="square" markerWidth="3" markerHeight="3" overflow="visible"
+ refX="5" refY="5" orient="auto" markerUnits="strokeWidth">
+ <path d="M 5,1 L 9,5 5,9 1,5 z" fill="#69a6db" />
+ </marker>
+ <marker id="markerSquare" markerWidth="7" markerHeight="7" refX="4" refY="4"
+ orient="auto" overflow="visible">
+ <rect x="1" y="1" width="5" height="5" fill="#69a6db" />
+ </marker>
+
+ <marker id="markerArrow" markerWidth="5" markerHeight="5" refX="2" refY="7"
+ orient="auto" overflow="visible">
+ <path d="M2,2 L2,13 L8,7 L2,2" fill="#69a6db" />
+ </marker>
+ </defs>;
{PresBox.Instance.paths}
- </svg></>}
- </>
- );
+ </svg></>}
+ </>);
}
render() {
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index a32c8b363..88fe03efd 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -339,10 +339,21 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
this._visible = false;
}
+ @undoBatch
@action
delete = () => {
- this.props.removeDocument(this.marqueeSelect(false));
+ const recent = Cast(Doc.UserDoc().myRecentlyClosed, Doc) as Doc;
+ const selected = this.marqueeSelect(false);
SelectionManager.DeselectAll();
+
+ selected.map(doc => {
+ const effectiveAcl = GetEffectiveAcl(doc);
+ if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) { // deletes whatever you have the right to delete
+ recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true);
+ this.props.removeDocument(doc);
+ }
+ });
+
this.cleanupInteractions(false);
MarqueeOptionsMenu.Instance.fadeOut(true);
this.hideMarquee();
diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.scss b/src/client/views/collections/collectionFreeForm/PropertiesView.scss
index 3ae94efb7..5e0c9fcbb 100644
--- a/src/client/views/collections/collectionFreeForm/PropertiesView.scss
+++ b/src/client/views/collections/collectionFreeForm/PropertiesView.scss
@@ -119,6 +119,19 @@
font-size: 10px;
padding: 10px;
margin-left: 5px;
+
+ .change-buttons {
+ display: flex;
+
+ button {
+ width: 5;
+ height: 5;
+ }
+
+ input {
+ width: 100%;
+ }
+ }
}
}
@@ -233,11 +246,15 @@
.propertiesView-sharingTable {
+ // whatever's commented out - add it back in when adding the buttons
+
+ // border: 1.5px solid black;
border: 1px solid black;
- padding: 5px;
- border-radius: 6px;
- /* width: 170px; */
- margin-right: 10px;
+ padding: 5px; // remove when adding buttons
+ border-radius: 6px; // remove when adding buttons
+ margin-right: 10px; // remove when adding buttons
+ // width: 100%;
+ // display: inline-table;
background-color: #ececec;
max-height: 130px;
overflow-y: scroll;
@@ -245,9 +262,11 @@
.propertiesView-sharingTable-item {
display: flex;
+ // padding: 5px;
padding: 3px;
align-items: center;
border-bottom: 0.5px solid grey;
+ cursor: pointer;
&:hover .propertiesView-sharingTable-item-name {
overflow-x: unset;
diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.tsx b/src/client/views/collections/collectionFreeForm/PropertiesView.tsx
index 9c1cbec99..1a8ee3ea1 100644
--- a/src/client/views/collections/collectionFreeForm/PropertiesView.tsx
+++ b/src/client/views/collections/collectionFreeForm/PropertiesView.tsx
@@ -2,13 +2,11 @@ import React = require("react");
import { observer } from "mobx-react";
import "./PropertiesView.scss";
import { observable, action, computed, runInAction } from "mobx";
-import { Doc, Field, DocListCast, WidthSym, HeightSym, AclSym, AclPrivate, AclReadonly, AclAddonly, AclEdit, AclAdmin, Opt } from "../../../../fields/Doc";
-import { DocumentView } from "../../nodes/DocumentView";
+import { Doc, Field, WidthSym, HeightSym, AclSym, AclPrivate, AclReadonly, AclAddonly, AclEdit, AclAdmin, Opt, DocCastAsync } from "../../../../fields/Doc";
import { ComputedField } from "../../../../fields/ScriptField";
import { EditableView } from "../../EditableView";
import { KeyValueBox } from "../../nodes/KeyValueBox";
import { Cast, NumCast, StrCast } from "../../../../fields/Types";
-import { listSpec } from "../../../../fields/Schema";
import { ContentFittingDocumentView } from "../../nodes/ContentFittingDocumentView";
import { returnFalse, returnOne, emptyFunction, emptyPath, returnTrue, returnZero, returnEmptyFilter, Utils } from "../../../../Utils";
import { Id } from "../../../../fields/FieldSymbols";
@@ -16,23 +14,26 @@ import { Transform } from "../../../util/Transform";
import { PropertiesButtons } from "../../PropertiesButtons";
import { SelectionManager } from "../../../util/SelectionManager";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Tooltip, Checkbox, Divider } from "@material-ui/core";
+import { Tooltip, Checkbox } from "@material-ui/core";
import SharingManager from "../../../util/SharingManager";
import { DocumentType } from "../../../documents/DocumentTypes";
-import FormatShapePane from "./FormatShapePane";
import { SharingPermissions, GetEffectiveAcl } from "../../../../fields/util";
import { InkField } from "../../../../fields/InkField";
import { undoBatch, UndoManager } from "../../../util/UndoManager";
import { ColorState, SketchPicker } from "react-color";
-import AntimodeMenu from "../../AntimodeMenu";
import "./FormatShapePane.scss";
import { discovery_v1 } from "googleapis";
import { PresBox } from "../../nodes/PresBox";
import { DocumentManager } from "../../../util/DocumentManager";
+import FormatShapePane from "./FormatShapePane";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
+// import * as fa from '@fortawesome/free-solid-svg-icons';
+// import { library } from "@fortawesome/fontawesome-svg-core";
+
+// library.add(fa.faPlus, fa.faMinus, fa.faCog);
interface PropertiesViewProps {
width: number;
@@ -70,6 +71,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
@observable openLayout: boolean = true;
@observable openAppearance: boolean = true;
@observable openTransform: boolean = true;
+ // @observable selectedUser: string = "";
+ // @observable addButtonPressed: boolean = false;
+
//Pres Trails booleans:
@observable openPresTransitions: boolean = false;
@observable openPresProgressivize: boolean = false;
@@ -288,13 +292,20 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
}
}
+ /**
+ * Handles the changing of a user's permissions from the permissions panel.
+ */
@undoBatch
changePermissions = (e: any, user: string) => {
SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, this.selectedDoc!);
}
- getPermissionsSelect(user: string) {
+ /**
+ * @returns the options for the permissions dropdown.
+ */
+ getPermissionsSelect(user: string, permission: string) {
return <select className="permissions-select"
+ defaultValue={permission}
onChange={e => this.changePermissions(e, user)}>
{Object.values(SharingPermissions).map(permission => {
return (
@@ -305,6 +316,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
</select>;
}
+ /**
+ * @returns the notification icon. On clicking, it should notify someone of a document been shared with them.
+ */
@computed get notifyIcon() {
return <Tooltip title={<><div className="dash-tooltip">Notify with message</div></>}>
<div className="notify-button">
@@ -313,6 +327,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
</Tooltip>;
}
+ /**
+ * ... next to the owner that opens the main SharingManager interface on click.
+ */
@computed get expansionIcon() {
return <Tooltip title={<><div className="dash-tooltip">{"Show more permissions"}</div></>}>
<div className="expansion-button" onPointerDown={() => {
@@ -325,17 +342,26 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
</Tooltip>;
}
- sharingItem(name: string, effectiveAcl: symbol, permission?: string) {
- return <div className="propertiesView-sharingTable-item">
+ /**
+ * @returns a row of the permissions panel
+ */
+ sharingItem(name: string, effectiveAcl: symbol, permission: string) {
+ return <div className="propertiesView-sharingTable-item"
+ // style={{ backgroundColor: this.selectedUser === name ? "#bcecfc" : "" }}
+ // onPointerDown={action(() => this.selectedUser = this.selectedUser === name ? "" : name)}
+ >
<div className="propertiesView-sharingTable-item-name" style={{ width: name !== "Me" ? "85px" : "80px" }}> {name} </div>
{/* {name !== "Me" ? this.notifyIcon : null} */}
<div className="propertiesView-sharingTable-item-permission">
- {effectiveAcl === AclAdmin && permission !== "Owner" ? this.getPermissionsSelect(name) : permission}
+ {effectiveAcl === AclAdmin && permission !== "Owner" ? this.getPermissionsSelect(name, permission) : permission}
{permission === "Owner" ? this.expansionIcon : null}
</div>
</div>;
}
+ /**
+ * @returns the sharing and permissiosn panel.
+ */
@computed get sharingTable() {
const AclMap = new Map<symbol, string>([
[AclPrivate, SharingPermissions.None],
@@ -348,13 +374,22 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
const effectiveAcl = GetEffectiveAcl(this.selectedDoc!);
const tableEntries = [];
+ // DocCastAsync(Doc.UserDoc().sidebarUsersDisplayed).then(sidebarUsersDisplayed => {
if (this.selectedDoc![AclSym]) {
for (const [key, value] of Object.entries(this.selectedDoc![AclSym])) {
const name = key.substring(4).replace("_", ".");
- if (name !== Doc.CurrentUserEmail && name !== this.selectedDoc!.author) tableEntries.push(this.sharingItem(name, effectiveAcl, AclMap.get(value)!));
+ if (name !== Doc.CurrentUserEmail && name !== this.selectedDoc!.author/* && sidebarUsersDisplayed![name] !== false*/) tableEntries.push(this.sharingItem(name, effectiveAcl, AclMap.get(value)!));
}
}
+ // if (Doc.UserDoc().sidebarUsersDisplayed) {
+ // for (const [name, value] of Object.entries(sidebarUsersDisplayed!)) {
+ // if (value === true && !this.selectedDoc![`ACL-${name.substring(8).replace(".", "_")}`]) tableEntries.push(this.sharingItem(name.substring(8), effectiveAcl, SharingPermissions.None));
+ // }
+ // }
+ // })
+
+ // shifts the current user and the owner to the top of the doc.
tableEntries.unshift(this.sharingItem("Me", effectiveAcl, Doc.CurrentUserEmail === this.selectedDoc!.author ? "Owner" : StrCast(this.selectedDoc![`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`])));
if (Doc.CurrentUserEmail !== this.selectedDoc!.author) tableEntries.unshift(this.sharingItem(StrCast(this.selectedDoc!.author), effectiveAcl, "Owner"));
@@ -753,8 +788,18 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
</div>;
}
- render() {
+ /**
+ * Handles adding and removing members from the sharing panel
+ */
+ // handleUserChange = (selectedUser: string, add: boolean) => {
+ // if (!Doc.UserDoc().sidebarUsersDisplayed) Doc.UserDoc().sidebarUsersDisplayed = new Doc;
+ // DocCastAsync(Doc.UserDoc().sidebarUsersDisplayed).then(sidebarUsersDisplayed => {
+ // sidebarUsersDisplayed![`display-${selectedUser}`] = add;
+ // !add && runInAction(() => this.selectedUser = "");
+ // });
+ // }
+ render() {
if (!this.selectedDoc && !this.isPres) {
return <div className="propertiesView" style={{ width: this.props.width }}>
<div className="propertiesView-title" style={{ width: this.props.width }}>
@@ -806,6 +851,36 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
{!this.openSharing ? (null) :
<div className="propertiesView-sharing-content">
{this.sharingTable}
+ {/* <div className="change-buttons">
+ <button
+ onPointerDown={action(() => this.addButtonPressed = !this.addButtonPressed)}
+ >
+ <FontAwesomeIcon icon={fa.faPlus} size={"sm"} style={{ marginTop: -3, marginLeft: -3 }} />
+ </button>
+ <button
+ id="sharingProperties-removeUser"
+ onPointerDown={() => this.handleUserChange(this.selectedUser, false)}
+ style={{ backgroundColor: this.selectedUser ? "#121721" : "#777777" }}
+ ><FontAwesomeIcon icon={fa.faMinus} size={"sm"} style={{ marginTop: -3, marginLeft: -3 }} /></button>
+ <button onClick={() => SharingManager.Instance.open(this.selectedDocumentView!)}><FontAwesomeIcon icon={fa.faCog} size={"sm"} style={{ marginTop: -3, marginLeft: -3 }} /></button>
+ {this.addButtonPressed ?
+ // <input type="text" onKeyDown={this.handleKeyPress} /> :
+ <select onChange={e => this.handleUserChange(e.target.value, true)}>
+ <option selected disabled hidden>
+ Add users
+ </option>
+ {SharingManager.Instance.users.map(user =>
+ (<option value={user.user.email}>
+ {user.user.email}
+ </option>)
+ )}
+ {GroupManager.Instance.getAllGroups().map(group =>
+ (<option value={StrCast(group.groupName)}>
+ {StrCast(group.groupName)}
+ </option>))}
+ </select> :
+ null}
+ </div> */}
</div>}
</div>
@@ -884,9 +959,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<div className="propertiesView-name">
{this.editableTitle}
<div className="propertiesView-presSelected">
- {PresBox.Instance._selectedArray.length} selected
+ {PresBox.Instance?._selectedArray.length} selected
<div className="propertiesView-selectedList">
- {PresBox.Instance.listOfSelected}
+ {PresBox.Instance?.listOfSelected}
</div>
</div>
</div>
diff --git a/src/client/views/linking/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss
index 98e4171f0..4dc25031d 100644
--- a/src/client/views/linking/LinkMenu.scss
+++ b/src/client/views/linking/LinkMenu.scss
@@ -6,6 +6,7 @@
position: absolute;
top: 0;
left: 0;
+ z-index: 999;
.linkMenu-list {
diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index 7b5fb0127..8ecde959f 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -4,14 +4,13 @@ import { DocumentView } from "../nodes/DocumentView";
import { LinkEditor } from "./LinkEditor";
import './LinkMenu.scss';
import React = require("react");
-import { Doc, Opt } from "../../../fields/Doc";
+import { Doc } from "../../../fields/Doc";
import { LinkManager } from "../../util/LinkManager";
import { LinkMenuGroup } from "./LinkMenuGroup";
import { faTrash } from '@fortawesome/free-solid-svg-icons';
import { library } from "@fortawesome/fontawesome-svg-core";
import { DocumentLinksButton } from "../nodes/DocumentLinksButton";
import { LinkDocPreview } from "../nodes/LinkDocPreview";
-import { isUndefined } from "util";
library.add(faTrash);
@@ -19,7 +18,6 @@ interface Props {
docView: DocumentView;
changeFlyout: () => void;
addDocTab: (document: Doc, where: string) => boolean;
- location: number[];
}
@observer
@@ -85,17 +83,25 @@ export class LinkMenu extends React.Component<Props> {
return linkItems;
}
+ @computed
+ get position() {
+ const docView = this.props.docView;
+ const transform = (docView.props.ScreenToLocalTransform().scale(docView.props.ContentScaling())).inverse();
+ const [sptX, sptY] = transform.transformPoint(0, 0);
+ const [bptX, bptY] = transform.transformPoint(docView.props.PanelWidth(), docView.props.PanelHeight());
+ return { x: sptX, y: sptY, r: bptX, b: bptY };
+ }
+
render() {
+ console.log("computed", this.position.x, this.position.b);
const sourceDoc = this.props.docView.props.Document;
const groups: Map<string, Doc[]> = LinkManager.Instance.getRelatedGroupedLinks(sourceDoc);
return <div className="linkMenu" ref={this._linkMenuRef} >
- {!this._editingLink ? <div className="linkMenu-list" style={{
- left: this.props.location[0], top: this.props.location[1]
- }}>
- {this.renderAllGroups(groups)}
- </div> : <div className="linkMenu-listEditor" style={{
- left: this.props.location[0], top: this.props.location[1]
- }}>
+ {!this._editingLink ?
+ <div className="linkMenu-list" style={{ left: this.position.x, top: this.position.b + 15 }}>
+ {this.renderAllGroups(groups)}
+ </div> :
+ <div className="linkMenu-listEditor" style={{ left: this.position.x, top: this.position.b + 15 }}>
<LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink}
showLinks={action(() => this._editingLink = undefined)} />
</div>
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index e9420a072..306062ced 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -46,6 +46,40 @@
width: 100%;
height: 100%;
position: relative;
+
+
+ }
+
+ .recording {
+ margin-top: auto;
+ margin-bottom: auto;
+ width: 100%;
+ height: 100%;
+ position: relative;
+ padding-right: 5px;
+ display: flex;
+ background-color: red;
+
+ .time {
+ position: relative;
+ height: 100%;
+ width: 100%;
+ font-size: 20;
+ text-align: center;
+ top: 5;
+ }
+
+ .buttons {
+ position: relative;
+ margin-top: auto;
+ margin-bottom: auto;
+ width: 25px;
+ padding: 5px;
+ }
+
+ .buttons:hover {
+ background-color: crimson;
+ }
}
.audiobox-controls {
@@ -54,6 +88,17 @@
position: relative;
display: flex;
padding-left: 2px;
+ background: black;
+
+ .audiobox-dictation {
+ position: absolute;
+ width: 30px;
+ height: 100%;
+ align-items: center;
+ display: inherit;
+ background: dimgray;
+ left: 0px;
+ }
.audiobox-player {
margin-top: auto;
@@ -64,16 +109,32 @@
padding-right: 5px;
display: flex;
- .audiobox-playhead,
- .audiobox-dictation {
+ .audiobox-playhead {
position: relative;
margin-top: auto;
margin-bottom: auto;
- width: 25px;
+ margin-right: 2px;
+ width: 30px;
+ height: 25px;
padding: 2px;
+ border-radius: 50%;
+ background-color: black;
+ color: white;
+ }
+
+ .audiobox-playhead:hover {
+ // background-color: black;
+ // border-radius: 5px;
+ background-color: grey;
+ color: lightgrey;
}
.audiobox-dictation {
+ position: relative;
+ margin-top: auto;
+ margin-bottom: auto;
+ width: 25px;
+ padding: 2px;
align-items: center;
display: inherit;
background: dimgray;
@@ -81,17 +142,29 @@
.audiobox-timeline {
position: relative;
- height: 100%;
+ height: 80%;
width: 100%;
background: white;
border: gray solid 1px;
border-radius: 3px;
+ z-index: 1000;
+ overflow: hidden;
.audiobox-current {
width: 1px;
height: 100%;
background-color: red;
position: absolute;
+ top: 0px;
+ }
+
+ .waveform {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ z-index: -1000;
+ bottom: -30%;
}
.audiobox-linker,
@@ -104,7 +177,6 @@
background: gray;
border-radius: 100%;
opacity: 0.9;
- background-color: transparent;
box-shadow: black 2px 2px 1px;
.linkAnchorBox-cont {
@@ -142,11 +214,37 @@
.audiobox-marker-minicontainer {
position: absolute;
width: 10px;
+ height: 10px;
+ top: 2.5%;
+ background: gray;
+ border-radius: 50%;
+ box-shadow: black 2px 2px 1px;
+ overflow: visible;
+ cursor: pointer;
+
+ .audiobox-marker {
+ position: relative;
+ height: 100%;
+ // height: calc(100% - 15px);
+ width: 100%;
+ //margin-top: 15px;
+ }
+
+ .audio-marker:hover {
+ border: orange 2px solid;
+ }
+ }
+
+ .audiobox-marker-container1,
+ .audiobox-marker-minicontainer {
+ position: absolute;
+ width: 10px;
height: 90%;
top: 2.5%;
background: gray;
border-radius: 5px;
box-shadow: black 2px 2px 1px;
+ opacity: 0.3;
.audiobox-marker {
position: relative;
@@ -157,6 +255,36 @@
.audio-marker:hover {
border: orange 2px solid;
}
+
+ .resizer {
+ position: absolute;
+ right: 0;
+ cursor: ew-resize;
+ height: 100%;
+ width: 2px;
+ z-index: 100;
+ }
+
+ .click {
+ position: relative;
+ height: 100%;
+ width: 100%;
+ z-index: 100;
+ }
+
+ .left-resizer {
+ position: absolute;
+ left: 0;
+ cursor: ew-resize;
+ height: 100%;
+ width: 2px;
+ z-index: 100;
+ }
+ }
+
+ .audiobox-marker-container1:hover,
+ .audiobox-marker-minicontainer:hover {
+ opacity: 0.8;
}
.audiobox-marker-minicontainer {
@@ -170,6 +298,22 @@
}
}
}
+
+ .current-time {
+ position: absolute;
+ font-size: 8;
+ top: calc(100% - 8px);
+ left: 30px;
+ color: white;
+ }
+
+ .total-time {
+ position: absolute;
+ top: calc(100% - 8px);
+ font-size: 8;
+ right: 2px;
+ color: white;
+ }
}
}
}
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 2396e6973..eba1046b2 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -2,31 +2,30 @@ import React = require("react");
import { FieldViewProps, FieldView } from './FieldView';
import { observer } from "mobx-react";
import "./AudioBox.scss";
-import { Cast, DateCast, NumCast } from "../../../fields/Types";
+import { Cast, DateCast, NumCast, FieldValue, ScriptCast } from "../../../fields/Types";
import { AudioField, nullAudio } from "../../../fields/URLField";
-import { ViewBoxBaseComponent } from "../DocComponent";
+import { ViewBoxAnnotatableComponent } from "../DocComponent";
import { makeInterface, createSchema } from "../../../fields/Schema";
import { documentSchema } from "../../../fields/documentSchemas";
-import { Utils, returnTrue, emptyFunction, returnOne, returnTransparent, returnFalse, returnZero } from "../../../Utils";
-import { runInAction, observable, reaction, IReactionDisposer, computed, action } from "mobx";
+import { Utils, returnTrue, emptyFunction, returnOne, returnTransparent, returnFalse, returnZero, formatTime } from "../../../Utils";
+import { runInAction, observable, reaction, IReactionDisposer, computed, action, trace, toJS } from "mobx";
import { DateField } from "../../../fields/DateField";
import { SelectionManager } from "../../util/SelectionManager";
-import { Doc, DocListCast } from "../../../fields/Doc";
+import { Doc, DocListCast, Opt } from "../../../fields/Doc";
import { ContextMenuProps } from "../ContextMenuItem";
import { ContextMenu } from "../ContextMenu";
import { Id } from "../../../fields/FieldSymbols";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DocumentView } from "./DocumentView";
import { Docs, DocUtils } from "../../documents/Documents";
-import { ComputedField } from "../../../fields/ScriptField";
+import { ComputedField, ScriptField } from "../../../fields/ScriptField";
import { Networking } from "../../Network";
import { LinkAnchorBox } from "./LinkAnchorBox";
-
-// testing testing
-
-interface Window {
- MediaRecorder: MediaRecorder;
-}
+import { List } from "../../../fields/List";
+import { Scripting } from "../../util/Scripting";
+import Waveform from "react-audio-waveform";
+import axios from "axios";
+const _global = (window /* browser */ || global /* node */) as any;
declare class MediaRecorder {
// whatever MediaRecorder has
@@ -40,21 +39,42 @@ type AudioDocument = makeInterface<[typeof documentSchema, typeof audioSchema]>;
const AudioDocument = makeInterface(documentSchema, audioSchema);
@observer
-export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument>(AudioDocument) {
+export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioDocument>(AudioDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); }
public static Enabled = false;
+
static Instance: AudioBox;
+ static RangeScript: ScriptField;
+ static LabelScript: ScriptField;
+
_linkPlayDisposer: IReactionDisposer | undefined;
_reactionDisposer: IReactionDisposer | undefined;
_scrubbingDisposer: IReactionDisposer | undefined;
_ele: HTMLAudioElement | null = null;
_recorder: any;
_recordStart = 0;
+ _pauseStart = 0;
+ _pauseEnd = 0;
+ _pausedTime = 0;
_stream: MediaStream | undefined;
- constructor(props: any) {
- super(props);
- AudioBox.Instance = this;
- }
+ _start: number = 0;
+ _hold: boolean = false;
+ _left: boolean = false;
+ _markers: Array<any> = [];
+ _first: boolean = false;
+ _dragging = false;
+
+ _count: Array<any> = [];
+ _timeline: Opt<HTMLDivElement>;
+ _duration = 0;
+
+ private _isPointerDown = false;
+ private _currMarker: any;
+
+ @observable _position: number = 0;
+ @observable _buckets: Array<number> = new Array<number>();
+ @observable private _height: number = NumCast(this.layoutDoc._height);
+ @observable private _paused: boolean = false;
@observable private static _scrubTime = 0;
@computed get audioState(): undefined | "recording" | "paused" | "playing" { return this.dataDoc.audioState as (undefined | "recording" | "paused" | "playing"); }
set audioState(value) { this.dataDoc.audioState = value; }
@@ -62,12 +82,29 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
@computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); }
async slideTemplate() { return (await Cast((await Cast(Doc.UserDoc().slidesBtn, Doc) as Doc).dragFactory, Doc) as Doc); }
+ constructor(props: Readonly<FieldViewProps>) {
+ super(props);
+
+ // onClick play script
+ if (!AudioBox.RangeScript) {
+ AudioBox.RangeScript = ScriptField.MakeScript(`scriptContext.playFrom((this.audioStart), (this.audioEnd))`, { scriptContext: "any" })!;
+ }
+
+ if (!AudioBox.LabelScript) {
+ AudioBox.LabelScript = ScriptField.MakeScript(`scriptContext.playFrom((this.audioStart))`, { scriptContext: "any" })!;
+ }
+ }
+
componentWillUnmount() {
this._reactionDisposer?.();
this._linkPlayDisposer?.();
this._scrubbingDisposer?.();
}
componentDidMount() {
+ if (!this.dataDoc.markerAmount) {
+ this.dataDoc.markerAmount = 0;
+ }
+
runInAction(() => this.audioState = this.path ? "paused" : undefined);
this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID,
scrollLinkId => {
@@ -79,15 +116,59 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
}
}, { fireImmediately: true });
+
+ // for play when link is selected
this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(),
selected => {
const sel = selected.length ? selected[0].props.Document : undefined;
- this.layoutDoc.playOnSelect && this.recordingStart && sel && sel.creationDate && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFromTime(DateCast(sel.creationDate).date.getTime());
- this.layoutDoc.playOnSelect && this.recordingStart && !sel && this.pause();
+ let link;
+ if (sel) {
+ // for determining if the link is created after recording (since it will use linkTime rather than creation date)
+ DocListCast(this.dataDoc.links).map((l, i) => {
+ let la1 = l.anchor1 as Doc;
+ let la2 = l.anchor2 as Doc;
+ if (la1 === sel || la2 === sel) { // if the selected document is linked to this audio
+ let linkTime = NumCast(l.anchor2_timecode);
+ let endTime;
+ if (Doc.AreProtosEqual(la1, this.dataDoc)) {
+ la1 = l.anchor2 as Doc;
+ la2 = l.anchor1 as Doc;
+ linkTime = NumCast(l.anchor1_timecode);
+ }
+ if (la2.audioStart) {
+ linkTime = NumCast(la2.audioStart);
+ }
+
+ if (la1.audioStart) {
+ linkTime = NumCast(la1.audioStart);
+ }
+
+ if (la1.audioEnd) {
+ endTime = NumCast(la1.audioEnd);
+ }
+
+ if (la2.audioEnd) {
+ endTime = NumCast(la2.audioEnd);
+ }
+
+ if (linkTime) {
+ link = true;
+ this.layoutDoc.playOnSelect && this.recordingStart && sel && !Doc.AreProtosEqual(sel, this.props.Document) && (endTime ? this.playFrom(linkTime, endTime) : this.playFrom(linkTime));
+ }
+ }
+ });
+ }
+
+ // for links created during recording
+ if (!link) {
+ this.layoutDoc.playOnSelect && this.recordingStart && sel && sel.creationDate && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFromTime(DateCast(sel.creationDate).date.getTime());
+ this.layoutDoc.playOnSelect && this.recordingStart && !sel && this.pause();
+ }
});
this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, (time) => this.layoutDoc.playOnSelect && this.playFromTime(AudioBox._scrubTime));
}
+ // for updating the timecode
timecodeChanged = () => {
const htmlEle = this._ele;
if (this.audioState !== "recording" && htmlEle) {
@@ -107,15 +188,23 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
}
}
+ // pause play back
pause = action(() => {
this._ele!.pause();
this.audioState = "paused";
});
+ // play audio for documents created during recording
playFromTime = (absoluteTime: number) => {
this.recordingStart && this.playFrom((absoluteTime - this.recordingStart) / 1000);
}
- playFrom = (seekTimeInSeconds: number) => {
+
+ // play back the audio from time
+ @action
+ playFrom = (seekTimeInSeconds: number, endTime: number = this.dataDoc.duration) => {
+ let play;
+ clearTimeout(play);
+ this._duration = endTime - seekTimeInSeconds;
if (this._ele && AudioBox.Enabled) {
if (seekTimeInSeconds < 0) {
if (seekTimeInSeconds > -1) {
@@ -127,20 +216,29 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
this._ele.currentTime = seekTimeInSeconds;
this._ele.play();
runInAction(() => this.audioState = "playing");
+ if (endTime !== this.dataDoc.duration) {
+ play = setTimeout(() => this.pause(), (this._duration) * 1000); // use setTimeout to play a specific duration
+ }
} else {
this.pause();
}
}
}
-
+ // update the recording time
updateRecordTime = () => {
if (this.audioState === "recording") {
- setTimeout(this.updateRecordTime, 30);
- this.layoutDoc.currentTimecode = (new Date().getTime() - this._recordStart) / 1000;
+ if (this._paused) {
+ setTimeout(this.updateRecordTime, 30);
+ this._pausedTime += (new Date().getTime() - this._recordStart) / 1000;
+ } else {
+ setTimeout(this.updateRecordTime, 30);
+ this.layoutDoc.currentTimecode = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
+ }
}
}
+ // starts recording
recordAudioAnnotation = async () => {
this._stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this._recorder = new MediaRecorder(this._stream);
@@ -156,26 +254,31 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
runInAction(() => this.audioState = "recording");
setTimeout(this.updateRecordTime, 0);
this._recorder.start();
- setTimeout(() => this._recorder && this.stopRecording(), 60 * 1000); // stop after an hour
+ setTimeout(() => this._recorder && this.stopRecording(), 60 * 60 * 1000); // stop after an hour
}
+ // context menu
specificContextMenu = (e: React.MouseEvent): void => {
const funcs: ContextMenuProps[] = [];
- funcs.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when document selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" });
-
+ funcs.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when link is selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" });
+ funcs.push({ description: (this.layoutDoc.hideMarkers ? "Don't hide" : "Hide") + " markers", event: () => this.layoutDoc.hideMarkers = !this.layoutDoc.hideMarkers, icon: "expand-arrows-alt" });
+ funcs.push({ description: (this.layoutDoc.hideLabels ? "Don't hide" : "Hide") + " labels", event: () => this.layoutDoc.hideLabels = !this.layoutDoc.hideLabels, icon: "expand-arrows-alt" });
+ funcs.push({ description: (this.layoutDoc.playOnClick ? "Don't play" : "Play") + " markers onClick", event: () => this.layoutDoc.playOnClick = !this.layoutDoc.playOnClick, icon: "expand-arrows-alt" });
ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
}
+ // stops the recording
stopRecording = action(() => {
this._recorder.stop();
this._recorder = undefined;
- this.dataDoc.duration = (new Date().getTime() - this._recordStart) / 1000;
+ this.dataDoc.duration = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
this.audioState = "paused";
this._stream?.getAudioTracks()[0].stop();
const ind = DocUtils.ActiveRecordings.indexOf(this.props.Document);
ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
});
+ // button for starting and stopping the recording
recordClick = (e: React.MouseEvent) => {
if (e.button === 0 && !e.ctrlKey) {
this._recorder ? this.stopRecording() : this.recordAudioAnnotation();
@@ -183,14 +286,13 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
}
}
+ // for play button
onPlay = (e: any) => {
this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1);
e.stopPropagation();
}
- onStop = (e: any) => {
- this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect;
- e.stopPropagation();
- }
+
+ // creates a text document for dictation
onFile = (e: any) => {
const newDoc = Docs.Create.TextDocument("", {
title: "", _chromeStatus: "disabled",
@@ -204,18 +306,21 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
e.stopPropagation();
}
+ // ref for updating time
setRef = (e: HTMLAudioElement | null) => {
e?.addEventListener("timeupdate", this.timecodeChanged);
e?.addEventListener("ended", this.pause);
this._ele = e;
}
+ // returns the path of the audio file
@computed get path() {
const field = Cast(this.props.Document[this.props.fieldKey], AudioField);
const path = (field instanceof AudioField) ? field.url.href : "";
return path === nullAudio ? "" : path;
}
+ // returns the html audio element
@computed get audio() {
const interactive = this.active() ? "-interactive" : "";
return <audio ref={this.setRef} className={`audiobox-control${interactive}`}>
@@ -224,33 +329,390 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
</audio>;
}
+ // pause the time during recording phase
+ @action
+ recordPause = (e: React.MouseEvent) => {
+ this._pauseStart = new Date().getTime();
+ this._paused = true;
+ this._recorder.pause();
+ e.stopPropagation();
+
+ }
+
+ // continue the recording
+ @action
+ recordPlay = (e: React.MouseEvent) => {
+ this._pauseEnd = new Date().getTime();
+ this._paused = false;
+ this._recorder.resume();
+ e.stopPropagation();
+
+ }
+
+ // return the total time paused to update the correct time
+ @computed get pauseTime() {
+ return (this._pauseEnd - this._pauseStart);
+ }
+
+ // creates a new label
+ @action
+ newMarker(marker: Doc) {
+ marker.data = "";
+ if (this.dataDoc[this.annotationKey]) {
+ this.dataDoc[this.annotationKey].push(marker);
+ } else {
+ this.dataDoc[this.annotationKey] = new List<Doc>([marker]);
+ }
+ }
+
+ // the starting time of the marker
+ start(startingPoint: number) {
+ this._hold = true;
+ this._start = startingPoint;
+ }
+
+ // creates a new marker
+ @action
+ end(marker: number) {
+ this._hold = false;
+ const newMarker = Docs.Create.LabelDocument({ title: ComputedField.MakeFunction(`formatToTime(self.audioStart) + "-" + formatToTime(self.audioEnd)`) as any, isLabel: false, useLinkSmallAnchor: true, hideLinkButton: true, audioStart: this._start, audioEnd: marker, _showSidebar: false, _autoHeight: true, annotationOn: this.props.Document });
+ newMarker.data = "";
+ if (this.dataDoc[this.annotationKey]) {
+ this.dataDoc[this.annotationKey].push(newMarker);
+ } else {
+ this.dataDoc[this.annotationKey] = new List<Doc>([newMarker]);
+ }
+
+ this._start = 0;
+ }
+
+ // starting the drag event for marker resizing
+ onPointerDown = (e: React.PointerEvent, m: any, left: boolean): void => {
+ e.stopPropagation();
+ e.preventDefault();
+ this._isPointerDown = true;
+ this._currMarker = m;
+ this._timeline?.setPointerCapture(e.pointerId);
+ this._left = left;
+
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointerup", this.onPointerUp);
+ }
+
+ // ending the drag event for marker resizing
+ @action
+ onPointerUp = (e: PointerEvent): void => {
+ e.stopPropagation();
+ e.preventDefault();
+ this._isPointerDown = false;
+ this._dragging = false;
+
+ const rect = (e.target as any).getBoundingClientRect();
+ this._ele!.currentTime = this.layoutDoc.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
+
+ this._timeline?.releasePointerCapture(e.pointerId);
+
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ // resizes the marker while dragging
+ onPointerMove = async (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ if (!this._isPointerDown) {
+ return;
+ }
+
+ const rect = await (e.target as any).getBoundingClientRect();
+
+ const newTime = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
+
+ this.changeMarker(this._currMarker, newTime);
+ }
+
+ // updates the marker with the new time
+ @action
+ changeMarker = (m: any, time: any) => {
+ DocListCast(this.dataDoc[this.annotationKey]).forEach((marker: Doc) => {
+ if (this.isSame(marker, m)) {
+ this._left ? marker.audioStart = time : marker.audioEnd = time;
+ }
+ });
+ }
+
+ // checks if the two markers are the same with start and end time
+ isSame = (m1: any, m2: any) => {
+ if (m1.audioStart === m2.audioStart && m1.audioEnd === m2.audioEnd) {
+ return true;
+ }
+ return false;
+ }
+
+ // instantiates a new array of size 500 for marker layout
+ markers = () => {
+ const increment = NumCast(this.layoutDoc.duration) / 500;
+ this._count = [];
+ for (let i = 0; i < 500; i++) {
+ this._count.push([increment * i, 0]);
+ }
+
+ }
+
+ // makes sure no markers overlaps each other by setting the correct position and width
+ isOverlap = (m: any) => {
+ if (this._first) {
+ this._first = false;
+ this.markers();
+ }
+ let max = 0;
+
+ for (let i = 0; i < 500; i++) {
+ if (this._count[i][0] >= m.audioStart && this._count[i][0] <= m.audioEnd) {
+ this._count[i][1]++;
+
+ if (this._count[i][1] > max) {
+ max = this._count[i][1];
+ }
+ }
+ }
+
+ for (let i = 0; i < 500; i++) {
+ if (this._count[i][0] >= m.audioStart && this._count[i][0] <= m.audioEnd) {
+ this._count[i][1] = max;
+ }
+
+ }
+
+ if (this.dataDoc.markerAmount < max) {
+ this.dataDoc.markerAmount = max;
+ }
+ return max - 1;
+ }
+
+ // returns the audio waveform
+ @computed get waveform() {
+ return <Waveform
+ color={"darkblue"}
+ height={this._height}
+ barWidth={0.1}
+ // pos={this.layoutDoc.currentTimecode}
+ pos={this.dataDoc.duration}
+ duration={this.dataDoc.duration}
+ peaks={this._buckets.length === 100 ? this._buckets : undefined}
+ progressColor={"blue"} />;
+ }
+
+ // decodes the audio file into peaks for generating the waveform
+ @action
+ buckets = async () => {
+ const audioCtx = new (window.AudioContext)();
+
+ axios({ url: this.path, responseType: "arraybuffer" })
+ .then(response => {
+ const audioData = response.data;
+
+ audioCtx.decodeAudioData(audioData, action(buffer => {
+ const decodedAudioData = buffer.getChannelData(0);
+ const NUMBER_OF_BUCKETS = 100;
+ const bucketDataSize = Math.floor(decodedAudioData.length / NUMBER_OF_BUCKETS);
+
+ for (let i = 0; i < NUMBER_OF_BUCKETS; i++) {
+ const startingPoint = i * bucketDataSize;
+ const endingPoint = i * bucketDataSize + bucketDataSize;
+ let max = 0;
+ for (let j = startingPoint; j < endingPoint; j++) {
+ if (decodedAudioData[j] > max) {
+ max = decodedAudioData[j];
+ }
+ }
+ const size = Math.abs(max);
+ this._buckets.push(size / 2);
+ }
+
+ }));
+ });
+ }
+
+ // Returns the peaks of the audio waveform
+ @computed get peaks() {
+ return this.buckets();
+ }
+
+ // for updating the width and height of the waveform with timeline ref
+ timelineRef = (timeline: HTMLDivElement) => {
+ const observer = new _global.ResizeObserver(action((entries: any) => {
+ for (const entry of entries) {
+ this.update(entry.contentRect.width, entry.contentRect.height);
+ this._position = entry.contentRect.width;
+ }
+ }));
+ timeline && observer.observe(timeline);
+
+ this._timeline = timeline;
+ }
+
+ // update the width and height of the audio waveform
+ @action
+ update = (width: number, height: number) => {
+ if (height) {
+ this._height = 0.8 * NumCast(this.layoutDoc._height);
+ const canvas2 = document.getElementsByTagName("canvas")[0];
+ if (canvas2) {
+ const oldWidth = canvas2.width;
+ const oldHeight = canvas2.height;
+ canvas2.style.height = `${this._height}`;
+ canvas2.style.width = `${width}`;
+
+ const ratio1 = oldWidth / window.innerWidth;
+ const ratio2 = oldHeight / window.innerHeight;
+ const context = canvas2.getContext('2d');
+ if (context) {
+ context.scale(ratio1, ratio2);
+ }
+ }
+
+ const canvas1 = document.getElementsByTagName("canvas")[1];
+ if (canvas1) {
+ const oldWidth = canvas1.width;
+ const oldHeight = canvas1.height;
+ canvas1.style.height = `${this._height}`;
+ canvas1.style.width = `${width}`;
+
+ const ratio1 = oldWidth / window.innerWidth;
+ const ratio2 = oldHeight / window.innerHeight;
+ const context = canvas1.getContext('2d');
+ if (context) {
+ context.scale(ratio1, ratio2);
+ }
+
+ const parent = canvas1.parentElement;
+ if (parent) {
+ parent.style.width = `${width}`;
+ parent.style.height = `${this._height}`;
+ }
+ }
+ }
+ }
+
+ rangeScript = () => AudioBox.RangeScript;
+
+ labelScript = () => AudioBox.LabelScript;
+
+ // for indicating the first marker that is rendered
+ reset = () => this._first = true;
+
render() {
const interactive = this.active() ? "-interactive" : "";
+ this.reset();
+ this.path && this._buckets.length !== 100 ? this.peaks : null; // render waveform if audio is done recording
return <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : undefined}>
{!this.path ?
<div className="audiobox-buttons">
<div className="audiobox-dictation" onClick={this.onFile}>
<FontAwesomeIcon style={{ width: "30px", background: this.layoutDoc.playOnSelect ? "yellow" : "rgba(0,0,0,0)" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
</div>
- <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this.audioState === "recording" ? "red" : "black" }}>
- {this.audioState === "recording" ? "STOP" : "RECORD"}
- </button>
+ {this.audioState === "recording" ?
+ <div className="recording" onClick={e => e.stopPropagation()}>
+ <div className="buttons" onClick={this.recordClick}>
+ <FontAwesomeIcon style={{ width: "100%" }} icon={"stop"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
+ </div>
+ <div className="buttons" onClick={this._paused ? this.recordPlay : this.recordPause}>
+ <FontAwesomeIcon style={{ width: "100%" }} icon={this._paused ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
+ </div>
+ <div className="time">{formatTime(Math.round(NumCast(this.layoutDoc.currentTimecode)))}</div>
+ </div>
+ :
+ <button className={`audiobox-record${interactive}`} style={{ backgroundColor: "black" }}>
+ RECORD
+ </button>}
</div> :
- <div className="audiobox-controls">
- <div className="audiobox-player" onClick={this.onPlay}>
- <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this.audioState === "paused" ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
- <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%", background: this.layoutDoc.playOnSelect ? "yellow" : "dimGray" }} icon="hand-point-left" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
- <div className="audiobox-timeline" onClick={e => e.stopPropagation()}
+ <div className="audiobox-controls" >
+ <div className="audiobox-dictation"></div>
+ <div className="audiobox-player" >
+ <div className="audiobox-playhead" title={this.audioState === "paused" ? "play" : "pause"} onClick={this.onPlay}> <FontAwesomeIcon style={{ width: "100%", position: "absolute", left: "0px", top: "5px", borderWidth: "thin", borderColor: "white" }} icon={this.audioState === "paused" ? "play" : "pause"} size={"1x"} /></div>
+ <div className="audiobox-timeline" ref={this.timelineRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }}
onPointerDown={e => {
+ e.stopPropagation();
+ e.preventDefault();
if (e.button === 0 && !e.ctrlKey) {
const rect = (e.target as any).getBoundingClientRect();
- const wasPaused = this.audioState === "paused";
+
+ if (e.target as HTMLElement !== document.getElementById("current")) {
+ const wasPaused = this.audioState === "paused";
+ this._ele!.currentTime = this.layoutDoc.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
+ wasPaused && this.pause();
+ }
+ }
+ if (e.button === 0 && e.altKey) {
+ this.newMarker(Docs.Create.LabelDocument({ title: ComputedField.MakeFunction(`formatToTime(self.audioStart)`) as any, useLinkSmallAnchor: true, hideLinkButton: true, isLabel: true, audioStart: this._ele!.currentTime, _showSidebar: false, _autoHeight: true, annotationOn: this.props.Document }));
+ }
+
+ if (e.button === 0 && e.shiftKey) {
+ const rect = (e.target as any).getBoundingClientRect();
this._ele!.currentTime = this.layoutDoc.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
- wasPaused && this.pause();
- e.stopPropagation();
+ this._hold ? this.end(this._ele!.currentTime) : this.start(this._ele!.currentTime);
}
- }} >
+ }}>
+ <div className="waveform" id="waveform" style={{ height: `${100}%`, width: "100%", bottom: "0px" }}>
+ {this.waveform}
+ </div>
+ {DocListCast(this.dataDoc[this.annotationKey]).map((m, i) => {
+ let rect;
+ (!m.isLabel) ?
+ (this.layoutDoc.hideMarkers) ? (null) :
+ rect =
+ <div key={i} id={"audiobox-marker-container1"} className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container1"}
+ title={`${formatTime(Math.round(NumCast(m.audioStart)))}` + " - " + `${formatTime(Math.round(NumCast(m.audioEnd)))}`}
+ style={{
+ left: `${NumCast(m.audioStart) / NumCast(this.dataDoc.duration, 1) * 100}%`,
+ width: `${(NumCast(m.audioEnd) - NumCast(m.audioStart)) / NumCast(this.dataDoc.duration, 1) * 100}%`, height: `${1 / (this.dataDoc.markerAmount + 1) * 100}%`,
+ top: `${this.isOverlap(m) * 1 / (this.dataDoc.markerAmount + 1) * 100}%`
+ }}
+ onClick={e => { this.playFrom(NumCast(m.audioStart), NumCast(m.audioEnd)); e.stopPropagation(); }} >
+ <div className="left-resizer" onPointerDown={e => this.onPointerDown(e, m, true)}></div>
+ <DocumentView {...this.props}
+ Document={m}
+ pointerEvents={true}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ rootSelected={returnFalse}
+ LayoutTemplate={undefined}
+ ContainingCollectionDoc={this.props.Document}
+ removeDocument={this.removeDocument}
+ parentActive={returnTrue}
+ onClick={this.layoutDoc.playOnClick ? this.rangeScript : undefined}
+ ignoreAutoHeight={false}
+ bringToFront={emptyFunction}
+ scriptContext={this} />
+ <div className="resizer" onPointerDown={e => this.onPointerDown(e, m, false)}></div>
+ </div>
+ :
+ (this.layoutDoc.hideLabels) ? (null) :
+ rect =
+ <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={i} style={{ left: `${NumCast(m.audioStart) / NumCast(this.dataDoc.duration, 1) * 100}%` }}>
+ <DocumentView {...this.props}
+ Document={m}
+ pointerEvents={true}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ rootSelected={returnFalse}
+ LayoutTemplate={undefined}
+ ContainingCollectionDoc={this.props.Document}
+ removeDocument={this.removeDocument}
+ parentActive={returnTrue}
+ onClick={this.layoutDoc.playOnClick ? this.labelScript : undefined}
+ ignoreAutoHeight={false}
+ bringToFront={emptyFunction}
+ scriptContext={this} />
+ </div>;
+ return rect;
+ })}
{DocListCast(this.dataDoc.links).map((l, i) => {
+
let la1 = l.anchor1 as Doc;
let la2 = l.anchor2 as Doc;
let linkTime = NumCast(l.anchor2_timecode);
@@ -259,32 +721,45 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
la2 = l.anchor1 as Doc;
linkTime = NumCast(l.anchor1_timecode);
}
+
+ if (la2.audioStart && !la2.audioEnd) {
+ linkTime = NumCast(la2.audioStart);
+ }
+
return !linkTime ? (null) :
- <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}>
- <div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}>
- <DocumentView {...this.props}
- Document={l}
- NativeHeight={returnZero}
- NativeWidth={returnZero}
- rootSelected={returnFalse}
- LayoutTemplate={undefined}
- LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(l, la2)}`)}
- ContainingCollectionDoc={this.props.Document}
- dontRegisterView={true}
- parentActive={returnTrue}
- bringToFront={emptyFunction}
- backgroundColor={returnTransparent} />
- </div>
- <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)}
- onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { const wasPaused = this.audioState === "paused"; this.playFrom(linkTime); wasPaused && this.pause(); e.stopPropagation(); } }} />
+ <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }} onClick={e => e.stopPropagation()}>
+ <DocumentView {...this.props}
+ Document={l}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ rootSelected={returnFalse}
+ ContainingCollectionDoc={this.props.Document}
+ parentActive={returnTrue}
+ bringToFront={emptyFunction}
+ backgroundColor={returnTransparent}
+ ContentScaling={returnOne}
+ forcedBackgroundColor={returnTransparent}
+ pointerEvents={false}
+ LayoutTemplate={undefined}
+ LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(l, la2)}`)}
+ />
+ <div key={i} className={`audiobox-marker`} onPointerEnter={() => Doc.linkFollowHighlight(la1)}
+ onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { const wasPaused = this.audioState === "paused"; this.playFrom(linkTime); e.stopPropagation(); e.preventDefault(); } }} />
</div>;
})}
- <div className="audiobox-current" style={{ left: `${NumCast(this.layoutDoc.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} />
+ <div className="audiobox-current" id="current" onClick={e => { e.stopPropagation(); e.preventDefault(); }} style={{ left: `${NumCast(this.layoutDoc.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%`, pointerEvents: "none" }} />
{this.audio}
</div>
+ <div className="current-time">
+ {formatTime(Math.round(NumCast(this.layoutDoc.currentTimecode)))}
+ </div>
+ <div className="total-time">
+ {formatTime(Math.round(NumCast(this.dataDoc.duration)))}
+ </div>
</div>
</div>
}
</div>;
}
-} \ No newline at end of file
+}
+Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); }); \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index cb79e1522..c2f27c85a 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -74,7 +74,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
}
} else if (!this.props.InMenu) {
DocumentLinksButton.EditLink = this.props.View;
- DocumentLinksButton.EditLinkLoc = [e.clientX + 10, e.clientY];
}
}));
}
@@ -91,7 +90,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
//action(() => Doc.BrushDoc(this.props.View.Document));
} else if (!this.props.InMenu) {
DocumentLinksButton.EditLink = this.props.View;
- DocumentLinksButton.EditLinkLoc = [e.clientX + 10, e.clientY];
}
}
@@ -147,8 +145,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
@observable
public static EditLink: DocumentView | undefined;
- public static EditLinkLoc: number[] = [0, 0];
-
@action clearLinks() {
DocumentLinksButton.StartLink = undefined;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b9e685b44..444583af3 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -179,7 +179,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15);
// RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "map-pin", selected: -1 });
- RadialMenu.Instance.addItem({ description: "Delete", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "external-link-square-alt", selected: -1 });
+ const effectiveAcl = GetEffectiveAcl(this.props.Document);
+ (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && RadialMenu.Instance.addItem({ description: "Delete", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "external-link-square-alt", selected: -1 });
// RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "onRight"), icon: "trash", selected: -1 });
RadialMenu.Instance.addItem({ description: "Pin", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin", selected: -1 });
RadialMenu.Instance.addItem({ description: "Open", event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: "trash", selected: -1 });
@@ -570,6 +571,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
alert("Can't delete the active workspace");
} else {
SelectionManager.DeselectAll();
+ this.props.Document.deleted = true;
this.props.removeDocument?.(this.props.Document);
}
}
@@ -762,7 +764,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" });
Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()) && moreItems.push({ description: "Toggle Always Show Link End", event: () => Doc.UserDoc()["documentLinksButton-hideEnd"] = !Doc.UserDoc()["documentLinksButton-hideEnd"], icon: "eye" });
}
- //GetEffectiveAcl(this.props.Document) === AclEdit && moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" });
const effectiveAcl = GetEffectiveAcl(this.props.Document);
(effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" });
@@ -891,20 +892,21 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this.rootDoc.type === DocumentType.LINK ||
this.props.dontRegisterView ? (null) : // view that are not registered
DocUtils.FilterDocs(this.directLinks, this.props.docFilters(), []).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) =>
- <div className="documentView-anchorCont" key={i + 1}> <DocumentView {...this.props}
- Document={d}
- ContainingCollectionView={this.props.ContainingCollectionView}
- ContainingCollectionDoc={this.props.Document} // bcz: hack this.props.Document is not a collection Need a better prop for passing the containing document to the LinkAnchorBox
- PanelWidth={this.anchorPanelWidth}
- PanelHeight={this.anchorPanelHeight}
- ContentScaling={returnOne}
- dontRegisterView={false}
- forcedBackgroundColor={returnTransparent}
- removeDocument={this.hideLinkAnchor}
- pointerEvents={false}
- LayoutTemplate={undefined}
- LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(d, this.props.Document)}`)}
- /></div >);
+ <div className="documentView-anchorCont" key={i + 1}>
+ <DocumentView {...this.props}
+ Document={d}
+ ContainingCollectionView={this.props.ContainingCollectionView}
+ ContainingCollectionDoc={this.props.Document} // bcz: hack this.props.Document is not a collection Need a better prop for passing the containing document to the LinkAnchorBox
+ PanelWidth={this.anchorPanelWidth}
+ PanelHeight={this.anchorPanelHeight}
+ ContentScaling={returnOne}
+ dontRegisterView={false}
+ forcedBackgroundColor={returnTransparent}
+ removeDocument={this.hideLinkAnchor}
+ pointerEvents={false}
+ LayoutTemplate={undefined}
+ LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(d, this.props.Document)}`)} />
+ </div >);
}
@computed get innards() {
TraceMobx();
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index be6292bb6..50b2af0d7 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -119,7 +119,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch
const y = NumCast(this.rootDoc[this.fieldKey + "_y"], 100);
const c = StrCast(this.layoutDoc._backgroundColor, StrCast(this.layoutDoc.backgroundColor, StrCast(this.dataDoc.backgroundColor, "lightBlue"))); // note this is not where the typical lightBlue default color comes from. See Documents.Create.LinkDocument()
const anchor = this.fieldKey === "anchor1" ? "anchor2" : "anchor1";
- const anchorScale = (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : .25;
+ const anchorScale = !this.dataDoc[this.fieldKey + "-useLinkSmallAnchor"] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : .25;
const timecode = this.dataDoc[anchor + "_timecode"];
const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title) + (timecode !== undefined ? ":" + timecode : "");
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index f2ef42162..fd4525ced 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -137,7 +137,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
this.gotoDocument(nextSelected, this.itemIndex);
const targetNext = Cast(activeNext.presentationTargetDoc, Doc, null);
if (activeNext && targetNext.type === DocumentType.AUDIO && activeNext.playAuto) {
- } else { this._moveOnFromAudio = false };
+ } else this._moveOnFromAudio = false;
}
}
@@ -1440,7 +1440,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
</select>
<div className="presBox-presentPanel" style={{ opacity: this.childDocs.length > 0 ? 1 : 0.3 }}>
<span className={`presBox-button ${this.layoutDoc.presStatus === "edit" ? "present" : ""}`}>
- <div className="presBox-button-left" onClick={() => { if (this.childDocs.length > 0) this.layoutDoc.presStatus = "manual" }}>
+ <div className="presBox-button-left" onClick={() => (this.childDocs.length > 0) && (this.layoutDoc.presStatus = "manual")}>
<FontAwesomeIcon icon={"play-circle"} />
<div style={{ display: this.props.PanelWidth() > 200 ? "inline-flex" : "none" }}>&nbsp; Present</div>
</div>
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index cc37cf586..b0bf54be6 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -2,7 +2,7 @@ import { library } from '@fortawesome/fontawesome-svg-core';
import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { isEqual } from "lodash";
-import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx";
+import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, trace } from "mobx";
import { observer } from "mobx-react";
import { baseKeymap, selectAll } from "prosemirror-commands";
import { history } from "prosemirror-history";
@@ -93,6 +93,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
private _undoTyping?: UndoManager.Batch;
private _disposers: { [name: string]: IReactionDisposer } = {};
private _dropDisposer?: DragManager.DragDropDisposer;
+ private _first: Boolean = true;
+ private _recordingStart: number = 0;
+ private _currentTime: number = 0;
+ private _linkTime: number | null = null;
+ private _pause: boolean = false;
@computed get _recording() { return this.dataDoc.audioState === "recording"; }
set _recording(value) { this.dataDoc.audioState = value ? "recording" : undefined; }
@@ -140,6 +145,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
super(props);
FormattedTextBox.Instance = this;
this.updateHighlights();
+ this._recordingStart = Date.now();
+ this.layoutDoc._timeStampOnEnter = true;
}
public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
@@ -197,9 +204,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
dispatchTransaction = (tx: Transaction) => {
+ let timeStamp;
+ clearTimeout(timeStamp);
if (this._editorView) {
+
const metadata = tx.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata);
if (metadata) {
+
const range = tx.selection.$from.blockRange(tx.selection.$to);
let text = range ? tx.doc.textBetween(range.start, range.end) : "";
let textEndSelection = tx.selection.to;
@@ -221,6 +232,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this.dataDoc[key] = value;
}
}
+
const state = this._editorView.state.apply(tx);
this._editorView.updateState(state);
@@ -233,6 +245,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const json = JSON.stringify(state.toJSON());
let unchanged = true;
const effectiveAcl = GetEffectiveAcl(this.dataDoc);
+
+
if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) {
if (!this._applyingChange && json.replace(/"selection":.*/, "") !== curProto?.Data.replace(/"selection":.*/, "")) {
this._applyingChange = true;
@@ -240,13 +254,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
(curText !== Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text) && (this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()))) && (this.dataDoc[lastmodified] = new DateField(new Date(Date.now())));
if ((!curTemp && !curProto) || curText || curLayout?.Data.includes("dash")) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
if (json.replace(/"selection":.*/, "") !== curLayout?.Data.replace(/"selection":.*/, "")) {
+ if (!this._pause && !this.layoutDoc._timeStampOnEnter) {
+ timeStamp = setTimeout(() => this.pause(), 10 * 1000); // 10 seconds delay for time stamp
+ }
+
+ // if 10 seconds have passed, insert time stamp the next time you type
+ if (this._pause) {
+ this._pause = false;
+ this.insertTime();
+ }
!curText && tx.storedMarks?.map(m => m.type.name === "pFontSize" && (Doc.UserDoc().fontSize = this.layoutDoc._fontSize = m.attrs.fontSize));
!curText && tx.storedMarks?.map(m => m.type.name === "pFontFamily" && (Doc.UserDoc().fontFamily = this.layoutDoc._fontFamily = m.attrs.fontFamily));
this.dataDoc[this.props.fieldKey] = new RichTextField(json, curText);
this.dataDoc[this.props.fieldKey + "-noTemplate"] = (curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited
ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText });
unchanged = false;
+
}
+
} else { // if we've deleted all the text in a note driven by a template, then restore the template data
this.dataDoc[this.props.fieldKey] = undefined;
this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((curProto || curTemp).Data)));
@@ -260,6 +285,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
} else {
+
const json = JSON.parse(Cast(this.dataDoc[this.fieldKey], RichTextField)?.Data!);
json.selection = state.toJSON().selection;
this._editorView.updateState(EditorState.fromJSON(this.config, json));
@@ -267,6 +293,61 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
+ pause = () => this._pause = true;
+
+ formatTime = (time: number) => {
+ const hours = Math.floor(time / 60 / 60);
+ const minutes = Math.floor(time / 60) - (hours * 60);
+ const seconds = time % 60;
+
+ return hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
+ }
+
+ // for inserting timestamps
+ insertTime = () => {
+ if (this._first) {
+ this._first = false;
+ DocListCast(this.dataDoc.links).map((l, i) => {
+ let la1 = l.anchor1 as Doc;
+ let la2 = l.anchor2 as Doc;
+ this._linkTime = NumCast(l.anchor2_timecode);
+ if (Doc.AreProtosEqual(la2, this.dataDoc)) {
+ la1 = l.anchor2 as Doc;
+ la2 = l.anchor1 as Doc;
+ this._linkTime = NumCast(l.anchor1_timecode);
+ }
+
+ });
+ }
+ this._currentTime = Date.now();
+ let time;
+ this._linkTime ? time = this.formatTime(Math.round(this._linkTime + this._currentTime / 1000 - this._recordingStart / 1000)) : time = null;
+
+ if (this._editorView) {
+ const state = this._editorView.state;
+ const now = Date.now();
+ let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(now / 1000) });
+ if (!this._break && state.selection.to !== state.selection.from) {
+ for (let i = state.selection.from; i <= state.selection.to; i++) {
+ const pos = state.doc.resolve(i);
+ const um = Array.from(pos.marks()).find(m => m.type === schema.marks.user_mark);
+ if (um) {
+ mark = um;
+ break;
+ }
+ }
+ }
+ if (time) {
+ let value = "";
+ this._break = false;
+ value = this.layoutDoc._timeStampOnEnter ? "[" + time + "] " : "\n" + "[" + time + "] ";
+ const from = state.selection.from;
+ const inserted = state.tr.insertText(value).addMark(from, from + value.length + 1, mark);
+ this._editorView.dispatch(this._editorView.state.tr.insertText(value));
+ }
+ }
+ }
+
updateTitle = () => {
if ((this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing
StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.rootDoc.customTitle) {
@@ -524,6 +605,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
uicontrols.push({ description: `${this.layoutDoc._showSidebar ? "Hide" : "Show"} Sidebar`, event: () => this.layoutDoc._showSidebar = !this.layoutDoc._showSidebar, 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" });
+ uicontrols.push({ description: `Create TimeStamp When ${this.layoutDoc._timeStampOnEnter ? "Pause" : "Enter"}`, event: () => this.layoutDoc._timeStampOnEnter = !this.layoutDoc._timeStampOnEnter, icon: "expand-arrows-alt" });
!Doc.UserDoc().noviceMode && uicontrols.push({
description: "Broadcast Message", event: () => DocServer.GetRefField("rtfProto").then(proto =>
proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.rootDoc[this.fieldKey], RichTextField)?.Text)), icon: "expand-arrows-alt"
@@ -1383,6 +1465,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
e.stopPropagation();
if (e.key === "Tab" || e.key === "Enter") {
+ if (e.key === "Enter" && this.layoutDoc._timeStampOnEnter) {
+ this.insertTime();
+ }
e.preventDefault();
}
if (e.key === " " || this._lastTimedMark?.attrs.userid !== Doc.CurrentUserEmail) {
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index bcd6f716b..ce784c3d9 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -31,7 +31,7 @@ export const marks: { [index: string]: MarkSpec } = {
inclusive: false,
parseDOM: [{
tag: "a[href]", getAttrs(dom: any) {
- return { allLinks: [{ href: dom.getAttribute("href"), title: dom.getAttribute("title"), linkId: dom.getAttribute("linkids"), targetId: dom.getAttribute("targetids") }], location: dom.getAttribute("location"), };
+ return { allLinks: [{ href: dom.getAttribute("href"), title: dom.getAttribute("title"), linkId: dom.getAttribute("linkids"), targetId: dom.dataset.targetids }], location: dom.getAttribute("location"), };
}
}],
toDOM(node: any) {
@@ -40,10 +40,10 @@ export const marks: { [index: string]: MarkSpec } = {
return node.attrs.docref && node.attrs.title ?
["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, href: node.attrs.allLinks[0].href, class: "prosemirror-attribution" }, node.attrs.title], ["br"]] :
node.attrs.allLinks.length === 1 ?
- ["a", { ...node.attrs, class: linkids, targetids, style: `text-decoration: ${linkids === " " ? "underline" : undefined}`, title: `${node.attrs.title}`, href: node.attrs.allLinks[0].href }, 0] :
+ ["a", { ...node.attrs, class: linkids, dataTargetids: targetids, title: `${node.attrs.title}`, href: node.attrs.allLinks[0].href, style: `text-decoration: ${linkids === " " ? "underline" : undefined}` }, 0] :
["div", { class: "prosemirror-anchor" },
["span", { class: "prosemirror-linkBtn" },
- ["a", { ...node.attrs, class: linkids, targetids, title: `${node.attrs.title}` }, 0],
+ ["a", { ...node.attrs, class: linkids, dataTargetids: targetids, title: `${node.attrs.title}` }, 0],
["input", { class: "prosemirror-hrefoptions" }],
],
["div", { class: "prosemirror-links" }, ...node.attrs.allLinks.map((item: { href: string, title: string }) =>