aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/ContextMenu.scss1
-rw-r--r--src/client/views/DocComponent.tsx11
-rw-r--r--src/client/views/DocumentButtonBar.tsx2
-rw-r--r--src/client/views/DocumentDecorations.scss42
-rw-r--r--src/client/views/DocumentDecorations.tsx66
-rw-r--r--src/client/views/GestureOverlay.tsx2
-rw-r--r--src/client/views/InkingControl.tsx2
-rw-r--r--src/client/views/MainView.scss6
-rw-r--r--src/client/views/MainView.tsx24
-rw-r--r--src/client/views/MetadataEntryMenu.scss9
-rw-r--r--src/client/views/ScriptBox.tsx4
-rw-r--r--src/client/views/SearchDocBox.tsx10
-rw-r--r--src/client/views/animationtimeline/Keyframe.scss105
-rw-r--r--src/client/views/animationtimeline/Keyframe.tsx560
-rw-r--r--src/client/views/animationtimeline/Timeline.scss322
-rw-r--r--src/client/views/animationtimeline/Timeline.tsx622
-rw-r--r--src/client/views/animationtimeline/TimelineMenu.scss94
-rw-r--r--src/client/views/animationtimeline/TimelineMenu.tsx77
-rw-r--r--src/client/views/animationtimeline/TimelineOverview.scss107
-rw-r--r--src/client/views/animationtimeline/TimelineOverview.tsx181
-rw-r--r--src/client/views/animationtimeline/Track.scss15
-rw-r--r--src/client/views/animationtimeline/Track.tsx380
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx36
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx6
-rw-r--r--src/client/views/collections/CollectionMapView.tsx8
-rw-r--r--src/client/views/collections/CollectionMasonryViewFieldRow.tsx137
-rw-r--r--src/client/views/collections/CollectionPileView.tsx9
-rw-r--r--src/client/views/collections/CollectionSchemaCells.tsx2
-rw-r--r--src/client/views/collections/CollectionSchemaMovableTableHOC.tsx4
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx14
-rw-r--r--src/client/views/collections/CollectionStackingView.scss7
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx54
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx10
-rw-r--r--src/client/views/collections/CollectionSubView.tsx90
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx4
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx67
-rw-r--r--src/client/views/collections/CollectionView.tsx44
-rw-r--r--src/client/views/collections/CollectionViewChromes.scss11
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx5
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx95
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx6
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx21
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx22
-rw-r--r--src/client/views/linking/LinkMenuGroup.tsx4
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx39
-rw-r--r--src/client/views/nodes/AudioBox.tsx14
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx30
-rw-r--r--src/client/views/nodes/ContentFittingDocumentView.tsx31
-rw-r--r--src/client/views/nodes/DocumentBox.scss3
-rw-r--r--src/client/views/nodes/DocumentBox.tsx151
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx52
-rw-r--r--src/client/views/nodes/DocumentView.scss1
-rw-r--r--src/client/views/nodes/DocumentView.tsx94
-rw-r--r--src/client/views/nodes/FieldView.tsx8
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx2
-rw-r--r--src/client/views/nodes/LinkAnchorBox.tsx14
-rw-r--r--src/client/views/nodes/PDFBox.scss1
-rw-r--r--src/client/views/nodes/PresBox.scss29
-rw-r--r--src/client/views/nodes/PresBox.tsx223
-rw-r--r--src/client/views/nodes/QueryBox.tsx3
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx6
-rw-r--r--src/client/views/nodes/VideoBox.tsx6
-rw-r--r--src/client/views/nodes/formattedText/DashDocCommentView.tsx95
-rw-r--r--src/client/views/nodes/formattedText/DashDocView.tsx269
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.scss36
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.tsx207
-rw-r--r--src/client/views/nodes/formattedText/FootnoteView.tsx162
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss (renamed from src/client/views/nodes/FormattedTextBox.scss)2
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx (renamed from src/client/views/nodes/FormattedTextBox.tsx)180
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBoxComment.scss (renamed from src/client/views/nodes/FormattedTextBoxComment.scss)0
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx (renamed from src/client/views/nodes/FormattedTextBoxComment.tsx)30
-rw-r--r--src/client/views/nodes/formattedText/ImageResizeView.tsx138
-rw-r--r--src/client/views/nodes/formattedText/ParagraphNodeSpec.ts143
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts241
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.scss124
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx875
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts319
-rw-r--r--src/client/views/nodes/formattedText/RichTextSchema.tsx537
-rw-r--r--src/client/views/nodes/formattedText/SummaryView.tsx81
-rw-r--r--src/client/views/nodes/formattedText/TooltipTextMenu.scss372
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts296
-rw-r--r--src/client/views/nodes/formattedText/nodes_rts.ts264
-rw-r--r--src/client/views/nodes/formattedText/prosemirrorPatches.js139
-rw-r--r--src/client/views/nodes/formattedText/schema_rts.ts26
-rw-r--r--src/client/views/pdf/PDFViewer.scss4
-rw-r--r--src/client/views/presentationview/PresElementBox.scss43
-rw-r--r--src/client/views/presentationview/PresElementBox.tsx134
-rw-r--r--src/client/views/search/SearchItem.tsx12
90 files changed, 7945 insertions, 797 deletions
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index 30938688d..1bf242d93 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -7,6 +7,7 @@
box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
flex-direction: column;
background: whitesmoke;
+ padding-top: 10px;
padding-bottom: 10px;
border-radius: 15px;
border: solid #BBBBBBBB 1px;
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 0a8f0c9a7..11866aebe 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,7 +1,7 @@
import { Doc, Opt, DataSym } from '../../new_fields/Doc';
import { Touchable } from './Touchable';
import { computed, action, observable } from 'mobx';
-import { Cast, BoolCast } from '../../new_fields/Types';
+import { Cast, BoolCast, ScriptCast } from '../../new_fields/Types';
import { listSpec } from '../../new_fields/Schema';
import { InkingControl } from './InkingControl';
import { InkTool } from '../../new_fields/InkField';
@@ -11,7 +11,7 @@ import { InteractionUtils } from '../util/InteractionUtils';
/// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView)
interface DocComponentProps {
Document: Doc;
- LayoutDoc?: () => Opt<Doc>;
+ LayoutTemplate?: () => Opt<Doc>;
}
export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: Doc) => T) {
class Component extends Touchable<P> {
@@ -20,7 +20,7 @@ export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: D
// This is the "The Document" -- it encapsulates, data, layout, and any templates
@computed get rootDoc() { return Cast(this.props.Document.rootDocument, Doc, null) || this.props.Document; }
// This is the rendering data of a document -- it may be "The Document", or it may be some template document that holds the rendering info
- @computed get layoutDoc() { return Doc.Layout(this.props.Document); }
+ @computed get layoutDoc() { return Doc.Layout(this.props.Document, this.props.LayoutTemplate?.()); }
// This is the data part of a document -- ie, the data that is constant across all views of the document
@computed get dataDoc() { return this.props.Document[DataSym] as Doc; }
@@ -33,6 +33,7 @@ export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: D
interface ViewBoxBaseProps {
Document: Doc;
DataDoc?: Doc;
+ ContainingCollectionDoc: Opt<Doc>;
fieldKey: string;
isSelected: (outsideReaction?: boolean) => boolean;
renderDepth: number;
@@ -53,6 +54,8 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps, T>(schemaCtor:
// key where data is stored
@computed get fieldKey() { return this.props.fieldKey; }
+ lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.ContainingCollectionDoc }).result;
+
active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0 || this.layoutDoc.forceActive);// && !InkingControl.Instance.selectedTool; // bcz: inking state shouldn't affect static tools
protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
}
@@ -87,6 +90,8 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
// key where data is stored
@computed get fieldKey() { return this.props.fieldKey; }
+ lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result;
+
protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
_annotationKey: string = "annotations";
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index c02f79187..3624cdb6d 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -15,7 +15,7 @@ import './collections/ParentDocumentSelector.scss';
import './DocumentButtonBar.scss';
import { LinkMenu } from "./linking/LinkMenu";
import { DocumentView } from './nodes/DocumentView';
-import { GoogleRef } from "./nodes/FormattedTextBox";
+import { GoogleRef } from "./nodes/formattedText/FormattedTextBox";
import { TemplateMenu } from "./TemplateMenu";
import { Template, Templates } from "./Templates";
import React = require("react");
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 28cf9fd47..4f34eb9e3 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -21,9 +21,13 @@ $linkGap : 3px;
background: none;
}
+
.documentDecorations-resizer {
pointer-events: auto;
background: $alt-accent;
+ opacity: 0.1;
+ }
+ .documentDecorations-resizer:hover {
opacity: 1;
}
@@ -40,7 +44,6 @@ $linkGap : 3px;
.documentDecorations-radius {
pointer-events: auto;
- background: black;
opacity: 1;
transform: translate(10px, 10px);
grid-row: 4;
@@ -80,7 +83,20 @@ $linkGap : 3px;
#documentDecorations-topLeftResizer,
#documentDecorations-bottomRightResizer {
cursor: nwse-resize;
- background: dimGray;
+ background: unset;
+ opacity: 1;
+ }
+ #documentDecorations-topLeftResizer {
+ border-left: 2px solid;
+ border-top: solid 2px;
+ }
+ #documentDecorations-bottomRightResizer {
+ border-right: 2px solid;
+ border-bottom: solid 2px;
+ }
+ #documentDecorations-topLeftResizer:hover,
+ #documentDecorations-bottomRightResizer:hover {
+ opacity: 1;
}
#documentDecorations-bottomRightResizer {
@@ -90,7 +106,22 @@ $linkGap : 3px;
#documentDecorations-topRightResizer,
#documentDecorations-bottomLeftResizer {
cursor: nesw-resize;
+ background: unset;
+ opacity: 1;
+ }
+ #documentDecorations-topRightResizer {
+ border-right: 2px solid;
+ border-top: 2px solid;
+ }
+ #documentDecorations-bottomLeftResizer {
+ border-left: 2px solid;
+ border-bottom: 2px solid;
+ }
+ #documentDecorations-topRightResizer:hover,
+ #documentDecorations-bottomLeftResizer:hover {
+ cursor: nesw-resize;
background: dimGray;
+ opacity: 1;
}
#documentDecorations-topResizer,
@@ -104,7 +135,6 @@ $linkGap : 3px;
}
.documentDecorations-contextMenu {
- background: $alt-accent;
width: 25px;
height: calc(100% + 8px); // 8px for the height of the top resizer bar
grid-column-start: 1;
@@ -112,14 +142,14 @@ $linkGap : 3px;
pointer-events: all;
}
.documentDecorations-title {
- background: $alt-accent;
opacity: 1;
grid-column-start: 3;
grid-column-end: 4;
pointer-events: auto;
overflow: hidden;
text-align: center;
- display:flex;
+ display: flex;
+ border-bottom: solid 1px;
}
.publishBox {
width: 20px;
@@ -136,7 +166,6 @@ $linkGap : 3px;
.documentDecorations-closeButton {
- background: $alt-accent;
opacity: 1;
grid-column-start: 4;
grid-column-end: 6;
@@ -146,7 +175,6 @@ $linkGap : 3px;
}
.documentDecorations-minimizeButton {
- background: $alt-accent;
opacity: 1;
grid-column-start: 1;
grid-column-end: 3;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 312acd5b2..f8e77d90a 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc, DataSym, Field } from "../../new_fields/Doc";
-import { PositionDocument } from '../../new_fields/documentSchemas';
+import { Document } from '../../new_fields/documentSchemas';
import { ScriptField } from '../../new_fields/ScriptField';
import { Cast, StrCast, NumCast } from "../../new_fields/Types";
import { Utils, setupMoveUpEvents, emptyFunction, returnFalse, simulateMouseClick } from "../../Utils";
@@ -20,6 +20,7 @@ import React = require("react");
import { Id } from '../../new_fields/FieldSymbols';
import e = require('express');
import { CollectionDockingView } from './collections/CollectionDockingView';
+import { MainView } from './MainView';
library.add(faCaretUp);
library.add(faObjectGroup);
@@ -46,6 +47,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
private _linkBoxHeight = 20 + 3; // link button height + margin
private _titleHeight = 20;
private _resizeUndo?: UndoManager.Batch;
+ private _offX = 0; _offY = 0; // offset from click pt to inner edge of resize border
+ private _snapX = 0; _snapY = 0; // last snapped location of resize border
@observable private _accumulatedTitle = "";
@observable private _titleControlString: string = "#title";
@observable private _edtingTitle = false;
@@ -238,12 +241,24 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, (e) => { });
if (e.button === 0) {
this._resizeHdlId = e.currentTarget.id;
+ const bounds = e.currentTarget.getBoundingClientRect();
+ this._offX = this._resizeHdlId.toLowerCase().includes("left") ? bounds.right - e.clientX : bounds.left - e.clientX;
+ this._offY = this._resizeHdlId.toLowerCase().includes("top") ? bounds.bottom - e.clientY : bounds.top - e.clientY;
this.Interacting = true;
this._resizeUndo = UndoManager.StartBatch("DocDecs resize");
+ SelectionManager.SelectedDocuments()[0].props.setupDragLines?.();
}
+ this._snapX = e.pageX;
+ this._snapY = e.pageY;
}
onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => {
+ const { thisX, thisY } = DragManager.snapDrag(e, -this._offX, -this._offY, this._offX, this._offY);
+ move[0] = thisX - this._snapX;
+ move[1] = thisY - this._snapY;
+ this._snapX = thisX;
+ this._snapY = thisY;
+
let dX = 0, dY = 0, dW = 0, dH = 0;
switch (this._resizeHdlId) {
@@ -286,12 +301,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => {
if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
- const doc = PositionDocument(element.props.Document);
- const layoutDoc = PositionDocument(Doc.Layout(element.props.Document));
- let nwidth = layoutDoc._nativeWidth || 0;
- let nheight = layoutDoc._nativeHeight || 0;
- const width = (layoutDoc._width || 0);
- const height = (layoutDoc._height || (nheight / nwidth * width));
+ const doc = Document(element.rootDoc);
+ let nwidth = doc._nativeWidth || 0;
+ let nheight = doc._nativeHeight || 0;
+ const width = (doc._width || 0);
+ const height = (doc._height || (nheight / nwidth * width));
const scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling();
if (nwidth && nheight) {
if (Math.abs(dW) > Math.abs(dH)) dH = dW * nheight / nwidth;
@@ -303,8 +317,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
doc.y = (doc.y || 0) + dY * (actualdH - height);
const fixedAspect = (nwidth && nheight);
if (fixedAspect && (!nwidth || !nheight)) {
- layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0;
- layoutDoc._nativeHeight = nheight = layoutDoc._height || 0;
+ doc._nativeWidth = nwidth = doc._width || 0;
+ doc._nativeHeight = nheight = doc._height || 0;
}
const anno = Cast(doc.annotationOn, Doc, null);
if (e.ctrlKey && anno) {
@@ -320,24 +334,24 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
else if (nwidth > 0 && nheight > 0) {
if (Math.abs(dW) > Math.abs(dH)) {
if (!fixedAspect) {
- layoutDoc._nativeWidth = actualdW / (layoutDoc._width || 1) * (layoutDoc._nativeWidth || 0);
+ doc._nativeWidth = actualdW / (doc._width || 1) * (doc._nativeWidth || 0);
}
- layoutDoc._width = actualdW;
- if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width;
- else layoutDoc._height = actualdH;
+ doc._width = actualdW;
+ if (fixedAspect && !doc._fitWidth) doc._height = nheight / nwidth * doc._width;
+ else doc._height = actualdH;
}
else {
if (!fixedAspect) {
- layoutDoc._nativeHeight = actualdH / (layoutDoc._height || 1) * (doc._nativeHeight || 0);
+ doc._nativeHeight = actualdH / (doc._height || 1) * (doc._nativeHeight || 0);
}
- layoutDoc._height = actualdH;
- if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._width = nwidth / nheight * layoutDoc._height;
- else layoutDoc._width = actualdW;
+ doc._height = actualdH;
+ if (fixedAspect && !doc._fitWidth) doc._width = nwidth / nheight * doc._height;
+ else doc._width = actualdW;
}
} else {
- dW && (layoutDoc._width = actualdW);
- dH && (layoutDoc._height = actualdH);
- dH && layoutDoc._autoHeight && (layoutDoc._autoHeight = false);
+ dW && (doc._width = actualdW);
+ dH && (doc._height = actualdH);
+ dH && doc._autoHeight && (doc._autoHeight = false);
}
}
}));
@@ -350,6 +364,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
this.Interacting = false;
(e.button === 0) && this._resizeUndo?.end();
this._resizeUndo = undefined;
+ DragManager.Vals.Instance.clearSnapLines();
}
@computed
@@ -388,7 +403,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
const darkScheme = Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "dimgray" : undefined;
const bounds = this.Bounds;
const seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined;
- if (SelectionManager.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)) {
+ if (DragManager.Vals.Instance.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 minimal = bounds.r - bounds.x < 100 ? true : false;
@@ -473,10 +488,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer"
onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
- {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) : <div id="documentDecorations-levelSelector" className="documentDecorations-selector" title="tap to select containing document"
- onPointerDown={this.onSelectorUp} onContextMenu={(e) => e.preventDefault()}>
- <FontAwesomeIcon className="documentdecorations-times" icon={faArrowAltCircleUp} size="lg" />
- </div>}
+ {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) :
+ <div id="documentDecorations-levelSelector" className="documentDecorations-selector"
+ title="tap to select containing document" onPointerDown={this.onSelectorUp} onContextMenu={e => e.preventDefault()}>
+ <FontAwesomeIcon className="documentdecorations-times" icon={faArrowAltCircleUp} size="lg" />
+ </div>}
<div id="documentDecorations-borderRadius" className="documentDecorations-radius"
onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}></div>
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 1977f2406..4f8f9ed69 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -634,7 +634,7 @@ export default class GestureOverlay extends Touchable {
}
// if we're not drawing in a toolglass try to recognize as gesture
else {
- const result = GestureUtils.GestureRecognizer.Recognize(new Array(points));
+ const result = points.length > 2 && GestureUtils.GestureRecognizer.Recognize(new Array(points));
let actionPerformed = false;
if (result && result.Score > 0.7) {
switch (result.Name) {
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 172c1864a..70ea955e1 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -8,7 +8,7 @@ import { Scripting } from "../util/Scripting";
import { SelectionManager } from "../util/SelectionManager";
import { undoBatch } from "../util/UndoManager";
import GestureOverlay from "./GestureOverlay";
-import { FormattedTextBox } from "./nodes/FormattedTextBox";
+import { FormattedTextBox } from "./nodes/formattedText/FormattedTextBox";
export class InkingControl {
@observable static Instance: InkingControl;
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index 81d427f64..04288a9e1 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -39,7 +39,12 @@
}
}
+.mainView-container {
+ color:dimgray;
+}
+
.mainView-container-dark {
+ color: lightgray;
.lm_goldenlayout {
background: dimgray;
}
@@ -54,7 +59,6 @@
}
.contextMenu-cont, .contextMenu-item {
background: dimGray;
- color: lightgray;
}
.contextMenu-item:hover {
background: gray;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 8fb67c435..71c2bf245 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,5 +1,5 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faTerminal, faCalculator, faWindowMaximize, faAddressCard, faQuestionCircle, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt, faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter, faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faThumbtack, faTree, faTv, faUndoAlt, faVideo } from '@fortawesome/free-solid-svg-icons';
+import { faTerminal, faFile as fileSolid, faLocationArrow, faSearch, faFileDownload, faStop, faCalculator, faWindowMaximize, faAddressCard, faQuestionCircle, faArrowLeft, faArrowRight, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt, faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter, faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faThumbtack, faTree, faTv, faUndoAlt, faVideo } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
@@ -19,7 +19,7 @@ import { DocServer } from '../DocServer';
import { Docs, DocumentOptions } from '../documents/Documents';
import { DocumentType } from '../documents/DocumentTypes';
import { HistoryUtil } from '../util/History';
-import RichTextMenu from '../util/RichTextMenu';
+import RichTextMenu from './nodes/formattedText/RichTextMenu';
import { Scripting } from '../util/Scripting';
import SettingsManager from '../util/SettingsManager';
import SharingManager from '../util/SharingManager';
@@ -42,6 +42,8 @@ import { OverlayView } from './OverlayView';
import PDFMenu from './pdf/PDFMenu';
import { PreviewCursor } from './PreviewCursor';
import { ScriptField } from '../../new_fields/ScriptField';
+import { TimelineMenu } from './animationtimeline/TimelineMenu';
+import { DragManager } from '../util/DragManager';
@observer
export class MainView extends React.Component {
@@ -103,6 +105,11 @@ export class MainView extends React.Component {
}
library.add(faTerminal);
+ library.add(faLocationArrow);
+ library.add(faSearch);
+ library.add(fileSolid);
+ library.add(faFileDownload);
+ library.add(faStop);
library.add(faCalculator);
library.add(faWindowMaximize);
library.add(faFileAlt);
@@ -141,6 +148,8 @@ export class MainView extends React.Component {
library.add(faCaretUp);
library.add(faFilter);
library.add(faBullseye);
+ library.add(faArrowLeft);
+ library.add(faArrowRight);
library.add(faArrowDown);
library.add(faArrowUp);
library.add(faCloudUploadAlt);
@@ -163,6 +172,9 @@ export class MainView extends React.Component {
if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) {
ContextMenu.Instance.closeMenu();
}
+ if (targets && (targets.length && targets[0].className.toString() !== "timeline-menu-desc" && targets[0].className.toString() !== "timeline-menu-item" && targets[0].className.toString() !== "timeline-menu-input")) {
+ TimelineMenu.Instance.closeMenu();
+ }
});
globalPointerUp = () => this.isPointerDown = false;
@@ -580,6 +592,14 @@ export class MainView extends React.Component {
<MarqueeOptionsMenu />
<RichTextMenu />
<OverlayView />
+ {// TO VIEW SNAP LINES
+ <div className="snapLines" style={{ position: "absolute", top: 0, left: 0, width: "100%", height: "100%", pointerEvents: "none" }}>
+ <svg style={{ width: "100%", height: "100%" }}>
+ {DragManager.Vals.Instance.horizSnapLines.map((l: any) => <line x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)}
+ {DragManager.Vals.Instance.vertSnapLines.map((l: any) => <line y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)}
+ </svg>
+ </div>}
+ <TimelineMenu />
</div >);
}
}
diff --git a/src/client/views/MetadataEntryMenu.scss b/src/client/views/MetadataEntryMenu.scss
index 5776cf070..28de0b7a5 100644
--- a/src/client/views/MetadataEntryMenu.scss
+++ b/src/client/views/MetadataEntryMenu.scss
@@ -9,9 +9,10 @@
}
.metadataEntry-autoSuggester {
- width: 100%;
+ width: 80%;
height: 100%;
- padding-right: 10px;
+ margin: 0;
+ display: inline-block;
}
#metadataEntry-outer {
@@ -25,7 +26,7 @@
flex-direction: column;
}
.metadataEntry-inputArea {
- display:flex;
+ display:inline-block;
flex-direction: row;
}
@@ -44,7 +45,7 @@
.react-autosuggest__input {
border: 1px solid #aaa;
border-radius: 4px;
- width: 100%;
+ width: 75%;
}
.react-autosuggest__input--focused {
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
index 153b81876..66d3b937e 100644
--- a/src/client/views/ScriptBox.tsx
+++ b/src/client/views/ScriptBox.tsx
@@ -81,9 +81,9 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
);
}
//let l = docList(this.source[0].data).length; if (l) { let ind = this.target[0].index !== undefined ? (this.target[0].index+1) % l : 0; this.target[0].index = ind; this.target[0].proto = getProto(docList(this.source[0].data)[ind]);}
- public static EditButtonScript(title: string, doc: Doc, fieldKey: string, clientX: number, clientY: number, contextParams?: { [name: string]: string }) {
+ public static EditButtonScript(title: string, doc: Doc, fieldKey: string, clientX: number, clientY: number, contextParams?: { [name: string]: string }, defaultScript?: ScriptField) {
let overlayDisposer: () => void = emptyFunction;
- const script = ScriptCast(doc[fieldKey]);
+ const script = ScriptCast(doc[fieldKey]) || defaultScript;
let originalText: string | undefined = undefined;
if (script) {
originalText = script.script.originalScript;
diff --git a/src/client/views/SearchDocBox.tsx b/src/client/views/SearchDocBox.tsx
index 799fa9d85..7bd689b19 100644
--- a/src/client/views/SearchDocBox.tsx
+++ b/src/client/views/SearchDocBox.tsx
@@ -6,7 +6,7 @@ import { observer } from "mobx-react";
import { Doc, DocListCast } from "../../new_fields/Doc";
import { Id } from "../../new_fields/FieldSymbols";
import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types";
-import { returnFalse } from "../../Utils";
+import { returnFalse, returnZero } from "../../Utils";
import { Docs } from "../documents/Documents";
import { SearchUtil } from "../util/SearchUtil";
import { EditableView } from "./EditableView";
@@ -399,7 +399,13 @@ export class SearchDocBox extends React.Component<FieldViewProps> {
<ContentFittingDocumentView {...this.props}
Document={this.content}
rootSelected={returnFalse}
- getTransform={this.props.ScreenToLocalTransform}>
+ bringToFront={returnFalse}
+ ContainingCollectionDoc={undefined}
+ ContainingCollectionView={undefined}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ parentActive={this.props.active}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}>
</ContentFittingDocumentView>
<div
style={{
diff --git a/src/client/views/animationtimeline/Keyframe.scss b/src/client/views/animationtimeline/Keyframe.scss
new file mode 100644
index 000000000..84c8de287
--- /dev/null
+++ b/src/client/views/animationtimeline/Keyframe.scss
@@ -0,0 +1,105 @@
+@import "./../globalCssVariables.scss";
+
+
+$timelineColor: #9acedf;
+$timelineDark: #77a1aa;
+
+.bar {
+ height: 100%;
+ width: 5px;
+ position: absolute;
+
+ // pointer-events: none;
+ .menubox {
+ width: 200px;
+ height: 200px;
+ top: 50%;
+ position: relative;
+ background-color: $light-color;
+
+ .menutable {
+ tr:nth-child(odd) {
+ background-color: $light-color-secondary;
+ }
+ }
+ }
+
+ .leftResize {
+ left: -10px;
+ border: 3px solid black;
+ }
+
+ .rightResize {
+ right: -10px;
+ border: 3px solid black;
+ }
+
+ .keyframe-indicator {
+ height: 20px;
+ width: 20px;
+ top: calc(50% - 10px);
+ background-color: white;
+ -ms-transform: rotate(45deg);
+ -webkit-transform: rotate(45deg);
+ transform: rotate(45deg);
+ z-index: 1000;
+ position: absolute;
+ }
+
+ .keyframe-information {
+ display: none;
+ position: relative;
+ // z-index: 100000;
+ // background: $timelineDark;
+ width: 100px;
+ // left: -50px;
+ height: 100px;
+ // top: 40px;
+ }
+
+ .keyframeCircle {
+ left: -10px;
+ border: 3px solid $timelineDark;
+ }
+
+ .fadeLeft {
+ left: 0px;
+ height: 100%;
+ position: absolute;
+ pointer-events: none;
+ background: linear-gradient(to left, $timelineColor 10%, $light-color);
+ }
+
+ .fadeRight {
+ right: 0px;
+ height: 100%;
+ position: absolute;
+ pointer-events: none;
+ background: linear-gradient(to right, $timelineColor 10%, $light-color);
+ }
+
+ .divider {
+ height: 100%;
+ width: 1px;
+ position: absolute;
+ background-color: black;
+ cursor: col-resize;
+ pointer-events: none;
+ }
+
+ .keyframe {
+ height: 100%;
+ position: absolute;
+ }
+
+ .fadeIn-container,
+ .fadeOut-container,
+ .body-container {
+ position: absolute;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ opacity: 0;
+ }
+
+
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx
new file mode 100644
index 000000000..bbd7b2676
--- /dev/null
+++ b/src/client/views/animationtimeline/Keyframe.tsx
@@ -0,0 +1,560 @@
+import * as React from "react";
+import "./Keyframe.scss";
+import "./Timeline.scss";
+import "../globalCssVariables.scss";
+import { observer } from "mobx-react";
+import { observable, reaction, action, IReactionDisposer, observe, computed, runInAction, trace } from "mobx";
+import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
+import { Cast, NumCast } from "../../../new_fields/Types";
+import { List } from "../../../new_fields/List";
+import { createSchema, defaultSpec, makeInterface, listSpec } from "../../../new_fields/Schema";
+import { Transform } from "../../util/Transform";
+import { TimelineMenu } from "./TimelineMenu";
+import { Docs } from "../../documents/Documents";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
+import { emptyPath } from "../../../Utils";
+
+
+
+/**
+ * Useful static functions that you can use. Mostly for logic, but you can also add UI logic here also
+ */
+export namespace KeyframeFunc {
+
+ export enum KeyframeType {
+ end = "end",
+ fade = "fade",
+ default = "default",
+ }
+
+ export enum Direction {
+ left = "left",
+ right = "right"
+ }
+
+ export const findAdjacentRegion = (dir: KeyframeFunc.Direction, currentRegion: Doc, regions: Doc[]): (RegionData | undefined) => {
+ let leftMost: (RegionData | undefined) = undefined;
+ let rightMost: (RegionData | undefined) = undefined;
+ regions.forEach(region => {
+ const neighbor = RegionData(region);
+ if (currentRegion.position! > neighbor.position) {
+ if (!leftMost || neighbor.position > leftMost.position) {
+ leftMost = neighbor;
+ }
+ } else if (currentRegion.position! < neighbor.position) {
+ if (!rightMost || neighbor.position < rightMost.position) {
+ rightMost = neighbor;
+ }
+ }
+ });
+ if (dir === Direction.left) {
+ return leftMost;
+ } else if (dir === Direction.right) {
+ return rightMost;
+ }
+ };
+
+ export const calcMinLeft = (region: Doc, currentBarX: number, ref?: Doc) => { //returns the time of the closet keyframe to the left
+ let leftKf: Opt<Doc>;
+ let time: number = 0;
+ const keyframes = DocListCast(region.keyframes!);
+ keyframes.map((kf) => {
+ let compTime = currentBarX;
+ if (ref) compTime = NumCast(ref.time);
+ if (NumCast(kf.time) < compTime && NumCast(kf.time) >= time) {
+ leftKf = kf;
+ time = NumCast(kf.time);
+ }
+ });
+ return leftKf;
+ };
+
+
+ export const calcMinRight = (region: Doc, currentBarX: number, ref?: Doc) => { //returns the time of the closest keyframe to the right
+ let rightKf: Opt<Doc>;
+ let time: number = Infinity;
+ DocListCast(region.keyframes!).forEach((kf) => {
+ let compTime = currentBarX;
+ if (ref) compTime = NumCast(ref.time);
+ if (NumCast(kf.time) > compTime && NumCast(kf.time) <= NumCast(time)) {
+ rightKf = kf;
+ time = NumCast(kf.time);
+ }
+ });
+ return rightKf;
+ };
+
+ export const defaultKeyframe = () => {
+ const regiondata = new Doc(); //creating regiondata in MILI
+ regiondata.duration = 4000;
+ regiondata.position = 0;
+ regiondata.fadeIn = 1000;
+ regiondata.fadeOut = 1000;
+ regiondata.functions = new List<Doc>();
+ regiondata.hasData = false;
+ return regiondata;
+ };
+
+
+ export const convertPixelTime = (pos: number, unit: "mili" | "sec" | "min" | "hr", dir: "pixel" | "time", tickSpacing: number, tickIncrement: number) => {
+ const time = dir === "pixel" ? (pos * tickSpacing) / tickIncrement : (pos / tickSpacing) * tickIncrement;
+ switch (unit) {
+ case "mili":
+ return time;
+ case "sec":
+ return dir === "pixel" ? time / 1000 : time * 1000;
+ case "min":
+ return dir === "pixel" ? time / 60000 : time * 60000;
+ case "hr":
+ return dir === "pixel" ? time / 3600000 : time * 3600000;
+ default:
+ return time;
+ }
+ };
+}
+
+export const RegionDataSchema = createSchema({
+ position: defaultSpec("number", 0),
+ duration: defaultSpec("number", 0),
+ keyframes: listSpec(Doc),
+ fadeIn: defaultSpec("number", 0),
+ fadeOut: defaultSpec("number", 0),
+ functions: listSpec(Doc),
+ hasData: defaultSpec("boolean", false)
+});
+export type RegionData = makeInterface<[typeof RegionDataSchema]>;
+export const RegionData = makeInterface(RegionDataSchema);
+
+interface IProps {
+ node: Doc;
+ RegionData: Doc;
+ collection: Doc;
+ tickSpacing: number;
+ tickIncrement: number;
+ time: number;
+ changeCurrentBarX: (x: number) => void;
+ transform: Transform;
+ makeKeyData: (region: RegionData, pos: number, kftype: KeyframeFunc.KeyframeType) => Doc;
+}
+
+
+/**
+ *
+ * This class handles the green region stuff
+ * Key facts:
+ *
+ * Structure looks like this
+ *
+ * region as a whole
+ * <------------------------------REGION------------------------------->
+ *
+ * region broken down
+ *
+ * <|---------|############ MAIN CONTENT #################|-----------|> .....followed by void.........
+ * (start) (Fade 2)
+ * (fade 1) (finish)
+ *
+ *
+ * As you can see, this is different from After Effect and Premiere Pro, but this is how TAG worked.
+ * If you want to checkout TAG, it's in the lockers, and the password is the usual lab door password. It's the blue laptop.
+ * If you want to know the exact location of the computer, message me.
+ *
+ * @author Andrew Kim
+ */
+@observer
+export class Keyframe extends React.Component<IProps> {
+
+ @observable private _bar = React.createRef<HTMLDivElement>();
+ @observable private _mouseToggled = false;
+ @observable private _doubleClickEnabled = false;
+
+ @computed private get regiondata() { return RegionData(this.props.RegionData); }
+ @computed private get regions() { return DocListCast(this.props.node.regions); }
+ @computed private get keyframes() { return DocListCast(this.regiondata.keyframes); }
+ @computed private get pixelPosition() { return KeyframeFunc.convertPixelTime(this.regiondata.position, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
+ @computed private get pixelDuration() { return KeyframeFunc.convertPixelTime(this.regiondata.duration, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
+ @computed private get pixelFadeIn() { return KeyframeFunc.convertPixelTime(this.regiondata.fadeIn, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
+ @computed private get pixelFadeOut() { return KeyframeFunc.convertPixelTime(this.regiondata.fadeOut, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
+
+ constructor(props: any) {
+ super(props);
+ }
+ componentDidMount() {
+ setTimeout(() => { //giving it a temporary 1sec delay...
+ if (!this.regiondata.keyframes) this.regiondata.keyframes = new List<Doc>();
+ const start = this.props.makeKeyData(this.regiondata, this.regiondata.position, KeyframeFunc.KeyframeType.end);
+ const fadeIn = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.fadeIn, KeyframeFunc.KeyframeType.fade);
+ const fadeOut = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut, KeyframeFunc.KeyframeType.fade);
+ const finish = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration, KeyframeFunc.KeyframeType.end);
+ (fadeIn.key as Doc).opacity = 1;
+ (fadeOut.key as Doc).opacity = 1;
+ (start.key as Doc).opacity = 0.1;
+ (finish.key as Doc).opacity = 0.1;
+ this.forceUpdate(); //not needed, if setTimeout is gone...
+ }, 1000);
+ }
+
+ @action
+ onBarPointerDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const clientX = e.clientX;
+ if (this._doubleClickEnabled) {
+ this.createKeyframe(clientX);
+ this._doubleClickEnabled = false;
+ } else {
+ setTimeout(() => {
+ if (!this._mouseToggled && this._doubleClickEnabled) this.props.changeCurrentBarX(this.pixelPosition + (clientX - this._bar.current!.getBoundingClientRect().left) * this.props.transform.Scale);
+ this._mouseToggled = false;
+ this._doubleClickEnabled = false;
+ }, 200);
+ this._doubleClickEnabled = true;
+ document.addEventListener("pointermove", this.onBarPointerMove);
+ document.addEventListener("pointerup", (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.onBarPointerMove);
+ });
+ }
+ }
+
+ @action
+ onBarPointerMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.movementX !== 0) {
+ this._mouseToggled = true;
+ }
+ const left = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions)!;
+ const right = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions)!;
+ const prevX = this.regiondata.position;
+ const futureX = this.regiondata.position + KeyframeFunc.convertPixelTime(e.movementX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ if (futureX <= 0) {
+ this.regiondata.position = 0;
+ } else if ((left && left.position + left.duration >= futureX)) {
+ this.regiondata.position = left.position + left.duration;
+ } else if ((right && right.position <= futureX + this.regiondata.duration)) {
+ this.regiondata.position = right.position - this.regiondata.duration;
+ } else {
+ this.regiondata.position = futureX;
+ }
+ const movement = this.regiondata.position - prevX;
+ this.keyframes.forEach(kf => kf.time = NumCast(kf.time) + movement);
+ }
+
+ @action
+ onResizeLeft = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.addEventListener("pointermove", this.onDragResizeLeft);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onDragResizeLeft);
+ });
+ }
+
+ @action
+ onResizeRight = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.addEventListener("pointermove", this.onDragResizeRight);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onDragResizeRight);
+ });
+ }
+
+ @action
+ onDragResizeLeft = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const bar = this._bar.current!;
+ const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ const leftRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions);
+ if (leftRegion && this.regiondata.position + offset <= leftRegion.position + leftRegion.duration) {
+ this.regiondata.position = leftRegion.position + leftRegion.duration;
+ this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time) - (leftRegion.position + leftRegion.duration);
+ } else if (NumCast(this.keyframes[1].time) + offset >= NumCast(this.keyframes[2].time)) {
+ this.regiondata.position = NumCast(this.keyframes[2].time) - this.regiondata.fadeIn;
+ this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time) - NumCast(this.keyframes[2].time) + this.regiondata.fadeIn;
+ } else if (NumCast(this.keyframes[0].time) + offset <= 0) {
+ this.regiondata.position = 0;
+ this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time);
+ } else {
+ this.regiondata.duration -= offset;
+ this.regiondata.position += offset;
+ }
+ this.keyframes[0].time = this.regiondata.position;
+ this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;
+ }
+
+
+ @action
+ onDragResizeRight = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const bar = this._bar.current!;
+ const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ const rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions);
+ const fadeOutKeyframeTime = NumCast(this.keyframes[this.keyframes.length - 3].time);
+ if (this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut + offset <= fadeOutKeyframeTime) { //case 1: when third to last keyframe is in the way
+ this.regiondata.duration = fadeOutKeyframeTime - this.regiondata.position + this.regiondata.fadeOut;
+ } else if (rightRegion && (this.regiondata.position + this.regiondata.duration + offset >= rightRegion.position)) {
+ this.regiondata.duration = rightRegion.position - this.regiondata.position;
+ } else {
+ this.regiondata.duration += offset;
+ }
+ this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut;
+ this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration;
+ }
+
+
+ @action
+ createKeyframe = async (clientX: number) => {
+ this._mouseToggled = true;
+ const bar = this._bar.current!;
+ const offset = KeyframeFunc.convertPixelTime(Math.round((clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ if (offset > this.regiondata.fadeIn && offset < this.regiondata.duration - this.regiondata.fadeOut) { //make sure keyframe is not created inbetween fades and ends
+ const position = this.regiondata.position;
+ this.props.makeKeyData(this.regiondata, Math.round(position + offset), KeyframeFunc.KeyframeType.default);
+ this.regiondata.hasData = true;
+ this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(Math.round(position + offset), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement)); //first move the keyframe to the correct location and make a copy so the correct file gets coppied
+
+ }
+ }
+
+
+ @action
+ moveKeyframe = async (e: React.MouseEvent, kf: Doc) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(NumCast(kf.time!), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement));
+ }
+
+ /**
+ * custom keyframe context menu items (when clicking on the keyframe circle)
+ */
+ @action
+ makeKeyframeMenu = (kf: Doc, e: MouseEvent) => {
+ TimelineMenu.Instance.addItem("button", "Show Data", () => {
+ runInAction(() => {
+ const kvp = Docs.Create.KVPDocument(kf, { _width: 300, _height: 300 });
+ CollectionDockingView.AddRightSplit(kvp, emptyPath);
+ });
+ }),
+ TimelineMenu.Instance.addItem("button", "Delete", () => {
+ runInAction(() => {
+ (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1);
+ this.forceUpdate();
+ });
+ }),
+ TimelineMenu.Instance.addItem("input", "Move", (val) => {
+ runInAction(() => {
+ let cannotMove: boolean = false;
+ const kfIndex: number = this.keyframes.indexOf(kf);
+ if (val < 0 || (val < NumCast(this.keyframes[kfIndex - 1].time) || val > NumCast(this.keyframes[kfIndex + 1].time))) {
+ cannotMove = true;
+ }
+ if (!cannotMove) {
+ this.keyframes[kfIndex].time = parseInt(val, 10);
+ this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;
+ }
+ });
+ });
+ TimelineMenu.Instance.addMenu("Keyframe");
+ TimelineMenu.Instance.openMenu(e.clientX, e.clientY);
+ }
+
+ /**
+ * context menu for region (anywhere on the green region).
+ */
+ @action
+ makeRegionMenu = (kf: Doc, e: MouseEvent) => {
+ TimelineMenu.Instance.addItem("button", "Remove Region", () =>
+ Cast(this.props.node.regions, listSpec(Doc))?.splice(this.regions.indexOf(this.props.RegionData), 1)),
+ TimelineMenu.Instance.addItem("input", `fadeIn: ${this.regiondata.fadeIn}ms`, (val) => {
+ runInAction(() => {
+ let cannotMove: boolean = false;
+ if (val < 0 || val > NumCast(this.keyframes[2].time) - this.regiondata.position) {
+ cannotMove = true;
+ }
+ if (!cannotMove) {
+ this.regiondata.fadeIn = parseInt(val, 10);
+ this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;
+ }
+ });
+ }),
+ TimelineMenu.Instance.addItem("input", `fadeOut: ${this.regiondata.fadeOut}ms`, (val) => {
+ runInAction(() => {
+ let cannotMove: boolean = false;
+ if (val < 0 || val > this.regiondata.position + this.regiondata.duration - NumCast(this.keyframes[this.keyframes.length - 3].time)) {
+ cannotMove = true;
+ }
+ if (!cannotMove) {
+ this.regiondata.fadeOut = parseInt(val, 10);
+ this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - val;
+ }
+ });
+ }),
+ TimelineMenu.Instance.addItem("input", `position: ${this.regiondata.position}ms`, (val) => {
+ runInAction(() => {
+ const prevPosition = this.regiondata.position;
+ let cannotMove: boolean = false;
+ this.regions.map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })).forEach(({ pos, dur }) => {
+ if (pos !== this.regiondata.position) {
+ if ((val < 0) || (val > pos && val < pos + dur || (this.regiondata.duration + val > pos && this.regiondata.duration + val < pos + dur))) {
+ cannotMove = true;
+ }
+ }
+ });
+ if (!cannotMove) {
+ this.regiondata.position = parseInt(val, 10);
+ this.updateKeyframes(this.regiondata.position - prevPosition);
+ }
+ });
+ }),
+ TimelineMenu.Instance.addItem("input", `duration: ${this.regiondata.duration}ms`, (val) => {
+ runInAction(() => {
+ let cannotMove: boolean = false;
+ this.regions.map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })).forEach(({ pos, dur }) => {
+ if (pos !== this.regiondata.position) {
+ val += this.regiondata.position;
+ if ((val < 0) || (val > pos && val < pos + dur)) {
+ cannotMove = true;
+ }
+ }
+ });
+ if (!cannotMove) {
+ this.regiondata.duration = parseInt(val, 10);
+ this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration;
+ this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut;
+ }
+ });
+ }),
+ TimelineMenu.Instance.addMenu("Region");
+ TimelineMenu.Instance.openMenu(e.clientX, e.clientY);
+ }
+
+ @action
+ updateKeyframes = (incr: number, filter: number[] = []) => {
+ this.keyframes.forEach(kf => {
+ if (!filter.includes(this.keyframes.indexOf(kf))) {
+ kf.time = NumCast(kf.time) + incr;
+ }
+ });
+ }
+
+ /**
+ * hovering effect when hovered (hidden div darkens)
+ */
+ @action
+ onContainerOver = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const div = ref.current!;
+ div.style.opacity = "1";
+ Doc.BrushDoc(this.props.node);
+ }
+
+ /**
+ * hovering effect when hovered out (hidden div becomes invisible)
+ */
+ @action
+ onContainerOut = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const div = ref.current!;
+ div.style.opacity = "0";
+ Doc.UnBrushDoc(this.props.node);
+ }
+
+
+ ///////////////////////UI STUFF /////////////////////////
+
+
+ /**
+ * drawing keyframe. Handles both keyframe with a circle (one that you create by double clicking) and one without circle (fades)
+ * this probably needs biggest change, since everyone expected all keyframes to have a circle (and draggable)
+ */
+ drawKeyframes = () => {
+ const keyframeDivs: JSX.Element[] = [];
+ return DocListCast(this.regiondata.keyframes).map(kf => {
+ if (kf.type as KeyframeFunc.KeyframeType !== KeyframeFunc.KeyframeType.end) {
+ return <>
+ <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}>
+ <div className="divider"></div>
+ <div className="keyframeCircle keyframe-indicator"
+ onPointerDown={(e) => { e.preventDefault(); e.stopPropagation(); this.moveKeyframe(e, kf); }}
+ onContextMenu={(e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.makeKeyframeMenu(kf, e.nativeEvent);
+ }}
+ onDoubleClick={(e) => { e.preventDefault(); e.stopPropagation(); }}>
+ </div>
+ </div>
+ <div className="keyframe-information" />
+ </>;
+ } else {
+ return <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}>
+ <div className="divider" />
+ </div>;
+ }
+ });
+ }
+
+ /**
+ * drawing the hidden divs that partition different intervals within a region.
+ */
+ @action
+ drawKeyframeDividers = () => {
+ const keyframeDividers: JSX.Element[] = [];
+ DocListCast(this.regiondata.keyframes).forEach(kf => {
+ const index = this.keyframes.indexOf(kf);
+ if (index !== this.keyframes.length - 1) {
+ const right = this.keyframes[index + 1];
+ const bodyRef = React.createRef<HTMLDivElement>();
+ const kfPos = KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
+ const rightPos = KeyframeFunc.convertPixelTime(NumCast(right.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
+ keyframeDividers.push(
+ <div ref={bodyRef} className="body-container" style={{ left: `${kfPos - this.pixelPosition}px`, width: `${rightPos - kfPos}px` }}
+ onPointerOver={(e) => { e.preventDefault(); e.stopPropagation(); this.onContainerOver(e, bodyRef); }}
+ onPointerOut={(e) => { e.preventDefault(); e.stopPropagation(); this.onContainerOut(e, bodyRef); }}
+ onContextMenu={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (index !== 0 || index !== this.keyframes.length - 2) {
+ this._mouseToggled = true;
+ }
+ this.makeRegionMenu(kf, e.nativeEvent);
+ }}>
+ </div>
+ );
+ }
+ });
+ return keyframeDividers;
+ }
+
+ /**
+ * rendering that green region
+ */
+ //154, 206, 223
+ render() {
+ trace();
+ console.log(this.props.RegionData.position);
+ console.log(this.regiondata.position);
+ console.log(this.pixelPosition);
+ return (
+ <div className="bar" ref={this._bar} style={{
+ transform: `translate(${this.pixelPosition}px)`,
+ width: `${this.pixelDuration}px`,
+ background: `linear-gradient(90deg, rgba(154, 206, 223, 0) 0%, rgba(154, 206, 223, 1) ${this.pixelFadeIn / this.pixelDuration * 100}%, rgba(154, 206, 223, 1) ${(this.pixelDuration - this.pixelFadeOut) / this.pixelDuration * 100}%, rgba(154, 206, 223, 0) 100% )`
+ }}
+ onPointerDown={this.onBarPointerDown}>
+ <div className="leftResize keyframe-indicator" onPointerDown={this.onResizeLeft} ></div>
+ {/* <div className="keyframe-information"></div> */}
+ <div className="rightResize keyframe-indicator" onPointerDown={this.onResizeRight}></div>
+ {/* <div className="keyframe-information"></div> */}
+ {this.drawKeyframes()}
+ {this.drawKeyframeDividers()}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/Timeline.scss b/src/client/views/animationtimeline/Timeline.scss
new file mode 100644
index 000000000..f90249771
--- /dev/null
+++ b/src/client/views/animationtimeline/Timeline.scss
@@ -0,0 +1,322 @@
+@import "./../globalCssVariables.scss";
+
+$timelineColor: #9acedf;
+$timelineDark: #77a1aa;
+
+.timeline-toolbox {
+ position: absolute;
+ margin: 0px;
+ padding: 0px;
+ display: flex;
+ align-items: flex-start;
+ flex-direction: row;
+ // justify-content: space-evenly;
+ align-items: center;
+ top: 3px;
+ width: 100%;
+
+ .overview-tool {
+ display: flex;
+ justify-content: center;
+ }
+
+ .playbackControls {
+ display: flex;
+ margin-left: 30px;
+ max-width: 84px;
+ width: 84px;
+
+ .timeline-icon {
+ color: $timelineColor;
+ margin-left: 3px;
+ }
+
+ }
+
+ .grid-box {
+ display: flex;
+ // grid-template-columns: [first] 20% [line2] 20% [line3] 60%;
+ width: calc(100% - 150px);
+ // width: 100%;
+ margin-left: 10px;
+
+ .time-box {
+ margin-left: 10px;
+ min-width: 140px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .resetView-tool {
+ width: 30px;
+ height: 30px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin: 3px;
+ color: $timelineDark;
+ }
+
+ .resetView-tool:hover {
+ -webkit-transform: scale(1.1);
+ -ms-transform: scale(1.1);
+ transform: scale(1.1);
+ transition: .2s ease;
+ }
+ }
+
+ .mode-box {
+ display: flex;
+ margin-left: 5px;
+ }
+
+ .overview-box {
+ width: 80%;
+ display: flex;
+ }
+
+ div {
+ padding: 0px;
+ // margin-left: 10px;
+ }
+ }
+
+ .overview-tool {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .animation-text {
+ // font-size: 16px;
+ height: auto;
+ width: auto;
+ white-space: nowrap;
+ font-size: 14px;
+ color: black;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ }
+
+ .round-toggle {
+ height: 20px;
+ width: 40px;
+ min-width: 40px;
+ background-color: white;
+ border: 2px solid $timelineDark;
+ border-radius: 20px;
+ animation-fill-mode: forwards;
+ animation-duration: 500ms;
+ top: 30px;
+ margin-left: 5px;
+
+ input {
+ position: absolute;
+ opacity: 0;
+ height: 0;
+ width: 0;
+ }
+
+ .round-toggle-slider {
+ height: 17px;
+ width: 17px;
+ background-color: white;
+ border: 2px solid $timelineDark;
+ border-radius: 50%;
+ transition: transform 500ms ease-in-out;
+ margin-left: 0px;
+ // margin-top: 0.5px;
+ }
+ }
+
+}
+
+.time-input {
+ height: 20px;
+ // width: 120px;
+ width: 100%;
+ white-space: nowrap;
+ font-size: 12px;
+ color: black;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ padding-left: 5px;
+ margin-left: 5px;
+}
+
+.tick {
+ height: 100%;
+ width: 2px;
+ background-color: black;
+ color: black;
+}
+
+.number-label {
+ color: black;
+ transform: rotate(-90deg) translate(-15px, 8px);
+ font-size: .85em;
+}
+
+.timeline-container {
+ width: 100%;
+ height: 300px;
+ position: absolute;
+ background-color: $light-color-secondary;
+ border-bottom: 2px solid $timelineDark;
+ transition: transform 500ms ease;
+
+ .info-container {
+ margin-top: 50px;
+ right: 20px;
+ position: absolute;
+ height: calc(100% - 100px);
+ width: calc(100% - 140px);
+ overflow: hidden;
+
+ .scrubberbox {
+ position: absolute;
+ background-color: transparent;
+ height: 30px;
+ width: 100%;
+
+ }
+
+ .scrubber {
+ top: 30px;
+ height: 100%;
+ width: 2px;
+ position: absolute;
+ z-index: 1001;
+ background-color: black;
+ pointer-events: none;
+
+ .scrubberhead {
+ top: -20px;
+ height: 20px;
+ width: 20px;
+ background-color: white;
+ border-radius: 50%;
+ border: 3px solid black;
+ left: -9px;
+ position: absolute;
+ pointer-events: all;
+ }
+ }
+
+ .trackbox {
+ top: 30px;
+ height: calc(100% - 30px);
+ // height: 100%;
+ // height: 100%;
+ width: 100%;
+ border-top: 2px solid black;
+ border-bottom: 2px solid black;
+ overflow: hidden;
+ // overflow-y: scroll;
+ background-color: white;
+ position: absolute;
+ // box-shadow: -10px 0px 10px 10px red;
+ }
+
+ }
+
+ .currentTime {
+ // background: red;
+ font-size: 12px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 40px;
+ top: 40px;
+ position: relative;
+ width: 100px;
+ margin-left: 20px;
+ }
+
+ .title-container {
+ // margin-top: 80px;
+ margin-top: 40px;
+ margin-left: 20px;
+ height: calc(100% - 100px - 30px);
+ // height: 100%;
+ width: 100px;
+ background-color: white;
+ overflow: hidden;
+ border-left: 2px solid black;
+ border-top: 2px solid black;
+ border-bottom: 2px solid black;
+ border-right: 2px solid $timelineDark;
+
+ .datapane {
+ top: 0px;
+ width: 100px;
+ height: 30%;
+ border: 1px solid $dark-color;
+ font-size: 12px;
+ line-height: 11px;
+ background-color: $timelineDark;
+ color: white;
+ position: relative;
+ float: left;
+ padding: 3px;
+ border-style: solid;
+ overflow-y: scroll;
+ overflow-x: hidden;
+
+ p {
+ hyphens: auto;
+ }
+
+ }
+ }
+
+ .resize {
+ bottom: 0px;
+ position: absolute;
+ height: 20px;
+ width: 40px;
+ left: calc(50% - 25px);
+ color: $timelineDark;
+ }
+}
+
+
+
+.overview {
+ position: absolute;
+ height: 50px;
+ width: 200px;
+ background-color: black;
+
+ .container {
+ position: absolute;
+ float: left 0px;
+ top: 25%;
+ height: 75%;
+ width: 100%;
+ background-color: grey;
+ }
+}
+
+
+.timeline-checker {
+ height: auto;
+ width: auto;
+ overflow: hidden;
+ position: absolute;
+ display: flex;
+ padding: 10px 10px;
+
+ div {
+ height: auto;
+ width: auto;
+ overflow: hidden;
+ margin: 0px 10px;
+ cursor: pointer
+ }
+
+ .check {
+ width: 50px;
+ height: 50px;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx
new file mode 100644
index 000000000..466cbb867
--- /dev/null
+++ b/src/client/views/animationtimeline/Timeline.tsx
@@ -0,0 +1,622 @@
+import * as React from "react";
+import "./Timeline.scss";
+import { listSpec } from "../../../new_fields/Schema";
+import { observer } from "mobx-react";
+import { Track } from "./Track";
+import { observable, action, computed, runInAction, IReactionDisposer, reaction, trace } from "mobx";
+import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
+import { List } from "../../../new_fields/List";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faPlayCircle, faBackward, faForward, faGripLines, faPauseCircle, faEyeSlash, faEye, faCheckCircle, faTimesCircle } from "@fortawesome/free-solid-svg-icons";
+import { ContextMenu } from "../ContextMenu";
+import { TimelineOverview } from "./TimelineOverview";
+import { FieldViewProps } from "../nodes/FieldView";
+import { KeyframeFunc } from "./Keyframe";
+import { Utils } from "../../../Utils";
+
+/**
+ * Timeline class controls most of timeline functions besides individual keyframe and track mechanism. Main functions are
+ * zooming, panning, currentBarX (scrubber movement). Most of the UI stuff is also handled here. You shouldn't really make
+ * any logical changes here. Most work is needed on UI.
+ *
+ * The hierarchy works this way:
+ *
+ * Timeline.tsx --> Track.tsx --> Keyframe.tsx
+ | |
+ | TimelineMenu.tsx (timeline's custom contextmenu)
+ |
+ |
+ TimelineOverview.tsx (youtube like dragging thing is play mode, complex dragging thing in editing mode)
+
+
+ Most style changes are in SCSS file.
+ If you have any questions, email me or text me.
+ @author Andrew Kim
+ */
+
+
+@observer
+export class Timeline extends React.Component<FieldViewProps> {
+
+
+ //readonly constants
+ private readonly DEFAULT_TICK_SPACING: number = 50;
+ private readonly MAX_TITLE_HEIGHT = 75;
+ private readonly MAX_CONTAINER_HEIGHT: number = 800;
+ private readonly DEFAULT_TICK_INCREMENT: number = 1000;
+
+ //height variables
+ private DEFAULT_CONTAINER_HEIGHT: number = 330;
+ private MIN_CONTAINER_HEIGHT: number = 205;
+
+ //react refs
+ @observable private _trackbox = React.createRef<HTMLDivElement>();
+ @observable private _titleContainer = React.createRef<HTMLDivElement>();
+ @observable private _timelineContainer = React.createRef<HTMLDivElement>();
+ @observable private _infoContainer = React.createRef<HTMLDivElement>();
+ @observable private _roundToggleRef = React.createRef<HTMLDivElement>();
+ @observable private _roundToggleContainerRef = React.createRef<HTMLDivElement>();
+ @observable private _timeInputRef = React.createRef<HTMLInputElement>();
+
+ //boolean vars and instance vars
+ @observable private _currentBarX: number = 0;
+ @observable private _windSpeed: number = 1;
+ @observable private _isPlaying: boolean = false; //scrubber playing
+ @observable private _totalLength: number = 0;
+ @observable private _visibleLength: number = 0;
+ @observable private _visibleStart: number = 0;
+ @observable private _containerHeight: number = this.DEFAULT_CONTAINER_HEIGHT;
+ @observable private _tickSpacing = this.DEFAULT_TICK_SPACING;
+ @observable private _tickIncrement = this.DEFAULT_TICK_INCREMENT;
+ @observable private _time = 100000; //DEFAULT
+ @observable private _playButton = faPlayCircle;
+ @observable private _mouseToggled = false;
+ @observable private _doubleClickEnabled = false;
+ @observable private _titleHeight = 0;
+
+ // so a reaction can be made
+ @observable public _isAuthoring = this.props.Document.isATOn;
+
+ /**
+ * collection get method. Basically defines what defines collection's children. These will be tracked in the timeline. Do not edit.
+ */
+ @computed
+ private get children(): List<Doc> {
+ const extendedDocument = ["image", "video", "pdf"].includes(StrCast(this.props.Document.type));
+ if (extendedDocument) {
+ if (this.props.Document.data_ext) {
+ return Cast((Cast(this.props.Document[Doc.LayoutFieldKey(this.props.Document) + "-annotations"], Doc) as Doc).annotations, listSpec(Doc)) as List<Doc>;
+ } else {
+ return new List<Doc>();
+ }
+ }
+ return Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)) as List<Doc>;
+ }
+
+ /////////lifecycle functions////////////
+ componentWillMount() {
+ const relativeHeight = window.innerHeight / 20; //sets height to arbitrary size, relative to innerHeight
+ this._titleHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT; //check if relHeight is less than Maxheight. Else, just set relheight to max
+ this.MIN_CONTAINER_HEIGHT = this._titleHeight + 130; //offset
+ this.DEFAULT_CONTAINER_HEIGHT = this._titleHeight * 2 + 130; //twice the titleheight + offset
+ }
+
+ componentDidMount() {
+ runInAction(() => {
+ if (!this.props.Document.AnimationLength) { //if animation length did not exist
+ this.props.Document.AnimationLength = this._time; //set it to default time
+ } else {
+ this._time = NumCast(this.props.Document.AnimationLength); //else, set time to animationlength stored from before
+ }
+ this._totalLength = this._tickSpacing * (this._time / this._tickIncrement); //the entire length of the timeline div (actual div part itself)
+ this._visibleLength = this._infoContainer.current!.getBoundingClientRect().width; //the visible length of the timeline (the length that you current see)
+ this._visibleStart = this._infoContainer.current!.scrollLeft; //where the div starts
+ this.props.Document.isATOn = !this.props.Document.isATOn; //turns the boolean on, saying AT (animation timeline) is on
+ this.toggleHandle();
+ });
+ }
+
+ componentWillUnmount() {
+ this.props.Document.AnimationLength = this._time; //save animation length
+ }
+ /////////////////////////////////////////////////
+
+ /**
+ * React Functional Component
+ * Purpose: For drawing Tick marks across the timeline in authoring mode
+ */
+ @action
+ drawTicks = () => {
+ const ticks = [];
+ for (let i = 0; i < this._time / this._tickIncrement; i++) {
+ ticks.push(<div key={Utils.GenerateGuid()} className="tick" style={{ transform: `translate(${i * this._tickSpacing}px)`, position: "absolute", pointerEvents: "none" }}> <p className="number-label">{this.toReadTime(i * this._tickIncrement)}</p></div>);
+ }
+ return ticks;
+ }
+
+ /**
+ * changes the scrubber to actual pixel position
+ */
+ @action
+ changeCurrentBarX = (pixel: number) => {
+ pixel <= 0 ? this._currentBarX = 0 : pixel >= this._totalLength ? this._currentBarX = this._totalLength : this._currentBarX = pixel;
+ }
+
+ //for playing
+ @action
+ onPlay = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.play();
+ }
+
+ /**
+ * when playbutton is clicked
+ */
+ @action
+ play = () => {
+ if (this._isPlaying) {
+ this._isPlaying = false;
+ this._playButton = faPlayCircle;
+ } else {
+ this._isPlaying = true;
+ this._playButton = faPauseCircle;
+ const playTimeline = () => {
+ if (this._isPlaying) {
+ if (this._currentBarX >= this._totalLength) {
+ this.changeCurrentBarX(0);
+ } else {
+ this.changeCurrentBarX(this._currentBarX + this._windSpeed);
+ }
+ setTimeout(playTimeline, 15);
+ }
+ };
+ playTimeline();
+ }
+ }
+
+
+ /**
+ * fast forward the timeline scrubbing
+ */
+ @action
+ windForward = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (this._windSpeed < 64) { //max speed is 32
+ this._windSpeed = this._windSpeed * 2;
+ }
+ }
+
+ /**
+ * rewind the timeline scrubbing
+ */
+ @action
+ windBackward = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (this._windSpeed > 1 / 16) { // min speed is 1/8
+ this._windSpeed = this._windSpeed / 2;
+ }
+ }
+
+ /**
+ * scrubber down
+ */
+ @action
+ onScrubberDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.addEventListener("pointermove", this.onScrubberMove);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onScrubberMove);
+ });
+ }
+
+ /**
+ * when there is any scrubber movement
+ */
+ @action
+ onScrubberMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const scrubberbox = this._infoContainer.current!;
+ const left = scrubberbox.getBoundingClientRect().left;
+ const offsetX = Math.round(e.clientX - left) * this.props.ScreenToLocalTransform().Scale;
+ this.changeCurrentBarX(offsetX + this._visibleStart); //changes scrubber to clicked scrubber position
+ }
+
+ /**
+ * when panning the timeline (in editing mode)
+ */
+ @action
+ onPanDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const clientX = e.clientX;
+ if (this._doubleClickEnabled) {
+ this._doubleClickEnabled = false;
+ } else {
+ setTimeout(() => {
+ if (!this._mouseToggled && this._doubleClickEnabled) this.changeCurrentBarX(this._trackbox.current!.scrollLeft + clientX - this._trackbox.current!.getBoundingClientRect().left);
+ this._mouseToggled = false;
+ this._doubleClickEnabled = false;
+ }, 200);
+ this._doubleClickEnabled = true;
+ document.addEventListener("pointermove", this.onPanMove);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onPanMove);
+ if (!this._doubleClickEnabled) {
+ this._mouseToggled = false;
+ }
+ });
+
+ }
+ }
+
+ /**
+ * when moving the timeline (in editing mode)
+ */
+ @action
+ onPanMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.movementX !== 0 || e.movementY !== 0) {
+ this._mouseToggled = true;
+ }
+ const trackbox = this._trackbox.current!;
+ const titleContainer = this._titleContainer.current!;
+ this.movePanX(this._visibleStart - e.movementX);
+ trackbox.scrollTop = trackbox.scrollTop - e.movementY;
+ titleContainer.scrollTop = titleContainer.scrollTop - e.movementY;
+ if (this._visibleStart + this._visibleLength + 20 >= this._totalLength) {
+ this._visibleStart -= e.movementX;
+ this._totalLength -= e.movementX;
+ this._time -= KeyframeFunc.convertPixelTime(e.movementX, "mili", "time", this._tickSpacing, this._tickIncrement);
+ this.props.Document.AnimationLength = this._time;
+ }
+
+ }
+
+
+ @action
+ movePanX = (pixel: number) => {
+ const infoContainer = this._infoContainer.current!;
+ infoContainer.scrollLeft = pixel;
+ this._visibleStart = infoContainer.scrollLeft;
+ }
+
+ /**
+ * resizing timeline (in editing mode) (the hamburger drag icon)
+ */
+ @action
+ onResizeDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.addEventListener("pointermove", this.onResizeMove);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onResizeMove);
+ });
+ }
+
+ @action
+ onResizeMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const offset = e.clientY - this._timelineContainer.current!.getBoundingClientRect().bottom;
+ // let offset = 0;
+ if (this._containerHeight + offset <= this.MIN_CONTAINER_HEIGHT) {
+ this._containerHeight = this.MIN_CONTAINER_HEIGHT;
+ } else if (this._containerHeight + offset >= this.MAX_CONTAINER_HEIGHT) {
+ this._containerHeight = this.MAX_CONTAINER_HEIGHT;
+ } else {
+ this._containerHeight += offset;
+ }
+ }
+
+ /**
+ * for displaying time to standard min:sec
+ */
+ @action
+ toReadTime = (time: number): string => {
+ time = time / 1000;
+ const inSeconds = Math.round(time * 100) / 100;
+
+ const min: (string | number) = Math.floor(inSeconds / 60);
+ const sec: (string | number) = (Math.round((inSeconds % 60) * 100) / 100);
+ let secString = sec.toFixed(2);
+
+ if (Math.floor(sec / 10) === 0) {
+ secString = "0" + secString;
+ }
+
+ return `${min}:${secString}`;
+ }
+
+
+ /**
+ * timeline zoom function
+ * use mouse middle button to zoom in/out the timeline
+ */
+ @action
+ onWheelZoom = (e: React.WheelEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const offset = e.clientX - this._infoContainer.current!.getBoundingClientRect().left;
+ const prevTime = KeyframeFunc.convertPixelTime(this._visibleStart + offset, "mili", "time", this._tickSpacing, this._tickIncrement);
+ const prevCurrent = KeyframeFunc.convertPixelTime(this._currentBarX, "mili", "time", this._tickSpacing, this._tickIncrement);
+ e.deltaY < 0 ? this.zoom(true) : this.zoom(false);
+ const currPixel = KeyframeFunc.convertPixelTime(prevTime, "mili", "pixel", this._tickSpacing, this._tickIncrement);
+ const currCurrent = KeyframeFunc.convertPixelTime(prevCurrent, "mili", "pixel", this._tickSpacing, this._tickIncrement);
+ this._infoContainer.current!.scrollLeft = currPixel - offset;
+ this._visibleStart = currPixel - offset > 0 ? currPixel - offset : 0;
+ this._visibleStart += this._visibleLength + this._visibleStart > this._totalLength ? this._totalLength - (this._visibleStart + this._visibleLength) : 0;
+ this.changeCurrentBarX(currCurrent);
+ }
+
+
+ resetView(doc: Doc) {
+ doc._panX = doc._customOriginX ?? 0;
+ doc._panY = doc._customOriginY ?? 0;
+ doc.scale = doc._customOriginScale ?? 1;
+ }
+
+ setView(doc: Doc) {
+ doc._customOriginX = doc._panX;
+ doc._customOriginY = doc._panY;
+ doc._customOriginScale = doc.scale;
+ }
+ /**
+ * zooming mechanism (increment and spacing changes)
+ */
+ @action
+ zoom = (dir: boolean) => {
+ let spacingChange = this._tickSpacing;
+ let incrementChange = this._tickIncrement;
+ if (dir) {
+ if (!(this._tickSpacing === 100 && this._tickIncrement === 1000)) {
+ if (this._tickSpacing >= 100) {
+ incrementChange /= 2;
+ spacingChange = 50;
+ } else {
+ spacingChange += 5;
+ }
+ }
+ } else {
+ if (this._tickSpacing <= 50) {
+ spacingChange = 100;
+ incrementChange *= 2;
+ } else {
+ spacingChange -= 5;
+ }
+ }
+ const finalLength = spacingChange * (this._time / incrementChange);
+ if (finalLength >= this._infoContainer.current!.getBoundingClientRect().width) {
+ this._totalLength = finalLength;
+ this._tickSpacing = spacingChange;
+ this._tickIncrement = incrementChange;
+ }
+ }
+
+ /**
+ * tool box includes the toggle buttons at the top of the timeline (both editing mode and play mode)
+ */
+ private timelineToolBox = (scale: number, totalTime: number) => {
+ const size = 40 * scale; //50 is default
+ const iconSize = 25;
+
+ //decides if information should be omitted because the timeline is very small
+ // if its less than 950 pixels then it's going to be overlapping
+ let shouldCompress = false;
+ const width: number = this.props.PanelWidth();
+ if (width < 850) {
+ shouldCompress = true;
+ }
+
+ let modeString, overviewString, lengthString;
+ const modeType = this.props.Document.isATOn ? "Author" : "Play";
+
+ if (!shouldCompress) {
+ modeString = "Mode: " + modeType;
+ overviewString = "Overview:";
+ lengthString = "Length: ";
+ }
+ else {
+ modeString = modeType;
+ overviewString = "";
+ lengthString = "";
+ }
+
+ // let rightInfo = this.timeIndicator;
+
+ return (
+ <div key="timeline_toolbox" className="timeline-toolbox" style={{ height: `${size}px` }}>
+ <div className="playbackControls">
+ <div className="timeline-icon" key="timeline_windBack" onClick={this.windBackward} title="Slow Down Animation"> <FontAwesomeIcon icon={faBackward} style={{ height: `${iconSize}px`, width: `${iconSize}px` }} /> </div>
+ <div className="timeline-icon" key=" timeline_play" onClick={this.onPlay} title="Play/Pause"> <FontAwesomeIcon icon={this._playButton} style={{ height: `${iconSize}px`, width: `${iconSize}px` }} /> </div>
+ <div className="timeline-icon" key="timeline_windForward" onClick={this.windForward} title="Speed Up Animation"> <FontAwesomeIcon icon={faForward} style={{ height: `${iconSize}px`, width: `${iconSize}px` }} /> </div>
+ </div>
+ <div className="grid-box overview-tool">
+ <div className="overview-box">
+ <div key="overview-text" className="animation-text">{overviewString}</div>
+ <TimelineOverview tickSpacing={this._tickSpacing} tickIncrement={this._tickIncrement} time={this._time} parent={this} isAuthoring={BoolCast(this.props.Document.isATOn)} currentBarX={this._currentBarX} totalLength={this._totalLength} visibleLength={this._visibleLength} visibleStart={this._visibleStart} changeCurrentBarX={this.changeCurrentBarX} movePanX={this.movePanX} />
+ </div>
+ <div className="mode-box overview-tool">
+ <div key="animation-text" className="animation-text">{modeString}</div>
+ <div key="round-toggle" ref={this._roundToggleContainerRef} className="round-toggle">
+ <div key="round-toggle-slider" ref={this._roundToggleRef} className="round-toggle-slider" onPointerDown={this.toggleChecked}> </div>
+ </div>
+ </div>
+ <div className="time-box overview-tool" style={{ display: "flex" }}>
+ {this.timeIndicator(lengthString, totalTime)}
+ <div className="resetView-tool" title="Return to Default View" onClick={() => this.resetView(this.props.Document)}><FontAwesomeIcon icon="compress-arrows-alt" size="lg" /></div>
+ <div className="resetView-tool" style={{ display: this._isAuthoring ? "flex" : "none" }} title="Set Default View" onClick={() => this.setView(this.props.Document)}><FontAwesomeIcon icon="expand-arrows-alt" size="lg" /></div>
+
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ timeIndicator(lengthString: string, totalTime: number) {
+ if (this.props.Document.isATOn) {
+ return (
+ <div key="time-text" className="animation-text" style={{ visibility: this.props.Document.isATOn ? "visible" : "hidden", display: this.props.Document.isATOn ? "flex" : "none" }}>{`Total: ${this.toReadTime(totalTime)}`}</div>
+ );
+ }
+ else {
+ const ctime = `Current: ${this.getCurrentTime()}`;
+ const ttime = `Total: ${this.toReadTime(this._time)}`;
+ return (
+ <div style={{ flexDirection: "column" }}>
+ <div className="animation-text" style={{ fontSize: "10px", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}>
+ {ctime}
+ </div>
+ <div className="animation-text" style={{ fontSize: "10px", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}>
+ {ttime}
+ </div>
+ </div>
+ );
+ }
+ }
+
+ /**
+ * when the user decides to click the toggle button (either user wants to enter editing mode or play mode)
+ */
+ @action
+ private toggleChecked = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.toggleHandle();
+ }
+
+ /**
+ * turns on the toggle button (the purple slide button that changes from editing mode and play mode
+ */
+ private toggleHandle = () => {
+ const roundToggle = this._roundToggleRef.current!;
+ const roundToggleContainer = this._roundToggleContainerRef.current!;
+ const timelineContainer = this._timelineContainer.current!;
+ if (BoolCast(this.props.Document.isATOn)) {
+ //turning on playmode...
+ roundToggle.style.transform = "translate(0px, 0px)";
+ roundToggle.style.animationName = "turnoff";
+ roundToggleContainer.style.animationName = "turnoff";
+ roundToggleContainer.style.backgroundColor = "white";
+ timelineContainer.style.top = `${-this._containerHeight}px`;
+ this.props.Document.isATOn = false;
+ this._isAuthoring = false;
+ this.toPlay();
+ } else {
+ //turning on authoring mode...
+ roundToggle.style.transform = "translate(20px, 0px)";
+ roundToggle.style.animationName = "turnon";
+ roundToggleContainer.style.animationName = "turnon";
+ roundToggleContainer.style.backgroundColor = "#9acedf";
+ timelineContainer.style.top = "0px";
+ this.props.Document.isATOn = true;
+ this._isAuthoring = true;
+ this.toAuthoring();
+ }
+ }
+
+
+ @action.bound
+ changeLengths() {
+ if (this._infoContainer.current) {
+ this._visibleLength = this._infoContainer.current.getBoundingClientRect().width; //the visible length of the timeline (the length that you current see)
+ this._visibleStart = this._infoContainer.current.scrollLeft; //where the div starts
+ }
+ }
+
+ // @computed
+ getCurrentTime = () => {
+ let current = KeyframeFunc.convertPixelTime(this._currentBarX, "mili", "time", this._tickSpacing, this._tickIncrement);
+ if (current > this._time) {
+ current = this._time;
+ }
+ return this.toReadTime(current);
+ }
+
+ @observable private mapOfTracks: (Track | null)[] = [];
+
+ @action
+ findLongestTime = () => {
+ let longestTime: number = 0;
+ this.mapOfTracks.forEach(track => {
+ if (track) {
+ const lastTime = track.getLastRegionTime();
+ if (this.children.length !== 0) {
+ if (longestTime <= lastTime) {
+ longestTime = lastTime;
+ }
+ }
+ } else {
+ //TODO: remove undefineds and duplicates
+ }
+ });
+ // console.log(longestTime);
+ return longestTime;
+ }
+
+ @action
+ toAuthoring = () => {
+ let longestTime = this.findLongestTime();
+ if (longestTime === 0) longestTime = 1;
+ const adjustedTime = Math.ceil(longestTime / 100000) * 100000;
+ // console.log(adjustedTime);
+ this._totalLength = KeyframeFunc.convertPixelTime(adjustedTime, "mili", "pixel", this._tickSpacing, this._tickIncrement);
+ this._time = adjustedTime;
+ }
+
+ @action
+ toPlay = () => {
+ const longestTime = this.findLongestTime();
+ this._time = longestTime;
+ this._totalLength = KeyframeFunc.convertPixelTime(this._time, "mili", "pixel", this._tickSpacing, this._tickIncrement);
+ }
+
+ /**
+ * if you have any question here, just shoot me an email or text.
+ * basically the only thing you need to edit besides render methods in track (individual track lines) and keyframe (green region)
+ */
+ render() {
+ setTimeout(() => {
+ this.changeLengths();
+ // this.toPlay();
+ // this._time = longestTime;
+ }, 0);
+
+ const longestTime = this.findLongestTime();
+ trace();
+ // change visible and total width
+ return (
+ <div style={{ visibility: "visible" }}>
+ <div key="timeline_wrapper" style={{ visibility: BoolCast(this.props.Document.isATOn) ? "visible" : "hidden", left: "0px", top: "0px", position: "absolute", width: "100%", transform: "translate(0px, 0px)" }}>
+ <div key="timeline_container" className="timeline-container" ref={this._timelineContainer} style={{ height: `${this._containerHeight}px`, top: `0px` }}>
+ <div key="timeline_info" className="info-container" ref={this._infoContainer} onWheel={this.onWheelZoom}>
+ {this.drawTicks()}
+ <div key="timeline_scrubber" className="scrubber" style={{ transform: `translate(${this._currentBarX}px)` }}>
+ <div key="timeline_scrubberhead" className="scrubberhead" onPointerDown={this.onScrubberDown} ></div>
+ </div>
+ <div key="timeline_trackbox" className="trackbox" ref={this._trackbox} onPointerDown={this.onPanDown} style={{ width: `${this._totalLength}px` }}>
+ {DocListCast(this.children).map(doc =>
+ <Track ref={ref => this.mapOfTracks.push(ref)} node={doc} currentBarX={this._currentBarX} changeCurrentBarX={this.changeCurrentBarX} transform={this.props.ScreenToLocalTransform()} time={this._time} tickSpacing={this._tickSpacing} tickIncrement={this._tickIncrement} collection={this.props.Document} timelineVisible={true} />
+ )}
+ </div>
+ </div>
+ <div className="currentTime">Current: {this.getCurrentTime()}</div>
+ <div key="timeline_title" className="title-container" ref={this._titleContainer}>
+ {DocListCast(this.children).map(doc => <div style={{ height: `${(this._titleHeight)}px` }} className="datapane" onPointerOver={() => { Doc.BrushDoc(doc); }} onPointerOut={() => { Doc.UnBrushDoc(doc); }}><p>{doc.title}</p></div>)}
+ </div>
+ <div key="timeline_resize" onPointerDown={this.onResizeDown}>
+ <FontAwesomeIcon className="resize" icon={faGripLines} />
+ </div>
+ </div>
+ </div>
+ {this.timelineToolBox(1, longestTime)}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/TimelineMenu.scss b/src/client/views/animationtimeline/TimelineMenu.scss
new file mode 100644
index 000000000..7ee0a43d5
--- /dev/null
+++ b/src/client/views/animationtimeline/TimelineMenu.scss
@@ -0,0 +1,94 @@
+@import "./../globalCssVariables.scss";
+
+
+.timeline-menu-container{
+ position: absolute;
+ display: flex;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
+ flex-direction: column;
+ background: whitesmoke;
+ z-index: 10000;
+ width: 200px;
+ padding-bottom: 10px;
+ border-radius: 15px;
+
+ border: solid #BBBBBBBB 1px;
+
+
+
+ .timeline-menu-input{
+ font: $sans-serif;
+ font-size: 13px;
+ width:100%;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ margin-left: 10px;
+ background-color: transparent;
+ border-width: 0px;
+ transition: border-width 500ms;
+ }
+
+ .timeline-menu-input:hover{
+ border-width: 2px;
+ }
+
+
+
+
+ .timeline-menu-header{
+ border-top-left-radius: 15px;
+ border-top-right-radius: 15px;
+ text-transform: uppercase;
+ background: $dark-color;
+ letter-spacing: 2px;
+
+ .timeline-menu-header-desc{
+ font:$sans-serif;
+ font-size: 13px;
+ text-align: center;
+ color: whitesmoke;
+ }
+ }
+
+
+ .timeline-menu-item {
+ // width: 11vw; //10vw
+ height: 30px; //2vh
+ background: whitesmoke;
+ display: flex; //comment out to allow search icon to be inline with search text
+ justify-content: left;
+ align-items: center;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ transition: all .1s;
+ border-style: none;
+ // padding: 10px 0px 10px 0px;
+ white-space: nowrap;
+ font-size: 13px;
+ color: grey;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ padding-right: 20px;
+ padding-left: 10px;
+ }
+
+ .timeline-menu-item:hover {
+ border-width: .11px;
+ border-style: none;
+ border-color: $intermediate-color;
+ border-bottom-style: solid;
+ border-top-style: solid;
+ background: $darker-alt-accent;
+ }
+
+ .timeline-menu-desc {
+ padding-left: 10px;
+ font:$sans-serif;
+ font-size: 13px;
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/TimelineMenu.tsx b/src/client/views/animationtimeline/TimelineMenu.tsx
new file mode 100644
index 000000000..53ca9acad
--- /dev/null
+++ b/src/client/views/animationtimeline/TimelineMenu.tsx
@@ -0,0 +1,77 @@
+import * as React from "react";
+import { observable, action, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import "./TimelineMenu.scss";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faChartLine, faRoad, faClipboard, faPen, faTrash, faTable } from "@fortawesome/free-solid-svg-icons";
+import { Utils } from "../../../Utils";
+
+
+@observer
+export class TimelineMenu extends React.Component {
+ public static Instance: TimelineMenu;
+
+ @observable private _opacity = 0;
+ @observable private _x = 0;
+ @observable private _y = 0;
+ @observable private _currentMenu: JSX.Element[] = [];
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+ TimelineMenu.Instance = this;
+ }
+
+ @action
+ openMenu = (x?: number, y?: number) => {
+ this._opacity = 1;
+ x ? this._x = x : this._x = 0;
+ y ? this._y = y : this._y = 0;
+ }
+
+ @action
+ closeMenu = () => {
+ this._opacity = 0;
+ this._currentMenu = [];
+ this._x = -1000000;
+ this._y = -1000000;
+ }
+
+ @action
+ addItem = (type: "input" | "button", title: string, event: (e: any, ...args: any[]) => void) => {
+ if (type === "input") {
+ const inputRef = React.createRef<HTMLInputElement>();
+ let text = "";
+ this._currentMenu.push(<div key={Utils.GenerateGuid()} className="timeline-menu-item"><FontAwesomeIcon icon={faClipboard} size="lg" /><input className="timeline-menu-input" ref={inputRef} placeholder={title} onChange={(e) => {
+ e.stopPropagation();
+ text = e.target.value;
+ }} onKeyDown={(e) => {
+ if (e.keyCode === 13) {
+ event(text);
+ this.closeMenu();
+ e.stopPropagation();
+ }
+ }} /></div>);
+ } else if (type === "button") {
+ this._currentMenu.push(<div key={Utils.GenerateGuid()} className="timeline-menu-item"><FontAwesomeIcon icon={faChartLine} size="lg" /><p className="timeline-menu-desc" onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ event(e);
+ this.closeMenu();
+ }}>{title}</p></div>);
+ }
+ }
+
+ @action
+ addMenu = (title: string) => {
+ this._currentMenu.unshift(<div key={Utils.GenerateGuid()} className="timeline-menu-header"><p className="timeline-menu-header-desc">{title}</p></div>);
+ }
+
+ render() {
+ return (
+ <div key={Utils.GenerateGuid()} className="timeline-menu-container" style={{ opacity: this._opacity, left: this._x, top: this._y }} >
+ {this._currentMenu}
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/TimelineOverview.scss b/src/client/views/animationtimeline/TimelineOverview.scss
new file mode 100644
index 000000000..283163ea7
--- /dev/null
+++ b/src/client/views/animationtimeline/TimelineOverview.scss
@@ -0,0 +1,107 @@
+@import "./../globalCssVariables.scss";
+
+$timelineColor: #9acedf;
+$timelineDark: #77a1aa;
+
+.timelineOverview-bounding {
+ width: 100%;
+ margin-right: 10px;
+}
+
+.timeline-flex {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+}
+
+.timeline-overview-container {
+ // padding: 0px;
+ margin-right: 5px;
+ // margin: 0px;
+ margin-left: 5px;
+ // width: 300px;
+ width: 100%;
+ height: 25px;
+ background: white;
+ position: relative;
+ border: 2px solid $timelineDark;
+ // width: 100%;
+
+ .timeline-overview-visible {
+ position: absolute;
+ height: 21px;
+ background: $timelineColor;
+ display: inline-block;
+ margin: 0px;
+ padding: 0px;
+ // top: 1px;
+ }
+
+ .timeline-overview-scrubber-container {
+ margin: 0px;
+ padding: 0px;
+ position: absolute;
+ height: 100%;
+ width: 2px;
+ top: 0px;
+ left: 0px;
+ z-index: 1001;
+ background-color: black;
+ display: inline-block;
+
+ .timeline-overview-scrubber-head {
+ padding: 0px;
+ margin: 0px;
+ position: absolute;
+ height: 10px;
+ width: 10px;
+ background-color: white;
+ border-radius: 50%;
+ border: 2px solid black;
+ left: -4px;
+ // top: -30px;
+ top: -10px;
+ }
+ }
+}
+
+
+
+.timeline-play-bar {
+ position: relative;
+ padding: 0px;
+ margin: 0px;
+ width: 100%;
+ height: 4px;
+ background-color: $timelineColor;
+ border-radius: 20px;
+ cursor: pointer;
+
+ .timeline-play-head {
+ position: absolute;
+ padding: 0px;
+ margin: 0px;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background-color: white;
+ border: 3px solid $timelineColor;
+ left: 0px;
+ top: -8px;
+ cursor: pointer;
+ }
+}
+
+.timeline-play-tail {
+ position: absolute;
+ padding: 0px;
+ margin: 0px;
+ height: 4px;
+ width: 0px;
+ z-index: 1000;
+ background-color: $timelineDark;
+ border-radius: 20px;
+ margin-top: -4px;
+ cursor: pointer;
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/TimelineOverview.tsx b/src/client/views/animationtimeline/TimelineOverview.tsx
new file mode 100644
index 000000000..31e248823
--- /dev/null
+++ b/src/client/views/animationtimeline/TimelineOverview.tsx
@@ -0,0 +1,181 @@
+import * as React from "react";
+import { observable, action, computed, runInAction, reaction, IReactionDisposer } from "mobx";
+import { observer } from "mobx-react";
+import "./TimelineOverview.scss";
+import * as $ from 'jquery';
+import { Timeline } from "./Timeline";
+import { Keyframe, KeyframeFunc } from "./Keyframe";
+
+
+interface TimelineOverviewProps {
+ totalLength: number;
+ visibleLength: number;
+ visibleStart: number;
+ currentBarX: number;
+ isAuthoring: boolean;
+ parent: Timeline;
+ changeCurrentBarX: (pixel: number) => void;
+ movePanX: (pixel: number) => any;
+ time: number;
+ tickSpacing: number;
+ tickIncrement: number;
+}
+
+
+@observer
+export class TimelineOverview extends React.Component<TimelineOverviewProps>{
+ @observable private _visibleRef = React.createRef<HTMLDivElement>();
+ @observable private _scrubberRef = React.createRef<HTMLDivElement>();
+ @observable private authoringContainer = React.createRef<HTMLDivElement>();
+ @observable private playbackContainer = React.createRef<HTMLDivElement>();
+ @observable private overviewBarWidth: number = 0;
+ @observable private playbarWidth: number = 0;
+ @observable private activeOverviewWidth: number = 0;
+ @observable private _authoringReaction?: IReactionDisposer;
+ @observable private visibleTime: number = 0;
+ @observable private currentX: number = 0;
+ @observable private visibleStart: number = 0;
+ private readonly DEFAULT_HEIGHT = 50;
+ private readonly DEFAULT_WIDTH = 300;
+
+ componentDidMount = () => {
+ this.setOverviewWidth();
+
+ this._authoringReaction = reaction(
+ () => this.props.parent._isAuthoring,
+ () => {
+ if (!this.props.parent._isAuthoring) {
+ runInAction(() => {
+ this.setOverviewWidth();
+ });
+ }
+ },
+ );
+ }
+
+ componentWillUnmount = () => {
+ this._authoringReaction && this._authoringReaction();
+ }
+
+ @action
+ setOverviewWidth() {
+ const width1 = this.authoringContainer.current?.clientWidth;
+ const width2 = this.playbackContainer.current?.clientWidth;
+ if (width1 && width1 !== 0) this.overviewBarWidth = width1;
+ if (width2 && width2 !== 0) this.playbarWidth = width2;
+
+ if (this.props.isAuthoring) {
+ this.activeOverviewWidth = this.overviewBarWidth;
+ }
+ else {
+ this.activeOverviewWidth = this.playbarWidth;
+ }
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ document.removeEventListener("pointermove", this.onPanX);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPanX);
+ document.addEventListener("pointerup", this.onPointerUp);
+ }
+
+ @action
+ onPanX = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ const movX = (this.props.visibleStart / this.props.totalLength) * (this.DEFAULT_WIDTH) + e.movementX;
+ this.props.movePanX((movX / (this.DEFAULT_WIDTH)) * this.props.totalLength);
+ }
+
+ @action
+ onPointerUp = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ document.removeEventListener("pointermove", this.onPanX);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ @action
+ onScrubberDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.onScrubberMove);
+ document.removeEventListener("pointerup", this.onScrubberUp);
+ document.addEventListener("pointermove", this.onScrubberMove);
+ document.addEventListener("pointerup", this.onScrubberUp);
+ }
+
+ @action
+ onScrubberMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const scrubberRef = this._scrubberRef.current!;
+ const left = scrubberRef.getBoundingClientRect().left;
+ const offsetX = Math.round(e.clientX - left);
+ this.props.changeCurrentBarX((((offsetX) / this.activeOverviewWidth) * this.props.totalLength) + this.props.currentBarX);
+ }
+
+ @action
+ onScrubberUp = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.onScrubberMove);
+ document.removeEventListener("pointerup", this.onScrubberUp);
+ }
+
+ @action
+ getTimes() {
+ const vis = KeyframeFunc.convertPixelTime(this.props.visibleLength, "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ const x = KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ const start = KeyframeFunc.convertPixelTime(this.props.visibleStart, "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ this.visibleTime = vis;
+ this.currentX = x;
+ this.visibleStart = start;
+ }
+
+ render() {
+ this.setOverviewWidth();
+ this.getTimes();
+
+ const percentVisible = this.visibleTime / this.props.time;
+ const visibleBarWidth = percentVisible * this.activeOverviewWidth;
+
+ const percentScrubberStart = this.currentX / this.props.time;
+ let scrubberStart = this.props.currentBarX / this.props.totalLength * this.activeOverviewWidth;
+ if (scrubberStart > this.activeOverviewWidth) scrubberStart = this.activeOverviewWidth;
+
+ const percentBarStart = this.visibleStart / this.props.time;
+ const barStart = percentBarStart * this.activeOverviewWidth;
+
+ let playWidth = (this.props.currentBarX / this.props.totalLength) * this.activeOverviewWidth;
+ if (playWidth > this.activeOverviewWidth) playWidth = this.activeOverviewWidth;
+
+ const timeline = this.props.isAuthoring ? [
+
+ <div key="timeline-overview-container" className="timeline-overview-container overviewBar" id="timelineOverview" ref={this.authoringContainer}>
+ <div ref={this._visibleRef} key="1" className="timeline-overview-visible" style={{ left: `${barStart}px`, width: `${visibleBarWidth}px` }} onPointerDown={this.onPointerDown}></div>,
+ <div ref={this._scrubberRef} key="2" className="timeline-overview-scrubber-container" style={{ left: `${scrubberStart}px` }} onPointerDown={this.onScrubberDown}>
+ <div key="timeline-overview-scrubber-head" className="timeline-overview-scrubber-head"></div>
+ </div>
+ </div>
+ ] : [
+ <div key="1" className="timeline-play-bar overviewBar" id="timelinePlay" ref={this.playbackContainer}>
+ <div ref={this._scrubberRef} className="timeline-play-head" style={{ left: `${scrubberStart}px` }} onPointerDown={this.onScrubberDown}></div>
+ </div>,
+ <div key="2" className="timeline-play-tail" style={{ width: `${playWidth}px` }}></div>
+ ];
+ return (
+ <div className="timeline-flex">
+ <div className="timelineOverview-bounding">
+ {timeline}
+ </div>
+ </div>
+ );
+ }
+
+}
+
+
diff --git a/src/client/views/animationtimeline/Track.scss b/src/client/views/animationtimeline/Track.scss
new file mode 100644
index 000000000..aec587a79
--- /dev/null
+++ b/src/client/views/animationtimeline/Track.scss
@@ -0,0 +1,15 @@
+@import "./../globalCssVariables.scss";
+
+.track-container {
+
+ .track {
+ .inner {
+ top: 0px;
+ width: calc(100%);
+ background-color: $light-color;
+ border: 1px solid $dark-color;
+ position: relative;
+ z-index: 100;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx
new file mode 100644
index 000000000..461db4858
--- /dev/null
+++ b/src/client/views/animationtimeline/Track.tsx
@@ -0,0 +1,380 @@
+import { action, computed, intercept, observable, reaction, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import * as React from "react";
+import { Doc, DocListCast, Opt, DocListCastAsync } from "../../../new_fields/Doc";
+import { Copy } from "../../../new_fields/FieldSymbols";
+import { List } from "../../../new_fields/List";
+import { ObjectField } from "../../../new_fields/ObjectField";
+import { listSpec } from "../../../new_fields/Schema";
+import { Cast, NumCast } from "../../../new_fields/Types";
+import { Transform } from "../../util/Transform";
+import { Keyframe, KeyframeFunc, RegionData } from "./Keyframe";
+import "./Track.scss";
+
+interface IProps {
+ node: Doc;
+ currentBarX: number;
+ transform: Transform;
+ collection: Doc;
+ time: number;
+ tickIncrement: number;
+ tickSpacing: number;
+ timelineVisible: boolean;
+ changeCurrentBarX: (x: number) => void;
+}
+
+@observer
+export class Track extends React.Component<IProps> {
+ @observable private _inner = React.createRef<HTMLDivElement>();
+ @observable private _currentBarXReaction: any;
+ @observable private _timelineVisibleReaction: any;
+ @observable private _autoKfReaction: any;
+ @observable private _newKeyframe: boolean = false;
+ private readonly MAX_TITLE_HEIGHT = 75;
+ private _trackHeight = 0;
+ private primitiveWhitelist = [
+ "x",
+ "y",
+ "_width",
+ "_height",
+ "opacity",
+ ];
+ private objectWhitelist = [
+ "data"
+ ];
+
+ @computed private get regions() { return DocListCast(this.props.node.regions); }
+ @computed private get time() { return NumCast(KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement)); }
+
+ async componentDidMount() {
+ const regions = await DocListCastAsync(this.props.node.regions);
+ if (!regions) this.props.node.regions = new List<Doc>(); //if there is no region, then create new doc to store stuff
+ //these two lines are exactly same from timeline.tsx
+ const relativeHeight = window.innerHeight / 20;
+ this._trackHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT; //for responsiveness
+ this._timelineVisibleReaction = this.timelineVisibleReaction();
+ this._currentBarXReaction = this.currentBarXReaction();
+ if (DocListCast(this.props.node.regions).length === 0) this.createRegion(this.time);
+ this.props.node.hidden = false;
+ this.props.node.opacity = 1;
+ // this.autoCreateKeyframe();
+ }
+
+ /**
+ * mainly for disposing reactions
+ */
+ componentWillUnmount() {
+ this._currentBarXReaction?.();
+ this._timelineVisibleReaction?.();
+ this._autoKfReaction?.();
+ }
+ ////////////////////////////////
+
+
+ getLastRegionTime = () => {
+ let lastTime: number = 0;
+ let lastRegion: Opt<Doc>;
+ this.regions.forEach(region => {
+ const time = NumCast(region.position);
+ if (lastTime <= time) {
+ lastTime = time;
+ lastRegion = region;
+ }
+ });
+ return lastRegion ? lastTime + NumCast(lastRegion.duration) : 0;
+ }
+
+ /**
+ * keyframe save logic. Needs to be changed so it's more efficient
+ *
+ */
+ @action
+ saveKeyframe = async () => {
+ const keyframes = Cast(this.saveStateRegion?.keyframes, listSpec(Doc)) as List<Doc>;
+ const kfIndex = keyframes.indexOf(this.saveStateKf!);
+ const kf = keyframes[kfIndex] as Doc; //index in the keyframe
+ if (this._newKeyframe) {
+ DocListCast(this.saveStateRegion?.keyframes).forEach((kf, index) => {
+ this.copyDocDataToKeyFrame(kf);
+ kf.opacity = (index === 0 || index === 3) ? 0.1 : 1;
+ });
+ this._newKeyframe = false;
+ }
+ if (!kf) return;
+ if (kf.type === KeyframeFunc.KeyframeType.default) { // only save for non-fades
+ this.copyDocDataToKeyFrame(kf);
+ const leftkf = KeyframeFunc.calcMinLeft(this.saveStateRegion!, this.time, kf); // lef keyframe, if it exists
+ const rightkf = KeyframeFunc.calcMinRight(this.saveStateRegion!, this.time, kf); //right keyframe, if it exists
+ if (leftkf?.type === KeyframeFunc.KeyframeType.fade) { //replicating this keyframe to fades
+ const edge = KeyframeFunc.calcMinLeft(this.saveStateRegion!, this.time, leftkf);
+ edge && this.copyDocDataToKeyFrame(edge);
+ leftkf && this.copyDocDataToKeyFrame(leftkf);
+ edge && (edge.opacity = 0.1);
+ leftkf && (leftkf.opacity = 1);
+ }
+ if (rightkf?.type === KeyframeFunc.KeyframeType.fade) {
+ const edge = KeyframeFunc.calcMinRight(this.saveStateRegion!, this.time, rightkf);
+ edge && this.copyDocDataToKeyFrame(edge);
+ rightkf && this.copyDocDataToKeyFrame(rightkf);
+ edge && (edge.opacity = 0.1);
+ rightkf && (rightkf.opacity = 1);
+ }
+ }
+ keyframes[kfIndex] = kf;
+ this.saveStateKf = undefined;
+ this.saveStateRegion = undefined;
+ }
+
+
+ /**
+ * autocreates keyframe
+ */
+ @action
+ autoCreateKeyframe = () => {
+ const objects = this.objectWhitelist.map(key => this.props.node[key]);
+ intercept(this.props.node, change => {
+ console.log(change);
+ return change;
+ });
+ return reaction(() => {
+ return [...this.primitiveWhitelist.map(key => this.props.node[key]), ...objects];
+ }, (changed, reaction) => {
+ //check for region
+ const region = this.findRegion(this.time);
+ if (region !== undefined) { //if region at scrub time exist
+ const r = region as RegionData; //for some region is returning undefined... which is not the case
+ if (DocListCast(r.keyframes).find(kf => kf.time === this.time) === undefined) { //basically when there is no additional keyframe at that timespot
+ this.makeKeyData(r, this.time, KeyframeFunc.KeyframeType.default);
+ }
+ }
+ }, { fireImmediately: false });
+ }
+
+
+
+ // @observable private _storedState:(Doc | undefined) = undefined;
+ // /**
+ // * reverting back to previous state before editing on AT
+ // */
+ // @action
+ // revertState = () => {
+ // if (this._storedState) this.applyKeys(this._storedState);
+ // }
+
+
+ /**
+ * Reaction when scrubber bar changes
+ * made into function so it's easier to dispose later
+ */
+ @action
+ currentBarXReaction = () => {
+ return reaction(() => this.props.currentBarX, () => {
+ const regiondata = this.findRegion(this.time);
+ if (regiondata) {
+ this.props.node.hidden = false;
+ // if (!this._autoKfReaction) {
+ // // console.log("creating another reaction");
+ // // this._autoKfReaction = this.autoCreateKeyframe();
+ // }
+ this.timeChange();
+ } else {
+ this.props.node.hidden = true;
+ this.props.node.opacity = 0;
+ //if (this._autoKfReaction) this._autoKfReaction();
+ }
+ });
+ }
+
+ /**
+ * when timeline is visible, reaction is ran so states are reverted
+ */
+ @action
+ timelineVisibleReaction = () => {
+ return reaction(() => {
+ return this.props.timelineVisible;
+ }, isVisible => {
+ if (isVisible) {
+ this.regions.filter(region => !region.hasData).forEach(region => {
+ for (let i = 0; i < 4; i++) {
+ this.copyDocDataToKeyFrame(DocListCast(region.keyframes)[i]);
+ if (i === 0 || i === 3) { //manually inputing fades
+ DocListCast(region.keyframes)[i].opacity = 0.1;
+ }
+ }
+ });
+ } else {
+ console.log("reverting state");
+ //this.revertState();
+ }
+ });
+ }
+
+ @observable private saveStateKf: (Doc | undefined) = undefined;
+ @observable private saveStateRegion: (Doc | undefined) = undefined;
+
+ /**w
+ * when scrubber position changes. Need to edit the logic
+ */
+ @action
+ timeChange = async () => {
+ if (this.saveStateKf !== undefined) {
+ await this.saveKeyframe();
+ } else if (this._newKeyframe) {
+ await this.saveKeyframe();
+ }
+ const regiondata = await this.findRegion(Math.round(this.time)); //finds a region that the scrubber is on
+ if (regiondata) {
+ const leftkf: (Doc | undefined) = await KeyframeFunc.calcMinLeft(regiondata, this.time); // lef keyframe, if it exists
+ const rightkf: (Doc | undefined) = await KeyframeFunc.calcMinRight(regiondata, this.time); //right keyframe, if it exists
+ const currentkf: (Doc | undefined) = await this.calcCurrent(regiondata); //if the scrubber is on top of the keyframe
+ if (currentkf) {
+ console.log("is current");
+ await this.applyKeys(currentkf);
+ this.saveStateKf = currentkf;
+ this.saveStateRegion = regiondata;
+ } else if (leftkf && rightkf) {
+ await this.interpolate(leftkf, rightkf);
+ }
+ }
+ }
+
+ /**
+ * applying changes (when saving the keyframe)
+ * need to change the logic here
+ */
+ @action
+ private applyKeys = async (kf: Doc) => {
+ this.primitiveWhitelist.forEach(key => {
+ if (!kf[key]) {
+ this.props.node[key] = undefined;
+ } else {
+ const stored = kf[key];
+ this.props.node[key] = stored instanceof ObjectField ? stored[Copy]() : stored;
+ }
+ });
+ }
+
+
+ /**
+ * calculating current keyframe, if the scrubber is right on the keyframe
+ */
+ @action
+ calcCurrent = (region: Doc) => {
+ let currentkf: (Doc | undefined) = undefined;
+ const keyframes = DocListCast(region.keyframes!);
+ keyframes.forEach((kf) => {
+ if (NumCast(kf.time) === Math.round(this.time)) currentkf = kf;
+ });
+ return currentkf;
+ }
+
+
+ /**
+ * basic linear interpolation function
+ */
+ @action
+ interpolate = async (left: Doc, right: Doc) => {
+ this.primitiveWhitelist.forEach(key => {
+ if (left[key] && right[key] && typeof (left[key]) === "number" && typeof (right[key]) === "number") { //if it is number, interpolate
+ const dif = NumCast(right[key]) - NumCast(left[key]);
+ const deltaLeft = this.time - NumCast(left.time);
+ const ratio = deltaLeft / (NumCast(right.time) - NumCast(left.time));
+ this.props.node[key] = NumCast(left[key]) + (dif * ratio);
+ } else { // case data
+ const stored = left[key];
+ this.props.node[key] = stored instanceof ObjectField ? stored[Copy]() : stored;
+ }
+ });
+ }
+
+ /**
+ * finds region that corresponds to specific time (is there a region at this time?)
+ * linear O(n) (maybe possible to optimize this with other Data structures?)
+ */
+ findRegion = (time: number) => {
+ return this.regions?.find(rd => (time >= NumCast(rd.position) && time <= (NumCast(rd.position) + NumCast(rd.duration))));
+ }
+
+
+ /**
+ * double click on track. Signalling keyframe creation.
+ */
+ @action
+ onInnerDoubleClick = (e: React.MouseEvent) => {
+ const inner = this._inner.current!;
+ const offsetX = Math.round((e.clientX - inner.getBoundingClientRect().left) * this.props.transform.Scale);
+ this.createRegion(KeyframeFunc.convertPixelTime(offsetX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement));
+ }
+
+
+ /**
+ * creates a region (KEYFRAME.TSX stuff).
+ */
+ @action
+ createRegion = (time: number) => {
+ if (this.findRegion(time) === undefined) { //check if there is a region where double clicking (prevents phantom regions)
+ const regiondata = KeyframeFunc.defaultKeyframe(); //create keyframe data
+
+ regiondata.position = time; //set position
+ const rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, regiondata, this.regions);
+
+ if (rightRegion && rightRegion.position - regiondata.position <= 4000) { //edge case when there is less than default 4000 duration space between this and right region
+ regiondata.duration = rightRegion.position - regiondata.position;
+ }
+ if (this.regions.length === 0 || !rightRegion || (rightRegion && rightRegion.position - regiondata.position >= NumCast(regiondata.fadeIn) + NumCast(regiondata.fadeOut))) {
+ Cast(this.props.node.regions, listSpec(Doc))?.push(regiondata);
+ this._newKeyframe = true;
+ this.saveStateRegion = regiondata;
+ return regiondata;
+ }
+ }
+ }
+
+ @action
+ makeKeyData = (regiondata: RegionData, time: number, type: KeyframeFunc.KeyframeType = KeyframeFunc.KeyframeType.default) => { //Kfpos is mouse offsetX, representing time
+ const trackKeyFrames = DocListCast(regiondata.keyframes);
+ const existingkf = trackKeyFrames.find(TK => TK.time === time);
+ if (existingkf) return existingkf;
+ //else creates a new doc.
+ const newKeyFrame: Doc = new Doc();
+ newKeyFrame.time = time;
+ newKeyFrame.type = type;
+ this.copyDocDataToKeyFrame(newKeyFrame);
+ //assuming there are already keyframes (for keeping keyframes in order, sorted by time)
+ if (trackKeyFrames.length === 0) regiondata.keyframes!.push(newKeyFrame);
+ trackKeyFrames.map(kf => NumCast(kf.time)).forEach((kfTime, index) => {
+ if ((kfTime < time && index === trackKeyFrames.length - 1) || (kfTime < time && time < NumCast(trackKeyFrames[index + 1].time))) {
+ regiondata.keyframes!.splice(index + 1, 0, newKeyFrame);
+ }
+ });
+ return newKeyFrame;
+ }
+
+ @action
+ copyDocDataToKeyFrame = (doc: Doc) => {
+ this.primitiveWhitelist.map(key => {
+ const originalVal = this.props.node[key];
+ doc[key] = originalVal instanceof ObjectField ? originalVal[Copy]() : originalVal;
+ });
+ }
+
+ /**
+ * UI sstuff here. Not really much to change
+ */
+ render() {
+ return (
+ <div className="track-container">
+ <div className="track">
+ <div className="inner" ref={this._inner} style={{ height: `${this._trackHeight}px` }}
+ onDoubleClick={this.onInnerDoubleClick}
+ onPointerOver={() => Doc.BrushDoc(this.props.node)}
+ onPointerOut={() => Doc.UnBrushDoc(this.props.node)} >
+ {this.regions?.map((region, i) => {
+ return <Keyframe key={`${i}`} {...this.props} RegionData={region} makeKeyData={this.makeKeyData} />;
+ })}
+ </div>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 2453acddf..a04136e51 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -2,21 +2,22 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { observable, computed } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { documentSchema } from '../../../new_fields/documentSchemas';
+import { documentSchema, collectionSchema } from '../../../new_fields/documentSchemas';
import { makeInterface } from '../../../new_fields/Schema';
-import { NumCast, StrCast } from '../../../new_fields/Types';
+import { NumCast, StrCast, ScriptCast, Cast } from '../../../new_fields/Types';
import { DragManager } from '../../util/DragManager';
import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView';
import "./CollectionCarouselView.scss";
import { CollectionSubView } from './CollectionSubView';
import { faCaretLeft, faCaretRight } from '@fortawesome/free-solid-svg-icons';
import { Doc } from '../../../new_fields/Doc';
-import { FormattedTextBox } from '../nodes/FormattedTextBox';
+import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { ContextMenu } from '../ContextMenu';
import { ObjectField } from '../../../new_fields/ObjectField';
+import { returnFalse } from '../../../Utils';
-type CarouselDocument = makeInterface<[typeof documentSchema,]>;
-const CarouselDocument = makeInterface(documentSchema);
+type CarouselDocument = makeInterface<[typeof documentSchema, typeof collectionSchema]>;
+const CarouselDocument = makeInterface(documentSchema, collectionSchema);
@observer
export class CollectionCarouselView extends CollectionSubView(CarouselDocument) {
@@ -39,7 +40,6 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument)
e.stopPropagation();
this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length;
}
-
panelHeight = () => this.props.PanelHeight() - 50;
@computed get content() {
const index = NumCast(this.layoutDoc._itemIndex);
@@ -47,13 +47,29 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument)
<>
<div className="collectionCarouselView-image" key="image">
<ContentFittingDocumentView {...this.props}
+ onDoubleClick={ScriptCast(this.layoutDoc.onChildDoubleClick)}
+ onClick={ScriptCast(this.layoutDoc.onChildClick)}
+ renderDepth={this.props.renderDepth + 1}
+ LayoutTemplate={this.props.ChildLayoutTemplate}
+ LayoutTemplateString={this.props.ChildLayoutString}
Document={this.childLayoutPairs[index].layout}
- DataDocument={this.childLayoutPairs[index].data}
+ DataDoc={this.childLayoutPairs[index].data}
PanelHeight={this.panelHeight}
- getTransform={this.props.ScreenToLocalTransform} />
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ bringToFront={returnFalse}
+ parentActive={this.props.active}
+ />
</div>
- <div className="collectionCarouselView-caption" key="caption" style={{ background: this.props.backgroundColor?.(this.props.Document) }}>
- <FormattedTextBox key={index} {...this.props} Document={this.childLayoutPairs[index].layout} DataDoc={undefined} fieldKey={"caption"}></FormattedTextBox>
+ <div className="collectionCarouselView-caption" key="caption"
+ style={{
+ background: StrCast(this.layoutDoc._captionBackgroundColor, this.props.backgroundColor?.(this.props.Document)),
+ color: StrCast(this.layoutDoc._captionColor, StrCast(this.layoutDoc.color)),
+ borderRadius: StrCast(this.layoutDoc._captionBorderRounding),
+ }}>
+ <FormattedTextBox key={index} {...this.props}
+ xMargin={NumCast(this.layoutDoc["_carousel-caption-xMargin"])}
+ yMargin={NumCast(this.layoutDoc["_carousel-caption-yMargin"])}
+ Document={this.childLayoutPairs[index].layout} DataDoc={undefined} fieldKey={"caption"} />
</div>
</>;
}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 0d859c3f1..33ece13cc 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -68,10 +68,11 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
//Why is this here?
(window as any).React = React;
(window as any).ReactDOM = ReactDOM;
+ DragManager.Vals.Instance.StartWindowDrag = this.StartOtherDrag;
}
hack: boolean = false;
undohack: any = null;
- public StartOtherDrag(e: any, dragDocs: Doc[]) {
+ public StartOtherDrag = (e: any, dragDocs: Doc[]) => {
let config: any;
if (dragDocs.length === 1) {
config = CollectionDockingView.makeDocumentConfig(dragDocs[0]);
@@ -499,7 +500,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
const stack = tab.contentItem.parent;
// shifts the focus to this tab when another tab is dragged over it
tab.element[0].onmouseenter = (e: any) => {
- if (!this._isPointerDown || !SelectionManager.GetIsDragging()) return;
+ if (!this._isPointerDown || !DragManager.Vals.Instance.GetIsDragging()) return;
const activeContentItem = tab.header.parent.getActiveContentItem();
if (tab.contentItem !== activeContentItem) {
tab.header.parent.setActiveContentItem(tab.contentItem);
@@ -685,6 +686,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
if (curPres) {
const pinDoc = Doc.MakeAlias(doc);
pinDoc.presentationTargetDoc = doc;
+ pinDoc.presZoomButton = true;
Doc.AddDocToList(curPres, "data", pinDoc);
if (!DocumentManager.Instance.getDocumentView(curPres)) {
CollectionDockingView.AddRightSplit(curPres);
diff --git a/src/client/views/collections/CollectionMapView.tsx b/src/client/views/collections/CollectionMapView.tsx
index 7b7828d7d..971224482 100644
--- a/src/client/views/collections/CollectionMapView.tsx
+++ b/src/client/views/collections/CollectionMapView.tsx
@@ -1,4 +1,4 @@
-import { GoogleApiWrapper, Map as GeoMap, MapProps, Marker } from "google-maps-react";
+import { GoogleApiWrapper, Map as GeoMap, IMapProps, Marker } from "google-maps-react";
import { observer } from "mobx-react";
import { Doc, Opt, DocListCast, FieldResult, Field } from "../../../new_fields/Doc";
import { documentSchema } from "../../../new_fields/documentSchemas";
@@ -42,7 +42,7 @@ const query = async (data: string | google.maps.LatLngLiteral) => {
};
@observer
-class CollectionMapView extends CollectionSubView<MapSchema, Partial<MapProps> & { google: any }>(MapSchema) {
+class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> & { google: any }>(MapSchema) {
private _cancelAddrReq = new Map<string, boolean>();
private _cancelLocReq = new Map<string, boolean>();
@@ -221,7 +221,7 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<MapProps> &
zoom={center.zoom || 10}
initialCenter={center}
center={center}
- onIdle={(_props?: MapProps, map?: google.maps.Map) => {
+ onIdle={(_props?: IMapProps, map?: google.maps.Map) => {
if (this.layoutDoc.lockedTransform) {
// reset zoom (ideally, we could probably can tell the map to disallow zooming somehow instead)
map?.setZoom(center?.zoom || 10);
@@ -233,7 +233,7 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<MapProps> &
}))();
}
}}
- onDragend={(_props?: MapProps, map?: google.maps.Map) => {
+ onDragend={(_props?: IMapProps, map?: google.maps.Map) => {
if (this.layoutDoc.lockedTransform) {
// reset the drag (ideally, we could probably can tell the map to disallow dragging somehow instead)
map?.setCenter({ lat: center?.lat!, lng: center?.lng! });
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 42f0c4311..c74cfbcf4 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -2,13 +2,13 @@ import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
import { faPalette } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable } from "mobx";
+import { action, computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc } from "../../../new_fields/Doc";
import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import { ScriptField } from "../../../new_fields/ScriptField";
import { StrCast, NumCast } from "../../../new_fields/Types";
-import { numberRange } from "../../../Utils";
+import { numberRange, setupMoveUpEvents, emptyFunction } from "../../../Utils";
import { Docs } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
import { CompileScript } from "../../util/Scripting";
@@ -44,37 +44,44 @@ interface CMVFieldRowProps {
export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowProps> {
@observable private _background = "inherit";
@observable private _createAliasSelected: boolean = false;
- @observable private _collapsed: boolean = false;
- @observable private _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading;
- @observable private _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
+ @observable private heading: string = "";
+ @observable private color: string = "#f1efeb";
+ @observable private collapsed: boolean = false;
+ private set _heading(value: string) { runInAction(() => this.props.headingObject && (this.props.headingObject.heading = this.heading = value)); }
+ private set _color(value: string) { runInAction(() => this.props.headingObject && (this.props.headingObject.color = this.color = value)); }
+ private set _collapsed(value: boolean) { runInAction(() => this.props.headingObject && (this.props.headingObject.collapsed = this.collapsed = value)); }
private _dropDisposer?: DragManager.DragDropDisposer;
private _headerRef: React.RefObject<HTMLDivElement> = React.createRef();
- private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 };
private _contRef: React.RefObject<HTMLDivElement> = React.createRef();
- private _sensitivity: number = 16;
private _ele: any;
createRowDropRef = (ele: HTMLDivElement | null) => {
- this._dropDisposer && this._dropDisposer();
+ this._dropDisposer?.();
if (ele) {
this._ele = ele;
this.props.observeHeight(ele);
this._dropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this));
}
}
+ @action
+ componentDidMount() {
+ this.heading = this.props.headingObject?.heading || "";
+ this.color = this.props.headingObject?.color || "#f1efeb";
+ this.collapsed = this.props.headingObject?.collapsed || false;
+ }
componentWillUnmount() {
this.props.unobserveHeight(this._ele);
}
getTrueHeight = () => {
- if (this._collapsed) {
- this.props.setDocHeight(this._heading, 20);
+ if (this.collapsed) {
+ this.props.setDocHeight(this.heading, 20);
} else {
const rawHeight = this._contRef.current!.getBoundingClientRect().height + 15; //+ 15 accounts for the group header
const transformScale = this.props.screenToLocalTransform().Scale;
const trueHeight = rawHeight * transformScale;
- this.props.setDocHeight(this._heading, trueHeight);
+ this.props.setDocHeight(this.heading, trueHeight);
}
}
@@ -86,7 +93,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
(this.props.parent.Document.dropConverter instanceof ScriptField) &&
this.props.parent.Document.dropConverter.script.run({ dragData: de.complete.docDragData });
const key = StrCast(this.props.parent.props.Document._pivotField);
- const castedValue = this.getValue(this._heading);
+ const castedValue = this.getValue(this.heading);
de.complete.docDragData.droppedDocuments.forEach(d => d[key] = castedValue);
this.props.parent.onInternalDrop(e, de);
e.stopPropagation();
@@ -113,10 +120,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
}
}
this.props.docList.forEach(d => d[key] = castedValue);
- if (this.props.headingObject) {
- this.props.headingObject.setHeading(castedValue.toString());
- this._heading = this.props.headingObject.heading;
- }
+ this._heading = castedValue.toString();
return true;
}
return false;
@@ -125,19 +129,15 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
@action
changeColumnColor = (color: string) => {
this._createAliasSelected = false;
- if (this.props.headingObject) {
- this.props.headingObject.setColor(color);
- this._color = color;
- }
+ this._color = color;
}
- pointerEnteredRow = action(() => SelectionManager.GetIsDragging() && (this._background = "#b4b4b4"));
+ pointerEnteredRow = action(() => DragManager.Vals.Instance.GetIsDragging() && (this._background = "#b4b4b4"));
@action
pointerLeaveRow = () => {
this._createAliasSelected = false;
this._background = "inherit";
- document.removeEventListener("pointermove", this.startDrag);
}
@action
@@ -161,65 +161,36 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
}));
@action
- collapseSection = () => {
+ collapseSection = (e: any) => {
this._createAliasSelected = false;
- if (this.props.headingObject) {
- this.props.headingObject.setCollapsed(!this.props.headingObject.collapsed);
- this.toggleVisibility();
- }
- }
-
- startDrag = (e: PointerEvent) => {
- const [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y);
- if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) {
- const alias = Doc.MakeAlias(this.props.parent.props.Document);
- const key = StrCast(this.props.parent.props.Document._pivotField);
- let value = this.getValue(this._heading);
- value = typeof value === "string" ? `"${value}"` : value;
- const script = `return doc.${key} === ${value}`;
- const compiled = CompileScript(script, { params: { doc: Doc.name } });
- if (compiled.compiled) {
- alias.viewSpecScript = new ScriptField(compiled);
- DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY);
- }
-
- e.stopPropagation();
- document.removeEventListener("pointermove", this.startDrag);
- document.removeEventListener("pointerup", this.pointerUp);
- }
- }
-
- pointerUp = (e: PointerEvent) => {
+ this.toggleVisibility();
e.stopPropagation();
- e.preventDefault();
+ }
- if (this.props.parent.props.Document._chromeStatus === "disabled") {
- this.collapseSection();
+ headerMove = (e: PointerEvent) => {
+ const alias = Doc.MakeAlias(this.props.parent.props.Document);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
+ let value = this.getValue(this.heading);
+ value = typeof value === "string" ? `"${value}"` : value;
+ const script = `return doc.${key} === ${value}`;
+ const compiled = CompileScript(script, { params: { doc: Doc.name } });
+ if (compiled.compiled) {
+ alias.viewSpecScript = new ScriptField(compiled);
+ DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY);
}
-
- document.removeEventListener("pointermove", this.startDrag);
- document.removeEventListener("pointerup", this.pointerUp);
+ return true;
}
@action
headerDown = (e: React.PointerEvent<HTMLDivElement>) => {
- e.stopPropagation();
- e.preventDefault();
-
- const [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY);
- this._startDragPosition = { x: dx, y: dy };
-
- if (this.props.parent.props.Document._chromeStatus === "disabled" || this._createAliasSelected) {
- document.removeEventListener("pointermove", this.startDrag);
- document.addEventListener("pointermove", this.startDrag);
- document.removeEventListener("pointerup", this.pointerUp);
- document.addEventListener("pointerup", this.pointerUp);
+ if (e.button === 0 && !e.ctrlKey) {
+ setupMoveUpEvents(this, e, this.headerMove, emptyFunction, () => (this.props.parent.props.Document._chromeStatus === "disabled") && this.collapseSection(e));
+ this._createAliasSelected = false;
}
- this._createAliasSelected = false;
}
renderColorPicker = () => {
- const selected = this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
+ const selected = this.color;
const pink = PastelSchemaPalette.get("pink2");
const purple = PastelSchemaPalette.get("purple4");
@@ -249,7 +220,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
}
toggleAlias = action(() => this._createAliasSelected = true);
- toggleVisibility = action(() => this._collapsed = !this._collapsed);
+ toggleVisibility = () => this._collapsed = !this.collapsed;
renderMenu = () => {
const selected = this._createAliasSelected;
@@ -264,7 +235,6 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
@computed get contentLayout() {
const rows = Math.max(1, Math.min(this.props.docList.length, Math.floor((this.props.parent.props.PanelWidth() - 2 * this.props.parent.xMargin) / (this.props.parent.columnWidth + this.props.parent.gridGap))));
const style = this.props.parent;
- const collapsed = this._collapsed;
const chromeStatus = this.props.parent.props.Document._chromeStatus;
const newEditableViewProps = {
GetValue: () => "",
@@ -272,9 +242,9 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
contents: "+ NEW",
HeadingObject: this.props.headingObject,
toggle: this.toggleVisibility,
- color: this._color
+ color: this.color
};
- return collapsed ? (null) :
+ return this.collapsed ? (null) :
<div style={{ position: "relative" }}>
{(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ?
<div className="collectionStackingView-addDocumentButton"
@@ -300,10 +270,9 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
}
@computed get headingView() {
- const heading = this._heading;
const noChrome = this.props.parent.props.Document._chromeStatus === "disabled";
const key = StrCast(this.props.parent.props.Document._pivotField);
- const evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
+ const evContents = this.heading ? this.heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
const headerEditableViewProps = {
GetValue: () => evContents,
SetValue: this.headingChanged,
@@ -311,7 +280,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
oneLine: true,
HeadingObject: this.props.headingObject,
toggle: this.toggleVisibility,
- color: this._color
+ color: this.color
};
return this.props.parent.props.Document.miniHeaders ?
<div className="collectionStackingView-miniHeader">
@@ -319,11 +288,11 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
</div> :
!this.props.headingObject ? (null) :
<div className="collectionStackingView-sectionHeader" ref={this._headerRef} >
- <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown} onClick={noChrome ? undefined : this.collapseSection}
+ <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 : "lightgrey" }}>
- <EditableView {...headerEditableViewProps} />
+ style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this.color : "lightgrey" }}>
+ {noChrome ? evContents : <EditableView {...headerEditableViewProps} />}
{noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
<div className="collectionStackingView-sectionColor">
<Flyout anchorPoint={anchorPoints.CENTER_RIGHT} content={this.renderColorPicker()}>
@@ -334,7 +303,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
</div>
}
{noChrome ? (null) : <button className="collectionStackingView-sectionDelete" onClick={noChrome ? undefined : this.collapseSection}>
- <FontAwesomeIcon icon={this._collapsed ? "chevron-down" : "chevron-up"} size="lg" />
+ <FontAwesomeIcon icon={this.collapsed ? "chevron-down" : "chevron-up"} size="lg" />
</button>}
{noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
<div className="collectionStackingView-sectionOptions">
@@ -349,17 +318,15 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
</div>;
}
render() {
- const background = this._background; //to account for observables in Measure
- const contentlayout = this.contentLayout;
- const headingview = this.headingView;
+ const background = this._background;
return <div className="collectionStackingView-masonrySection"
style={{ width: this.props.parent.NodeWidth, background }}
ref={this.createRowDropRef}
onPointerEnter={this.pointerEnteredRow}
onPointerLeave={this.pointerLeaveRow}
>
- {headingview}
- {contentlayout}
+ {this.headingView}
+ {this.contentLayout}
</div >;
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index 3bbfcc4d7..0a10c24b3 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -12,6 +12,7 @@ import React = require("react");
import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../../Utils";
import { SelectionManager } from "../../util/SelectionManager";
import { UndoManager } from "../../util/UndoManager";
+import { DragManager } from "../../util/DragManager";
@observer
export class CollectionPileView extends CollectionSubView(doc => doc) {
@@ -53,7 +54,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
toggleStarburst = action(() => {
if (this._layoutEngine === 'starburst') {
const defaultSize = 110;
- this.layoutDoc.overflow = undefined;
+ this.layoutDoc._overflow = undefined;
this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - NumCast(this.layoutDoc._starburstPileWidth, defaultSize) / 2;
this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - NumCast(this.layoutDoc._starburstPileHeight, defaultSize) / 2;
this.layoutDoc._width = NumCast(this.layoutDoc._starburstPileWidth, defaultSize);
@@ -61,7 +62,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
this._layoutEngine = 'pass';
} else {
const defaultSize = 25;
- this.layoutDoc.overflow = 'visible';
+ this.layoutDoc._overflow = 'visible';
!this.layoutDoc._starburstRadius && (this.layoutDoc._starburstRadius = 500);
!this.layoutDoc._starburstDocScale && (this.layoutDoc._starburstDocScale = 2.5);
if (this._layoutEngine === 'pass') {
@@ -78,7 +79,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
_undoBatch: UndoManager.Batch | undefined;
pointerDown = (e: React.PointerEvent) => {
let dist = 0;
- SelectionManager.SetIsDragging(true);
+ DragManager.Vals.Instance.SetIsDragging(true);
// this._lastTap should be set to 0, and this._doubleTap should be set to false in the class header
setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => {
if (this.layoutEngine() === "pass" && this.childDocs.length && this.props.isSelected(true)) {
@@ -98,7 +99,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
}, () => {
this._undoBatch?.end();
this._undoBatch = undefined;
- SelectionManager.SetIsDragging(false);
+ DragManager.Vals.Instance.SetIsDragging(false);
if (!this.childDocs.length) {
this.props.ContainingCollectionView?.removeDocument(this.props.Document);
}
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index 82204ca7b..0e6489947 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -189,7 +189,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e);
};
const onPointerEnter = (e: React.PointerEvent): void => {
- if (e.buttons === 1 && SelectionManager.GetIsDragging() && (type === "document" || type === undefined)) {
+ if (e.buttons === 1 && DragManager.Vals.Instance.GetIsDragging() && (type === "document" || type === undefined)) {
dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over";
}
};
diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
index 972714e34..f9cd9a628 100644
--- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
+++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
@@ -32,7 +32,7 @@ export class MovableColumn extends React.Component<MovableColumnProps> {
private _dragRef: React.RefObject<HTMLDivElement> = React.createRef();
onPointerEnter = (e: React.PointerEvent): void => {
- if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
+ if (e.buttons === 1 && DragManager.Vals.Instance.GetIsDragging()) {
this._header!.current!.className = "collectionSchema-col-wrapper";
document.addEventListener("pointermove", this.onDragMove, true);
}
@@ -143,7 +143,7 @@ export class MovableRow extends React.Component<MovableRowProps> {
private _rowDropDisposer?: DragManager.DragDropDisposer;
onPointerEnter = (e: React.PointerEvent): void => {
- if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
+ if (e.buttons === 1 && DragManager.Vals.Instance.GetIsDragging()) {
this._header!.current!.className = "collectionSchema-row-wrapper";
document.addEventListener("pointermove", this.onDragMove, true);
}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 380d91d2f..c0024293f 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -27,7 +27,7 @@ import "./CollectionSchemaView.scss";
import { CollectionSubView } from "./CollectionSubView";
import { CollectionView } from "./CollectionView";
import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView";
-import { setupMoveUpEvents, emptyFunction, returnZero, returnOne } from "../../../Utils";
+import { setupMoveUpEvents, emptyFunction, returnZero, returnOne, returnFalse } from "../../../Utils";
import { DocumentView } from "../nodes/DocumentView";
library.add(faCog, faPlus, faSortUp, faSortDown);
@@ -121,7 +121,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
{!this.previewDocument ? (null) :
<ContentFittingDocumentView
Document={this.previewDocument}
- DataDocument={undefined}
+ DataDoc={undefined}
NativeHeight={returnZero}
NativeWidth={returnZero}
fitToBox={true}
@@ -132,16 +132,18 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
rootSelected={this.rootSelected}
PanelWidth={this.previewWidth}
PanelHeight={this.previewHeight}
- getTransform={this.getPreviewTransform}
- CollectionDoc={this.props.CollectionView?.props.Document}
- CollectionView={this.props.CollectionView}
+ ScreenToLocalTransform={this.getPreviewTransform}
+ ContainingCollectionDoc={this.props.CollectionView?.props.Document}
+ ContainingCollectionView={this.props.CollectionView}
moveDocument={this.props.moveDocument}
addDocument={this.props.addDocument}
removeDocument={this.props.removeDocument}
- active={this.props.active}
+ parentActive={this.props.active}
whenActiveChanged={this.props.whenActiveChanged}
addDocTab={this.props.addDocTab}
pinToPres={this.props.pinToPres}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne}
/>}
</div>;
}
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 47faa9239..5eaf29316 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -180,13 +180,16 @@
.collectionStackingView-sectionHeader-subCont {
outline: none;
border: 0px;
- color: $light-color;
width: 100%;
- color: grey;
letter-spacing: 2px;
font-size: 75%;
transition: transform 0.2s;
position: relative;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: lightGray;
.editableView-container-editing-oneLine,
.editableView-container-editing {
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index e3720bf01..f6cdebc9b 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -4,15 +4,17 @@ import { CursorProperty } from "csstype";
import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import Switch from 'rc-switch';
-import { Doc, HeightSym, WidthSym, DataSym } from "../../../new_fields/Doc";
+import { DataSym, Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
+import { collectionSchema, documentSchema } from "../../../new_fields/documentSchemas";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
-import { listSpec } from "../../../new_fields/Schema";
+import { listSpec, makeInterface } from "../../../new_fields/Schema";
import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../new_fields/Types";
import { TraceMobx } from "../../../new_fields/util";
-import { Utils, setupMoveUpEvents, emptyFunction, returnZero, returnOne } from "../../../Utils";
+import { emptyFunction, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from "../../../Utils";
import { DragManager, dropActionType } from "../../util/DragManager";
+import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
import { undoBatch } from "../../util/UndoManager";
import { ContextMenu } from "../ContextMenu";
@@ -24,11 +26,14 @@ import "./CollectionStackingView.scss";
import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewFieldColumn";
import { CollectionSubView } from "./CollectionSubView";
import { CollectionViewType } from "./CollectionView";
-import { SelectionManager } from "../../util/SelectionManager";
+import { ScriptField } from "../../../new_fields/ScriptField";
const _global = (window /* browser */ || global /* node */) as any;
+type StackingDocument = makeInterface<[typeof collectionSchema, typeof documentSchema]>;
+const StackingDocument = makeInterface(collectionSchema, documentSchema);
+
@observer
-export class CollectionStackingView extends CollectionSubView(doc => doc) {
+export class CollectionStackingView extends CollectionSubView(StackingDocument) {
_masonryGridRef: HTMLDivElement | null = null;
_draggerRef = React.createRef<HTMLDivElement>();
_pivotFieldDisposer?: IReactionDisposer;
@@ -116,7 +121,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
getSimpleDocHeight(d?: Doc) {
if (!d) return 0;
- const layoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
+ const layoutDoc = Doc.Layout(d, this.props.ChildLayoutTemplate?.());
const nw = NumCast(layoutDoc._nativeWidth);
const nh = NumCast(layoutDoc._nativeHeight);
let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
@@ -128,7 +133,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
return layoutDoc._fitWidth ? wid * NumCast(layoutDoc.scrollHeight, nh) / (nw || 1) : layoutDoc[HeightSym]();
}
componentDidMount() {
- super.componentDidMount();
+ super.componentDidMount?.();
// reset section headers when a new filter is inputted
this._pivotFieldDisposer = reaction(
@@ -150,8 +155,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
this.createDashEventsTarget(ele!); //so the whole grid is the drop target?
}
- @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
- @computed get onClickHandler() { return ScriptCast(this.Document.onChildClick); }
+ @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) {
@@ -160,14 +165,16 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
return this.props.addDocTab(doc, where);
}
+
getDisplayDoc(doc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) {
- const layoutDoc = Doc.Layout(doc, this.props.childLayoutTemplate?.());
+ const layoutDoc = Doc.Layout(doc, this.props.ChildLayoutTemplate?.());
const height = () => this.getDocHeight(doc);
return <ContentFittingDocumentView
Document={doc}
- DataDocument={dataDoc || (doc[DataSym] !== doc && doc[DataSym])}
+ DataDoc={dataDoc || (doc[DataSym] !== doc && doc[DataSym])}
backgroundColor={this.props.backgroundColor}
- LayoutDoc={this.props.childLayoutTemplate}
+ LayoutTemplate={this.props.ChildLayoutTemplate}
+ LayoutTemplateString={this.props.ChildLayoutString}
LibraryPath={this.props.LibraryPath}
FreezeDimensions={this.props.freezeChildDimensions}
renderDepth={this.props.renderDepth + 1}
@@ -175,33 +182,36 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
PanelHeight={height}
NativeHeight={returnZero}
NativeWidth={returnZero}
- fitToBox={BoolCast(this.props.Document._freezeChildDimensions)}
+ fitToBox={false}
rootSelected={this.rootSelected}
dropAction={StrCast(this.props.Document.childDropAction) as dropActionType}
- onClick={layoutDoc.isTemplateDoc ? this.onClickHandler : this.onChildClickHandler}
- getTransform={dxf}
+ onClick={this.onChildClickHandler}
+ onDoubleClick={this.onChildDoubleClickHandler}
+ ScreenToLocalTransform={dxf}
focus={this.props.focus}
- CollectionDoc={this.props.CollectionView?.props.Document}
- CollectionView={this.props.CollectionView}
+ ContainingCollectionDoc={this.props.CollectionView?.props.Document}
+ ContainingCollectionView={this.props.CollectionView}
addDocument={this.props.addDocument}
moveDocument={this.props.moveDocument}
removeDocument={this.props.removeDocument}
- active={this.props.active}
+ parentActive={this.props.active}
whenActiveChanged={this.props.whenActiveChanged}
addDocTab={this.addDocTab}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne}
pinToPres={this.props.pinToPres}
/>;
}
getDocWidth(d?: Doc) {
if (!d) return 0;
- const layoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
+ const layoutDoc = Doc.Layout(d, this.props.ChildLayoutTemplate?.());
const nw = NumCast(layoutDoc._nativeWidth);
return Math.min(nw && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
}
getDocHeight(d?: Doc) {
if (!d) return 0;
- const layoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
+ const layoutDoc = Doc.Layout(d, this.props.ChildLayoutTemplate?.());
const nw = NumCast(layoutDoc._nativeWidth);
const nh = NumCast(layoutDoc._nativeHeight);
let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
@@ -302,7 +312,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
this.refList.push(ref);
const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc;
this.observer = new _global.ResizeObserver(action((entries: any) => {
- if (this.props.Document._autoHeight && ref && this.refList.length && !SelectionManager.GetIsDragging()) {
+ if (this.props.Document._autoHeight && ref && this.refList.length && !DragManager.Vals.Instance.GetIsDragging()) {
Doc.Layout(doc)._height = Math.min(1200, Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", "")))));
}
}));
@@ -349,7 +359,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
this.refList.push(ref);
const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc;
this.observer = new _global.ResizeObserver(action((entries: any) => {
- if (this.props.Document._autoHeight && ref && this.refList.length && !SelectionManager.GetIsDragging()) {
+ if (this.props.Document._autoHeight && ref && this.refList.length && !DragManager.Vals.Instance.GetIsDragging()) {
Doc.Layout(doc)._height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0);
}
}));
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 323d7be25..1d16a5478 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -120,7 +120,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
pointerEntered = () => {
- if (SelectionManager.GetIsDragging()) {
+ if (DragManager.Vals.Instance.GetIsDragging()) {
this._background = "#b4b4b4";
}
}
@@ -322,11 +322,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
<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={{
- width: "100%",
- background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "inherit",
- color: "grey"
- }}>
+ style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "inherit" }}>
<EditableView {...headerEditableViewProps} />
{evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
<div className="collectionStackingView-sectionColor">
@@ -359,7 +355,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
<div className="collectionStackingViewFieldColumn" key={heading}
style={{
width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`,
- height: undefined, // SelectionManager.GetIsDragging() ? "100%" : undefined,
+ height: undefined, // DragManager.Vals.Instance.GetIsDragging() ? "100%" : undefined,
background: this._background
}}
ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}>
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 49abc6ee6..b4ca29b19 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,30 +1,30 @@
import { action, computed, IReactionDisposer, reaction } from "mobx";
+import { basename } from 'path';
import CursorField from "../../../new_fields/CursorField";
-import { Doc, DocListCast, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { Doc, Opt } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
import { ScriptField } from "../../../new_fields/ScriptField";
-import { Cast, StrCast } from "../../../new_fields/Types";
+import { Cast } from "../../../new_fields/Types";
+import { GestureUtils } from "../../../pen-gestures/GestureUtils";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { Upload } from "../../../server/SharedMediaTypes";
import { Utils } from "../../../Utils";
+import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
import { DocServer } from "../../DocServer";
-import { DocumentType } from "../../documents/DocumentTypes";
import { Docs, DocumentOptions } from "../../documents/Documents";
-import { DragManager, dropActionType } from "../../util/DragManager";
+import { DocumentType } from "../../documents/DocumentTypes";
+import { Networking } from "../../Network";
+import { DragManager } from "../../util/DragManager";
+import { ImageUtils } from "../../util/Import & Export/ImageUtils";
+import { InteractionUtils } from "../../util/InteractionUtils";
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { DocComponent } from "../DocComponent";
import { FieldViewProps } from "../nodes/FieldView";
-import { FormattedTextBox, GoogleRef } from "../nodes/FormattedTextBox";
+import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTextBox";
import { CollectionView } from "./CollectionView";
import React = require("react");
-import { basename } from 'path';
-import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
-import { ImageUtils } from "../../util/Import & Export/ImageUtils";
-import { Networking } from "../../Network";
-import { GestureUtils } from "../../../pen-gestures/GestureUtils";
-import { InteractionUtils } from "../../util/InteractionUtils";
-import { Upload } from "../../../server/SharedMediaTypes";
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc) => boolean;
@@ -43,6 +43,10 @@ export interface CollectionViewProps extends FieldViewProps {
export interface SubCollectionViewProps extends CollectionViewProps {
CollectionView: Opt<CollectionView>;
children?: never | (() => JSX.Element[]) | React.ReactNode;
+ ChildLayoutTemplate?: () => Doc;
+ ChildLayoutString?: string;
+ childClickScript?: ScriptField;
+ childDoubleClickScript?: ScriptField;
freezeChildDimensions?: boolean; // used by TimeView to coerce documents to treat their width height as their native width/height
overrideDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox)
ignoreFields?: string[]; // used in TreeView to ignore specified fields (see LinkBox)
@@ -56,7 +60,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
private dropDisposer?: DragManager.DragDropDisposer;
private gestureDisposer?: GestureUtils.GestureEventDisposer;
protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
- private _childLayoutDisposer?: IReactionDisposer;
protected _mainCont?: HTMLDivElement;
protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
this.dropDisposer?.();
@@ -64,7 +67,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
this.multiTouchDisposer?.();
if (ele) {
this._mainCont = ele;
- this.dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
+ this.dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc, this.onInternalPreDrop.bind(this));
this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this));
this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(ele, this.onTouchStart.bind(this));
}
@@ -73,25 +76,9 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
this.createDashEventsTarget(ele);
}
- componentDidMount() {
- this._childLayoutDisposer = reaction(() => ({ childDocs: this.childDocs, childLayout: Cast(this.props.Document.childLayout, Doc) }),
- ({ childDocs, childLayout }) => {
- if (childLayout instanceof Doc) {
- childDocs.map(doc => {
- doc.layout_fromParent = childLayout;
- doc.layoutKey = "layout_fromParent";
- });
- }
- else if (!(childLayout instanceof Promise)) {
- childDocs.filter(d => !d.isTemplateForField).map(doc => doc.layoutKey === "layout_fromParent" && (doc.layoutKey = "layout"));
- }
- }, { fireImmediately: true });
-
- }
componentWillUnmount() {
this.gestureDisposer?.();
this.multiTouchDisposer?.();
- this._childLayoutDisposer?.();
}
@computed get dataDoc() {
@@ -208,6 +195,15 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
protected onGesture(e: Event, ge: GestureUtils.GestureEvent) {
}
+ protected onInternalPreDrop(e: Event, de: DragManager.DropEvent) {
+ if (de.complete.docDragData) {
+ if (de.complete.docDragData.draggedDocuments.some(d => this.childDocs.includes(d))) {
+ de.complete.docDragData.dropAction = "move";
+ }
+ e.stopPropagation();
+ }
+ }
+
@undoBatch
@action
protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean {
@@ -332,23 +328,23 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
}));
return;
}
- let matches: RegExpExecArray | null;
- if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) {
- const newBox = Docs.Create.TextDocument("", { ...options, _width: 400, _height: 200, title: "Awaiting title from Google Docs..." });
- const proto = newBox.proto!;
- const documentId = matches[2];
- proto[GoogleRef] = documentId;
- proto.data = "Please select this document and then click on its pull button to load its contents from from Google Docs...";
- proto.backgroundColor = "#eeeeff";
- addDocument(newBox);
- return;
- }
- if ((matches = /(https:\/\/)?photos\.google\.com\/(u\/3\/)?album\/([^\\]+)/g.exec(text)) !== null) {
- const albumId = matches[3];
- const mediaItems = await GooglePhotos.Query.AlbumSearch(albumId);
- console.log(mediaItems);
- return;
- }
+ // let matches: RegExpExecArray | null;
+ // if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) {
+ // const newBox = Docs.Create.TextDocument("", { ...options, _width: 400, _height: 200, title: "Awaiting title from Google Docs..." });
+ // const proto = newBox.proto!;
+ // const documentId = matches[2];
+ // proto[GoogleRef] = documentId;
+ // proto.data = "Please select this document and then click on its pull button to load its contents from from Google Docs...";
+ // proto.backgroundColor = "#eeeeff";
+ // addDocument(newBox);
+ // return;
+ // }
+ // if ((matches = /(https:\/\/)?photos\.google\.com\/(u\/3\/)?album\/([^\\]+)/g.exec(text)) !== null) {
+ // const albumId = matches[3];
+ // const mediaItems = await GooglePhotos.Query.AlbumSearch(albumId);
+ // console.log(mediaItems);
+ // return;
+ // }
}
const { items } = e.dataTransfer;
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 045134225..a2d4774c8 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -28,7 +28,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
@observable _childClickedScript: Opt<ScriptField>;
@observable _viewDefDivClick: Opt<ScriptField>;
async componentDidMount() {
- const detailView = (await DocCastAsync(this.props.Document.childDetailView)) || Doc.findTemplate("detailView", StrCast(this.props.Document.type), "");
+ const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || Doc.findTemplate("detailView", StrCast(this.props.Document.type), "");
const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); ";
runInAction(() => {
this._childClickedScript = ScriptField.MakeScript(childText, { this: Doc.name, shiftKey: "boolean" }, { detailView: detailView! });
@@ -84,7 +84,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
@computed get contents() {
return <div className="collectionTimeView-innards" key="timeline" style={{ width: "100%", pointerEvents: this.props.active() ? undefined : "none" }} onPointerDown={this.contentsDown}>
- <CollectionFreeFormView {...this.props} childClickScript={this._childClickedScript} viewDefDivClick={this._viewDefDivClick} fitToBox={true} freezeChildDimensions={BoolCast(this.layoutDoc._freezeChildDimensions, true)} layoutEngine={this.layoutEngine} />
+ <CollectionFreeFormView {...this.props} childClickScript={this._childClickedScript} viewDefDivClick={this._viewDefDivClick} fitToBox={true} freezeChildDimensions={true} layoutEngine={this.layoutEngine} />
</div>;
}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index d938bd7ad..288fa8794 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -6,7 +6,6 @@ import { observer } from "mobx-react";
import { DataSym, Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
-import { RichTextField } from '../../../new_fields/RichTextField';
import { Document, listSpec } from '../../../new_fields/Schema';
import { ComputedField, ScriptField } from '../../../new_fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../new_fields/Types';
@@ -15,7 +14,6 @@ import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from "../../documents/DocumentTypes";
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager";
-import { makeTemplate } from '../../util/DropConverter';
import { Scripting } from '../../util/Scripting';
import { SelectionManager } from '../../util/SelectionManager';
import { Transform } from '../../util/Transform';
@@ -33,6 +31,7 @@ import { CollectionSubView } from "./CollectionSubView";
import "./CollectionTreeView.scss";
import { CollectionViewType } from './CollectionView';
import React = require("react");
+import { PrefetchProxy } from '../../../new_fields/Proxy';
export interface TreeViewProps {
@@ -148,7 +147,7 @@ class TreeView extends React.Component<TreeViewProps> {
onPointerEnter = (e: React.PointerEvent): void => {
this.props.active(true) && Doc.BrushDoc(this.dataDoc);
- if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
+ if (e.buttons === 1 && DragManager.Vals.Instance.GetIsDragging()) {
this._header!.current!.className = "treeViewItem-header";
document.addEventListener("pointermove", this.onDragMove, true);
}
@@ -353,27 +352,31 @@ class TreeView extends React.Component<TreeViewProps> {
return <div ref={this._dref} style={{ display: "inline-block", height: panelHeight() }} key={this.props.document[Id] + this.props.document.title}>
<ContentFittingDocumentView
Document={layoutDoc}
- DataDocument={this.templateDataDoc}
+ DataDoc={this.templateDataDoc}
LibraryPath={emptyPath}
renderDepth={this.props.renderDepth + 1}
rootSelected={returnTrue}
backgroundColor={this.props.backgroundColor}
fitToBox={this.boundsOfCollectionDocument !== undefined}
FreezeDimensions={true}
- NativeWidth={layoutDoc.type === DocumentType.RTF ? this.rtfWidth : undefined}
- NativeHeight={layoutDoc.type === DocumentType.RTF ? this.rtfHeight : undefined}
+ NativeWidth={layoutDoc.type === DocumentType.RTF ? this.rtfWidth : returnZero}
+ NativeHeight={layoutDoc.type === DocumentType.RTF ? this.rtfHeight : returnZero}
PanelWidth={panelWidth}
PanelHeight={panelHeight}
- getTransform={this.docTransform}
- CollectionDoc={this.props.containingCollection}
- CollectionView={undefined}
+ focus={returnFalse}
+ ScreenToLocalTransform={this.docTransform}
+ ContainingCollectionDoc={this.props.containingCollection}
+ ContainingCollectionView={undefined}
addDocument={returnFalse}
moveDocument={this.props.moveDocument}
removeDocument={returnFalse}
- active={this.props.active}
+ parentActive={this.props.active}
whenActiveChanged={emptyFunction}
addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres} />
+ pinToPres={this.props.pinToPres}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne}
+ />
</div>;
}
}
@@ -448,7 +451,7 @@ class TreeView extends React.Component<TreeViewProps> {
fontWeight: this.props.document.searchMatch ? "bold" : undefined,
textDecoration: Doc.GetT(this.props.document, "title", "string", true) ? "underline" : undefined,
outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined,
- pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? undefined : "none"
+ pointerEvents: this.props.active() || DragManager.Vals.Instance.GetIsDragging() ? undefined : "none"
}} >
{Doc.GetT(this.props.document, "editTitle", "boolean", true) ?
this.editableView("title") :
@@ -477,7 +480,7 @@ class TreeView extends React.Component<TreeViewProps> {
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- dontRegisterView={BoolCast(this.props.treeViewId.dontRegisterChildren)}
+ dontRegisterView={BoolCast(this.props.treeViewId.dontRegisterChildViews)}
ContainingCollectionView={undefined}
ContainingCollectionDoc={this.props.containingCollection}
/>}
@@ -721,14 +724,6 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
}
ContextMenu.Instance.addItem({
description: "Buxton Layout", icon: "eye", event: () => {
- DocListCast(this.dataDoc[this.props.fieldKey]).map(d => {
- DocListCast(d.data).map((img, i) => {
- const caption = (d.captions as any)[i];
- if (caption) {
- Doc.GetProto(img).caption = caption;
- }
- });
- });
const { ImageDocument } = Docs.Create;
const { Document } = this.props;
const fallbackImg = "http://www.cs.brown.edu/~bcz/face.gif";
@@ -738,22 +733,20 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
heroView._showTitle = "title";
heroView._showTitleHover = "titlehover";
- Doc.AddDocToList(Doc.UserDoc().dockedBtns as Doc, "data",
- Docs.Create.FontIconDocument({
- title: "hero view", _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias",
- dragFactory: heroView, removeDropProperties: new List<string>(["dropAction"]), icon: "portrait",
- onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
- }));
-
- Doc.AddDocToList(Doc.UserDoc().dockedBtns as Doc, "data",
- Docs.Create.FontIconDocument({
- title: "detail view", _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias",
- dragFactory: detailView, removeDropProperties: new List<string>(["dropAction"]), icon: "file-alt",
- onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
- }));
-
- Document.childLayout = heroView;
- Document.childDetailView = detailView;
+ const doubleClickView = ImageDocument("http://cs.brown.edu/~bcz/face.gif", { _width: 400 }); // replace with desired double click target
+ DocListCast(this.dataDoc[this.props.fieldKey]).map(d => {
+ DocListCast(d.data).map((img, i) => {
+ const caption = (d.captions as any)[i];
+ if (caption) {
+ Doc.GetProto(img).caption = caption;
+ Doc.GetProto(img).doubleClickView = doubleClickView;
+ }
+ });
+ Doc.GetProto(d).proto = heroView; // all devices "are" heroViews that share the same layout & defaults. Seems better than making them all be independent and copy a layout string // .layout = ImageBox.LayoutString("hero");
+ });
+
+ Document.childLayoutTemplate = heroView;
+ Document.childClickedOpenTemplateView = new PrefetchProxy(detailView);
Document._viewType = CollectionViewType.Time;
Document._forceActive = true;
Document._pivotField = "company";
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index e8edf7279..c22ebbcbd 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -73,6 +73,11 @@ export enum CollectionViewType {
Grid = "grid",
Pile = "pileup"
}
+export interface CollectionViewCustomProps {
+ filterAddDocument: (doc: Doc) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example)
+ childLayoutTemplate?: () => Opt<Doc>; // specify a layout Doc template to use for children of the collection
+ childLayoutString?: string; // specify a layout string to use for children of the collection
+}
export interface CollectionRenderProps {
addDocument: (document: Doc) => boolean;
@@ -81,10 +86,12 @@ export interface CollectionRenderProps {
active: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
PanelWidth: () => number;
+ ChildLayoutTemplate?: () => Doc;
+ ChildLayoutString?: string;
}
@observer
-export class CollectionView extends Touchable<FieldViewProps> {
+export class CollectionView extends Touchable<FieldViewProps & CollectionViewCustomProps> {
public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); }
private _isChildActive = false; //TODO should this be observable?
@@ -115,12 +122,16 @@ export class CollectionView extends Touchable<FieldViewProps> {
@action.bound
addDocument(doc: Doc): boolean {
+ if (this.props.filterAddDocument?.(doc) === false) {
+ return false;
+ }
+
const targetDataDoc = this.props.Document[DataSym];
const docList = DocListCast(targetDataDoc[this.props.fieldKey]);
!docList.includes(doc) && (targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, doc])); // DocAddToList may write to targetdataDoc's parent ... we don't want this. should really change GetProto to GetDataDoc and test for resolvedDataDoc there
// Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc);
- doc.context = this.props.Document;
targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));
+ doc.context = this.props.Document;
Doc.GetProto(doc).lastOpened = new DateField;
return true;
}
@@ -193,8 +204,8 @@ export class CollectionView extends Touchable<FieldViewProps> {
private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
// currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip
const chrome = this.props.Document._chromeStatus === "disabled" || this.props.Document._chromeStatus === "replaced" || type === CollectionViewType.Docking ? (null) :
- <CollectionViewBaseChrome CollectionView={this} key="chrome" PanelWidth={this.bodyPanelWidth} type={type} collapse={this.collapse} />;
- return [chrome, this.SubViewHelper(type, renderProps)];
+ <CollectionViewBaseChrome key="chrome" CollectionView={this} PanelWidth={this.bodyPanelWidth} type={type} collapse={this.collapse} />;
+ return <>{chrome} {this.SubViewHelper(type, renderProps)}</>;
}
@@ -272,8 +283,8 @@ export class CollectionView extends Touchable<FieldViewProps> {
if (this.props.Document.childLayout instanceof Doc) {
layoutItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, "onRight"), icon: "project-diagram" });
}
- if (this.props.Document.childDetailView instanceof Doc) {
- layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childDetailView as Doc, "onRight"), icon: "project-diagram" });
+ if (this.props.Document.childClickedOpenTemplateView instanceof Doc) {
+ layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childClickedOpenTemplateView as Doc, "onRight"), icon: "project-diagram" });
}
layoutItems.push({ description: `${this.props.Document.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.props.Document.isInPlaceContainer = !this.props.Document.isInPlaceContainer, icon: "project-diagram" });
@@ -281,15 +292,20 @@ export class CollectionView extends Touchable<FieldViewProps> {
const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
const onClicks = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
- const funcs = [{ key: "onChildClick", name: "On Child Clicked", script: undefined as any as ScriptField }];
- DocListCast(Cast(Doc.UserDoc().childClickFuncs, Doc, null).data).forEach(childClick =>
- funcs.push({ key: "onChildClick", name: StrCast(childClick.title), script: ScriptCast(childClick.script) }));
+ const funcs = [
+ { key: "onChildClick", name: "On Child Clicked" },
+ { key: "onChildDoubleClick", name: "On Child Double Clicked" }];
funcs.map(func => onClicks.push({
description: `Edit ${func.name} script`, icon: "edit", event: (obj: any) => {
- func.script && (this.props.Document[func.key] = ObjectField.MakeCopy(func.script));
ScriptBox.EditButtonScript(func.name + "...", this.props.Document, func.key, obj.x, obj.y, { thisContainer: Doc.name });
}
}));
+ DocListCast(Cast(Doc.UserDoc()["clickFuncs-child"], Doc, null).data).forEach(childClick =>
+ onClicks.push({
+ description: `Set child ${childClick.title}`,
+ icon: "edit",
+ event: () => this.props.Document[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data)),
+ }));
!existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
const more = ContextMenu.Instance.findByDescription("More...");
@@ -496,6 +512,8 @@ export class CollectionView extends Touchable<FieldViewProps> {
</div>
</div>;
}
+ childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.props.Document.childLayoutTemplate, Doc, null);
+ childLayoutString = this.props.childLayoutString || StrCast(this.props.Document.childLayoutString);
render() {
TraceMobx();
@@ -505,7 +523,9 @@ export class CollectionView extends Touchable<FieldViewProps> {
moveDocument: this.moveDocument,
active: this.active,
whenActiveChanged: this.whenActiveChanged,
- PanelWidth: this.bodyPanelWidth
+ PanelWidth: this.bodyPanelWidth,
+ ChildLayoutTemplate: this.childLayoutTemplate,
+ ChildLayoutString: this.childLayoutString,
};
return (<div className={"collectionView"}
style={{
@@ -515,7 +535,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
}}
onContextMenu={this.onContextMenu}>
{this.showIsTagged()}
- <div style={{ width: `calc(100% - ${this.facetWidth()}px)` }}>
+ <div className="collectionView-facetCont" style={{ width: `calc(100% - ${this.facetWidth()}px)` }}>
{this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)}
</div>
{this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => d.type === DocumentType.IMG).map(d =>
diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss
index 5203eb55f..e4581eb46 100644
--- a/src/client/views/collections/CollectionViewChromes.scss
+++ b/src/client/views/collections/CollectionViewChromes.scss
@@ -33,6 +33,17 @@
outline-color: black;
}
+ .collectionViewBaseChrome-button{
+ font-size: 75%;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ background: rgb(238, 238, 238);
+ color: purple;
+ outline-color: black;
+ border: none;
+ padding: 12px 10px 11px 10px;
+ margin-left: 10px;
+ }
.collectionViewBaseChrome-cmdPicker {
margin-left: 3px;
margin-right: 0px;
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index d26e3a38b..62b03bbdc 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -44,9 +44,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
initialize: emptyFunction,
};
_narrativeCommand = {
- params: ["target", "source"], title: "=> click item view",
- script: "this.target.childDetailView = getDocTemplate(this.source?.[0])",
- immediate: (source: Doc[]) => this.target.childDetailView = Doc.getDocTemplate(source?.[0]),
+ params: ["target", "source"], title: "=> click clicked open view",
+ script: "this.target.childClickedOpenTemplateView = getDocTemplate(this.source?.[0])",
+ immediate: (source: Doc[]) => this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]),
initialize: emptyFunction,
};
_contentCommand = {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index cf12ef382..d67d1993e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -40,7 +40,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
const bpt = Utils.closestPtBetweenRectangles(bbounds.left, bbounds.top, bbounds.width, bbounds.height,
abounds.left, abounds.top, abounds.width, abounds.height,
apt.point.x, apt.point.y);
- const afield = StrCast(this.props.A.props.Document[StrCast(this.props.A.props.layoutKey, "layout")]).indexOf("anchor1") === -1 ? "anchor2" : "anchor1";
+ const afield = this.props.A.props.LayoutTemplateString?.indexOf("anchor1") === -1 ? "anchor2" : "anchor1";
const bfield = afield === "anchor1" ? "anchor2" : "anchor1";
// really hacky stuff to make the LinkAnchorBox display where we want it to:
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 4b5e977df..0873fd1bb 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -10,6 +10,7 @@ import React = require("react");
import { Utils, emptyFunction } from "../../../../Utils";
import { SelectionManager } from "../../../util/SelectionManager";
import { DocumentType } from "../../../documents/DocumentTypes";
+import { DragManager } from "../../../util/DragManager";
@observer
export class CollectionFreeFormLinksView extends React.Component {
@@ -30,13 +31,13 @@ export class CollectionFreeFormLinksView extends React.Component {
return drawnPairs;
}, [] as { a: DocumentView, b: DocumentView, l: Doc[] }[]);
return connections.filter(c =>
- c.a.props.layoutKey && c.b.props.layoutKey && c.a.props.Document.type === DocumentType.LINK &&
+ c.a.props.Document.type === DocumentType.LINK &&
c.a.props.bringToFront !== emptyFunction && c.b.props.bringToFront !== emptyFunction // bcz: this prevents links to be drawn to anchors in CollectionTree views -- this is a hack that should be fixed
).map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />);
}
render() {
- return SelectionManager.GetIsDragging() ? (null) : <div className="collectionfreeformlinksview-container">
+ return DragManager.Vals.Instance.GetIsDragging() ? (null) : <div className="collectionfreeformlinksview-container">
<svg className="collectionfreeformlinksview-svgCanvas">
{this.uniqueConnections}
</svg>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 28b461313..2aa9b1f5f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,13 +1,13 @@
import { library } from "@fortawesome/fontawesome-svg-core";
-import { faEye } from "@fortawesome/free-regular-svg-icons";
+import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons";
import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons";
import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { computedFn } from "mobx-utils";
import { Doc, HeightSym, Opt, WidthSym, DocListCast } from "../../../../new_fields/Doc";
-import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas";
+import { documentSchema, collectionSchema } from "../../../../new_fields/documentSchemas";
import { Id } from "../../../../new_fields/FieldSymbols";
-import { InkData, InkField, InkTool } from "../../../../new_fields/InkField";
+import { InkData, InkField, InkTool, PointData } from "../../../../new_fields/InkField";
import { List } from "../../../../new_fields/List";
import { RichTextField } from "../../../../new_fields/RichTextField";
import { createSchema, listSpec, makeInterface } from "../../../../new_fields/Schema";
@@ -32,7 +32,7 @@ import { ContextMenuProps } from "../../ContextMenuItem";
import { InkingControl } from "../../InkingControl";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
import { DocumentViewProps, DocumentView } from "../../nodes/DocumentView";
-import { FormattedTextBox } from "../../nodes/FormattedTextBox";
+import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox";
import { pageSchema } from "../../nodes/ImageBox";
import PDFMenu from "../../pdf/PDFMenu";
import { CollectionDockingView } from "../CollectionDockingView";
@@ -44,6 +44,7 @@ import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
import { CollectionViewType } from "../CollectionView";
+import { Timeline } from "../../animationtimeline/Timeline";
library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
@@ -65,11 +66,10 @@ export const panZoomSchema = createSchema({
fitH: "number"
});
-type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof documentSchema, typeof positionSchema, typeof pageSchema]>;
-const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSchema, pageSchema);
+type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof collectionSchema, typeof documentSchema, typeof pageSchema]>;
+const PanZoomDocument = makeInterface(panZoomSchema, collectionSchema, documentSchema, pageSchema);
export type collectionFreeformViewProps = {
forceScaling?: boolean; // whether to force scaling of content (needed by ImageBox)
- childClickScript?: ScriptField;
viewDefDivClick?: ScriptField;
};
@@ -94,6 +94,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title?.toString() + ")"; } // this makes mobx trace() statements more descriptive
@observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables
@observable _clusterSets: (Doc[])[] = [];
+ @observable _timelineRef = React.createRef<Timeline>();
@computed get fitToContentScaling() { return this.fitToContent ? NumCast(this.layoutDoc.fitToContentScaling, 1) : 1; }
@computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; }
@@ -853,9 +854,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
@computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; }
@computed get onChildClickHandler() { return this.props.childClickScript || ScriptCast(this.Document.onChildClick); }
- backgroundHalo = () => BoolCast(this.Document.useClusters);
+ @computed get onChildDoubleClickHandler() { return this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); }
@computed get backgroundActive() { return this.layoutDoc.isBackground && (this.props.ContainingCollectionView?.active() || this.props.active()); }
- parentActive = () => this.props.active() || this.backgroundActive ? true : false;
+ backgroundHalo = () => BoolCast(this.Document.useClusters);
+ parentActive = (outsideReaction: boolean) => this.props.active(outsideReaction) || this.backgroundActive ? true : false;
getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps {
return {
...this.props,
@@ -865,12 +867,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
DataDoc: childData,
Document: childLayout,
LibraryPath: this.libraryPath,
+ LayoutTemplate: this.props.ChildLayoutTemplate,
+ LayoutTemplateString: this.props.ChildLayoutString,
FreezeDimensions: this.props.freezeChildDimensions,
layoutKey: undefined,
+ setupDragLines: this.setupDragLines,
rootSelected: childData ? this.rootSelected : returnFalse,
dropAction: StrCast(this.props.Document.childDropAction) as dropActionType,
- //onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them
onClick: this.onChildClickHandler,
+ onDoubleClick: this.onChildDoubleClickHandler,
ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform,
renderDepth: this.props.renderDepth + 1,
PanelWidth: childLayout[WidthSym],
@@ -994,7 +999,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
return { newPool, computedElementData: this.doFreeformLayout(newPool) };
}
- childLayoutDocFunc = () => this.props.childLayoutTemplate?.() || Cast(this.props.Document.childLayoutTemplate, Doc, null);
get doLayoutComputation() {
const { newPool, computedElementData } = this.doInternalLayoutComputation;
runInAction(() =>
@@ -1020,7 +1024,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
replica={entry[1].replica}
dataProvider={this.childDataProvider}
sizeProvider={this.childSizeProvider}
- LayoutDoc={this.childLayoutDocFunc}
pointerEvents={
this.backgroundActive ?
true :
@@ -1037,7 +1040,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}
componentDidMount() {
- super.componentDidMount();
+ super.componentDidMount?.();
this._layoutComputeReaction = reaction(() => this.doLayoutComputation,
(elements) => this._layoutElements = elements || [],
{ fireImmediately: true, name: "doLayout" });
@@ -1126,10 +1129,60 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
input.click();
}
});
-
ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" });
+
+
+ ContextMenu.Instance.addItem({
+ description: (this._timelineVisible ? "Close" : "Open") + " Animation Timeline", event: action(() => {
+ this._timelineVisible = !this._timelineVisible;
+ }), icon: this._timelineVisible ? faEyeSlash : faEye
+ });
+ }
+ @observable _timelineVisible = false;
+
+ intersectRect(r1: { left: number, top: number, width: number, height: number },
+ r2: { left: number, top: number, width: number, height: number }) {
+ return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top);
}
+ @action
+ setupDragLines = () => {
+ const size = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth(), this.props.PanelHeight());
+ const selRect = { left: this.panX() - size[0] / 2, top: this.panY() - size[1] / 2, width: size[0], height: size[1] };
+ const docDims = (doc: Doc) => ({ left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) });
+ const isDocInView = (doc: Doc, rect: { left: number, top: number, width: number, height: number }) => {
+ if (this.intersectRect(docDims(doc), rect)) {
+ snappableDocs.push(doc);
+ }
+ };
+ const snappableDocs: Doc[] = []; // the set of documents in the visible viewport that we will try to snap to;
+ const otherBounds = { left: this.panX(), top: this.panY(), width: Math.abs(size[0]), height: Math.abs(size[1]) };
+ this.getActiveDocuments().filter(doc => !doc.isBackground && doc.z === undefined).map(doc => isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to
+ !snappableDocs.length && this.getActiveDocuments().filter(doc => doc.z === undefined).map(doc => isDocInView(doc, selRect)); // if not, see if there are background docs to snap to
+ !snappableDocs.length && this.getActiveDocuments().filter(doc => doc.z !== undefined).map(doc => isDocInView(doc, otherBounds)); // if not, then why not snap to floating docs
+
+ const horizLines: number[] = [];
+ const vertLines: number[] = [];
+ snappableDocs.filter(doc => !DragManager.docsBeingDragged.includes(Cast(doc.rootDocument, Doc, null) || doc)).forEach(doc => {
+ const { left, top, width, height } = docDims(doc);
+ const topLeftInScreen = this.getTransform().inverse().transformPoint(left, top);
+ const docSize = this.getTransform().inverse().transformDirection(width, height);
+
+ horizLines.push(topLeftInScreen[1], topLeftInScreen[1] + docSize[1] / 2, topLeftInScreen[1] + docSize[1]); // horiz center line
+ vertLines.push(topLeftInScreen[0], topLeftInScreen[0] + docSize[0] / 2, topLeftInScreen[0] + docSize[0]);// right line
+ });
+ DragManager.SetSnapLines(horizLines, vertLines);
+ }
+ onPointerOver = (e: React.PointerEvent) => {
+ if (DragManager.Vals.Instance.GetIsDragging()) {
+ this.setupDragLines();
+ }
+ e.stopPropagation();
+ }
+
+ @observable private _hLines: number[] | undefined;
+ @observable private _vLines: number[] | undefined;
+
private childViews = () => {
const children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : [];
return [
@@ -1168,6 +1221,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
easing={this.easing} viewDefDivClick={this.props.viewDefDivClick} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
{this.children}
</CollectionFreeFormViewPannableContents>
+ {this._timelineVisible ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)}
</MarqueeView>;
}
@@ -1179,7 +1233,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
const wscale = nw ? this.props.PanelWidth() / nw : 1;
return wscale < hscale ? wscale : hscale;
}
- @computed get backgroundEvents() { return this.layoutDoc.isBackground && SelectionManager.GetIsDragging(); }
+ @computed get backgroundEvents() { return this.layoutDoc.isBackground && DragManager.Vals.Instance.GetIsDragging(); }
render() {
TraceMobx();
const clientRect = this._mainCont?.getBoundingClientRect();
@@ -1192,7 +1246,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
// otherwise, they are stored in fieldKey. All annotations to this document are stored in the extension document
return <div className={"collectionfreeformview-container"}
ref={this.createDashEventsTarget}
- onWheel={this.onPointerWheel} onClick={this.onClick} //pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
+ onPointerOver={this.onPointerOver}
+ onWheel={this.onPointerWheel} onClick={this.onClick} //pointerEvents: DragManager.Vals.Instance.GetIsDragging() ? "all" : undefined,
onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onExternalDrop.bind(this)} onContextMenu={this.onContextMenu}
style={{
pointerEvents: this.backgroundEvents ? "all" : undefined,
@@ -1216,7 +1271,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}}>
</div>
-
+ {// uncomment to show snap lines
+ <div className="snapLines" style={{ position: "absolute", top: 0, left: 0, width: "100%", height: "100%", pointerEvents: "none" }}>
+ <svg style={{ width: "100%", height: "100%" }}>
+ {this._hLines?.map(l => <line x1="0" y1={l} x2="1000" y2={l} stroke="black" />)}
+ {this._vLines?.map(l => <line y1="0" x1={l} y2="1000" x2={l} stroke="black" />)}
+ </svg>
+ </div>}
</div >;
}
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index cd8166309..5bac075b4 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -19,7 +19,7 @@ import React = require("react");
import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
import { RichTextField } from "../../../../new_fields/RichTextField";
import { CollectionView } from "../CollectionView";
-import { FormattedTextBox } from "../../nodes/FormattedTextBox";
+import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox";
import { ScriptField } from "../../../../new_fields/ScriptField";
interface MarqueeViewProps {
@@ -113,7 +113,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
if (template instanceof Doc) {
tbox._width = NumCast(template._width);
tbox.layoutKey = "layout_" + StrCast(template.title);
- tbox[StrCast(tbox.layoutKey)] = template;
+ Doc.GetProto(tbox)[StrCast(tbox.layoutKey)] = template;
}
this.props.addLiveTextDocument(tbox);
}
@@ -613,7 +613,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
render() {
return <div className="marqueeView"
- style={{ overflow: StrCast(this.props.Document.overflow), }}
+ style={{ overflow: StrCast(this.props.Document._overflow), }}
onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} onClick={this.onClick} onPointerDown={this.onPointerDown}>
{this._visible ? this.marqueeDiv : null}
{this.props.children}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 9d09ecc3b..38b16e184 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -14,7 +14,7 @@ import "./collectionMulticolumnView.scss";
import ResizeBar from './MulticolumnResizer';
import WidthLabel from './MulticolumnWidthLabel';
import { List } from '../../../../new_fields/List';
-import { returnZero } from '../../../../Utils';
+import { returnZero, returnFalse, returnOne } from '../../../../Utils';
type MulticolumnDocument = makeInterface<[typeof documentSchema]>;
const MulticolumnDocument = makeInterface(documentSchema);
@@ -203,6 +203,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
@computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
+ @computed get onChildDoubleClickHandler() { return ScriptCast(this.Document.onChildDoubleClick); }
addDocTab = (doc: Doc, where: string) => {
@@ -215,9 +216,10 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) {
return <ContentFittingDocumentView
Document={layout}
- DataDocument={layout.resolvedDataDoc as Doc}
+ DataDoc={layout.resolvedDataDoc as Doc}
backgroundColor={this.props.backgroundColor}
- LayoutDoc={this.props.childLayoutTemplate}
+ LayoutTemplate={this.props.ChildLayoutTemplate}
+ LayoutTemplateString={this.props.ChildLayoutString}
LibraryPath={this.props.LibraryPath}
FreezeDimensions={this.props.freezeChildDimensions}
renderDepth={this.props.renderDepth + 1}
@@ -225,21 +227,24 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
PanelHeight={height}
NativeHeight={returnZero}
NativeWidth={returnZero}
- fitToBox={BoolCast(this.props.Document._freezeChildDimensions)}
+ fitToBox={false}
rootSelected={this.rootSelected}
dropAction={StrCast(this.props.Document.childDropAction) as dropActionType}
onClick={this.onChildClickHandler}
- getTransform={dxf}
+ onDoubleClick={this.onChildDoubleClickHandler}
+ ScreenToLocalTransform={dxf}
focus={this.props.focus}
- CollectionDoc={this.props.CollectionView?.props.Document}
- CollectionView={this.props.CollectionView}
+ ContainingCollectionDoc={this.props.CollectionView?.props.Document}
+ ContainingCollectionView={this.props.CollectionView}
addDocument={this.props.addDocument}
moveDocument={this.props.moveDocument}
removeDocument={this.props.removeDocument}
- active={this.props.active}
+ parentActive={this.props.active}
whenActiveChanged={this.props.whenActiveChanged}
addDocTab={this.addDocTab}
pinToPres={this.props.pinToPres}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne}
/>;
}
/**
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index af0cc3b5c..b55b67a9e 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -6,7 +6,7 @@ import * as React from "react";
import { Doc } from '../../../../new_fields/Doc';
import { NumCast, StrCast, BoolCast, ScriptCast } from '../../../../new_fields/Types';
import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView';
-import { Utils, returnZero } from '../../../../Utils';
+import { Utils, returnZero, returnFalse, returnOne } from '../../../../Utils';
import "./collectionMultirowView.scss";
import { computed, trace, observable, action } from 'mobx';
import { Transform } from '../../../util/Transform';
@@ -203,7 +203,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
@computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
-
+ @computed get onChildDoubleClickHandler() { return ScriptCast(this.Document.onChildDoubleClick); }
addDocTab = (doc: Doc, where: string) => {
if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) {
@@ -215,9 +215,10 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) {
return <ContentFittingDocumentView
Document={layout}
- DataDocument={layout.resolvedDataDoc as Doc}
+ DataDoc={layout.resolvedDataDoc as Doc}
backgroundColor={this.props.backgroundColor}
- LayoutDoc={this.props.childLayoutTemplate}
+ LayoutTemplate={this.props.ChildLayoutTemplate}
+ LayoutTemplateString={this.props.ChildLayoutString}
LibraryPath={this.props.LibraryPath}
FreezeDimensions={this.props.freezeChildDimensions}
renderDepth={this.props.renderDepth + 1}
@@ -225,21 +226,24 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
PanelHeight={height}
NativeHeight={returnZero}
NativeWidth={returnZero}
- fitToBox={BoolCast(this.props.Document._freezeChildDimensions)}
+ fitToBox={false}
rootSelected={this.rootSelected}
dropAction={StrCast(this.props.Document.childDropAction) as dropActionType}
onClick={this.onChildClickHandler}
- getTransform={dxf}
+ onDoubleClick={this.onChildDoubleClickHandler}
+ ScreenToLocalTransform={dxf}
focus={this.props.focus}
- CollectionDoc={this.props.CollectionView?.props.Document}
- CollectionView={this.props.CollectionView}
+ ContainingCollectionDoc={this.props.CollectionView?.props.Document}
+ ContainingCollectionView={this.props.CollectionView}
addDocument={this.props.addDocument}
moveDocument={this.props.moveDocument}
removeDocument={this.props.removeDocument}
- active={this.props.active}
+ parentActive={this.props.active}
whenActiveChanged={this.props.whenActiveChanged}
addDocTab={this.addDocTab}
pinToPres={this.props.pinToPres}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne}
/>;
}
/**
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index 928413a11..c97e1062d 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -9,7 +9,7 @@ import { DragManager, SetupDrag } from "../../util/DragManager";
import { LinkManager } from "../../util/LinkManager";
import { DocumentView } from "../nodes/DocumentView";
import './LinkMenu.scss';
-import { LinkMenuItem } from "./LinkMenuItem";
+import { LinkMenuItem, StartLinkTargetsDrag } from "./LinkMenuItem";
import React = require("react");
interface LinkMenuGroupProps {
@@ -47,7 +47,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
document.removeEventListener("pointerup", this.onLinkButtonUp);
const targets = this.props.group.map(l => LinkManager.Instance.getOppositeAnchor(l, this.props.sourceDoc)).filter(d => d) as Doc[];
- DragManager.StartLinkTargetsDrag(this._drag.current, this.props.docView, e.x, e.y, this.props.sourceDoc, targets);
+ StartLinkTargetsDrag(this._drag.current, this.props.docView, e.x, e.y, this.props.sourceDoc, targets);
}
e.stopPropagation();
}
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index d091e06ef..d4e58fa8e 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -3,7 +3,7 @@ import { faArrowRight, faChevronDown, faChevronUp, faEdit, faEye, faTimes } from
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, observable } from 'mobx';
import { observer } from "mobx-react";
-import { Doc } from '../../../new_fields/Doc';
+import { Doc, DocListCast } from '../../../new_fields/Doc';
import { Cast, StrCast } from '../../../new_fields/Types';
import { DragManager } from '../../util/DragManager';
import { LinkManager } from '../../util/LinkManager';
@@ -26,6 +26,41 @@ interface LinkMenuItemProps {
addDocTab: (document: Doc, where: string) => boolean;
}
+// drag links and drop link targets (aliasing them if needed)
+export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: DocumentView, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) {
+ const draggedDocs = (specificLinks ? specificLinks : DocListCast(sourceDoc.links)).map(link => LinkManager.Instance.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[];
+
+ if (draggedDocs.length) {
+ const moddrag: Doc[] = [];
+ for (const draggedDoc of draggedDocs) {
+ const doc = await Cast(draggedDoc.annotationOn, Doc);
+ if (doc) moddrag.push(doc);
+ }
+
+ const dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs);
+ dragData.moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (doc: Doc) => boolean): boolean => {
+ docView.props.removeDocument?.(doc);
+ addDocument(doc);
+ return true;
+ };
+ const containingView = docView.props.ContainingCollectionView;
+ const finishDrag = (e: DragManager.DragCompleteEvent) =>
+ e.docDragData && (e.docDragData.droppedDocuments =
+ dragData.draggedDocuments.reduce((droppedDocs, d) => {
+ const dvs = DocumentManager.Instance.getDocumentViews(d).filter(dv => dv.props.ContainingCollectionView === containingView);
+ if (dvs.length) {
+ dvs.forEach(dv => droppedDocs.push(dv.props.Document));
+ } else {
+ droppedDocs.push(Doc.MakeAlias(d));
+ }
+ return droppedDocs;
+ }, [] as Doc[]));
+
+ DragManager.StartDrag([dragEle], dragData, downX, downY, undefined, finishDrag);
+ }
+}
+
+
@observer
export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
private _drag = React.createRef<HTMLDivElement>();
@@ -83,7 +118,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
document.removeEventListener("pointerup", this.onLinkButtonUp);
this._eleClone.style.transform = `translate(${e.x}px, ${e.y}px)`;
- DragManager.StartLinkTargetsDrag(this._eleClone, this.props.docView, e.x, e.y, this.props.sourceDoc, [this.props.linkDoc]);
+ StartLinkTargetsDrag(this._eleClone, this.props.docView, e.x, e.y, this.props.sourceDoc, [this.props.linkDoc]);
}
e.stopPropagation();
}
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 6ff6d1b42..1c5e13620 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -17,10 +17,10 @@ import { ContextMenu } from "../ContextMenu";
import { Id } from "../../../new_fields/FieldSymbols";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DocumentView } from "./DocumentView";
-import { Docs } from "../../documents/Documents";
+import { Docs, DocUtils } from "../../documents/Documents";
import { ComputedField } from "../../../new_fields/ScriptField";
import { Networking } from "../../Network";
-import { Upload } from "../../../server/SharedMediaTypes";
+import { LinkAnchorBox } from "./LinkAnchorBox";
// testing testing
@@ -56,7 +56,6 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
@computed get audioState(): undefined | "recording" | "paused" | "playing" { return this.dataDoc.audioState as (undefined | "recording" | "paused" | "playing"); }
set audioState(value) { this.dataDoc.audioState = value; }
public static SetScrubTime = (timeInMillisFrom1970: number) => { runInAction(() => AudioBox._scrubTime = 0); runInAction(() => AudioBox._scrubTime = timeInMillisFrom1970); };
- public static ActiveRecordings: Doc[] = [];
@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); }
@@ -144,7 +143,7 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
this._stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this._recorder = new MediaRecorder(this._stream);
this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date());
- AudioBox.ActiveRecordings.push(this.props.Document);
+ DocUtils.ActiveRecordings.push(this.props.Document);
this._recorder.ondataavailable = async (e: any) => {
const [{ result }] = await Networking.UploadFilesToServer(e.data);
if (!(result instanceof Error)) {
@@ -171,8 +170,8 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
this.dataDoc.duration = (new Date().getTime() - this._recordStart) / 1000;
this.audioState = "paused";
this._stream?.getAudioTracks()[0].stop();
- const ind = AudioBox.ActiveRecordings.indexOf(this.props.Document);
- ind !== -1 && (AudioBox.ActiveRecordings.splice(ind, 1));
+ const ind = DocUtils.ActiveRecordings.indexOf(this.props.Document);
+ ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
});
recordClick = (e: React.MouseEvent) => {
@@ -266,7 +265,8 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
NativeHeight={returnZero}
NativeWidth={returnZero}
rootSelected={returnFalse}
- layoutKey={Doc.LinkEndpoint(l, la2)}
+ LayoutTemplate={undefined}
+ LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(l, la2)}`)}
ContainingCollectionDoc={this.props.Document}
parentActive={returnTrue}
bringToFront={emptyFunction}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 1c7d116c5..cdbe506a5 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -7,20 +7,15 @@ import { DocComponent } from "../DocComponent";
import "./CollectionFreeFormDocumentView.scss";
import { DocumentView, DocumentViewProps } from "./DocumentView";
import React = require("react");
-import { PositionDocument } from "../../../new_fields/documentSchemas";
+import { Document } from "../../../new_fields/documentSchemas";
import { TraceMobx } from "../../../new_fields/util";
import { ContentFittingDocumentView } from "./ContentFittingDocumentView";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
dataProvider?: (doc: Doc, replica: string) => { x: number, y: number, zIndex?: number, highlight?: boolean, z: number, transition?: string } | undefined;
sizeProvider?: (doc: Doc, replica: string) => { width: number, height: number } | undefined;
- x?: number;
- y?: number;
- z?: number;
zIndex?: number;
highlight?: boolean;
- width?: number;
- height?: number;
jitterRotation: number;
transition?: string;
fitToBox?: boolean;
@@ -28,7 +23,7 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
}
@observer
-export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) {
+export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, Document>(Document) {
@observable _animPos: number[] | undefined = undefined;
random(min: number, max: number) { // min should not be equal to max
const mseed = Math.abs(this.X * this.Y);
@@ -38,13 +33,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}
get displayName() { return "CollectionFreeFormDocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive
get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${this.random(-1, 1) * this.props.jitterRotation}deg)`; }
- get X() { return this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); }
- get Y() { return this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); }
+ get X() { return this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); }
+ get Y() { return this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); }
get ZInd() { return this.dataProvider ? this.dataProvider.zIndex : (this.Document.zIndex || 0); }
get Highlight() { return this.dataProvider?.highlight; }
- get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.sizeProvider && this.sizeProvider ? this.sizeProvider.width : this.layoutDoc[WidthSym](); }
+ get width() { return this.props.sizeProvider && this.sizeProvider ? this.sizeProvider.width : this.layoutDoc[WidthSym](); }
get height() {
- const hgt = this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.sizeProvider && this.sizeProvider ? this.sizeProvider.height : this.layoutDoc[HeightSym]();
+ const hgt = this.props.sizeProvider && this.sizeProvider ? this.sizeProvider.height : this.layoutDoc[HeightSym]();
return (hgt === undefined && this.nativeWidth && this.nativeHeight) ? this.width * this.nativeHeight / this.nativeWidth : hgt;
}
@computed get freezeDimensions() { return this.props.FreezeDimensions; }
@@ -75,10 +70,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
contentScaling = () => this.nativeWidth > 0 && !this.props.fitToBox && !this.freezeDimensions ? this.width / this.nativeWidth : 1;
panelWidth = () => (this.sizeProvider?.width || this.props.PanelWidth?.());
panelHeight = () => (this.sizeProvider?.height || this.props.PanelHeight?.());
- getTransform = (): Transform => this.props.ScreenToLocalTransform()
- .translate(-this.X, -this.Y)
- .scale(1 / this.contentScaling())
-
+ getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.X, -this.Y).scale(1 / this.contentScaling());
focusDoc = (doc: Doc) => this.props.focus(doc, false);
NativeWidth = () => this.nativeWidth;
NativeHeight = () => this.nativeHeight;
@@ -115,13 +107,11 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight} />
: <ContentFittingDocumentView {...this.props}
- CollectionDoc={this.props.ContainingCollectionDoc}
- DataDocument={this.props.DataDoc}
- getTransform={this.getTransform}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}
+ DataDoc={this.props.DataDoc}
+ ScreenToLocalTransform={this.getTransform}
NativeHeight={this.NativeHeight}
NativeWidth={this.NativeWidth}
- active={this.props.parentActive}
- focus={this.focusDoc}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
/>}
diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx
index 814f8fd9c..7e45e8fcf 100644
--- a/src/client/views/nodes/ContentFittingDocumentView.tsx
+++ b/src/client/views/nodes/ContentFittingDocumentView.tsx
@@ -3,16 +3,16 @@ import { computed } from "mobx";
import { observer } from "mobx-react";
import "react-table/react-table.css";
import { Doc, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc";
-import { ScriptField } from "../../../new_fields/ScriptField";
-import { NumCast, StrCast } from "../../../new_fields/Types";
+import { NumCast, StrCast, Cast } from "../../../new_fields/Types";
import { TraceMobx } from "../../../new_fields/util";
import { emptyFunction, returnOne } from "../../../Utils";
-import { Transform } from "../../util/Transform";
-import { CollectionView } from "../collections/CollectionView";
import '../DocumentDecorations.scss';
-import { DocumentView } from "../nodes/DocumentView";
+import { DocumentView, DocumentViewProps } from "../nodes/DocumentView";
import "./ContentFittingDocumentView.scss";
import { dropActionType } from "../../util/DragManager";
+import { CollectionView } from "../collections/CollectionView";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { Transform } from "nodemailer/lib/xoauth2";
interface ContentFittingDocumentViewProps {
Document: Doc;
@@ -47,9 +47,13 @@ interface ContentFittingDocumentViewProps {
}
@observer
-export class ContentFittingDocumentView extends React.Component<ContentFittingDocumentViewProps>{
+export class ContentFittingDocumentView extends React.Component<DocumentViewProps>{
public get displayName() { return "DocumentView(" + this.props.Document?.title + ")"; } // this makes mobx trace() statements more descriptive
- private get layoutDoc() { return this.props.LayoutDoc?.() || Doc.Layout(this.props.Document); }
+ private get layoutDoc() {
+ return this.props.LayoutTemplate?.() ||
+ (this.props.layoutKey && Doc.Layout(this.props.Document, Cast(this.props.Document[this.props.layoutKey], Doc, null))) ||
+ Doc.Layout(this.props.Document);
+ }
@computed get freezeDimensions() { return this.props.FreezeDimensions; }
nativeWidth = () => NumCast(this.layoutDoc?._nativeWidth, this.props.NativeWidth?.() || (this.freezeDimensions && this.layoutDoc ? this.layoutDoc[WidthSym]() : this.props.PanelWidth()));
nativeHeight = () => NumCast(this.layoutDoc?._nativeHeight, this.props.NativeHeight?.() || (this.freezeDimensions && this.layoutDoc ? this.layoutDoc[HeightSym]() : this.props.PanelHeight()));
@@ -68,7 +72,7 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo
@computed get panelWidth() { return this.nativeWidth && !this.props.Document._fitWidth ? this.nativeWidth() * this.contentScaling() : this.props.PanelWidth(); }
@computed get panelHeight() { return this.nativeHeight && !this.props.Document._fitWidth ? this.nativeHeight() * this.contentScaling() : this.props.PanelHeight(); }
- private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, -this.centeringYOffset).scale(1 / this.contentScaling());
+ private getTransform = () => this.props.ScreenToLocalTransform().translate(-this.centeringOffset, -this.centeringYOffset).scale(1 / this.contentScaling());
private get centeringOffset() { return this.nativeWidth() && !this.props.Document._fitWidth ? (this.props.PanelWidth() - this.nativeWidth() * this.contentScaling()) / 2 : 0; }
private get centeringYOffset() { return Math.abs(this.centeringOffset) < 0.001 ? (this.props.PanelHeight() - this.nativeHeight() * this.contentScaling()) / 2 : 0; }
@@ -91,8 +95,9 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo
}}>
<DocumentView {...this.props}
Document={this.props.Document}
- DataDoc={this.props.DataDocument}
- LayoutDoc={this.props.LayoutDoc}
+ DataDoc={this.props.DataDoc}
+ LayoutTemplate={this.props.LayoutTemplate}
+ LayoutTemplateString={this.props.LayoutTemplateString}
LibraryPath={this.props.LibraryPath}
NativeWidth={this.nativeWidth}
NativeHeight={this.nativeHeight}
@@ -108,11 +113,11 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo
removeDocument={this.props.removeDocument}
moveDocument={this.props.moveDocument}
whenActiveChanged={this.props.whenActiveChanged}
- ContainingCollectionView={this.props.CollectionView}
- ContainingCollectionDoc={this.props.CollectionDoc}
+ ContainingCollectionView={this.props.ContainingCollectionView}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}
addDocTab={this.props.addDocTab}
pinToPres={this.props.pinToPres}
- parentActive={this.props.active}
+ parentActive={this.props.parentActive}
ScreenToLocalTransform={this.getTransform}
renderDepth={this.props.renderDepth}
focus={this.props.focus || emptyFunction}
diff --git a/src/client/views/nodes/DocumentBox.scss b/src/client/views/nodes/DocumentBox.scss
index ce21391ce..3a27c16c1 100644
--- a/src/client/views/nodes/DocumentBox.scss
+++ b/src/client/views/nodes/DocumentBox.scss
@@ -2,7 +2,8 @@
width: 100%;
height: 100%;
pointer-events: all;
- background: gray;
+ background: rgb(241, 239, 235);
+ position: absolute;
.documentBox-lock {
margin: auto;
color: white;
diff --git a/src/client/views/nodes/DocumentBox.tsx b/src/client/views/nodes/DocumentBox.tsx
index 0d18baaed..b53c7cfe6 100644
--- a/src/client/views/nodes/DocumentBox.tsx
+++ b/src/client/views/nodes/DocumentBox.tsx
@@ -1,34 +1,38 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { IReactionDisposer, reaction, computed } from "mobx";
+import { action, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
import { Doc, Field } from "../../../new_fields/Doc";
-import { documentSchema } from "../../../new_fields/documentSchemas";
+import { collectionSchema, documentSchema } from "../../../new_fields/documentSchemas";
import { makeInterface } from "../../../new_fields/Schema";
import { ComputedField } from "../../../new_fields/ScriptField";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { emptyPath } from "../../../Utils";
+import { TraceMobx } from "../../../new_fields/util";
+import { emptyPath, returnFalse, returnOne, returnZero } from "../../../Utils";
+import { DocumentType } from "../../documents/DocumentTypes";
+import { DragManager } from "../../util/DragManager";
+import { undoBatch } from "../../util/UndoManager";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from "../ContextMenuItem";
import { ViewBoxAnnotatableComponent } from "../DocComponent";
import { ContentFittingDocumentView } from "./ContentFittingDocumentView";
import "./DocumentBox.scss";
+import { DocumentView } from "./DocumentView";
import { FieldView, FieldViewProps } from "./FieldView";
import React = require("react");
-import { TraceMobx } from "../../../new_fields/util";
-import { DocumentView } from "./DocumentView";
-import { Docs } from "../../documents/Documents";
-type DocHolderBoxSchema = makeInterface<[typeof documentSchema]>;
-const DocHolderBoxDocument = makeInterface(documentSchema);
+type DocHolderBoxSchema = makeInterface<[typeof documentSchema, typeof collectionSchema]>;
+const DocHolderBoxDocument = makeInterface(documentSchema, collectionSchema);
@observer
export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, DocHolderBoxSchema>(DocHolderBoxDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DocHolderBox, fieldKey); }
_prevSelectionDisposer: IReactionDisposer | undefined;
+ _dropDisposer?: DragManager.DragDropDisposer;
_selections: Doc[] = [];
+ _contRef = React.createRef<HTMLDivElement>();
_curSelection = -1;
componentDidMount() {
- this._prevSelectionDisposer = reaction(() => this.contentDoc[this.props.fieldKey], (data) => {
+ this._prevSelectionDisposer = reaction(() => this.layoutDoc[this.props.fieldKey], (data) => {
if (data instanceof Doc && !this.isSelectionLocked()) {
this._selections.indexOf(data) !== -1 && this._selections.splice(this._selections.indexOf(data), 1);
this._selections.push(data);
@@ -42,22 +46,20 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do
specificContextMenu = (e: React.MouseEvent): void => {
const funcs: ContextMenuProps[] = [];
funcs.push({ description: (this.isSelectionLocked() ? "Show" : "Lock") + " Selection", event: () => this.toggleLockSelection, icon: "expand-arrows-alt" });
- funcs.push({ description: (this.props.Document.excludeCollections ? "Include" : "Exclude") + " Collections", event: () => Doc.GetProto(this.props.Document).excludeCollections = !this.props.Document.excludeCollections, icon: "expand-arrows-alt" });
- funcs.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" });
+ funcs.push({ description: (this.layoutDoc.excludeCollections ? "Include" : "Exclude") + " Collections", event: () => this.layoutDoc.excludeCollections = !this.layoutDoc.excludeCollections, icon: "expand-arrows-alt" });
+ funcs.push({ description: `${this.layoutDoc.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.layoutDoc.forceActive = !this.layoutDoc.forceActive, icon: "project-diagram" });
+ funcs.push({ description: `Show ${this.layoutDoc.childLayoutTemplateName !== "keyValue" ? "key values" : "contents"}`, event: () => this.layoutDoc.childLayoutString = this.layoutDoc.childLayoutString ? undefined : "<KeyValueBox {...props} />", icon: "project-diagram" });
ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
}
- @computed get contentDoc() {
- return (this.props.Document.isTemplateDoc || this.props.Document.isTemplateForField ? this.props.Document : Doc.GetProto(this.props.Document));
- }
lockSelection = () => {
- this.contentDoc[this.props.fieldKey] = this.props.Document[this.props.fieldKey];
+ this.layoutDoc[this.props.fieldKey] = this.layoutDoc[this.props.fieldKey];
}
showSelection = () => {
- this.contentDoc[this.props.fieldKey] = ComputedField.MakeFunction(`selectedDocs(self,this.excludeCollections,[_last_])?.[0]`);
+ this.layoutDoc[this.props.fieldKey] = ComputedField.MakeFunction(`selectedDocs(self,this.excludeCollections,[_last_])?.[0]`);
}
isSelectionLocked = () => {
- const kvpstring = Field.toKeyValueString(this.contentDoc, this.props.fieldKey);
+ const kvpstring = Field.toKeyValueString(this.layoutDoc, this.props.fieldKey);
return !kvpstring || kvpstring.includes("DOC");
}
toggleLockSelection = () => {
@@ -67,13 +69,13 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do
prevSelection = () => {
this.lockSelection();
if (this._curSelection > 0) {
- this.contentDoc[this.props.fieldKey] = this._selections[--this._curSelection];
+ this.layoutDoc[this.props.fieldKey] = this._selections[--this._curSelection];
return true;
}
}
nextSelection = () => {
if (this._curSelection < this._selections.length - 1 && this._selections.length) {
- this.contentDoc[this.props.fieldKey] = this._selections[++this._curSelection];
+ this.layoutDoc[this.props.fieldKey] = this._selections[++this._curSelection];
return true;
}
}
@@ -102,41 +104,70 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do
e.stopPropagation();
}
}
- _contRef = React.createRef<HTMLDivElement>();
pwidth = () => this.props.PanelWidth() - 2 * this.xPad;
pheight = () => this.props.PanelHeight() - 2 * this.yPad;
getTransform = () => this.props.ScreenToLocalTransform().translate(-this.xPad, -this.yPad);
+ isActive = () => this.active() || !this.props.renderDepth;
+ layoutTemplateDoc = () => Cast(this.props.Document.childLayoutTemplate, Doc, null);
get renderContents() {
- const containedDoc = Cast(this.contentDoc[this.props.fieldKey], Doc, null);
- const childTemplateName = StrCast(this.props.Document.childTemplateName);
- if (containedDoc && childTemplateName && !containedDoc["layout_" + childTemplateName]) {
- setTimeout(() => {
- Doc.createCustomView(containedDoc, Docs.Create.StackingDocument, childTemplateName);
- Doc.expandTemplateLayout(Cast(containedDoc["layout_" + childTemplateName], Doc, null), containedDoc, undefined);
- }, 0);
- }
- const contents = !(containedDoc instanceof Doc) ? (null) : <ContentFittingDocumentView
- Document={containedDoc}
- DataDocument={undefined}
- LibraryPath={emptyPath}
- CollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected
- fitToBox={true}
- layoutKey={childTemplateName ? "layout_" + childTemplateName : "layout"}
- rootSelected={this.props.isSelected}
- addDocument={this.props.addDocument}
- moveDocument={this.props.moveDocument}
- removeDocument={this.props.removeDocument}
- addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
- getTransform={this.getTransform}
- renderDepth={this.props.renderDepth + 1}
- PanelWidth={this.pwidth}
- PanelHeight={this.pheight}
- focus={this.props.focus}
- active={this.props.active}
- dontRegisterView={!this.isSelectionLocked()}
- whenActiveChanged={this.props.whenActiveChanged}
- />;
+ const containedDoc = Cast(this.dataDoc[this.props.fieldKey], Doc, null);
+ const layoutTemplate = StrCast(this.layoutDoc.childLayoutString);
+ const contents = !(containedDoc instanceof Doc) ? (null) : this.layoutDoc.childLayoutString || this.layoutTemplateDoc() ?
+ <DocumentView
+ Document={containedDoc}
+ DataDoc={undefined}
+ LibraryPath={emptyPath}
+ ContainingCollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected
+ ContainingCollectionDoc={undefined}
+ fitToBox={true}
+ LayoutTemplateString={layoutTemplate}
+ LayoutTemplate={this.layoutTemplateDoc}
+ rootSelected={this.props.isSelected}
+ addDocument={this.props.addDocument}
+ moveDocument={this.props.moveDocument}
+ removeDocument={this.props.removeDocument}
+ addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
+ ScreenToLocalTransform={this.getTransform}
+ renderDepth={this.props.renderDepth + 1}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ PanelWidth={this.pwidth}
+ PanelHeight={this.pheight}
+ focus={this.props.focus}
+ parentActive={this.isActive}
+ dontRegisterView={true}
+ whenActiveChanged={this.props.whenActiveChanged}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne} /> :
+ <ContentFittingDocumentView
+ Document={containedDoc}
+ DataDoc={undefined}
+ LibraryPath={emptyPath}
+ ContainingCollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected
+ ContainingCollectionDoc={undefined}
+ fitToBox={true}
+ LayoutTemplateString={layoutTemplate}
+ LayoutTemplate={this.layoutTemplateDoc}
+ rootSelected={this.props.isSelected}
+ addDocument={this.props.addDocument}
+ moveDocument={this.props.moveDocument}
+ removeDocument={this.props.removeDocument}
+ addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
+ ScreenToLocalTransform={this.getTransform}
+ renderDepth={this.props.renderDepth + 1}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ PanelWidth={this.pwidth}
+ PanelHeight={this.pheight}
+ focus={this.props.focus}
+ parentActive={this.isActive}
+ dontRegisterView={true}
+ whenActiveChanged={this.props.whenActiveChanged}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne}
+ />;
return contents;
}
render() {
@@ -145,16 +176,32 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do
onContextMenu={this.specificContextMenu}
onPointerDown={this.onPointerDown} onClick={this.onClick}
style={{
- background: StrCast(this.props.Document.backgroundColor),
+ background: StrCast(this.layoutDoc.backgroundColor),
border: `#00000021 solid ${this.xPad}px`,
borderTop: `#0000005e solid ${this.yPad}px`,
borderBottom: `#0000005e solid ${this.yPad}px`,
}}>
{this.renderContents}
- <div className="documentBox-lock" onClick={this.onLockClick}
+ <div className="documentBox-lock" onClick={this.onLockClick} ref={this.createDropTarget}
style={{ marginTop: - this.yPad }}>
<FontAwesomeIcon icon={this.isSelectionLocked() ? "lock" : "unlock"} size="sm" />
</div>
</div >;
}
+
+ @undoBatch
+ @action
+ drop = (e: Event, de: DragManager.DropEvent) => {
+ if (de.complete.docDragData) {
+ if (de.complete.docDragData.draggedDocuments[0].type === DocumentType.FONTICON) {
+ const doc = Cast(de.complete.docDragData.draggedDocuments[0].dragFactory, Doc, null);
+ this.props.Document.childLayoutTemplate = doc;
+ }
+ }
+ }
+ protected createDropTarget = (ele: HTMLDivElement) => {
+ this._dropDisposer?.();
+ ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document));
+ }
+
}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index cd78ac7b3..81667e549 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -19,7 +19,7 @@ import { DocumentViewProps } from "./DocumentView";
import "./DocumentView.scss";
import { FontIconBox } from "./FontIconBox";
import { FieldView, FieldViewProps } from "./FieldView";
-import { FormattedTextBox } from "./FormattedTextBox";
+import { FormattedTextBox } from "./formattedText/FormattedTextBox";
import { ImageBox } from "./ImageBox";
import { KeyValueBox } from "./KeyValueBox";
import { PDFBox } from "./PDFBox";
@@ -77,11 +77,11 @@ export class HTMLtag extends React.Component<HTMLtagProps> {
render() {
const style: { [key: string]: any } = {};
const divKeys = OmitKeys(this.props, ["children", "htmltag", "RootDoc", "Document", "key", "onInput", "onClick", "__proto__"]).omit;
+ const replacer = (match: any, expr: string, offset: any, string: any) => { // bcz: this executes a script to convert a propery expression string: { script } into a value
+ return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ self: this.props.RootDoc, this: this.props.Document }).result as string || "";
+ };
Object.keys(divKeys).map((prop: string) => {
const p = (this.props as any)[prop] as string;
- const replacer = (match: any, expr: string, offset: any, string: any) => { // bcz: this executes a script to convert a propery expression string: { script } into a value
- return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ self: this.props.RootDoc, this: this.props.Document }).result as string || "";
- };
style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer);
});
const Tag = this.props.htmltag as keyof JSX.IntrinsicElements;
@@ -96,8 +96,6 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
isSelected: (outsideReaction: boolean) => boolean,
select: (ctrl: boolean) => void,
layoutKey: string,
- forceLayout?: string,
- forceFieldKey?: string,
hideOnLeave?: boolean,
makeLink?: () => Opt<Doc>, // function to call when a link is made
}> {
@@ -105,6 +103,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
TraceMobx();
if (!this.layoutDoc) return "<p>awaiting layout</p>";
// const layout = Cast(this.layoutDoc[StrCast(this.layoutDoc.layoutKey, this.layoutDoc === this.props.Document ? this.props.layoutKey : "layout")], "string"); // bcz: replaced this with below... is it right?
+ if (this.props.LayoutTemplateString) return this.props.LayoutTemplateString;
const layout = Cast(this.layoutDoc[this.layoutDoc === this.props.Document && this.props.layoutKey ? this.props.layoutKey : StrCast(this.layoutDoc.layoutKey, "layout")], "string");
if (this.props.layoutKey === "layout_keyValue") {
return StrCast(this.props.Document.layout_keyValue, KeyValueBox.LayoutString("data"));
@@ -127,8 +126,8 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
get layoutDoc() {
const params = StrCast(this.props.Document.PARAMS);
// bcz: replaced this with below : is it correct? change was made to accommodate passing fieldKey's from a layout script
- // const template: Doc = this.props.LayoutDoc?.() || Doc.Layout(this.props.Document, this.props.layoutKey ? Cast(this.props.Document[this.props.layoutKey], Doc, null) : undefined);
- const template: Doc = this.props.LayoutDoc?.() ||
+ // const template: Doc = this.props.LayoutTemplate?.() || Doc.Layout(this.props.Document, this.props.layoutKey ? Cast(this.props.Document[this.props.layoutKey], Doc, null) : undefined);
+ const template: Doc = this.props.LayoutTemplate?.() || (this.props.LayoutTemplateString && this.props.Document) ||
(this.props.layoutKey && StrCast(this.props.Document[this.props.layoutKey]) && this.props.Document) ||
Doc.Layout(this.props.Document, this.props.layoutKey ? Cast(this.props.Document[this.props.layoutKey], Doc, null) : undefined);
return Doc.expandTemplateLayout(template, this.props.Document, params ? "(" + params + ")" : this.props.layoutKey);
@@ -186,25 +185,22 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
// layoutFrame = splits.length > 1 ? splits[0] + splits[1].replace(/{([^{}]|(?R))*}/, replacer4) : ""; // might have been more elegant if javascript supported recursive patterns
return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc) ? (null) :
- this.props.forceLayout === "FormattedTextBox" && this.props.forceFieldKey ?
- <FormattedTextBox {...bindings.props} fieldKey={this.props.forceFieldKey} />
- :
- <ObserverJsxParser
- key={42}
- blacklistedAttrs={[]}
- renderInWrapper={false}
- components={{
- FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, LabelBox, SliderBox, FieldView,
- CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
- PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, QueryBox,
- ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, DocHolderBox, LinkBox, ScriptingBox,
- RecommendationsBox, ScreenshotBox, HTMLtag
- }}
- bindings={bindings}
- jsx={layoutFrame}
- showWarnings={true}
-
- onError={(test: any) => { console.log(test); }}
- />;
+ <ObserverJsxParser
+ key={42}
+ blacklistedAttrs={[]}
+ renderInWrapper={false}
+ components={{
+ FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, LabelBox, SliderBox, FieldView,
+ CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
+ PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, QueryBox,
+ ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, DocHolderBox, LinkBox, ScriptingBox,
+ RecommendationsBox, ScreenshotBox, HTMLtag
+ }}
+ bindings={bindings}
+ jsx={layoutFrame}
+ showWarnings={true}
+
+ onError={(test: any) => { console.log(test); }}
+ />;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index c9a745809..dea09cb30 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -75,6 +75,7 @@
display: inline-block;
width: 100%;
height: 100%;
+ border-radius: inherit;
.documentView-styleContentWrapper {
width: 100%;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 4b5fadd93..c4cd5978a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -5,22 +5,21 @@ import { action, computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import * as rp from "request-promise";
import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
-import { Document, PositionDocument } from '../../../new_fields/documentSchemas';
+import { Document } from '../../../new_fields/documentSchemas';
import { Id } from '../../../new_fields/FieldSymbols';
import { InkTool } from '../../../new_fields/InkField';
-import { RichTextField } from '../../../new_fields/RichTextField';
import { listSpec } from "../../../new_fields/Schema";
import { SchemaHeaderField } from '../../../new_fields/SchemaHeaderField';
import { ScriptField } from '../../../new_fields/ScriptField';
import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { AudioField, ImageField, PdfField, VideoField } from '../../../new_fields/URLField';
+import { ImageField } from '../../../new_fields/URLField';
import { TraceMobx } from '../../../new_fields/util';
import { GestureUtils } from '../../../pen-gestures/GestureUtils';
import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils } from "../../../Utils";
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { ClientRecommender } from '../../ClientRecommender';
import { DocServer } from "../../DocServer";
-import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents";
+import { Docs, DocUtils } from "../../documents/Documents";
import { DocumentType } from '../../documents/DocumentTypes';
import { ClientUtils } from '../../util/ClientUtils';
import { DocumentManager } from "../../util/DocumentManager";
@@ -42,6 +41,7 @@ import { InkingControl } from '../InkingControl';
import { KeyphraseQueryView } from '../KeyphraseQueryView';
import { DocumentContentsView } from "./DocumentContentsView";
import "./DocumentView.scss";
+import { LinkAnchorBox } from './LinkAnchorBox';
import { RadialMenu } from './RadialMenu';
import React = require("react");
@@ -58,12 +58,14 @@ export interface DocumentViewProps {
NativeHeight: () => number;
Document: Doc;
DataDoc?: Doc;
- LayoutDoc?: () => Opt<Doc>;
+ LayoutTemplateString?: string;
+ LayoutTemplate?: () => Opt<Doc>;
LibraryPath: Doc[];
fitToBox?: boolean;
contextMenuItems?: () => { script: ScriptField, label: string }[];
rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected
onClick?: ScriptField;
+ onDoubleClick?: ScriptField;
onPointerDown?: ScriptField;
onPointerUp?: ScriptField;
dropAction?: dropActionType;
@@ -73,6 +75,7 @@ export interface DocumentViewProps {
removeDocument?: (doc: Doc) => boolean;
moveDocument?: (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
+ setupDragLines?: () => void;
renderDepth: number;
ContentScaling: () => number;
PanelWidth: () => number;
@@ -116,6 +119,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth, this.props.NativeWidth() || (this.freezeDimensions ? this.layoutDoc[WidthSym]() : 0)); }
@computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight, this.props.NativeHeight() || (this.freezeDimensions ? this.layoutDoc[HeightSym]() : 0)); }
@computed get onClickHandler() { return this.props.onClick || Cast(this.layoutDoc.onClick, ScriptField, null) || this.Document.onClick; }
+ @computed get onDoubleClickHandler() { return this.props.onDoubleClick || Cast(this.layoutDoc.onDoubleClick, ScriptField, null) || this.Document.onDoubleClick; }
@computed get onPointerDownHandler() { return this.props.onPointerDown ? this.props.onPointerDown : this.Document.onPointerDown; }
@computed get onPointerUpHandler() { return this.props.onPointerUp ? this.props.onPointerUp : this.Document.onPointerUp; }
NativeWidth = () => this.nativeWidth;
@@ -289,13 +293,22 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
!this.props.Document.isBackground && this.props.bringToFront(this.props.Document);
if (this._doubleTap && this.props.renderDepth && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
if (!(e.nativeEvent as any).formattedHandled) {
- const fullScreenAlias = Doc.MakeAlias(this.props.Document);
- if (StrCast(fullScreenAlias.layoutKey) !== "layout_fullScreen" && fullScreenAlias.layout_fullScreen) {
- fullScreenAlias.layoutKey = "layout_fullScreen";
+ if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) { // bcz: hack? don't execute script if you're clicking on a scripting box itself
+ const func = () => this.onDoubleClickHandler.script.run({
+ this: this.layoutDoc,
+ self: this.rootDoc,
+ thisContainer: this.props.ContainingCollectionDoc, shiftKey: e.shiftKey
+ }, console.log);
+ func();
+ } else {
+ const fullScreenAlias = Doc.MakeAlias(this.props.Document);
+ if (StrCast(fullScreenAlias.layoutKey) !== "layout_fullScreen" && fullScreenAlias.layout_fullScreen) {
+ fullScreenAlias.layoutKey = "layout_fullScreen";
+ }
+ UndoManager.RunInBatch(() => this.props.addDocTab(fullScreenAlias, "inTab"), "double tap");
+ SelectionManager.DeselectAll();
+ Doc.UnBrushDoc(this.props.Document);
}
- UndoManager.RunInBatch(() => this.props.addDocTab(fullScreenAlias, "inTab"), "double tap");
- SelectionManager.DeselectAll();
- Doc.UnBrushDoc(this.props.Document);
}
} else if (this.onClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) { // bcz: hack? don't execute script if you're clicking on a scripting box itself
//SelectionManager.DeselectAll();
@@ -441,8 +454,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const dY = -1 * Math.sign(dH);
if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
- const doc = PositionDocument(this.props.Document);
- const layoutDoc = PositionDocument(Doc.Layout(this.props.Document));
+ const doc = Document(this.props.Document);
+ const layoutDoc = Document(Doc.Layout(this.props.Document));
let nwidth = layoutDoc._nativeWidth || 0;
let nheight = layoutDoc._nativeHeight || 0;
const width = (layoutDoc._width || 0);
@@ -653,7 +666,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@undoBatch
@action
toggleBackground = (temporary: boolean): void => {
- this.Document.overflow = temporary ? "visible" : "hidden";
+ this.Document._overflow = temporary ? "visible" : "hidden";
this.Document.isBackground = !temporary ? !this.Document.isBackground : (this.Document.isBackground ? undefined : true);
this.Document.isBackground && this.props.bringToFront(this.Document, true);
}
@@ -938,9 +951,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this._queries = kps.toString();
}
- onPointerEnter = (e: React.PointerEvent): void => { Doc.BrushDoc(this.props.Document); };
- onPointerLeave = (e: React.PointerEvent): void => { Doc.UnBrushDoc(this.props.Document); };
-
// does Document set a layout prop
// does Document set a layout prop
setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)] && this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)];
@@ -974,13 +984,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@computed get contents() {
TraceMobx();
return (<>
- <DocumentContentsView key={1} ContainingCollectionView={this.props.ContainingCollectionView}
+ <DocumentContentsView key={1}
+ ContainingCollectionView={this.props.ContainingCollectionView}
ContainingCollectionDoc={this.props.ContainingCollectionDoc}
NativeWidth={this.NativeWidth}
NativeHeight={this.NativeHeight}
Document={this.props.Document}
DataDoc={this.props.DataDoc}
- LayoutDoc={this.props.LayoutDoc}
+ LayoutTemplateString={this.props.LayoutTemplateString}
+ LayoutTemplate={this.props.LayoutTemplate}
makeLink={this.makeLink}
rootSelected={this.rootSelected}
dontRegisterView={this.props.dontRegisterView}
@@ -1010,7 +1022,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
</>
);
}
- linkEndpoint = (linkDoc: Doc) => Doc.LinkEndpoint(linkDoc, this.props.Document);
// used to decide whether a link anchor view should be created or not.
// if it's a tempoarl link (currently just for Audio), then the audioBox will display the anchor and we don't want to display it here.
@@ -1038,12 +1049,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
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}
- layoutKey={this.linkEndpoint(d)}
ContentScaling={returnOne}
backgroundColor={returnTransparent}
removeDocument={this.hideLinkAnchor}
pointerEvents={false}
- LayoutDoc={undefined}
+ LayoutTemplate={undefined}
+ LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(d, this.props.Document)}`)}
/>);
}
@computed get innards() {
@@ -1059,11 +1070,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const showCaption = StrCast(this.layoutDoc._showCaption);
const showTextTitle = showTitle && (StrCast(this.layoutDoc.layout).indexOf("PresBox") !== -1 || StrCast(this.layoutDoc.layout).indexOf("FormattedTextBox") !== -1) ? showTitle : undefined;
const captionView = (!showCaption ? (null) :
- <div className="documentView-captionWrapper">
+ <div className="documentView-captionWrapper" style={{ backgroundColor: StrCast(this.layoutDoc["caption-backgroundColor"]), color: StrCast(this.layoutDoc["caption-color"]) }}>
<DocumentContentsView {...OmitKeys(this.props, ['children']).omit}
hideOnLeave={true}
- forceLayout={"FormattedTextBox"}
- forceFieldKey={showCaption}
+ layoutTemplateString={`<FormattedTextBox {...props} fieldKey={'${showCaption}'}/>`}
ContentScaling={this.childScaling}
ChromeHeight={this.chromeHeight}
isSelected={this.isSelected}
@@ -1092,7 +1102,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
@computed get ignorePointerEvents() {
return this.props.pointerEvents === false ||
- (this.Document.isBackground && !this.isSelected() && !SelectionManager.GetIsDragging()) ||
+ (this.Document.isBackground && !this.isSelected() && !DragManager.Vals.Instance.GetIsDragging()) ||
(this.Document.type === DocumentType.INK && InkingControl.Instance.selectedTool !== InkTool.None);
}
@undoBatch
@@ -1103,17 +1113,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
Doc.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined);
}
}
- @observable _animate = 0;
+ @observable _animateScalingTo = 0;
switchViews = action((custom: boolean, view: string) => {
- SelectionManager.SetIsDragging(true);
- this._animate = 0.1;
+ this._animateScalingTo = 0.1; // shrink doc
setTimeout(action(() => {
this.setCustomView(custom, view);
- this._animate = 1;
- setTimeout(action(() => {
- this._animate = 0;
- SelectionManager.SetIsDragging(false);
- }), 400);
+ this._animateScalingTo = 1; // expand it
+ setTimeout(action(() => this._animateScalingTo = 0), 400);
}), 400);
});
@@ -1133,11 +1139,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
highlighting = highlighting && this.props.focus !== emptyFunction; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way
return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onKeyDown={this.onKeyDown}
onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
- onPointerEnter={e => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)}
+ // onPointerEnter={e => Doc.BrushDoc(this.props.Document)}
+ // onPointerLeave={e => Doc.BrushDoc(this.props.Document)}
+ onPointerEnter={action(() => Doc.BrushDoc(this.props.Document))}
+ onPointerLeave={action((e: React.PointerEvent<HTMLDivElement>) => {
+ let entered = false;
+ const target = document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y);
+ for (let child: any = target; child; child = child?.parentElement) {
+ if (child === this.ContentDiv) {
+ entered = true;
+ }
+ }
+ !entered && Doc.UnBrushDoc(this.props.Document);
+ })}
style={{
- transformOrigin: this._animate ? "center center" : undefined,
- transform: this._animate ? `scale(${this._animate})` : undefined,
- transition: !this._animate ? StrCast(this.Document.transition) : this._animate < 1 ? "transform 0.5s ease-in" : "transform 0.5s ease-out",
+ transformOrigin: this._animateScalingTo ? "center center" : undefined,
+ transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
+ transition: !this._animateScalingTo ? StrCast(this.Document.transition) : this._animateScalingTo < 1 ? "transform 0.5s ease-in" : "transform 0.5s ease-out",
pointerEvents: this.ignorePointerEvents ? "none" : undefined,
color: StrCast(this.layoutDoc.color, "inherit"),
outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px",
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index a3790d38b..016d2a1ae 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -50,7 +50,13 @@ export interface FieldViewProps {
setVideoBox?: (player: VideoBox) => void;
ContentScaling: () => number;
ChromeHeight?: () => number;
- childLayoutTemplate?: () => Opt<Doc>;
+ // properties intended to be used from within layout strings (otherwise use the function equivalents that work more efficiently with React)
+ height?: number;
+ width?: number;
+ background?: string;
+ color?: string;
+ xMargin?: number;
+ yMargin?: number;
}
@observer
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 2970674a2..39d7109b1 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -238,8 +238,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : [];
openItems.push({
description: "Default Perspective", event: () => {
- this.props.addDocTab(this.fieldDocToLayout, "inTab");
this.props.addDocTab(this.props.Document, "close");
+ this.props.addDocTab(this.fieldDocToLayout, "onRight");
}, icon: "image"
});
!open && cm.addItem({ description: "Change Perspective...", subitems: openItems, icon: "external-link-alt" });
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index 707b9d12a..bc36e056e 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -4,7 +4,7 @@ import { Doc, DocListCast } from "../../../new_fields/Doc";
import { documentSchema } from "../../../new_fields/documentSchemas";
import { makeInterface } from "../../../new_fields/Schema";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { Utils, setupMoveUpEvents } from '../../../Utils';
+import { Utils, setupMoveUpEvents, emptyFunction } from '../../../Utils';
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager } from "../../util/DragManager";
import { ViewBoxBaseComponent } from "../DocComponent";
@@ -17,7 +17,6 @@ import { LinkEditor } from "../linking/LinkEditor";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SelectionManager } from "../../util/SelectionManager";
import { TraceMobx } from "../../../new_fields/util";
-import { DocumentView } from "./DocumentView";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -40,7 +39,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch
@observable _forceOpen = false;
onPointerDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, this.onPointerMove, () => { }, this.onClick, false);
+ setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, false);
}
onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => {
const cdiv = this._ref && this._ref.current && this._ref.current.parentElement;
@@ -63,9 +62,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch
return false;
});
@action
- onClick = (e: PointerEvent) => {
- this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0);
- this._lastTap = Date.now();
+ onClick = (e: React.MouseEvent) => {
if ((e.button === 2 || e.ctrlKey || !this.layoutDoc.isLinkButton)) {
this.props.select(false);
}
@@ -82,6 +79,9 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch
} else {
this._timeout && clearTimeout(this._timeout);
this._timeout = undefined;
+ this._doubleTap = false;
+ this.openLinkEditor(e);
+ e.stopPropagation();
}
}
@@ -130,7 +130,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch
</div>
);
const small = this.props.PanelWidth() <= 1;
- return <div className={`linkAnchorBox-cont${small ? "-small" : ""}`} onPointerDown={this.onPointerDown} title={targetTitle} onContextMenu={this.specificContextMenu}
+ return <div className={`linkAnchorBox-cont${small ? "-small" : ""}`} onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle} onContextMenu={this.specificContextMenu}
ref={this._ref} style={{
background: c,
left: !small ? `calc(${x}% - 7.5px)` : undefined,
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index bccf0f291..6f18b1321 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -198,7 +198,6 @@
}
.pdfBox {
- pointer-events: none;
.pdfViewer-text {
.textLayer {
span {
diff --git a/src/client/views/nodes/PresBox.scss b/src/client/views/nodes/PresBox.scss
index 78c19f351..d48000e16 100644
--- a/src/client/views/nodes/PresBox.scss
+++ b/src/client/views/nodes/PresBox.scss
@@ -1,10 +1,10 @@
.presBox-cont {
position: absolute;
+ pointer-events: inherit;
z-index: 2;
box-shadow: #AAAAAA .2vw .2vw .4vw;
- bottom: 0;
width: 100%;
- min-width: 120px;
+ min-width: 20px;
height: 100%;
min-height: 41px;
letter-spacing: 2px;
@@ -17,17 +17,36 @@
width: 100%;
}
.presBox-buttons {
- padding: 10px;
width: 100%;
background: gray;
- padding-right: 10px;
padding-top: 5px;
padding-bottom: 5px;
+ display: grid;
+ grid-column-end: 4;
+ grid-column-start: 1;
+ .presBox-viewPicker {
+ height: 25;
+ position: relative;
+ display: inline-block;
+ grid-column: 1/2;
+ min-width: 15px;
+ }
+ select {
+ background: #323232;
+ color: white;
+ }
.presBox-button {
margin-right: 2.5%;
margin-left: 2.5%;
- width: 20%;
+ height: 25px;
border-radius: 5px;
+ display: flex;
+ align-items: center;
+ background: #323232;
+ color: white;
+ svg {
+ margin: auto;
+ }
}
.collectionViewBaseChrome-viewPicker {
min-width: 50;
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 80d043db1..343e74c87 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -1,12 +1,10 @@
import React = require("react");
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faHandPointLeft, faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCast, DocCastAsync } from "../../../new_fields/Doc";
import { InkTool } from "../../../new_fields/InkField";
-import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
import { returnFalse } from "../../../Utils";
import { documentSchema } from "../../../new_fields/documentSchemas";
import { DocumentManager } from "../../util/DocumentManager";
@@ -18,16 +16,11 @@ import { FieldView, FieldViewProps } from './FieldView';
import "./PresBox.scss";
import { ViewBoxBaseComponent } from "../DocComponent";
import { makeInterface } from "../../../new_fields/Schema";
-
-library.add(faArrowLeft);
-library.add(faArrowRight);
-library.add(faPlay);
-library.add(faStop);
-library.add(faHandPointLeft);
-library.add(faPlus);
-library.add(faTimes);
-library.add(faMinus);
-library.add(faEdit);
+import { List } from "../../../new_fields/List";
+import { Docs } from "../../documents/Documents";
+import { PrefetchProxy } from "../../../new_fields/Proxy";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { Scripting } from "../../util/Scripting";
type PresBoxSchema = makeInterface<[typeof documentSchema]>;
const PresBoxDocument = makeInterface(documentSchema);
@@ -35,22 +28,37 @@ const PresBoxDocument = makeInterface(documentSchema);
@observer
export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>(PresBoxDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresBox, fieldKey); }
- _childReaction: IReactionDisposer | undefined;
@observable _isChildActive = false;
- componentDidMount() {
- this.layoutDoc._forceRenderEngine = "timeline";
- this.layoutDoc._replacedChrome = "replaced";
- this._childReaction = reaction(() => this.childDocs.slice(), (children) => children.forEach((child, i) => child.presentationIndex = i), { fireImmediately: true });
- }
- componentWillUnmount() {
- this._childReaction?.();
- }
-
@computed get childDocs() { return DocListCast(this.dataDoc[this.fieldKey]); }
- @computed get currentIndex() { return NumCast(this.layoutDoc._itemIndex); }
+ @computed get currentIndex() { return NumCast(this.presElement?.currentIndex); }
+ @computed get presElement() { return Cast(Doc.UserDoc().presElement, Doc, null); }
+ constructor(props: any) {
+ super(props);
+ if (!this.presElement) { // create exactly one presElmentBox template to use by any and all presentations.
+ Doc.UserDoc().presElement = new PrefetchProxy(Docs.Create.PresElementBoxDocument({
+ title: "pres element template", backgroundColor: "transparent", _xMargin: 5, _height: 46, isTemplateDoc: true, isTemplateForField: "data"
+ }));
+ // this script will be called by each presElement to get rendering-specific info that the PresBox knows about but which isn't written to the PresElement
+ // this is a design choice -- we could write this data to the presElements which would require a reaction to keep it up to date, and it would prevent
+ // the preselement docs from being part of multiple presentations since they would all have the same field, or we'd have to keep per-presentation data
+ // stored on each pres element.
+ (this.presElement as Doc).lookupField = ScriptField.MakeScript(
+ `if (field === 'indexInPres') return docList(container[container.presentationFieldKey]).indexOf(data);` +
+ "if (field === 'presCollapsedHeight') return container._viewType === CollectionViewType.Stacking ? 50 : 46;" +
+ "return undefined;", { field: "string", data: Doc.name, container: Doc.name });
+ }
+ this.props.Document.presentationFieldKey = this.fieldKey; // provide info to the presElement script so that it can look up rendering information about the presBox
+ }
- updateCurrentPresentation = action(() => Doc.UserDoc().activePresentation = this.rootDoc);
+ componentDidMount() {
+ this.rootDoc.presBox = this.rootDoc;
+ this.rootDoc._forceRenderEngine = "timeline";
+ this.rootDoc._replacedChrome = "replaced";
+ }
+ updateCurrentPresentation = () => Doc.UserDoc().activePresentation = this.rootDoc;
+ @undoBatch
+ @action
next = () => {
this.updateCurrentPresentation();
if (this.childDocs[this.currentIndex + 1] !== undefined) {
@@ -66,6 +74,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
}
}
}
+
+ @undoBatch
+ @action
back = () => {
this.updateCurrentPresentation();
const docAtCurrent = this.childDocs[this.currentIndex];
@@ -83,10 +94,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
}
}
- whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive));
- active = (outsideReaction?: boolean) => ((InkingControl.Instance.selectedTool === InkTool.None && !this.layoutDoc.isBackground) &&
- (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
-
/**
* This is the method that checks for the actions that need to be performed
* after the document has been presented, which involves 3 button options:
@@ -95,15 +102,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
showAfterPresented = (index: number) => {
this.updateCurrentPresentation();
this.childDocs.forEach((doc, ind) => {
+ const presTargetDoc = doc.presentationTargetDoc as Doc;
//the order of cases is aligned based on priority
- if (doc.hideTillShownButton && ind <= index) {
- (doc.presentationTargetDoc as Doc).opacity = 1;
+ if (doc.presHideTillShownButton && ind <= index) {
+ presTargetDoc.opacity = 1;
}
- if (doc.hideAfterButton && ind < index) {
- (doc.presentationTargetDoc as Doc).opacity = 0;
+ if (doc.presHideAfterButton && ind < index) {
+ presTargetDoc.opacity = 0;
}
- if (doc.fadeButton && ind < index) {
- (doc.presentationTargetDoc as Doc).opacity = 0.5;
+ if (doc.presFadeButton && ind < index) {
+ presTargetDoc.opacity = 0.5;
}
});
}
@@ -117,15 +125,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
this.updateCurrentPresentation();
this.childDocs.forEach((key, ind) => {
//the order of cases is aligned based on priority
-
+ const presTargetDoc = key.presentationTargetDoc as Doc;
if (key.hideAfterButton && ind >= index) {
- (key.presentationTargetDoc as Doc).opacity = 1;
+ presTargetDoc.opacity = 1;
}
if (key.fadeButton && ind >= index) {
- (key.presentationTargetDoc as Doc).opacity = 1;
+ presTargetDoc.opacity = 1;
}
if (key.hideTillShownButton && ind > index) {
- (key.presentationTargetDoc as Doc).opacity = 0;
+ presTargetDoc.opacity = 0;
}
});
}
@@ -151,11 +159,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
}
currentDocGroups.forEach((doc: Doc, index: number) => {
- if (doc.navButton) {
+ if (doc.presNavButton) {
docToJump = doc;
willZoom = false;
}
- if (doc.zoomButton) {
+ if (doc.presZoomButton) {
docToJump = doc;
willZoom = true;
}
@@ -166,10 +174,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
const srcContext = aliasOf && await DocCastAsync(aliasOf.context);
if (docToJump === curDoc) {
//checking if curDoc has navigation open
- const target = await DocCastAsync(curDoc.presentationTargetDoc);
- if (curDoc.navButton && target) {
+ const target = (await DocCastAsync(curDoc.presentationTargetDoc)) || curDoc;
+ if (curDoc.presNavButton && target) {
DocumentManager.Instance.jumpToDocument(target, false, undefined, srcContext);
- } else if (curDoc.zoomButton && target) {
+ } else if (curDoc.presZoomButton && target) {
//awaiting jump so that new scale can be found, since jumping is async
await DocumentManager.Instance.jumpToDocument(target, true, undefined, srcContext);
}
@@ -180,22 +188,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
}
}
-
- @undoBatch
- public removeDocument = (doc: Doc) => {
- return Doc.RemoveDocFromList(this.dataDoc, this.fieldKey, doc);
- }
-
//The function that is called when a document is clicked or reached through next or back.
//it'll also execute the necessary actions if presentation is playing.
public gotoDocument = (index: number, fromDoc: number) => {
this.updateCurrentPresentation();
Doc.UnBrushAllDocs();
if (index >= 0 && index < this.childDocs.length) {
- this.layoutDoc._itemIndex = index;
+ this.presElement.currentIndex = index;
- if (!this.layoutDoc.presStatus) {
- this.layoutDoc.presStatus = true;
+ if (!this.presElement.presStatus) {
+ this.presElement.presStatus = true;
this.startPresentation(index);
}
@@ -208,29 +210,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
//The function that starts or resets presentaton functionally, depending on status flag.
startOrResetPres = () => {
this.updateCurrentPresentation();
- if (this.layoutDoc.presStatus) {
+ if (this.presElement.presStatus) {
this.resetPresentation();
} else {
- this.layoutDoc.presStatus = true;
+ this.presElement.presStatus = true;
this.startPresentation(0);
this.gotoDocument(0, this.currentIndex);
}
}
- addDocument = (doc: Doc) => {
- const newPinDoc = Doc.MakeAlias(doc);
- newPinDoc.presentationTargetDoc = doc;
- return Doc.AddDocToList(this.dataDoc, this.fieldKey, newPinDoc);
- }
-
-
//The function that resets the presentation by removing every action done by it. It also
//stops the presentaton.
resetPresentation = () => {
this.updateCurrentPresentation();
this.childDocs.forEach(doc => (doc.presentationTargetDoc as Doc).opacity = 1);
- this.layoutDoc._itemIndex = 0;
- this.layoutDoc.presStatus = false;
+ this.rootDoc._itemIndex = 0;
+ this.presElement.presStatus = false;
}
//The function that starts the presentation, also checking if actions should be applied
@@ -238,88 +233,90 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
startPresentation = (startIndex: number) => {
this.updateCurrentPresentation();
this.childDocs.map(doc => {
- if (doc.hideTillShownButton && this.childDocs.indexOf(doc) > startIndex) {
- (doc.presentationTargetDoc as Doc).opacity = 0;
+ const presTargetDoc = doc.presentationTargetDoc as Doc;
+ if (doc.presHideTillShownButton && this.childDocs.indexOf(doc) > startIndex) {
+ presTargetDoc.opacity = 0;
}
- if (doc.hideAfterButton && this.childDocs.indexOf(doc) < startIndex) {
- (doc.presentationTargetDoc as Doc).opacity = 0;
+ if (doc.presHideAfterButton && this.childDocs.indexOf(doc) < startIndex) {
+ presTargetDoc.opacity = 0;
}
- if (doc.fadeButton && this.childDocs.indexOf(doc) < startIndex) {
- (doc.presentationTargetDoc as Doc).opacity = 0.5;
+ if (doc.presFadeButton && this.childDocs.indexOf(doc) < startIndex) {
+ presTargetDoc.opacity = 0.5;
}
});
}
- updateMinimize = undoBatch(action((e: React.ChangeEvent, mode: CollectionViewType) => {
+ updateMinimize = action((e: React.ChangeEvent, mode: CollectionViewType) => {
if (BoolCast(this.layoutDoc.inOverlay) !== (mode === CollectionViewType.Invalid)) {
if (this.layoutDoc.inOverlay) {
Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocuments as Doc), undefined, this.rootDoc);
CollectionDockingView.AddRightSplit(this.rootDoc);
this.layoutDoc.inOverlay = false;
} else {
- this.layoutDoc.x = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0)[0];// 500;//e.clientX + 25;
- this.layoutDoc.y = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0)[1];////e.clientY - 25;
+ const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
+ this.rootDoc.x = pt[0];// 500;//e.clientX + 25;
+ this.rootDoc.y = pt[1];////e.clientY - 25;
this.props.addDocTab?.(this.rootDoc, "close");
Doc.AddDocToList((Doc.UserDoc().myOverlayDocuments as Doc), undefined, this.rootDoc);
}
}
- }));
-
- initializeViewAliases = (docList: Doc[], viewtype: CollectionViewType) => {
- const hgt = (viewtype === CollectionViewType.Tree) ? 50 : 46;
- docList.forEach(doc => {
- doc.presBox = this.rootDoc; // give contained documents a reference to the presentation
- doc.collapsedHeight = hgt; // set the collpased height for documents based on the type of view (Tree or Stack) they will be displaye din
- });
- }
-
- selectElement = (doc: Doc) => {
- this.gotoDocument(this.childDocs.indexOf(doc), NumCast(this.layoutDoc._itemIndex));
- }
-
- getTransform = () => {
- return this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight
- }
- panelHeight = () => {
- return this.props.PanelHeight() - 20;
- }
+ });
@undoBatch
viewChanged = action((e: React.ChangeEvent) => {
//@ts-ignore
- this.layoutDoc._viewType = e.target.selectedOptions[0].value;
- this.layoutDoc._viewType === CollectionViewType.Stacking && (this.layoutDoc._pivotField = undefined); // pivot field may be set by the user in timeline view (or some other way) -- need to reset it here
- this.updateMinimize(e, StrCast(this.layoutDoc._viewType));
+ const viewType = e.target.selectedOptions[0].value as CollectionViewType;
+ viewType === CollectionViewType.Stacking && (this.rootDoc._pivotField = undefined); // pivot field may be set by the user in timeline view (or some other way) -- need to reset it here
+ this.updateMinimize(e, this.rootDoc._viewType = viewType);
});
- childLayoutTemplate = () => this.layoutDoc._viewType === CollectionViewType.Stacking ? Cast(Doc.UserDoc()["template-presentation"], Doc, null) : undefined;
+ whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive));
+ addDocumentFilter = (doc: Doc) => {
+ doc.aliasOf instanceof Doc && (doc.presentationTargetDoc = doc.aliasOf);
+ !this.childDocs.includes(doc) && (doc.presZoomButton = true);
+ return true;
+ }
+ childLayoutTemplate = () => this.rootDoc._viewType !== CollectionViewType.Stacking ? undefined : this.presElement;
+ removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.dataDoc, this.fieldKey, doc);
+ selectElement = (doc: Doc) => this.gotoDocument(this.childDocs.indexOf(doc), NumCast(this.rootDoc._itemIndex));
+ getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight
+ panelHeight = () => this.props.PanelHeight() - 20;
+ active = (outsideReaction?: boolean) => ((InkingControl.Instance.selectedTool === InkTool.None && !this.layoutDoc.isBackground) &&
+ (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
+
render() {
- const mode = StrCast(this.layoutDoc._viewType) as CollectionViewType;
- this.initializeViewAliases(this.childDocs, mode);
- return <div className="presBox-cont" style={{ minWidth: this.layoutDoc.inOverlay ? 240 : undefined, pointerEvents: this.active() || this.layoutDoc.inOverlay ? "all" : "none" }} >
- <div className="presBox-buttons" style={{ display: this.layoutDoc._chromeStatus === "disabled" ? "none" : undefined }}>
- <select className="collectionViewBaseChrome-viewPicker"
+ this.rootDoc.presOrderedDocs = new List<Doc>(this.childDocs.map((child, i) => child));
+ const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
+ return <div className="presBox-cont" style={{ minWidth: this.layoutDoc.inOverlay ? 240 : undefined }} >
+ <div className="presBox-buttons" style={{ display: this.rootDoc._chromeStatus === "disabled" ? "none" : undefined }}>
+ <select className="presBox-viewPicker"
onPointerDown={e => e.stopPropagation()}
onChange={this.viewChanged}
value={mode}>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Invalid}>Min</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Stacking}>List</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Time}>Time</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Carousel}>Slides</option>
+ <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Invalid}>Min</option>
+ <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Stacking}>List</option>
+ <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Time}>Time</option>
+ <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Carousel}>Slides</option>
</select>
- <button className="presBox-button" title="Back" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
- <button className="presBox-button" title={"Reset Presentation" + this.layoutDoc.presStatus ? "" : " From Start"} onClick={this.startOrResetPres}>
+ <div className="presBox-button" title="Back" style={{ gridColumn: 2 }} onClick={this.back}>
+ <FontAwesomeIcon icon={"arrow-left"} />
+ </div>
+ <div className="presBox-button" title={"Reset Presentation" + this.layoutDoc.presStatus ? "" : " From Start"} style={{ gridColumn: 3 }} onClick={this.startOrResetPres}>
<FontAwesomeIcon icon={this.layoutDoc.presStatus ? "stop" : "play"} />
- </button>
- <button className="presBox-button" title="Next" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
+ </div>
+ <div className="presBox-button" title="Next" style={{ gridColumn: 4 }} onClick={this.next}>
+ <FontAwesomeIcon icon={"arrow-right"} />
+ </div>
</div>
<div className="presBox-listCont" >
{mode !== CollectionViewType.Invalid ?
<CollectionView {...this.props}
+ ContainingCollectionDoc={this.props.Document}
+ PanelWidth={this.props.PanelWidth}
PanelHeight={this.panelHeight}
moveDocument={returnFalse}
childLayoutTemplate={this.childLayoutTemplate}
- addDocument={this.addDocument}
+ filterAddDocument={this.addDocumentFilter}
removeDocument={returnFalse}
focus={this.selectElement}
ScreenToLocalTransform={this.getTransform} />
@@ -328,4 +325,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
</div>
</div>;
}
-} \ No newline at end of file
+}
+Scripting.addGlobal(function lookupPresBoxField(presLayout: Doc, data: Doc, fieldKey: string) {
+});
diff --git a/src/client/views/nodes/QueryBox.tsx b/src/client/views/nodes/QueryBox.tsx
index 76885eada..d248b098c 100644
--- a/src/client/views/nodes/QueryBox.tsx
+++ b/src/client/views/nodes/QueryBox.tsx
@@ -11,6 +11,7 @@ import { SearchBox } from "../search/SearchBox";
import { FieldView, FieldViewProps } from './FieldView';
import "./QueryBox.scss";
import { List } from "../../../new_fields/List";
+import { DragManager } from "../../util/DragManager";
type QueryDocument = makeInterface<[typeof documentSchema]>;
const QueryDocument = makeInterface(documentSchema);
@@ -27,7 +28,7 @@ export class QueryBox extends ViewBoxAnnotatableComponent<FieldViewProps, QueryD
}
render() {
- const dragging = !SelectionManager.GetIsDragging() ? "" : "-dragging";
+ const dragging = !DragManager.Vals.Instance.GetIsDragging() ? "" : "-dragging";
return <div className={`queryBox${dragging}`} onWheel={(e) => e.stopPropagation()} >
<SearchBox
id={this.props.Document[Id]}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 125690dc7..a0ecc9ff5 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, IReactionDisposer, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import * as rp from 'request-promise';
-import { documentSchema, positionSchema } from "../../../new_fields/documentSchemas";
+import { documentSchema } from "../../../new_fields/documentSchemas";
import { makeInterface } from "../../../new_fields/Schema";
import { Cast, NumCast } from "../../../new_fields/Types";
import { VideoField } from "../../../new_fields/URLField";
@@ -20,8 +20,8 @@ import { FieldView, FieldViewProps } from './FieldView';
import "./ScreenshotBox.scss";
const path = require('path');
-type ScreenshotDocument = makeInterface<[typeof documentSchema, typeof positionSchema]>;
-const ScreenshotDocument = makeInterface(documentSchema, positionSchema);
+type ScreenshotDocument = makeInterface<[typeof documentSchema]>;
+const ScreenshotDocument = makeInterface(documentSchema);
library.add(faVideo);
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 613929bca..266b7f43f 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -21,14 +21,14 @@ import { DocumentDecorations } from "../DocumentDecorations";
import { InkingControl } from "../InkingControl";
import { FieldView, FieldViewProps } from './FieldView';
import "./VideoBox.scss";
-import { documentSchema, positionSchema } from "../../../new_fields/documentSchemas";
+import { documentSchema } from "../../../new_fields/documentSchemas";
const path = require('path');
export const timeSchema = createSchema({
currentTimecode: "number", // the current time of a video or other linear, time-based document. Note, should really get set on an extension field, but that's more complicated when it needs to be set since the extension doc needs to be found first
});
-type VideoDocument = makeInterface<[typeof documentSchema, typeof positionSchema, typeof timeSchema]>;
-const VideoDocument = makeInterface(documentSchema, positionSchema, timeSchema);
+type VideoDocument = makeInterface<[typeof documentSchema, typeof timeSchema]>;
+const VideoDocument = makeInterface(documentSchema, timeSchema);
library.add(faVideo);
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
new file mode 100644
index 000000000..d94fe7fc6
--- /dev/null
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -0,0 +1,95 @@
+import { IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { baseKeymap, toggleMark } from "prosemirror-commands";
+import { redo, undo } from "prosemirror-history";
+import { keymap } from "prosemirror-keymap";
+import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
+import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
+import { EditorState, NodeSelection, Plugin, TextSelection } from "prosemirror-state";
+import { StepMap } from "prosemirror-transform";
+import { EditorView } from "prosemirror-view";
+import * as ReactDOM from 'react-dom';
+import { Doc, DocListCast, Field, HeightSym, WidthSym } from "../../../../new_fields/Doc";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { List } from "../../../../new_fields/List";
+import { ObjectField } from "../../../../new_fields/ObjectField";
+import { listSpec } from "../../../../new_fields/Schema";
+import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField";
+import { ComputedField } from "../../../../new_fields/ScriptField";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils, returnZero } from "../../../../Utils";
+import { DocServer } from "../../../DocServer";
+
+import React = require("react");
+
+import { schema } from "./schema_rts";
+
+interface IDashDocCommentView {
+ node: any;
+ view: any;
+ getPos: any;
+}
+
+export class DashDocCommentView extends React.Component<IDashDocCommentView>{
+ constructor(props: IDashDocCommentView) {
+ super(props);
+ }
+
+ targetNode = () => { // search forward in the prosemirror doc for the attached dashDocNode that is the target of the comment anchor
+ for (let i = this.props.getPos() + 1; i < this.props.view.state.doc.content.size; i++) {
+ const m = this.props.view.state.doc.nodeAt(i);
+ if (m && m.type === this.props.view.state.schema.nodes.dashDoc && m.attrs.docid === this.props.node.attrs.docid) {
+ return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any, pos: number, hidden: boolean };
+ }
+ }
+ const dashDoc = this.props.view.state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: this.props.node.attrs.docid, float: "right" });
+ this.props.view.dispatch(this.props.view.state.tr.insert(this.props.getPos() + 1, dashDoc));
+ setTimeout(() => { try { this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + 2))); } catch (e) { } }, 0);
+ return undefined;
+ }
+
+ onPointerDownCollapse = (e: any) => e.stopPropagation();
+
+ onPointerUpCollapse = (e: any) => {
+ const target = this.targetNode();
+ if (target) {
+ const expand = target.hidden;
+ const tr = this.props.view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true });
+ this.props.view.dispatch(tr.setSelection(TextSelection.create(tr.doc, this.props.getPos() + (expand ? 2 : 1)))); // update the attrs
+ setTimeout(() => {
+ expand && DocServer.GetRefField(this.props.node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
+ try { this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + (expand ? 2 : 1)))); } catch (e) { }
+ }, 0);
+ }
+ e.stopPropagation();
+ }
+
+ onPointerEnterCollapse = (e: any) => {
+ DocServer.GetRefField(this.props.node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ onPointerLeaveCollapse = (e: any) => {
+ DocServer.GetRefField(this.props.node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ render() {
+
+ const collapsedId = "DashDocCommentView-" + this.props.node.attrs.docid;
+
+ return (
+ <span
+ className="formattedTextBox-inlineComment"
+ id={collapsedId}
+ onPointerDown={this.onPointerDownCollapse}
+ onPointerUp={this.onPointerUpCollapse}
+ onPointerEnter={this.onPointerEnterCollapse}
+ onPointerLeave={this.onPointerLeaveCollapse}
+ >
+
+ </span >
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
new file mode 100644
index 000000000..7130fee2b
--- /dev/null
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -0,0 +1,269 @@
+import { IReactionDisposer, reaction } from "mobx";
+import { NodeSelection } from "prosemirror-state";
+import { Doc, HeightSym, WidthSym } from "../../../../new_fields/Doc";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { ObjectField } from "../../../../new_fields/ObjectField";
+import { ComputedField } from "../../../../new_fields/ScriptField";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { emptyFunction, returnEmptyString, returnFalse, Utils, returnZero } from "../../../../Utils";
+import { DocServer } from "../../../DocServer";
+import { Docs } from "../../../documents/Documents";
+import { DocumentView } from "../DocumentView";
+import { FormattedTextBox } from "./FormattedTextBox";
+import { Transform } from "../../../util/Transform";
+import React = require("react");
+
+interface IDashDocView {
+ node: any;
+ view: any;
+ getPos: any;
+ tbox?: FormattedTextBox;
+ self: any;
+}
+
+export class DashDocView extends React.Component<IDashDocView> {
+
+ _dashDoc: Doc | undefined;
+ _reactionDisposer: IReactionDisposer | undefined;
+ _renderDisposer: IReactionDisposer | undefined;
+ _textBox: FormattedTextBox;
+ _finalLayout: any;
+ _resolvedDataDoc: any;
+
+
+ // constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+
+ constructor(props: IDashDocView) {
+ super(props);
+
+ const node = this.props.node;
+ this._textBox = this.props.tbox as FormattedTextBox;
+
+ const alias = node.attrs.alias;
+ const docid = node.attrs.docid || this._textBox.props.Document[Id];
+
+ DocServer.GetRefField(docid + alias).then(async dashDoc => {
+ if (!(dashDoc instanceof Doc)) {
+ alias && DocServer.GetRefField(docid).then(async dashDocBase => {
+ if (dashDocBase instanceof Doc) {
+ const aliasedDoc = Doc.MakeAlias(dashDocBase, docid + alias);
+ aliasedDoc.layoutKey = "layout";
+ node.attrs.fieldKey && Doc.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, node.attrs.fieldKey, undefined);
+ this._dashDoc = aliasedDoc;
+ // self.doRender(aliasedDoc, removeDoc, node, view, getPos);
+ }
+ });
+ } else {
+ this._dashDoc = dashDoc;
+ // self.doRender(dashDoc, removeDoc, node, view, getPos);
+ }
+ });
+
+ this.onPointerLeave = this.onPointerLeave.bind(this);
+ this.onPointerEnter = this.onPointerEnter.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onKeyPress = this.onKeyPress.bind(this);
+ this.onKeyUp = this.onKeyUp.bind(this);
+ this.onWheel = this.onWheel.bind(this);
+ }
+ /* #region Internal functions */
+
+ removeDoc = () => {
+ const view = this.props.view;
+ const pos = this.props.getPos();
+ const ns = new NodeSelection(view.state.doc.resolve(pos));
+ view.dispatch(view.state.tr.setSelection(ns).deleteSelection());
+ return true;
+ }
+
+ getDocTransform = () => {
+ const outerElement = document.getElementById('dash-document-view-outer') as HTMLElement;
+ const { scale, translateX, translateY } = Utils.GetScreenTransform(outerElement);
+ return new Transform(-translateX, -translateY, 1).scale(1 / this.contentScaling() / scale);
+ }
+ contentScaling = () => NumCast(this._dashDoc!._nativeWidth) > 0 ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!._nativeWidth) : 1;
+
+ outerFocus = (target: Doc) => this._textBox.props.focus(this._textBox.props.Document); // ideally, this would scroll to show the focus target
+
+ onKeyPress = (e: any) => {
+ e.stopPropagation();
+ }
+ onWheel = (e: any) => {
+ e.preventDefault();
+ }
+ onKeyUp = (e: any) => {
+ e.stopPropagation();
+ }
+ onKeyDown = (e: any) => {
+ e.stopPropagation();
+ if (e.key === "Tab" || e.key === "Enter") {
+ e.preventDefault();
+ }
+ }
+ onPointerLeave = () => {
+ const ele = document.getElementById("DashDocCommentView-" + this.props.node.attrs.docid);
+ if (ele) {
+ (ele as HTMLDivElement).style.backgroundColor = "";
+ }
+ }
+ onPointerEnter = () => {
+ const ele = document.getElementById("DashDocCommentView-" + this.props.node.attrs.docid);
+ if (ele) {
+ (ele as HTMLDivElement).style.backgroundColor = "orange";
+ }
+ }
+ /*endregion*/
+
+ componentWillMount = () => {
+ this._reactionDisposer?.();
+ }
+
+ componentDidUpdate = () => {
+
+ this._renderDisposer?.();
+ this._renderDisposer = reaction(() => {
+
+ const dashDoc = this._dashDoc as Doc;
+ const dashLayoutDoc = Doc.Layout(dashDoc);
+ const finalLayout = this.props.node.attrs.docid ? dashDoc : Doc.expandTemplateLayout(dashLayoutDoc, dashDoc, this.props.node.attrs.fieldKey);
+
+ if (finalLayout) {
+ if (!Doc.AreProtosEqual(finalLayout, dashDoc)) {
+ finalLayout.rootDocument = dashDoc.aliasOf;
+ }
+ const layoutKey = StrCast(finalLayout.layoutKey);
+ const finalKey = layoutKey && StrCast(finalLayout[layoutKey]).split("'")?.[1];
+ if (finalLayout !== dashDoc && finalKey) {
+ const finalLayoutField = finalLayout[finalKey];
+ if (finalLayoutField instanceof ObjectField) {
+ finalLayout[finalKey + "-textTemplate"] = ComputedField.MakeFunction(`copyField(this.${finalKey})`, { this: Doc.name });
+ }
+ }
+ this._finalLayout = finalLayout;
+ this._resolvedDataDoc = Cast(finalLayout.resolvedDataDoc, Doc, null);
+ return { finalLayout, resolvedDataDoc: Cast(finalLayout.resolvedDataDoc, Doc, null) };
+ }
+ },
+ (res) => {
+
+ if (res) {
+ this._finalLayout = res.finalLayout;
+ this._resolvedDataDoc = res.resolvedDataDoc;
+
+ this.forceUpdate(); // doReactRender(res.finalLayout, res.resolvedDataDoc),
+ }
+ },
+ { fireImmediately: true });
+
+ }
+
+ render() {
+ // doRender(dashDoc: Doc, removeDoc: any, node: any, view: any, getPos: any) {
+
+ const node = this.props.node;
+ const view = this.props.view;
+ const getPos = this.props.getPos;
+
+ const spanStyle = {
+ width: this.props.node.props.width,
+ height: this.props.node.props.height,
+ position: 'absolute' as 'absolute',
+ display: 'inline-block'
+ };
+
+
+ const outerStyle = {
+ position: "relative" as "relative",
+ textIndent: "0",
+ border: "1px solid " + StrCast(this._textBox.Document.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray")),
+ width: this.props.node.props.width,
+ height: this.props.node.props.height,
+ display: this.props.node.props.hidden ? "none" : "inline-block",
+ float: this.props.node.props.float,
+ };
+
+ const dashDoc = this._dashDoc as Doc;
+ const self = this;
+ const dashLayoutDoc = Doc.Layout(dashDoc);
+ const finalLayout = node.attrs.docid ? dashDoc : Doc.expandTemplateLayout(dashLayoutDoc, dashDoc, node.attrs.fieldKey);
+ const resolvedDataDoc = this._resolvedDataDoc; //Added this
+
+ if (!finalLayout) {
+ return <div></div>;
+ // if (!finalLayout) setTimeout(() => self.doRender(dashDoc, removeDoc, node, view, getPos), 0);
+ } else {
+
+ this._reactionDisposer?.();
+ this._reactionDisposer = reaction(() =>
+ ({
+ dim: [finalLayout[WidthSym](), finalLayout[HeightSym]()],
+ color: finalLayout.color
+ }),
+ ({ dim, color }) => {
+ spanStyle.width = outerStyle.width = Math.max(20, dim[0]) + "px";
+ spanStyle.height = outerStyle.height = Math.max(20, dim[1]) + "px";
+ outerStyle.border = "1px solid " + StrCast(finalLayout.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray"));
+ }, { fireImmediately: true });
+
+ if (node.attrs.width !== dashDoc._width + "px" || node.attrs.height !== dashDoc._height + "px") {
+ try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made
+ view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc._width + "px", height: dashDoc._height + "px" }));
+ } catch (e) {
+ console.log(e);
+ }
+ }
+
+
+ //const doReactRender = (finalLayout: Doc, resolvedDataDoc: Doc) => {
+ // ReactDOM.unmountComponentAtNode(this._dashSpan);
+
+ return (
+ <span id="dash-document-view-outer"
+ className="outer"
+ style={outerStyle}
+ >
+ <div id="dashSpan"
+ className="dash-span"
+ style={spanStyle}
+ onPointerLeave={this.onPointerLeave}
+ onPointerEnter={this.onPointerEnter}
+ onKeyDown={this.onKeyDown}
+ onKeyPress={this.onKeyPress}
+ onKeyUp={this.onKeyUp}
+ onWheel={this.onWheel}
+ >
+ <DocumentView
+ Document={finalLayout}
+ DataDoc={resolvedDataDoc}
+ LibraryPath={this._textBox.props.LibraryPath}
+ fitToBox={BoolCast(dashDoc._fitToBox)}
+ addDocument={returnFalse}
+ rootSelected={this._textBox.props.isSelected}
+ removeDocument={this.removeDoc}
+ ScreenToLocalTransform={this.getDocTransform}
+ addDocTab={this._textBox.props.addDocTab}
+ pinToPres={returnFalse}
+ renderDepth={self._textBox.props.renderDepth + 1}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ PanelWidth={finalLayout[WidthSym]}
+ PanelHeight={finalLayout[HeightSym]}
+ focus={this.outerFocus}
+ backgroundColor={returnEmptyString}
+ parentActive={returnFalse}
+ whenActiveChanged={returnFalse}
+ bringToFront={emptyFunction}
+ dontRegisterView={false}
+ ContainingCollectionView={this._textBox.props.ContainingCollectionView}
+ ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc}
+ ContentScaling={this.contentScaling}
+ />
+
+ </div>
+ </span>
+ );
+
+ }
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss
new file mode 100644
index 000000000..35ff9c1e6
--- /dev/null
+++ b/src/client/views/nodes/formattedText/DashFieldView.scss
@@ -0,0 +1,36 @@
+.dashFieldView {
+ position: relative;
+ display: inline-block;
+
+ .dashFieldView-enumerables {
+ width: 10px;
+ height: 10px;
+ position: relative;
+ display: inline-block;
+ background: dimGray;
+ }
+ .dashFieldView-fieldCheck {
+ min-width: 12px;
+ position: relative;
+ display: inline-block;
+ background-color: rgba(155, 155, 155, 0.24);
+ }
+ .dashFieldView-labelSpan {
+ position: relative;
+ display: inline-block;
+ font-size: small;
+ }
+ .dashFieldView-fieldSpan {
+ min-width: 20px;
+ margin-left: 2px;
+ margin-right: 5px;
+ position: relative;
+ display: inline-block;
+ background-color: rgba(155, 155, 155, 0.24);
+ span {
+ min-width: 100%;
+ display: inline-block;
+ }
+ }
+}
+ \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
new file mode 100644
index 000000000..d87d6e424
--- /dev/null
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -0,0 +1,207 @@
+import { IReactionDisposer, observable, runInAction, computed, action } from "mobx";
+import { Doc, DocListCast, Field } from "../../../../new_fields/Doc";
+import { List } from "../../../../new_fields/List";
+import { listSpec } from "../../../../new_fields/Schema";
+import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField";
+import { ComputedField } from "../../../../new_fields/ScriptField";
+import { Cast, StrCast } from "../../../../new_fields/Types";
+import { DocServer } from "../../../DocServer";
+import { CollectionViewType } from "../../collections/CollectionView";
+import { FormattedTextBox } from "./FormattedTextBox";
+import React = require("react");
+import * as ReactDOM from 'react-dom';
+import "./DashFieldView.scss";
+import { observer } from "mobx-react";
+
+
+export class DashFieldView {
+ _fieldWrapper: HTMLDivElement; // container for label and value
+
+ constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+ this._fieldWrapper = document.createElement("div");
+ this._fieldWrapper.style.width = node.attrs.width;
+ this._fieldWrapper.style.height = node.attrs.height;
+ this._fieldWrapper.style.fontWeight = "bold";
+ this._fieldWrapper.style.position = "relative";
+ this._fieldWrapper.style.display = "inline-block";
+ this._fieldWrapper.onkeypress = function (e: any) { e.stopPropagation(); };
+ this._fieldWrapper.onkeydown = function (e: any) { e.stopPropagation(); };
+ this._fieldWrapper.onkeyup = function (e: any) { e.stopPropagation(); };
+ this._fieldWrapper.onmousedown = function (e: any) { e.stopPropagation(); };
+
+ ReactDOM.render(<DashFieldViewInternal
+ fieldKey={node.attrs.fieldKey}
+ docid={node.attrs.docid}
+ width={node.attrs.width}
+ height={node.attrs.height}
+ tbox={tbox}
+ />, this._fieldWrapper);
+ (this as any).dom = this._fieldWrapper;
+ }
+ destroy() {
+ ReactDOM.unmountComponentAtNode(this._fieldWrapper);
+ }
+ selectNode() { }
+
+}
+interface IDashFieldViewInternal {
+ fieldKey: string;
+ docid: string;
+ tbox: FormattedTextBox;
+ width: number;
+ height: number;
+}
+
+@observer
+export class DashFieldViewInternal extends React.Component<IDashFieldViewInternal> {
+ _reactionDisposer: IReactionDisposer | undefined;
+ _textBoxDoc: Doc;
+ _fieldKey: string;
+ _fieldStringRef = React.createRef<HTMLSpanElement>();
+ @observable _showEnumerables: boolean = false;
+ @observable _dashDoc: Doc | undefined;
+
+ constructor(props: IDashFieldViewInternal) {
+ super(props);
+ this._fieldKey = this.props.fieldKey;
+ this._textBoxDoc = this.props.tbox.props.Document;
+
+ if (this.props.docid) {
+ DocServer.GetRefField(this.props.docid).
+ then(action(async dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc)));
+ } else {
+ this._dashDoc = this.props.tbox.props.DataDoc || this.props.tbox.dataDoc;
+ }
+ }
+ componentWillUnmount() {
+ this._reactionDisposer?.();
+ }
+
+ // set the display of the field's value (checkbox for booleans, span of text for strings)
+ @computed get fieldValueContent() {
+ if (this._dashDoc) {
+ const dashVal = this._dashDoc[this._fieldKey];
+ const fval = StrCast(dashVal).startsWith(":=") || dashVal === "" ? Doc.Layout(this._textBoxDoc)[this._fieldKey] : dashVal;
+ const boolVal = Cast(fval, "boolean", null);
+ const strVal = Field.toString(fval as Field) || "";
+
+ // field value is a boolean, so use a checkbox or similar widget to display it
+ if (boolVal === true || boolVal === false) {
+ return <input
+ className="dashFieldView-fieldCheck"
+ type="checkbox" checked={boolVal}
+ onChange={e => this._dashDoc![this._fieldKey] = e.target.checked}
+ />;
+ }
+ else // field value is a string, so display it as an editable span
+ {
+ // bcz: this is unfortunate, but since this React component is nested within a non-React text box (prosemirror), we can't
+ // use React events. Essentially, React events occur after native events have been processed, so corresponding React events
+ // will never fire because Prosemirror has handled the native events. So we add listeners for native events here.
+ return <span contentEditable={true} suppressContentEditableWarning={true} defaultValue={strVal} ref={r => {
+ r?.addEventListener("keydown", e => this.fieldSpanKeyDown(e, r));
+ r?.addEventListener("blur", e => r && this.updateText(r.textContent!, false));
+ r?.addEventListener("pointerdown", action((e) => this._showEnumerables = true));
+ }} >
+ {strVal}
+ </span>;
+ }
+ }
+ }
+
+ // we need to handle all key events on the input span or else they will propagate to prosemirror.
+ @action
+ fieldSpanKeyDown = (e: KeyboardEvent, span: HTMLSpanElement) => {
+ if (e.key === "Enter") { // handle the enter key by "submitting" the current text to Dash's database.
+ e.ctrlKey && Doc.addFieldEnumerations(this._textBoxDoc, this._fieldKey, [{ title: span.textContent! }]);
+ this.updateText(span.textContent!, true);
+ e.preventDefault();// prevent default to avoid a newline from being generated and wiping out this field view
+ }
+ if (e.key === "a" && (e.ctrlKey || e.metaKey)) { // handle ctrl-A to select all the text within the span
+ if (window.getSelection) {
+ const range = document.createRange();
+ range.selectNodeContents(span);
+ window.getSelection()!.removeAllRanges();
+ window.getSelection()!.addRange(range);
+ }
+ e.preventDefault(); //prevent default so that all the text in the prosemirror text box isn't selected
+ }
+ e.stopPropagation(); // we need to handle all events or else they will propagate to prosemirror.
+ }
+
+ @action
+ updateText = (nodeText: string, forceMatch: boolean) => {
+ this._showEnumerables = false;
+ if (nodeText) {
+ const newText = nodeText.startsWith(":=") || nodeText.startsWith("=:=") ? ":=-computed-" : nodeText;
+
+ // look for a document whose id === the fieldKey being displayed. If there's a match, then that document
+ // holds the different enumerated values for the field in the titles of its collected documents.
+ // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down.
+ DocServer.GetRefField(this._fieldKey).then(options => {
+ let modText = "";
+ (options instanceof Doc) && DocListCast(options.data).forEach(opt => (forceMatch ? StrCast(opt.title).startsWith(newText) : StrCast(opt.title) === newText) && (modText = StrCast(opt.title)));
+ if (modText) {
+ // elementfieldSpan.innerHTML = this._dashDoc![this._fieldKey as string] = modText;
+ Doc.addFieldEnumerations(this._textBoxDoc, this._fieldKey, []);
+ this._dashDoc![this._fieldKey] = modText;
+ } // if the text starts with a ':=' then treat it as an expression by making a computed field from its value storing it in the key
+ else if (nodeText.startsWith(":=")) {
+ this._dashDoc![this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(2));
+ } else if (nodeText.startsWith("=:=")) {
+ Doc.Layout(this._textBoxDoc)[this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(3));
+ } else {
+ this._dashDoc![this._fieldKey] = newText;
+ }
+ });
+ }
+ }
+
+ // display a collection of all the enumerable values for this field
+ onPointerDownEnumerables = async (e: any) => {
+ e.stopPropagation();
+ const collview = await Doc.addFieldEnumerations(this._textBoxDoc, this._fieldKey, [{ title: this._fieldKey }]);
+ collview instanceof Doc && this.props.tbox.props.addDocTab(collview, "onRight");
+ }
+
+
+ // clicking on the label creates a pivot view collection of all documents
+ // in the same collection. The pivot field is the fieldKey of this label
+ onPointerDownLabelSpan = (e: any) => {
+ e.stopPropagation();
+ let container = this.props.tbox.props.ContainingCollectionView;
+ while (container?.props.Document.isTemplateForField || container?.props.Document.isTemplateDoc) {
+ container = container.props.ContainingCollectionView;
+ }
+ if (container) {
+ const alias = Doc.MakeAlias(container.props.Document);
+ alias.viewType = CollectionViewType.Time;
+ let list = Cast(alias.schemaColumns, listSpec(SchemaHeaderField));
+ if (!list) {
+ alias.schemaColumns = list = new List<SchemaHeaderField>();
+ }
+ list.map(c => c.heading).indexOf(this._fieldKey) === -1 && list.push(new SchemaHeaderField(this._fieldKey, "#f1efeb"));
+ list.map(c => c.heading).indexOf("text") === -1 && list.push(new SchemaHeaderField("text", "#f1efeb"));
+ alias._pivotField = this._fieldKey;
+ this.props.tbox.props.addDocTab(alias, "onRight");
+ }
+ }
+
+ render() {
+ return <div className="dashFieldView" style={{
+ width: this.props.width,
+ height: this.props.height,
+ }}>
+ <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}>
+ {this._fieldKey}
+ </span>
+
+ <div className="dashFieldView-fieldSpan">
+ {this.fieldValueContent}
+ </div>
+
+ {!this._showEnumerables ? (null) : <div className="dashFieldView-enumerables" onPointerDown={this.onPointerDownEnumerables} />}
+
+ </div >;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/FootnoteView.tsx b/src/client/views/nodes/formattedText/FootnoteView.tsx
new file mode 100644
index 000000000..ee21fb765
--- /dev/null
+++ b/src/client/views/nodes/formattedText/FootnoteView.tsx
@@ -0,0 +1,162 @@
+import { EditorView } from "prosemirror-view";
+import { EditorState } from "prosemirror-state";
+import { keymap } from "prosemirror-keymap";
+import { baseKeymap, toggleMark } from "prosemirror-commands";
+import { schema } from "./schema_rts";
+import { redo, undo } from "prosemirror-history";
+import { StepMap } from "prosemirror-transform";
+
+import React = require("react");
+
+interface IFootnoteView {
+ innerView: any;
+ outerView: any;
+ node: any;
+ dom: any;
+ getPos: any;
+}
+
+export class FootnoteView extends React.Component<IFootnoteView> {
+ _innerView: any;
+ _node: any;
+
+ constructor(props: IFootnoteView) {
+ super(props);
+ const node = this.props.node;
+ const outerView = this.props.outerView;
+ const _innerView = this.props.innerView;
+ const getPos = this.props.getPos;
+ }
+
+ selectNode() {
+ const attrs = { ...this.props.node.attrs };
+ attrs.visibility = true;
+ this.dom.classList.add("ProseMirror-selectednode");
+ if (!this.props.innerView) this.open();
+ }
+
+ deselectNode() {
+ const attrs = { ...this.props.node.attrs };
+ attrs.visibility = false;
+ this.dom.classList.remove("ProseMirror-selectednode");
+ if (this.props.innerView) this.close();
+ }
+ open() {
+ // Append a tooltip to the outer node
+ const tooltip = this.dom.appendChild(document.createElement("div"));
+ tooltip.className = "footnote-tooltip";
+ // And put a sub-ProseMirror into that
+ this.props.innerView.defineProperty(new EditorView(tooltip, {
+ // You can use any node as an editor document
+ state: EditorState.create({
+ doc: this.props.node,
+ plugins: [keymap(baseKeymap),
+ keymap({
+ "Mod-z": () => undo(this.props.outerView.state, this.props.outerView.dispatch),
+ "Mod-y": () => redo(this.props.outerView.state, this.props.outerView.dispatch),
+ "Mod-b": toggleMark(schema.marks.strong)
+ }),
+ // new Plugin({
+ // view(newView) {
+ // // TODO -- make this work with RichTextMenu
+ // // return FormattedTextBox.getToolTip(newView);
+ // }
+ // })
+ ],
+
+ }),
+ // This is the magic part
+ dispatchTransaction: this.dispatchInner.bind(this),
+ handleDOMEvents: {
+ pointerdown: ((view: any, e: PointerEvent) => {
+ // Kludge to prevent issues due to the fact that the whole
+ // footnote is node-selected (and thus DOM-selected) when
+ // the parent editor is focused.
+ e.stopPropagation();
+ document.addEventListener("pointerup", this.ignore, true);
+ if (this.props.outerView.hasFocus()) this.props.innerView.focus();
+ }) as any
+ }
+ }));
+ setTimeout(() => this.props.innerView && this.props.innerView.docView.setSelection(0, 0, this.props.innerView.root, true), 0);
+ }
+
+ ignore = (e: PointerEvent) => {
+ e.stopPropagation();
+ document.removeEventListener("pointerup", this.ignore, true);
+ }
+
+ dispatchInner(tr: any) {
+ const { state, transactions } = this.props.innerView.state.applyTransaction(tr);
+ this.props.innerView.updateState(state);
+
+ if (!tr.getMeta("fromOutside")) {
+ const outerTr = this.props.outerView.state.tr, offsetMap = StepMap.offset(this.props.getPos() + 1);
+ for (const transaction of transactions) {
+ const steps = transaction.steps;
+ for (const step of steps) {
+ outerTr.step(step.map(offsetMap));
+ }
+ }
+ if (outerTr.docChanged) this.props.outerView.dispatch(outerTr);
+ }
+ }
+ update(node: any) {
+ if (!node.sameMarkup(this.props.node)) return false;
+ this._node = node; //not sure
+ if (this.props.innerView) {
+ const state = this.props.innerView.state;
+ const start = node.content.findDiffStart(state.doc.content);
+ if (start !== null) {
+ let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content);
+ const overlap = start - Math.min(endA, endB);
+ if (overlap > 0) { endA += overlap; endB += overlap; }
+ this.props.innerView.dispatch(
+ state.tr
+ .replace(start, endB, node.slice(start, endA))
+ .setMeta("fromOutside", true));
+ }
+ }
+ return true;
+ }
+ onPointerUp = (e: any) => {
+ this.toggle(e);
+ }
+
+ toggle = (e: any) => {
+ e.preventDefault();
+ if (this.props.innerView) this.close();
+ else {
+ this.open();
+ }
+ }
+
+ close() {
+ this.props.innerView && this.props.innerView.destroy();
+ this._innerView = null;
+ this.dom.textContent = "";
+ }
+
+ destroy() {
+ if (this.props.innerView) this.close();
+ }
+
+ stopEvent(event: any) {
+ return this.props.innerView && this.props.innerView.dom.contains(event.target);
+ }
+
+ ignoreMutation() { return true; }
+
+
+ render() {
+ return (
+ <div
+ className="footnote"
+ onPointerUp={this.onPointerUp}>
+ <div className="footnote-tooltip" >
+
+ </div >
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 3bedb7127..477a2ca08 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -1,4 +1,4 @@
-@import "../globalCssVariables";
+@import "../../globalCssVariables";
.ProseMirror {
width: 100%;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 3d6ca66f0..658a55f51 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -12,49 +12,62 @@ import { Fragment, Mark, Node, Slice } from "prosemirror-model";
import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state";
import { ReplaceStep } from 'prosemirror-transform';
import { EditorView } from "prosemirror-view";
-import { DateField } from '../../../new_fields/DateField';
-import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
-import { documentSchema } from '../../../new_fields/documentSchemas';
-import { Id } from '../../../new_fields/FieldSymbols';
-import { InkTool } from '../../../new_fields/InkField';
-import { PrefetchProxy } from '../../../new_fields/Proxy';
-import { RichTextField } from "../../../new_fields/RichTextField";
-import { RichTextUtils } from '../../../new_fields/RichTextUtils';
-import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { Cast, DateCast, NumCast, StrCast } from "../../../new_fields/Types";
-import { TraceMobx } from '../../../new_fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils } from '../../../Utils';
-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 { DictationManager } from '../../util/DictationManager';
-import { DragManager } from "../../util/DragManager";
-import { makeTemplate } from '../../util/DropConverter';
-import buildKeymap from "../../util/ProsemirrorExampleTransfer";
-import RichTextMenu from '../../util/RichTextMenu';
-import { RichTextRules } from "../../util/RichTextRules";
-import { DashDocCommentView, DashDocView, DashFieldView, FootnoteView, ImageResizeView, OrderedListView, schema, SummaryView } from "../../util/RichTextSchema";
-import { SelectionManager } from "../../util/SelectionManager";
-import { undoBatch, UndoManager } from "../../util/UndoManager";
-import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
-import { ContextMenu } from '../ContextMenu';
-import { ContextMenuProps } from '../ContextMenuItem';
-import { ViewBoxAnnotatableComponent } from "../DocComponent";
-import { DocumentButtonBar } from '../DocumentButtonBar';
-import { InkingControl } from "../InkingControl";
-import { AudioBox } from './AudioBox';
-import { FieldView, FieldViewProps } from "./FieldView";
+import { DateField } from '../../../../new_fields/DateField';
+import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
+import { documentSchema } from '../../../../new_fields/documentSchemas';
+import { Id } from '../../../../new_fields/FieldSymbols';
+import { InkTool } from '../../../../new_fields/InkField';
+import { PrefetchProxy } from '../../../../new_fields/Proxy';
+import { RichTextField } from "../../../../new_fields/RichTextField";
+import { RichTextUtils } from '../../../../new_fields/RichTextUtils';
+import { createSchema, makeInterface } from "../../../../new_fields/Schema";
+import { Cast, DateCast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { TraceMobx } from '../../../../new_fields/util';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils } from '../../../../Utils';
+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 { DictationManager } from '../../../util/DictationManager';
+import { DragManager } from "../../../util/DragManager";
+import { makeTemplate } from '../../../util/DropConverter';
+import buildKeymap from "./ProsemirrorExampleTransfer";
+import RichTextMenu from './RichTextMenu';
+import { RichTextRules } from "./RichTextRules";
+import { DashDocCommentView, DashDocView, FootnoteView, ImageResizeView, OrderedListView, SummaryView } from "./RichTextSchema";
+// import { DashDocCommentView, DashDocView, DashFieldView, FootnoteView, SummaryView } from "./RichTextSchema";
+// import { OrderedListView } from "./RichTextSchema";
+// import { ImageResizeView } from "./ImageResizeView";
+// import { DashDocCommentView } from "./DashDocCommentView";
+// import { FootnoteView } from "./FootnoteView";
+// import { SummaryView } from "./SummaryView";
+// import { DashDocView } from "./DashDocView";
+import { DashFieldView } from "./DashFieldView";
+
+import { schema } from "./schema_rts";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { undoBatch, UndoManager } from "../../../util/UndoManager";
+import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView';
+import { ContextMenu } from '../../ContextMenu';
+import { ContextMenuProps } from '../../ContextMenuItem';
+import { ViewBoxAnnotatableComponent } from "../../DocComponent";
+import { DocumentButtonBar } from '../../DocumentButtonBar';
+import { InkingControl } from "../../InkingControl";
+import { AudioBox } from '../AudioBox';
+import { FieldView, FieldViewProps } from "../FieldView";
import "./FormattedTextBox.scss";
import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment';
import React = require("react");
+import { ScriptField } from '../../../../new_fields/ScriptField';
library.add(faEdit);
library.add(faSmile, faTextHeight, faUpload);
export interface FormattedTextBoxProps {
- hideOnLeave?: boolean;
- makeLink?: () => Opt<Doc>;
+ makeLink?: () => Opt<Doc>; // bcz: hack: notifies the text document when the container has made a link. allows the text doc to react and setup a hyeprlink for any selected text
+ hideOnLeave?: boolean; // used by DocumentView for setting caption's hide on leave (bcz: would prefer to have caption-hideOnLeave field set or something similar)
+ xMargin?: number; // used to override document's settings for xMargin --- see CollectionCarouselView
+ yMargin?: number;
}
const richTextSchema = createSchema({
@@ -185,15 +198,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const tsel = this._editorView.state.selection.$from;
tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 1000)));
const curText = state.doc.textBetween(0, state.doc.content.size, " \n");
- const curTemp = Cast(this.props.Document[this.props.fieldKey + "-textTemplate"], RichTextField);
- if (!this._applyingChange) {
+ const curTemp = Cast(this.props.Document[this.props.fieldKey + "-textTemplate"], RichTextField); // the actual text in the text box
+ const curProto = Cast(Cast(this.dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype
+ const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template
+ const json = JSON.stringify(state.toJSON());
+ if (!this._applyingChange && json.replace(/"selection":.*/, "") !== curProto?.Data.replace(/"selection":.*/, "")) {
this._applyingChange = true;
this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));
- if (!curTemp || curText) { // if no template, or there's text, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
- this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()), 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
+ if ((!curTemp && !curProto) || curText) { // 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 (curText !== curLayout?.Text) {
+ 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
+ }
} else { // if we've deleted all the text in a note driven by a template, then restore the template data
- this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(curTemp.Data)));
+ this.dataDoc[this.props.fieldKey] = undefined;
+ this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((curProto || curTemp).Data)));
this.dataDoc[this.props.fieldKey + "-noTemplate"] = undefined; // mark the data field as not being split from any template it might have
}
this._applyingChange = false;
@@ -216,7 +235,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
public hyperlinkTerms = (terms: string[], target: Doc) => {
if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) {
const res = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term));
- let tr = this._editorView.state.tr;
+ const tr = this._editorView.state.tr;
const flattened: TextSelection[] = [];
res.map(r => r.map(h => flattened.push(h)));
const lastSel = Math.min(flattened.length - 1, this._searchIndex);
@@ -406,14 +425,31 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const cm = ContextMenu.Instance;
const funcs: ContextMenuProps[] = [];
- this.props.Document.isTemplateDoc && funcs.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.props.Document), icon: "eye" });
+ this.rootDoc.isTemplateDoc && funcs.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.props.Document), icon: "eye" });
funcs.push({ description: "Reset Default Layout", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" });
- !this.props.Document.rootDocument && funcs.push({
+ !this.layoutDoc.isTemplateDoc && funcs.push({
description: "Make Template", event: () => {
- this.props.Document.isTemplateDoc = makeTemplate(this.props.Document);
+ this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc);
Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.props.Document);
}, icon: "eye"
});
+ this.layoutDoc.isTemplateDoc && funcs.push({
+ description: "Make New Template", event: () => {
+ const title = this.rootDoc.title as string;
+ this.rootDoc.layout = (this.layoutDoc as Doc).layout as string;
+ this.rootDoc.title = this.layoutDoc.isTemplateForField as string;
+ this.rootDoc.isTemplateDoc = false;
+ this.rootDoc.isTemplateForField = "";
+ this.rootDoc.layoutKey = "layout";
+ this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc, true, title);
+ setTimeout(() => {
+ this.rootDoc._width = this.layoutDoc._width || 300; // the width and height are stored on the template, since we're getting rid of the old template
+ this.rootDoc._height = this.layoutDoc._height || 200; // we need to copy them over to the root. This should probably apply to all '_' fields
+ this.rootDoc._backgroundColor = Cast(this.layoutDoc._backgroundColor, "string", null);
+ }, 10);
+ Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.rootDoc);
+ }, icon: "eye"
+ });
funcs.push({ description: "Toggle Single Line", event: () => this.props.Document._singleLine = !this.props.Document._singleLine, icon: "expand-arrows-alt" });
funcs.push({ description: "Toggle Sidebar", event: () => this.props.Document._showSidebar = !this.props.Document._showSidebar, icon: "expand-arrows-alt" });
funcs.push({ description: "Toggle Dictation Icon", event: () => this.props.Document._showAudio = !this.props.Document._showAudio, icon: "expand-arrows-alt" });
@@ -867,7 +903,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
dashComment(node, view, getPos) { return new DashDocCommentView(node, view, getPos); },
dashField(node, view, getPos) { return new DashFieldView(node, view, getPos, self); },
dashDoc(node, view, getPos) { return new DashDocView(node, view, getPos, self); },
- image(node, view, getPos) { return new ImageResizeView(node, view, getPos, self.props.addDocTab); },
+ // dashDoc(node, view, getPos) { return new DashDocView({ node, view, getPos, self }); },
+
+ // image(node, view, getPos) {
+ // //const addDocTab = this.props.addDocTab;
+ // return new ImageResizeView({ node, view, getPos, addDocTab: this.props.addDocTab });
+ // },
+ // // WAS :
+ // //image(node, view, getPos) { return new ImageResizeView(node, view, getPos, this.props.addDocTab); },
+
summary(node, view, getPos) { return new SummaryView(node, view, getPos); },
ordered_list(node, view, getPos) { return new OrderedListView(); },
footnote(node, view, getPos) { return new FootnoteView(node, view, getPos); }
@@ -1178,11 +1222,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this.layoutDoc._autoHeight = false;
}
const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.dataDoc._nativeHeight, 0);
- const dh = NumCast(this.layoutDoc._height, 0);
+ const dh = NumCast(this.rootDoc._height, 0);
const newHeight = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0));
if (Math.abs(newHeight - dh) > 1) { // bcz: Argh! without this, we get into a React crash if the same document is opened in a freeform view and in the treeview. no idea why, but after dragging the freeform document, selecting it, and selecting text, it will compute to 1 pixel higher than the treeview which causes a cycle
- this.layoutDoc._height = newHeight;
- this.dataDoc._nativeHeight = nh ? scrollHeight : undefined;
+ if (this.rootDoc !== this.layoutDoc && !this.layoutDoc.resolvedDataDoc) {
+ // 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...
+ console.log("Delayed height adjustment...");
+ setTimeout(() => {
+ this.rootDoc._height = newHeight;
+ this.dataDoc._nativeHeight = nh ? scrollHeight : undefined;
+ }, 10);
+ } else {
+ this.rootDoc._height = newHeight;
+ this.dataDoc._nativeHeight = nh ? scrollHeight : undefined;
+ }
}
}
}
@@ -1193,6 +1246,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
@computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); }
render() {
TraceMobx();
+ const style: { [key: string]: any } = {};
+ const divKeys = ["width", "height", "background"];
+ const replacer = (match: any, expr: string, offset: any, string: any) => { // bcz: this executes a script to convert a propery expression string: { script } into a value
+ return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ self: this.rootDoc, this: this.layoutDoc }).result as string || "";
+ };
+ divKeys.map((prop: string) => {
+ const p = (this.props as any)[prop] as string;
+ p && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer));
+ });
const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
const interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground;
if (this.props.isSelected()) {
@@ -1201,15 +1263,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
FormattedTextBoxComment.Hide();
}
return (
+
<div className={`formattedTextBox-cont`} ref={this._ref}
style={{
- height: this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : `calc(100% - ${this.props.ChromeHeight?.() || 0}px`,
- background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"]),
+ height: this.props.height ? this.props.height : this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : `calc(100% - ${this.props.ChromeHeight?.() || 0}px`,
+ background: this.props.background ? this.props.background : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : ""),
opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1,
- color: this.props.hideOnLeave ? "white" : "inherit",
+ color: this.props.color ? this.props.color : StrCast(this.layoutDoc[this.props.fieldKey + "-color"], this.props.hideOnLeave ? "white" : "inherit"),
pointerEvents: interactive ? "none" : undefined,
fontSize: Cast(this.layoutDoc._fontSize, "number", null),
fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit"),
+ ...style
}}
onContextMenu={this.specificContextMenu}
onKeyDown={this.onKeyPress}
@@ -1222,12 +1286,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
onMouseUp={this.onMouseUp}
onWheel={this.onPointerWheel}
onPointerEnter={action(() => this._entered = true)}
- onPointerLeave={action(() => this._entered = false)}
+ onPointerLeave={action((e: React.PointerEvent<HTMLDivElement>) => {
+ this._entered = false;
+ const target = document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y);
+ for (let child: any = target; child; child = child?.parentElement) {
+ if (child === this._ref.current!) {
+ this._entered = true;
+ }
+ }
+ })}
>
<div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }} onScroll={this.onscrolled} ref={this._scrollRef}>
<div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget}
style={{
- padding: `${NumCast(this.layoutDoc._yMargin, 0)}px ${NumCast(this.layoutDoc._xMargin, 0)}px`,
+ padding: `${NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0)}px ${NumCast(this.layoutDoc._xMargin, this.props.xMargin || 0)}px`,
pointerEvents: ((this.layoutDoc.isLinkButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined
}} />
</div>
diff --git a/src/client/views/nodes/FormattedTextBoxComment.scss b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
index 2dd63ec21..2dd63ec21 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
index 7d441a48b..9ad5aafb8 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -2,20 +2,20 @@ import { Mark, ResolvedPos } from "prosemirror-model";
import { EditorState, Plugin } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import * as ReactDOM from 'react-dom';
-import { Doc, DocCastAsync } from "../../../new_fields/Doc";
-import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
-import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath } from "../../../Utils";
-import { DocServer } from "../../DocServer";
-import { DocumentManager } from "../../util/DocumentManager";
-import { schema } from "../../util/RichTextSchema";
-import { Transform } from "../../util/Transform";
-import { ContentFittingDocumentView } from "./ContentFittingDocumentView";
+import { Doc, DocCastAsync } from "../../../../new_fields/Doc";
+import { Cast, FieldValue, NumCast } from "../../../../new_fields/Types";
+import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath, returnZero, returnOne } from "../../../../Utils";
+import { DocServer } from "../../../DocServer";
+import { DocumentManager } from "../../../util/DocumentManager";
+import { schema } from "./schema_rts";
+import { Transform } from "../../../util/Transform";
+import { ContentFittingDocumentView } from "../ContentFittingDocumentView";
import { FormattedTextBox } from "./FormattedTextBox";
import './FormattedTextBoxComment.scss';
import React = require("react");
-import { Docs } from "../../documents/Documents";
+import { Docs } from "../../../documents/Documents";
import wiki from "wikijs";
-import { DocumentType } from "../../documents/DocumentTypes";
+import { DocumentType } from "../../../documents/DocumentTypes";
export let formattedTextBoxCommentPlugin = new Plugin({
view(editorView) { return new FormattedTextBoxComment(editorView); }
@@ -192,18 +192,24 @@ export class FormattedTextBoxComment {
fitToBox={true}
moveDocument={returnFalse}
rootSelected={returnFalse}
- getTransform={Transform.Identity}
- active={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ parentActive={returnFalse}
addDocument={returnFalse}
removeDocument={returnFalse}
addDocTab={returnFalse}
pinToPres={returnFalse}
dontRegisterView={true}
+ ContainingCollectionDoc={undefined}
+ ContainingCollectionView={undefined}
renderDepth={1}
PanelWidth={() => Math.min(350, NumCast(target._width, 350))}
PanelHeight={() => Math.min(250, NumCast(target._height, 250))}
focus={emptyFunction}
whenActiveChanged={returnFalse}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
/>, FormattedTextBoxComment.tooltipText);
FormattedTextBoxComment.tooltip.style.width = NumCast(target.width) ? `${NumCast(target.width)}` : "100%";
FormattedTextBoxComment.tooltip.style.height = NumCast(target.height) ? `${NumCast(target.height)}` : "100%";
diff --git a/src/client/views/nodes/formattedText/ImageResizeView.tsx b/src/client/views/nodes/formattedText/ImageResizeView.tsx
new file mode 100644
index 000000000..8f98da0fd
--- /dev/null
+++ b/src/client/views/nodes/formattedText/ImageResizeView.tsx
@@ -0,0 +1,138 @@
+import { NodeSelection } from "prosemirror-state";
+import { Doc } from "../../../../new_fields/Doc";
+import { DocServer } from "../../../DocServer";
+import { DocumentManager } from "../../../util/DocumentManager";
+import React = require("react");
+
+import { schema } from "./schema_rts";
+
+interface IImageResizeView {
+ node: any;
+ view: any;
+ getPos: any;
+ addDocTab: any;
+}
+
+export class ImageResizeView extends React.Component<IImageResizeView> {
+ constructor(props: IImageResizeView) {
+ super(props);
+ }
+
+ onClickImg = (e: any) => {
+ e.stopPropagation();
+ e.preventDefault();
+ if (this.props.view.state.selection.node && this.props.view.state.selection.node.type !== this.props.view.state.schema.nodes.image) {
+ this.props.view.dispatch(this.props.view.state.tr.setSelection(new NodeSelection(this.props.view.state.doc.resolve(this.props.view.state.selection.from - 2))));
+ }
+ }
+
+ onPointerDownImg = (e: any) => {
+ if (e.ctrlKey) {
+ e.preventDefault();
+ e.stopPropagation();
+ DocServer.GetRefField(this.props.node.attrs.docid).then(async linkDoc =>
+ (linkDoc instanceof Doc) &&
+ DocumentManager.Instance.FollowLink(linkDoc, this.props.view.state.schema.Document,
+ document => this.props.addDocTab(document, this.props.node.attrs.location ? this.props.node.attrs.location : "inTab"), false));
+ }
+ }
+
+ onPointerDownHandle = (e: any) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const elementImage = document.getElementById("imageId") as HTMLElement;
+ const wid = Number(getComputedStyle(elementImage).width.replace(/px/, ""));
+ const hgt = Number(getComputedStyle(elementImage).height.replace(/px/, ""));
+ const startX = e.pageX;
+ const startWidth = parseFloat(this.props.node.attrs.width);
+
+ const onpointermove = (e: any) => {
+ const elementOuter = document.getElementById("outerId") as HTMLElement;
+
+ const currentX = e.pageX;
+ const diffInPx = currentX - startX;
+ elementOuter.style.width = `${startWidth + diffInPx}`;
+ elementOuter.style.height = `${(startWidth + diffInPx) * hgt / wid}`;
+ };
+
+ const onpointerup = () => {
+ document.removeEventListener("pointermove", onpointermove);
+ document.removeEventListener("pointerup", onpointerup);
+ const pos = this.props.view.state.selection.from;
+ const elementOuter = document.getElementById("outerId") as HTMLElement;
+ this.props.view.dispatch(this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, { ...this.props.node.attrs, width: elementOuter.style.width, height: elementOuter.style.height }));
+ this.props.view.dispatch(this.props.view.state.tr.setSelection(new NodeSelection(this.props.view.state.doc.resolve(pos))));
+ };
+
+ document.addEventListener("pointermove", onpointermove);
+ document.addEventListener("pointerup", onpointerup);
+ }
+
+ selectNode() {
+ const elementImage = document.getElementById("imageId") as HTMLElement;
+ const elementHandle = document.getElementById("handleId") as HTMLElement;
+
+ elementImage.classList.add("ProseMirror-selectednode");
+ elementHandle.style.display = "";
+ }
+
+ deselectNode() {
+ const elementImage = document.getElementById("imageId") as HTMLElement;
+ const elementHandle = document.getElementById("handleId") as HTMLElement;
+
+ elementImage.classList.remove("ProseMirror-selectednode");
+ elementHandle.style.display = "none";
+ }
+
+
+ render() {
+
+ const outerStyle = {
+ width: this.props.node.attrs.width,
+ height: this.props.node.attrs.height,
+ display: "inline-block",
+ overflow: "hidden",
+ float: this.props.node.attrs.float
+ };
+
+ const imageStyle = {
+ width: "100%",
+ };
+
+ const handleStyle = {
+ position: "absolute",
+ width: "20px",
+ heiht: "20px",
+ backgroundColor: "blue",
+ borderRadius: "15px",
+ display: "none",
+ bottom: "-10px",
+ right: "-10px"
+
+ };
+
+
+
+ return (
+ <div id="outer"
+ style={outerStyle}
+ >
+ <img
+ id="imageId"
+ style={imageStyle}
+ src={this.props.node.src}
+ onClick={this.onClickImg}
+ onPointerDown={this.onPointerDownImg}
+
+ >
+ </img>
+ <span
+ id="handleId"
+ onPointerDown={this.onPointerDownHandle}
+ >
+
+ </span>
+ </div >
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts b/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts
new file mode 100644
index 000000000..d80e64634
--- /dev/null
+++ b/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts
@@ -0,0 +1,143 @@
+import clamp from '../../../util/clamp';
+import convertToCSSPTValue from '../../../util/convertToCSSPTValue';
+import toCSSLineSpacing from '../../../util/toCSSLineSpacing';
+import { Node, DOMOutputSpec } from 'prosemirror-model';
+
+//import type { NodeSpec } from './Types';
+type NodeSpec = {
+ attrs?: { [key: string]: any },
+ content?: string,
+ draggable?: boolean,
+ group?: string,
+ inline?: boolean,
+ name?: string,
+ parseDOM?: Array<any>,
+ toDOM?: (node: any) => DOMOutputSpec,
+};
+
+// This assumes that every 36pt maps to one indent level.
+export const INDENT_MARGIN_PT_SIZE = 36;
+export const MIN_INDENT_LEVEL = 0;
+export const MAX_INDENT_LEVEL = 7;
+export const ATTRIBUTE_INDENT = 'data-indent';
+
+export const EMPTY_CSS_VALUE = new Set(['', '0%', '0pt', '0px']);
+
+const ALIGN_PATTERN = /(left|right|center|justify)/;
+
+// https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.js
+// :: NodeSpec A plain paragraph textblock. Represented in the DOM
+// as a `<p>` element.
+const ParagraphNodeSpec: NodeSpec = {
+ attrs: {
+ align: { default: null },
+ color: { default: null },
+ id: { default: null },
+ indent: { default: null },
+ inset: { default: null },
+ lineSpacing: { default: null },
+ // TODO: Add UI to let user edit / clear padding.
+ paddingBottom: { default: null },
+ // TODO: Add UI to let user edit / clear padding.
+ paddingTop: { default: null },
+ },
+ content: 'inline*',
+ group: 'block',
+ parseDOM: [{ tag: 'p', getAttrs }],
+ toDOM,
+};
+
+function getAttrs(dom: HTMLElement): Object {
+ const {
+ lineHeight,
+ textAlign,
+ marginLeft,
+ paddingTop,
+ paddingBottom,
+ } = dom.style;
+
+ let align = dom.getAttribute('align') || textAlign || '';
+ align = ALIGN_PATTERN.test(align) ? align : "";
+
+ let indent = parseInt(dom.getAttribute(ATTRIBUTE_INDENT) || "", 10);
+
+ if (!indent && marginLeft) {
+ indent = convertMarginLeftToIndentValue(marginLeft);
+ }
+
+ indent = indent || MIN_INDENT_LEVEL;
+
+ const lineSpacing = lineHeight ? toCSSLineSpacing(lineHeight) : null;
+
+ const id = dom.getAttribute('id') || '';
+ return { align, indent, lineSpacing, paddingTop, paddingBottom, id };
+}
+
+function toDOM(node: Node): DOMOutputSpec {
+ const {
+ align,
+ indent,
+ inset,
+ lineSpacing,
+ paddingTop,
+ paddingBottom,
+ id,
+ } = node.attrs;
+ const attrs: { [key: string]: any } | null = {};
+
+ let style = '';
+ if (align && align !== 'left') {
+ style += `text-align: ${align};`;
+ }
+
+ if (lineSpacing) {
+ const cssLineSpacing = toCSSLineSpacing(lineSpacing);
+ style +=
+ `line-height: ${cssLineSpacing};` +
+ // This creates the local css variable `--czi-content-line-height`
+ // that its children may apply.
+ `--czi-content-line-height: ${cssLineSpacing}`;
+ }
+
+ if (paddingTop && !EMPTY_CSS_VALUE.has(paddingTop)) {
+ style += `padding-top: ${paddingTop};`;
+ }
+
+ if (paddingBottom && !EMPTY_CSS_VALUE.has(paddingBottom)) {
+ style += `padding-bottom: ${paddingBottom};`;
+ }
+
+ if (indent) {
+ style += `text-indent: ${indent}; padding-left: ${indent < 0 ? -indent : undefined};`;
+ }
+
+ if (inset) {
+ style += `margin-left: ${inset}; margin-right: ${inset};`;
+ }
+
+ style && (attrs.style = style);
+
+ if (indent) {
+ attrs[ATTRIBUTE_INDENT] = String(indent);
+ }
+
+ if (id) {
+ attrs.id = id;
+ }
+
+ return ['p', attrs, 0];
+}
+
+export const toParagraphDOM = toDOM;
+export const getParagraphNodeAttrs = getAttrs;
+
+export function convertMarginLeftToIndentValue(marginLeft: string): number {
+ const ptValue = convertToCSSPTValue(marginLeft);
+ return clamp(
+ MIN_INDENT_LEVEL,
+ Math.floor(ptValue / INDENT_MARGIN_PT_SIZE),
+ MAX_INDENT_LEVEL
+ );
+}
+
+export default ParagraphNodeSpec; \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
new file mode 100644
index 000000000..a0b02880e
--- /dev/null
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -0,0 +1,241 @@
+import { chainCommands, exitCode, joinDown, joinUp, lift, selectParentNode, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn } from "prosemirror-commands";
+import { redo, undo } from "prosemirror-history";
+import { undoInputRule } from "prosemirror-inputrules";
+import { Schema } from "prosemirror-model";
+import { liftListItem, sinkListItem } from "./prosemirrorPatches.js";
+import { splitListItem, wrapInList, } from "prosemirror-schema-list";
+import { EditorState, Transaction, TextSelection } from "prosemirror-state";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { Docs } from "../../../documents/Documents";
+import { NumCast, BoolCast, Cast, StrCast } from "../../../../new_fields/Types";
+import { Doc } from "../../../../new_fields/Doc";
+import { FormattedTextBox } from "./FormattedTextBox";
+import { Id } from "../../../../new_fields/FieldSymbols";
+
+const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
+
+export type KeyMap = { [key: string]: any };
+
+export let updateBullets = (tx2: Transaction, schema: Schema, mapStyle?: string) => {
+ let fontSize: number | undefined = undefined;
+ tx2.doc.descendants((node: any, offset: any, index: any) => {
+ if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
+ const path = (tx2.doc.resolve(offset) as any).path;
+ let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
+ if (node.type === schema.nodes.ordered_list) depth++;
+ fontSize = depth === 1 && node.attrs.setFontSize ? Number(node.attrs.setFontSize) : fontSize;
+ const fsize = fontSize && node.type === schema.nodes.ordered_list ? Math.max(6, fontSize - (depth - 1) * 4) : undefined;
+ tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: mapStyle ? mapStyle : node.attrs.mapStyle, bulletStyle: depth, inheritedFontSize: fsize }, node.marks);
+ }
+ });
+ return tx2;
+};
+export default function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKeys?: KeyMap): KeyMap {
+ const keys: { [key: string]: any } = {};
+
+ function bind(key: string, cmd: any) {
+ if (mapKeys) {
+ const mapped = mapKeys[key];
+ if (mapped === false) return;
+ if (mapped) key = mapped;
+ }
+ keys[key] = cmd;
+ }
+
+ bind("Mod-z", undo);
+ bind("Shift-Mod-z", redo);
+ bind("Backspace", undoInputRule);
+
+ !mac && bind("Mod-y", redo);
+
+ bind("Alt-ArrowUp", joinUp);
+ bind("Alt-ArrowDown", joinDown);
+ bind("Mod-BracketLeft", lift);
+ bind("Escape", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
+ (document.activeElement as any).blur?.();
+ SelectionManager.DeselectAll();
+ });
+
+ bind("Mod-b", toggleMark(schema.marks.strong));
+ bind("Mod-B", toggleMark(schema.marks.strong));
+
+ bind("Mod-e", toggleMark(schema.marks.em));
+ bind("Mod-E", toggleMark(schema.marks.em));
+
+ bind("Mod-u", toggleMark(schema.marks.underline));
+ bind("Mod-U", toggleMark(schema.marks.underline));
+
+ bind("Mod-`", toggleMark(schema.marks.code));
+
+ bind("Ctrl-.", wrapInList(schema.nodes.bullet_list));
+
+ bind("Ctrl-n", wrapInList(schema.nodes.ordered_list));
+
+ bind("Ctrl->", wrapIn(schema.nodes.blockquote));
+
+ // bind("^", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ // let newNode = schema.nodes.footnote.create({});
+ // if (dispatch && state.selection.from === state.selection.to) {
+ // let tr = state.tr;
+ // tr.replaceSelectionWith(newNode); // replace insertion with a footnote.
+ // dispatch(tr.setSelection(new NodeSelection( // select the footnote node to open its display
+ // tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
+ // tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize))));
+ // return true;
+ // }
+ // return false;
+ // });
+
+
+ const cmd = chainCommands(exitCode, (state, dispatch) => {
+ if (dispatch) {
+ dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView());
+ return true;
+ }
+ return false;
+ });
+ bind("Mod-Enter", cmd);
+ bind("Shift-Enter", cmd);
+ mac && bind("Ctrl-Enter", cmd);
+
+
+ bind("Shift-Ctrl-0", setBlockType(schema.nodes.paragraph));
+
+ bind("Shift-Ctrl-\\", setBlockType(schema.nodes.code_block));
+
+ for (let i = 1; i <= 6; i++) {
+ bind("Shift-Ctrl-" + i, setBlockType(schema.nodes.heading, { level: i }));
+ }
+
+ const hr = schema.nodes.horizontal_rule;
+ bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView());
+ return true;
+ });
+
+ bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ const ref = state.selection;
+ const range = ref.$from.blockRange(ref.$to);
+ const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ if (!sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
+ const tx3 = updateBullets(tx2, schema);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+ dispatch(tx3);
+ })) { // couldn't sink into an existing list, so wrap in a new one
+ const newstate = state.applyTransaction(state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end)));
+ if (!wrapInList(schema.nodes.ordered_list)(newstate.state, (tx2: Transaction) => {
+ const tx3 = updateBullets(tx2, schema);
+ // when promoting to a list, assume list will format things so don't copy the stored marks.
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+ dispatch(tx3);
+ })) {
+ console.log("bullet promote fail");
+ }
+ }
+ });
+
+ bind("Shift-Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+
+ if (!liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => {
+ const tx3 = updateBullets(tx2, schema);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+ dispatch(tx3);
+ })) {
+ console.log("bullet demote fail");
+ }
+ });
+ bind("Ctrl-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ const layoutDoc = props.Document;
+ const originalDoc = layoutDoc.rootDocument || layoutDoc;
+ if (originalDoc instanceof Doc) {
+ const layoutKey = StrCast(originalDoc.layoutKey);
+ const newDoc = Docs.Create.TextDocument("", {
+ layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout,
+ layoutKey,
+ _singleLine: BoolCast(originalDoc._singleLine),
+ x: NumCast(originalDoc.x), y: NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10, _width: NumCast(layoutDoc._width), _height: NumCast(layoutDoc._height)
+ });
+ if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) {
+ newDoc[layoutKey] = originalDoc[layoutKey];
+ }
+ FormattedTextBox.SelectOnLoad = newDoc[Id];
+ props.addDocument(newDoc);
+ }
+ });
+
+ const splitMetadata = (marks: any, tx: Transaction) => {
+ marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
+ marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
+ return tx;
+ };
+ const addTextOnRight = (force: boolean) => {
+ const layoutDoc = props.Document;
+ const originalDoc = layoutDoc.rootDocument || layoutDoc;
+ if (force || props.Document._singleLine) {
+ const layoutKey = StrCast(originalDoc.layoutKey);
+ const newDoc = Docs.Create.TextDocument("", {
+ layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout,
+ layoutKey,
+ _singleLine: BoolCast(originalDoc._singleLine),
+ x: NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10, y: NumCast(originalDoc.y), _width: NumCast(layoutDoc._width), _height: NumCast(layoutDoc._height)
+ });
+ if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) {
+ newDoc[layoutKey] = originalDoc[layoutKey];
+ }
+ FormattedTextBox.SelectOnLoad = newDoc[Id];
+ props.addDocument(newDoc);
+ return true;
+ }
+ return false;
+ };
+ bind("Alt-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => {
+ return addTextOnRight(true);
+ });
+ bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => {
+ if (addTextOnRight(false)) return true;
+ const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ if (!splitListItem(schema.nodes.list_item)(state, dispatch)) {
+ if (!splitBlockKeepMarks(state, (tx3: Transaction) => {
+ splitMetadata(marks, tx3);
+ if (!liftListItem(schema.nodes.list_item)(tx3, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) {
+ dispatch(tx3);
+ }
+ })) {
+ return false;
+ }
+ }
+ return true;
+ });
+ bind("Space", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ dispatch(splitMetadata(marks, state.tr));
+ return false;
+ });
+ bind(":", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ const range = state.selection.$from.blockRange(state.selection.$to, (node: any) => {
+ return !node.marks || !node.marks.find((m: any) => m.type === schema.marks.metadata);
+ });
+ const path = (state.doc.resolve(state.selection.from - 1) as any).path;
+ const spaceSeparator = path[path.length - 3].childCount > 1 ? 0 : -1;
+ const anchor = range!.end - path[path.length - 3].lastChild.nodeSize + spaceSeparator;
+ if (anchor >= 0) {
+ const textsel = TextSelection.create(state.doc, anchor, range!.end);
+ const text = range ? state.doc.textBetween(textsel.from, textsel.to) : "";
+ let whitespace = text.length - 1;
+ for (; whitespace >= 0 && text[whitespace] !== " "; whitespace--) { }
+ if (text.endsWith(":")) {
+ dispatch(state.tr.addMark(textsel.from + whitespace + 1, textsel.to, schema.marks.metadata.create() as any).
+ addMark(textsel.from + whitespace + 1, textsel.to - 2, schema.marks.metadataKey.create() as any));
+ }
+ }
+ return false;
+ });
+
+
+ return keys;
+}
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.scss b/src/client/views/nodes/formattedText/RichTextMenu.scss
new file mode 100644
index 000000000..7a0718c16
--- /dev/null
+++ b/src/client/views/nodes/formattedText/RichTextMenu.scss
@@ -0,0 +1,124 @@
+@import "../../globalCssVariables";
+
+.button-dropdown-wrapper {
+ position: relative;
+
+ .dropdown-button {
+ width: 15px;
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+
+ .dropdown-button-combined {
+ width: 50px;
+ display: flex;
+ justify-content: space-between;
+
+ svg:nth-child(2) {
+ margin-top: 2px;
+ }
+ }
+
+ .dropdown {
+ position: absolute;
+ top: 35px;
+ left: 0;
+ background-color: #323232;
+ color: $light-color-secondary;
+ border: 1px solid #4d4d4d;
+ border-radius: 0 6px 6px 6px;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+ min-width: 150px;
+ padding: 5px;
+ font-size: 12px;
+ z-index: 10001;
+
+ button {
+ background-color: #323232;
+ border: 1px solid black;
+ border-radius: 1px;
+ padding: 6px;
+ margin: 5px 0;
+ font-size: 10px;
+
+ &:hover {
+ background-color: black;
+ }
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ input {
+ color: black;
+ }
+
+}
+
+.richTextMenu {
+ select {
+ background-color: #323232;
+ color: white;
+ border: 1px solid black;
+ // border-top: none;
+ // border-bottom: none;
+ font-size: 12px;
+ height: 100%;
+ margin-right: 3px;
+
+ &:focus,
+ &:hover {
+ background-color: black;
+ }
+
+ &::-ms-expand {
+ color: white;
+ }
+ }
+}
+
+.link-menu {
+ .divider {
+ background-color: white;
+ height: 1px;
+ width: 100%;
+ }
+}
+
+.color-preview-button {
+ .color-preview {
+ width: 100%;
+ height: 3px;
+ margin-top: 3px;
+ }
+}
+
+.color-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+
+ button.color-button {
+ width: 20px;
+ height: 20px;
+ border-radius: 15px !important;
+ margin: 3px;
+ border: 2px solid transparent !important;
+ padding: 3px;
+
+ &.active {
+ border: 2px solid white !important;
+ }
+ }
+}
+
+.row-2 {
+ display: flex;
+ justify-content: space-between;
+
+ >div {
+ display: flex;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
new file mode 100644
index 000000000..cc04e0d6d
--- /dev/null
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -0,0 +1,875 @@
+import React = require("react");
+import AntimodeMenu from "../../AntimodeMenu";
+import { observable, action, } from "mobx";
+import { observer } from "mobx-react";
+import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model";
+import { schema } from "./schema_rts";
+import { EditorView } from "prosemirror-view";
+import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
+import { faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller, faSleigh } from "@fortawesome/free-solid-svg-icons";
+import { updateBullets } from "./ProsemirrorExampleTransfer";
+import { FieldViewProps } from "../FieldView";
+import { Cast, StrCast } from "../../../../new_fields/Types";
+import { FormattedTextBoxProps } from "./FormattedTextBox";
+import { unimplementedFunction, Utils } from "../../../../Utils";
+import { wrapInList } from "prosemirror-schema-list";
+import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../../../new_fields/SchemaHeaderField';
+import "./RichTextMenu.scss";
+import { DocServer } from "../../../DocServer";
+import { Doc } from "../../../../new_fields/Doc";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { LinkManager } from "../../../util/LinkManager";
+const { toggleMark, setBlockType } = require("prosemirror-commands");
+
+library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller);
+
+@observer
+export default class RichTextMenu extends AntimodeMenu {
+ static Instance: RichTextMenu;
+ public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable
+
+ private view?: EditorView;
+ public editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
+
+ public _brushMap: Map<string, Set<Mark>> = new Map();
+ private fontSizeOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean, style?: {} }[];
+ private fontFamilyOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean, style?: {} }[];
+ private listTypeOptions: { node: NodeType | any | null, title: string, label: string, command: any, style?: {} }[];
+ private fontColors: (string | undefined)[];
+ private highlightColors: (string | undefined)[];
+
+ @observable private collapsed: boolean = false;
+ @observable private boldActive: boolean = false;
+ @observable private italicsActive: boolean = false;
+ @observable private underlineActive: boolean = false;
+ @observable private strikethroughActive: boolean = false;
+ @observable private subscriptActive: boolean = false;
+ @observable private superscriptActive: boolean = false;
+
+ @observable private activeFontSize: string = "";
+ @observable private activeFontFamily: string = "";
+ @observable private activeListType: string = "";
+
+ @observable private brushIsEmpty: boolean = true;
+ @observable private brushMarks: Set<Mark> = new Set();
+ @observable private showBrushDropdown: boolean = false;
+
+ @observable private activeFontColor: string = "black";
+ @observable private showColorDropdown: boolean = false;
+
+ @observable private activeHighlightColor: string = "transparent";
+ @observable private showHighlightDropdown: boolean = false;
+
+ @observable private currentLink: string | undefined = "";
+ @observable private showLinkDropdown: boolean = false;
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+ RichTextMenu.Instance = this;
+ this._canFade = false;
+
+ this.fontSizeOptions = [
+ { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 8 }), title: "Set font size", label: "8pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 9 }), title: "Set font size", label: "9pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 10 }), title: "Set font size", label: "10pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 12 }), title: "Set font size", label: "12pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 14 }), title: "Set font size", label: "14pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 16 }), title: "Set font size", label: "16pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 18 }), title: "Set font size", label: "18pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 20 }), title: "Set font size", label: "20pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 24 }), title: "Set font size", label: "24pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 32 }), title: "Set font size", label: "32pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 48 }), title: "Set font size", label: "48pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 72 }), title: "Set font size", label: "72pt", command: this.changeFontSize },
+ { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
+ { mark: null, title: "", label: "13pt", command: unimplementedFunction, hidden: true }, // this is here because the default size is 13, but there is no actual 13pt option
+ ];
+
+ this.fontFamilyOptions = [
+ { mark: schema.marks.pFontFamily.create({ family: "Times New Roman" }), title: "Set font family", label: "Times New Roman", command: this.changeFontFamily, style: { fontFamily: "Times New Roman" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Arial" }), title: "Set font family", label: "Arial", command: this.changeFontFamily, style: { fontFamily: "Arial" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Georgia" }), title: "Set font family", label: "Georgia", command: this.changeFontFamily, style: { fontFamily: "Georgia" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Comic Sans MS" }), title: "Set font family", label: "Comic Sans MS", command: this.changeFontFamily, style: { fontFamily: "Comic Sans MS" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Tahoma" }), title: "Set font family", label: "Tahoma", command: this.changeFontFamily, style: { fontFamily: "Tahoma" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Impact" }), title: "Set font family", label: "Impact", command: this.changeFontFamily, style: { fontFamily: "Impact" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Crimson Text" }), title: "Set font family", label: "Crimson Text", command: this.changeFontFamily, style: { fontFamily: "Crimson Text" } },
+ { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
+ // { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true },
+ ];
+
+ this.listTypeOptions = [
+ { node: schema.nodes.ordered_list.create({ mapStyle: "bullet" }), title: "Set list type", label: ":", command: this.changeListType },
+ { node: schema.nodes.ordered_list.create({ mapStyle: "decimal" }), title: "Set list type", label: "1.1", command: this.changeListType },
+ { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "1.A", command: this.changeListType },
+ { node: undefined, title: "Set list type", label: "Remove", command: this.changeListType },
+ ];
+
+ this.fontColors = [
+ DarkPastelSchemaPalette.get("pink2"),
+ DarkPastelSchemaPalette.get("purple4"),
+ DarkPastelSchemaPalette.get("bluegreen1"),
+ DarkPastelSchemaPalette.get("yellow4"),
+ DarkPastelSchemaPalette.get("red2"),
+ DarkPastelSchemaPalette.get("bluegreen7"),
+ DarkPastelSchemaPalette.get("bluegreen5"),
+ DarkPastelSchemaPalette.get("orange1"),
+ "#757472",
+ "#000"
+ ];
+
+ this.highlightColors = [
+ PastelSchemaPalette.get("pink2"),
+ PastelSchemaPalette.get("purple4"),
+ PastelSchemaPalette.get("bluegreen1"),
+ PastelSchemaPalette.get("yellow4"),
+ PastelSchemaPalette.get("red2"),
+ PastelSchemaPalette.get("bluegreen7"),
+ PastelSchemaPalette.get("bluegreen5"),
+ PastelSchemaPalette.get("orange1"),
+ "white",
+ "transparent"
+ ];
+ }
+
+ @action
+ changeView(view: EditorView) {
+ this.view = view;
+ }
+
+ update(view: EditorView, lastState: EditorState | undefined) {
+ this.updateFromDash(view, lastState, this.editorProps);
+ }
+
+
+ public MakeLinkToSelection = (linkDocId: string, title: string, location: string, targetDocId: string): string => {
+ if (this.view) {
+ const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, linkId: linkDocId, targetId: targetDocId });
+ this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link).
+ addMark(this.view.state.selection.from, this.view.state.selection.to, link));
+ return this.view.state.selection.$from.nodeAfter?.text || "";
+ }
+ return "";
+ }
+
+ @action
+ public async updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) {
+ if (!view) {
+ console.log("no editor? why?");
+ return;
+ }
+ this.view = view;
+ const state = view.state;
+ props && (this.editorProps = props);
+
+ // Don't do anything if the document/selection didn't change
+ if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) return;
+
+ // update active marks
+ const activeMarks = this.getActiveMarksOnSelection();
+ this.setActiveMarkButtons(activeMarks);
+
+ // update active font family and size
+ const active = this.getActiveFontStylesOnSelection();
+ const activeFamilies = active && active.get("families");
+ const activeSizes = active && active.get("sizes");
+
+ this.activeFontFamily = !activeFamilies || activeFamilies.length === 0 ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
+ this.activeFontSize = !activeSizes || activeSizes.length === 0 ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) + "pt" : "various";
+
+ // update link in current selection
+ const targetTitle = await this.getTextLinkTargetTitle();
+ this.setCurrentLink(targetTitle);
+ }
+
+ setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => {
+ if (mark) {
+ const node = (state.selection as NodeSelection).node;
+ if (node?.type === schema.nodes.ordered_list) {
+ let attrs = node.attrs;
+ if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family };
+ if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize };
+ if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, setFontColor: mark.attrs.color };
+ const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema);
+ dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from))));
+ } else {
+ toggleMark(mark.type, mark.attrs)(state, (tx: any) => {
+ const { from, $from, to, empty } = tx.selection;
+ if (!tx.doc.rangeHasMark(from, to, mark.type)) {
+ toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch);
+ } else dispatch(tx);
+ });
+ }
+ }
+ }
+
+ // finds font sizes and families in selection
+ getActiveFontStylesOnSelection() {
+ if (!this.view) return;
+
+ const activeFamilies: string[] = [];
+ const activeSizes: string[] = [];
+ const state = this.view.state;
+ const pos = this.view.state.selection.$from;
+ const ref_node = this.reference_node(pos);
+ if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
+ ref_node.marks.forEach(m => {
+ m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family);
+ m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt");
+ });
+ }
+
+ const styles = new Map<String, String[]>();
+ styles.set("families", activeFamilies);
+ styles.set("sizes", activeSizes);
+ return styles;
+ }
+
+ getMarksInSelection(state: EditorState<any>) {
+ const found = new Set<Mark>();
+ const { from, to } = state.selection as TextSelection;
+ state.doc.nodesBetween(from, to, (node) => node.marks.forEach(m => found.add(m)));
+ return found;
+ }
+
+ //finds all active marks on selection in given group
+ getActiveMarksOnSelection() {
+ if (!this.view) return;
+
+ const markGroup = [schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript];
+ if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type);
+ //current selection
+ const { empty, ranges, $to } = this.view.state.selection as TextSelection;
+ const state = this.view.state;
+ let activeMarks: MarkType[] = [];
+ if (!empty) {
+ activeMarks = markGroup.filter(mark => {
+ const has = false;
+ for (let i = 0; !has && i < ranges.length; i++) {
+ return state.doc.rangeHasMark(ranges[i].$from.pos, ranges[i].$to.pos, mark);
+ }
+ return false;
+ });
+ }
+ else {
+ const pos = this.view.state.selection.$from;
+ const ref_node: ProsNode | null = this.reference_node(pos);
+ if (ref_node !== null && ref_node !== this.view.state.doc) {
+ if (ref_node.isText) {
+ }
+ else {
+ return [];
+ }
+ activeMarks = markGroup.filter(mark_type => {
+ if (mark_type === state.schema.marks.pFontSize) {
+ return ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name);
+ }
+ const mark = state.schema.mark(mark_type);
+ return ref_node.marks.includes(mark);
+ });
+ }
+ }
+ return activeMarks;
+ }
+
+ destroy() {
+ this.fadeOut(true);
+ }
+
+ @action
+ setActiveMarkButtons(activeMarks: MarkType[] | undefined) {
+ if (!activeMarks) return;
+
+ this.boldActive = false;
+ this.italicsActive = false;
+ this.underlineActive = false;
+ this.strikethroughActive = false;
+ this.subscriptActive = false;
+ this.superscriptActive = false;
+
+ activeMarks.forEach(mark => {
+ switch (mark.name) {
+ case "strong": this.boldActive = true; break;
+ case "em": this.italicsActive = true; break;
+ case "underline": this.underlineActive = true; break;
+ case "strikethrough": this.strikethroughActive = true; break;
+ case "subscript": this.subscriptActive = true; break;
+ case "superscript": this.superscriptActive = true; break;
+ }
+ });
+ }
+
+ createButton(faIcon: string, title: string, isActive: boolean = false, command?: any, onclick?: any) {
+ const self = this;
+ function onClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && command && command(self.view.state, self.view.dispatch, self.view);
+ self.view && onclick && onclick(self.view.state, self.view.dispatch, self.view);
+ self.setActiveMarkButtons(self.getActiveMarksOnSelection());
+ }
+
+ return (
+ <button className={"antimodeMenu-button" + (isActive ? " active" : "")} key={title} title={title} onPointerDown={onClick}>
+ <FontAwesomeIcon icon={faIcon as IconProp} size="lg" />
+ </button>
+ );
+ }
+
+ createMarksDropdown(activeOption: string, options: { mark: Mark | null, title: string, label: string, command: (mark: Mark, view: EditorView) => void, hidden?: boolean, style?: {} }[], key: string): JSX.Element {
+ const items = options.map(({ title, label, hidden, style }) => {
+ if (hidden) {
+ return label === activeOption ?
+ <option value={label} title={title} key={label} style={style ? style : {}} selected hidden>{label}</option> :
+ <option value={label} title={title} key={label} style={style ? style : {}} hidden>{label}</option>;
+ }
+ return label === activeOption ?
+ <option value={label} title={title} key={label} style={style ? style : {}} selected>{label}</option> :
+ <option value={label} title={title} key={label} style={style ? style : {}}>{label}</option>;
+ });
+
+ const self = this;
+ function onChange(e: React.ChangeEvent<HTMLSelectElement>) {
+ e.stopPropagation();
+ e.preventDefault();
+ options.forEach(({ label, mark, command }) => {
+ if (e.target.value === label) {
+ self.view && mark && command(mark, self.view);
+ }
+ });
+ }
+ return <select onChange={onChange} key={key}>{items}</select>;
+ }
+
+ createNodesDropdown(activeOption: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean, style?: {} }[], key: string): JSX.Element {
+ const items = options.map(({ title, label, hidden, style }) => {
+ if (hidden) {
+ return label === activeOption ?
+ <option value={label} title={title} key={label} style={style ? style : {}} selected hidden>{label}</option> :
+ <option value={label} title={title} key={label} style={style ? style : {}} hidden>{label}</option>;
+ }
+ return label === activeOption ?
+ <option value={label} title={title} key={label} style={style ? style : {}} selected>{label}</option> :
+ <option value={label} title={title} key={label} style={style ? style : {}}>{label}</option>;
+ });
+
+ const self = this;
+ function onChange(val: string) {
+ options.forEach(({ label, node, command }) => {
+ if (val === label) {
+ self.view && node && command(node);
+ }
+ });
+ }
+ return <select onChange={e => onChange(e.target.value)} key={key}>{items}</select>;
+ }
+
+ changeFontSize = (mark: Mark, view: EditorView) => {
+ this.setMark(view.state.schema.marks.pFontSize.create({ fontSize: mark.attrs.fontSize }), view.state, view.dispatch);
+ }
+
+ changeFontFamily = (mark: Mark, view: EditorView) => {
+ this.setMark(view.state.schema.marks.pFontFamily.create({ family: mark.attrs.family }), view.state, view.dispatch);
+ }
+
+ // TODO: remove doesn't work
+ //remove all node type and apply the passed-in one to the selected text
+ changeListType = (nodeType: NodeType | undefined) => {
+ if (!this.view) return;
+
+ if (nodeType === schema.nodes.bullet_list) {
+ wrapInList(nodeType)(this.view.state, this.view.dispatch);
+ } else {
+ const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks());
+ if (!wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => {
+ const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+
+ this.view!.dispatch(tx2);
+ })) {
+ const tx2 = this.view.state.tr;
+ const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+
+ this.view.dispatch(tx3);
+ }
+ }
+ }
+
+ insertSummarizer(state: EditorState<any>, dispatch: any) {
+ if (state.selection.empty) return false;
+ const mark = state.schema.marks.summarize.create();
+ const tr = state.tr;
+ tr.addMark(state.selection.from, state.selection.to, mark);
+ const content = tr.selection.content();
+ const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() });
+ dispatch && dispatch(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
+ return true;
+ }
+
+ @action toggleBrushDropdown() { this.showBrushDropdown = !this.showBrushDropdown; }
+
+ // todo: add brushes to brushMap to save with a style name
+ onBrushNameKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ RichTextMenu.Instance.brushMarks && RichTextMenu.Instance._brushMap.set(this._brushNameRef.current!.value, RichTextMenu.Instance.brushMarks);
+ this._brushNameRef.current!.style.background = "lightGray";
+ }
+ }
+ _brushNameRef = React.createRef<HTMLInputElement>();
+
+ createBrushButton() {
+ const self = this;
+ function onBrushClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && self.fillBrush(self.view.state, self.view.dispatch);
+ }
+
+ let label = "Stored marks: ";
+ if (this.brushMarks && this.brushMarks.size > 0) {
+ this.brushMarks.forEach((mark: Mark) => {
+ const markType = mark.type;
+ label += markType.name;
+ label += ", ";
+ });
+ label = label.substring(0, label.length - 2);
+ } else {
+ label = "No marks are currently stored";
+ }
+
+ const button =
+ <button className="antimodeMenu-button" title="" onPointerDown={onBrushClick} style={this.brushMarks?.size > 0 ? { backgroundColor: "121212" } : {}}>
+ <FontAwesomeIcon icon="paint-roller" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.brushMarks?.size > 0 ? 45 : 0}deg)` }} />
+ </button>;
+
+ const dropdownContent =
+ <div className="dropdown">
+ <p>{label}</p>
+ <button onPointerDown={this.clearBrush}>Clear brush</button>
+ <input placeholder="-brush name-" ref={this._brushNameRef} onKeyPress={this.onBrushNameKeyPress}></input>
+ </div>;
+
+ return (
+ <ButtonDropdown view={this.view} key={"brush dropdown"} button={button} dropdownContent={dropdownContent} />
+ );
+ }
+
+ @action
+ clearBrush() {
+ RichTextMenu.Instance.brushIsEmpty = true;
+ RichTextMenu.Instance.brushMarks = new Set();
+ }
+
+ @action
+ fillBrush(state: EditorState<any>, dispatch: any) {
+ if (!this.view) return;
+
+ if (this.brushIsEmpty) {
+ const selected_marks = this.getMarksInSelection(this.view.state);
+ if (selected_marks.size >= 0) {
+ this.brushMarks = selected_marks;
+ this.brushIsEmpty = !this.brushIsEmpty;
+ }
+ }
+ else {
+ const { from, to, $from } = this.view.state.selection;
+ if (!this.view.state.selection.empty && $from && $from.nodeAfter) {
+ if (this.brushMarks && to - from > 0) {
+ this.view.dispatch(this.view.state.tr.removeMark(from, to));
+ Array.from(this.brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => {
+ this.setMark(mark, this.view!.state, this.view!.dispatch);
+ });
+ }
+ }
+ else {
+ this.brushIsEmpty = !this.brushIsEmpty;
+ }
+ }
+ }
+
+ @action toggleColorDropdown() { this.showColorDropdown = !this.showColorDropdown; }
+ @action setActiveColor(color: string) { this.activeFontColor = color; }
+
+ createColorButton() {
+ const self = this;
+ function onColorClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch);
+ }
+ function changeColor(e: React.PointerEvent, color: string) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.setActiveColor(color);
+ self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch);
+ }
+
+ const button =
+ <button className="antimodeMenu-button color-preview-button" title="" onPointerDown={onColorClick}>
+ <FontAwesomeIcon icon="palette" size="lg" />
+ <div className="color-preview" style={{ backgroundColor: this.activeFontColor }}></div>
+ </button>;
+
+ const dropdownContent =
+ <div className="dropdown" >
+ <p>Change font color:</p>
+ <div className="color-wrapper">
+ {this.fontColors.map(color => {
+ if (color) {
+ return this.activeFontColor === color ?
+ <button className="color-button active" key={"active" + color} style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button> :
+ <button className="color-button" key={"other" + color} style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button>;
+ }
+ })}
+ </div>
+ </div>;
+
+ return (
+ <ButtonDropdown view={this.view} key={"color dropdown"} button={button} dropdownContent={dropdownContent} />
+ );
+ }
+
+ public insertColor(color: String, state: EditorState<any>, dispatch: any) {
+ const colorMark = state.schema.mark(state.schema.marks.pFontColor, { color: color });
+ if (state.selection.empty) {
+ dispatch(state.tr.addStoredMark(colorMark));
+ return false;
+ }
+ this.setMark(colorMark, state, dispatch);
+ }
+
+ @action toggleHighlightDropdown() { this.showHighlightDropdown = !this.showHighlightDropdown; }
+ @action setActiveHighlight(color: string) { this.activeHighlightColor = color; }
+
+ createHighlighterButton() {
+ const self = this;
+ function onHighlightClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch);
+ }
+ function changeHighlight(e: React.PointerEvent, color: string) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.setActiveHighlight(color);
+ self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch);
+ }
+
+ const button =
+ <button className="antimodeMenu-button color-preview-button" title="" key="highilghter-button" onPointerDown={onHighlightClick}>
+ <FontAwesomeIcon icon="highlighter" size="lg" />
+ <div className="color-preview" style={{ backgroundColor: this.activeHighlightColor }}></div>
+ </button>;
+
+ const dropdownContent =
+ <div className="dropdown">
+ <p>Change highlight color:</p>
+ <div className="color-wrapper">
+ {this.highlightColors.map(color => {
+ if (color) {
+ return this.activeHighlightColor === color ?
+ <button className="color-button active" key={`active ${color}`} style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}>{color === "transparent" ? "X" : ""}</button> :
+ <button className="color-button" key={`inactive ${color}`} style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}>{color === "transparent" ? "X" : ""}</button>;
+ }
+ })}
+ </div>
+ </div>;
+
+ return (
+ <ButtonDropdown view={this.view} key={"highlighter"} button={button} dropdownContent={dropdownContent} />
+ );
+ }
+
+ insertHighlight(color: String, state: EditorState<any>, dispatch: any) {
+ if (state.selection.empty) return false;
+ toggleMark(state.schema.marks.marker, { highlight: color })(state, dispatch);
+ }
+
+ @action toggleLinkDropdown() { this.showLinkDropdown = !this.showLinkDropdown; }
+ @action setCurrentLink(link: string) { this.currentLink = link; }
+
+ createLinkButton() {
+ const self = this;
+
+ function onLinkChange(e: React.ChangeEvent<HTMLInputElement>) {
+ self.setCurrentLink(e.target.value);
+ }
+
+ const link = this.currentLink ? this.currentLink : "";
+
+ const button = <FontAwesomeIcon icon="link" size="lg" />;
+
+ const dropdownContent =
+ <div className="dropdown link-menu">
+ <p>Linked to:</p>
+ <input value={link} placeholder="Enter URL" onChange={onLinkChange} />
+ <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, "onRight")}>Apply hyperlink</button>
+ <div className="divider"></div>
+ <button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button>
+ </div>;
+
+ return (
+ <ButtonDropdown view={this.view} key={"link button"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} />
+ );
+ }
+
+ async getTextLinkTargetTitle() {
+ if (!this.view) return;
+
+ const node = this.view.state.selection.$from.nodeAfter;
+ const link = node && node.marks.find(m => m.type.name === "link");
+ if (link) {
+ const href = link.attrs.href;
+ if (href) {
+ if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+ const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ if (linkclicked) {
+ const linkDoc = await DocServer.GetRefField(linkclicked);
+ if (linkDoc instanceof Doc) {
+ const anchor1 = await Cast(linkDoc.anchor1, Doc);
+ const anchor2 = await Cast(linkDoc.anchor2, Doc);
+ const currentDoc = SelectionManager.SelectedDocuments().length && SelectionManager.SelectedDocuments()[0].props.Document;
+ if (currentDoc && anchor1 && anchor2) {
+ if (Doc.AreProtosEqual(currentDoc, anchor1)) {
+ return StrCast(anchor2.title);
+ }
+ if (Doc.AreProtosEqual(currentDoc, anchor2)) {
+ return StrCast(anchor1.title);
+ }
+ }
+ }
+ }
+ } else {
+ return href;
+ }
+ } else {
+ return link.attrs.title;
+ }
+ }
+ }
+
+ // TODO: should check for valid URL
+ makeLinkToURL = (target: String, lcoation: string) => {
+ if (!this.view) return;
+
+ let node = this.view.state.selection.$from.nodeAfter;
+ let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location });
+ this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link));
+ this.view.dispatch(this.view.state.tr.addMark(this.view.state.selection.from, this.view.state.selection.to, link));
+ node = this.view.state.selection.$from.nodeAfter;
+ link = node && node.marks.find(m => m.type.name === "link");
+ }
+
+ deleteLink = () => {
+ if (!this.view) return;
+
+ const node = this.view.state.selection.$from.nodeAfter;
+ const link = node && node.marks.find(m => m.type === this.view!.state.schema.marks.link);
+ const href = link!.attrs.href;
+ if (href) {
+ if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+ const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ if (linkclicked) {
+ DocServer.GetRefField(linkclicked).then(async linkDoc => {
+ if (linkDoc instanceof Doc) {
+ LinkManager.Instance.deleteLink(linkDoc);
+ this.view!.dispatch(this.view!.state.tr.removeMark(this.view!.state.selection.from, this.view!.state.selection.to, this.view!.state.schema.marks.link));
+ }
+ });
+ }
+ } else {
+ if (node) {
+ const { tr, schema, selection } = this.view.state;
+ const extension = this.linkExtend(selection.$anchor, href);
+ this.view.dispatch(tr.removeMark(extension.from, extension.to, schema.marks.link));
+ }
+ }
+ }
+ }
+
+ linkExtend($start: ResolvedPos, href: string) {
+ const mark = this.view!.state.schema.marks.link;
+
+ let startIndex = $start.index();
+ let endIndex = $start.indexAfter();
+
+ while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.href === href).length) startIndex--;
+ while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.href === href).length) endIndex++;
+
+ let startPos = $start.start();
+ let endPos = startPos;
+ for (let i = 0; i < endIndex; i++) {
+ const size = $start.parent.child(i).nodeSize;
+ if (i < startIndex) startPos += size;
+ endPos += size;
+ }
+ return { from: startPos, to: endPos };
+ }
+
+ reference_node(pos: ResolvedPos<any>): ProsNode | null {
+ if (!this.view) return null;
+
+ let ref_node: ProsNode = this.view.state.doc;
+ if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) {
+ ref_node = pos.nodeBefore;
+ }
+ else if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) {
+ ref_node = pos.nodeAfter;
+ }
+ else if (pos.pos > 0) {
+ let skip = false;
+ for (let i: number = pos.pos - 1; i > 0; i--) {
+ this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode) => {
+ if (node.isLeaf && !skip) {
+ ref_node = node;
+ skip = true;
+ }
+
+ });
+ }
+ }
+ if (!ref_node.isLeaf && ref_node.childCount > 0) {
+ ref_node = ref_node.child(0);
+ }
+ return ref_node;
+ }
+
+ @action onPointerEnter(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = true; }
+ @action onPointerLeave(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = false; }
+
+ @action
+ toggleMenuPin = (e: React.MouseEvent) => {
+ this.Pinned = !this.Pinned;
+ if (!this.Pinned) {
+ this.fadeOut(true);
+ }
+ }
+
+ @action
+ protected toggleCollapse = (e: React.MouseEvent) => {
+ this.collapsed = !this.collapsed;
+ setTimeout(() => {
+ const x = Math.min(this._left, window.innerWidth - RichTextMenu.Instance.width);
+ RichTextMenu.Instance.jumpTo(x, this._top);
+ }, 0);
+ }
+
+ render() {
+
+ const row1 = <div className="antimodeMenu-row" key="row1" style={{ display: this.collapsed ? "none" : undefined }}>{[
+ this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)),
+ this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)),
+ this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)),
+ this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)),
+ this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)),
+ this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)),
+ this.createColorButton(),
+ this.createHighlighterButton(),
+ this.createLinkButton(),
+ this.createBrushButton(),
+ this.createButton("indent", "Summarize", undefined, this.insertSummarizer),
+ ]}</div>;
+
+ const row2 = <div className="antimodeMenu-row row-2" key="antimodemenu row2">
+ <div key="row" style={{ display: this.collapsed ? "none" : undefined }}>
+ {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size"),
+ this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family"),
+ this.createNodesDropdown(this.activeListType, this.listTypeOptions, "nodes")]}
+ </div>
+ <div key="button">
+ <div key="collapser">
+ <button className="antimodeMenu-button" key="collapse menu" title="Collapse menu" onClick={this.toggleCollapse} style={{ backgroundColor: this.collapsed ? "#121212" : "", width: 25 }}>
+ <FontAwesomeIcon icon="chevron-left" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.3s", transform: `rotate(${this.collapsed ? 180 : 0}deg)` }} />
+ </button>
+ </div>
+ <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: this.Pinned ? "#121212" : "", display: this.collapsed ? "none" : undefined }}>
+ <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} />
+ </button>
+ {this.getDragger()}
+ </div>
+ </div>;
+
+ return (
+ <div className="richTextMenu" onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+ {this.getElementWithRows([row1, row2], 2, false)}
+ </div>
+ );
+ }
+}
+
+interface ButtonDropdownProps {
+ view?: EditorView;
+ button: JSX.Element;
+ dropdownContent: JSX.Element;
+ openDropdownOnButton?: boolean;
+}
+
+@observer
+class ButtonDropdown extends React.Component<ButtonDropdownProps> {
+
+ @observable private showDropdown: boolean = false;
+ private ref: HTMLDivElement | null = null;
+
+ componentDidMount() {
+ document.addEventListener("pointerdown", this.onBlur);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("pointerdown", this.onBlur);
+ }
+
+ @action
+ setShowDropdown(show: boolean) {
+ this.showDropdown = show;
+ }
+ @action
+ toggleDropdown() {
+ this.showDropdown = !this.showDropdown;
+ }
+
+ onDropdownClick = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.props.view && this.props.view.focus();
+ this.toggleDropdown();
+ }
+
+ onBlur = (e: PointerEvent) => {
+ setTimeout(() => {
+ if (this.ref !== null && !this.ref.contains(e.target as Node)) {
+ this.setShowDropdown(false);
+ }
+ }, 0);
+ }
+
+ render() {
+ return (
+ <div className="button-dropdown-wrapper" ref={node => this.ref = node}>
+ {this.props.openDropdownOnButton ?
+ <button className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.onDropdownClick}>
+ {this.props.button}
+ <FontAwesomeIcon icon="caret-down" size="sm" />
+ </button> :
+ <>
+ {this.props.button}
+ <button className="dropdown-button antimodeMenu-button" key="antimodebutton" onPointerDown={this.onDropdownClick}>
+ <FontAwesomeIcon icon="caret-down" size="sm" />
+ </button>
+ </>}
+
+ {this.showDropdown ? this.props.dropdownContent : (null)}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
new file mode 100644
index 000000000..d619bc4a0
--- /dev/null
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -0,0 +1,319 @@
+import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from "prosemirror-inputrules";
+import { NodeSelection, TextSelection } from "prosemirror-state";
+import { DataSym, Doc } from "../../../../new_fields/Doc";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { ComputedField } from "../../../../new_fields/ScriptField";
+import { Cast, NumCast } from "../../../../new_fields/Types";
+import { returnFalse, Utils } from "../../../../Utils";
+import { DocServer } from "../../../DocServer";
+import { Docs, DocUtils } from "../../../documents/Documents";
+import { FormattedTextBox } from "./FormattedTextBox";
+import { wrappingInputRule } from "./prosemirrorPatches";
+import RichTextMenu from "./RichTextMenu";
+import { schema } from "./schema_rts";
+
+export class RichTextRules {
+ public Document: Doc;
+ public TextBox: FormattedTextBox;
+ public EnteringStyle: boolean = false;
+ constructor(doc: Doc, textBox: FormattedTextBox) {
+ this.Document = doc;
+ this.TextBox = textBox;
+ }
+ public inpRules = {
+ rules: [
+ ...smartQuotes,
+ ellipsis,
+ emDash,
+
+ // > blockquote
+ wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote),
+
+ // 1. ordered list
+ wrappingInputRule(
+ /^1\.\s$/,
+ schema.nodes.ordered_list,
+ () => {
+ return ({ mapStyle: "decimal", bulletStyle: 1 });
+ },
+ (match: any, node: any) => {
+ return node.childCount + node.attrs.order === +match[1];
+ },
+ (type: any) => ({ type: type, attrs: { mapStyle: "decimal", bulletStyle: 1 } })
+ ),
+ // a. alphabbetical list
+ wrappingInputRule(
+ /^a\.\s$/,
+ schema.nodes.ordered_list,
+ // match => {
+ () => {
+ return ({ mapStyle: "alpha", bulletStyle: 1 });
+ // return ({ order: +match[1] })
+ },
+ (match: any, node: any) => {
+ return node.childCount + node.attrs.order === +match[1];
+ },
+ (type: any) => ({ type: type, attrs: { mapStyle: "alpha", bulletStyle: 1 } })
+ ),
+
+ // * bullet list
+ wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list),
+
+ // ``` code block
+ textblockTypeInputRule(/^```$/, schema.nodes.code_block),
+
+ // create an inline view of a tag stored under the '#' field
+ new InputRule(
+ new RegExp(/#([a-zA-Z_\-]+[a-zA-Z_\-0-9]*)\s$/),
+ (state, match, start, end) => {
+ const tag = match[1];
+ if (!tag) return state.tr;
+ this.Document[DataSym]["#"] = tag;
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey: "#" });
+ return state.tr.deleteRange(start, end).insert(start, fieldView);
+ }),
+
+ // # heading
+ textblockTypeInputRule(
+ new RegExp(/^(#{1,6})\s$/),
+ schema.nodes.heading,
+ match => {
+ return ({ level: match[1].length });
+ }
+ ),
+
+ // set the font size using #<font-size>
+ new InputRule(
+ new RegExp(/%([0-9]+)\s$/),
+ (state, match, start, end) => {
+ const size = Number(match[1]);
+ return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size }));
+ }),
+
+ // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document [[ <fieldKey> : <Doc>]] // [[:Doc]] => hyperlink [[fieldKey]] => show field [[fieldKey:Doc]] => show field of doc
+ new InputRule(
+ new RegExp(/\[\[([a-zA-Z_@\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@\? \-0-9]+)?\]\]$/),
+ (state, match, start, end) => {
+ const fieldKey = match[1];
+ const docid = match[3]?.substring(1);
+ const value = match[2]?.substring(1);
+ if (!fieldKey) {
+ if (docid) {
+ DocServer.GetRefField(docid).then(docx => {
+ const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true, }, docid);
+ DocUtils.Publish(target, docid, returnFalse, returnFalse);
+ DocUtils.MakeLink({ doc: this.Document }, { doc: target }, "portal to");
+ });
+ const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + docid), location: "onRight", title: docid, targetId: docid });
+ return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link);
+ }
+ return state.tr;
+ }
+ if (value !== "" && value !== undefined) {
+ const num = value.match(/^[0-9.]$/);
+ this.Document[DataSym][fieldKey] = value === "true" ? true : value === "false" ? false : (num ? Number(value) : value);
+ }
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid });
+ return state.tr.deleteRange(start, end).insert(start, fieldView);
+ }),
+ // create an inline view of a document {{ <layoutKey> : <Doc> }} // {{:Doc}} => show default view of document {{<layout>}} => show layout for this doc {{<layout> : Doc}} => show layout for another doc
+ new InputRule(
+ new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(\([a-zA-Z0-9…._/\-]*\))?(:[a-zA-Z_ \-0-9]+)?\}\}$/),
+ (state, match, start, end) => {
+ const fieldKey = match[1] || "";
+ const fieldParam = match[2]?.replace("…", "...") || "";
+ const docid = match[3]?.substring(1);
+ if (!fieldKey && !docid) return state.tr;
+ docid && DocServer.GetRefField(docid).then(docx => {
+ if (!(docx instanceof Doc && docx)) {
+ const docx = Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true }, docid);
+ DocUtils.Publish(docx, docid, returnFalse, returnFalse);
+ }
+ });
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: "dashDoc", docid, fieldKey: fieldKey + fieldParam, float: "unset", alias: Utils.GenerateGuid() });
+ const sm = state.storedMarks || undefined;
+ return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
+ }),
+ new InputRule(
+ new RegExp(/>>$/),
+ (state, match, start, end) => {
+ const textDoc = this.Document[DataSym];
+ const numInlines = NumCast(textDoc.inlineTextCount);
+ textDoc.inlineTextCount = numInlines + 1;
+ const inlineFieldKey = "inline" + numInlines; // which field on the text document this annotation will write to
+ const inlineLayoutKey = "layout_" + inlineFieldKey; // the field holding the layout string that will render the inline annotation
+ const textDocInline = Docs.Create.TextDocument("", { layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, _fontSize: 9, title: "inline comment" });
+ textDocInline.title = inlineFieldKey; // give the annotation its own title
+ textDocInline.customTitle = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc
+ textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point
+ textDocInline.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]]
+ textDocInline._textContext = ComputedField.MakeFunction(`copyField(self.${inlineFieldKey})`);
+ textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text
+ textDoc[inlineFieldKey] = ""; // set a default value for the annotation
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const newNode = schema.nodes.dashComment.create({ docid: textDocInline[Id] });
+ const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: textDocInline[Id], float: "right" });
+ const sm = state.storedMarks || undefined;
+ const replaced = node ? state.tr.insert(start, newNode).replaceRangeWith(start + 1, end + 1, dashDoc).insertText(" ", start + 2).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced;
+ }),
+ // stop using active style
+ new InputRule(
+ new RegExp(/%%$/),
+ (state, match, start, end) => {
+ const tr = state.tr.deleteRange(start, end);
+ const marks = state.tr.selection.$anchor.nodeBefore?.marks;
+ return marks ? Array.from(marks).filter(m => m !== state.schema.marks.user_mark).reduce((tr, m) => tr.removeStoredMark(m), tr) : tr;
+ }),
+
+ // set the Todo user-tag on the current selection (assumes % was used to initiate an EnteringStyle mode)
+ new InputRule(
+ new RegExp(/[ti!x]$/),
+ (state, match, start, end) => {
+ if (state.selection.to === state.selection.from || !this.EnteringStyle) return null;
+ const tag = match[0] === "t" ? "todo" : match[0] === "i" ? "ignore" : match[0] === "x" ? "disagree" : match[0] === "!" ? "important" : "??";
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag);
+ return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ }),
+
+ // set the First-line indent node type for the selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
+ new InputRule(
+ new RegExp(/(%d|d)$/),
+ (state, match, start, end) => {
+ if (!match[0].startsWith("%") && !this.EnteringStyle) return null;
+ const pos = (state.doc.resolve(start) as any);
+ for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
+ const node = pos.node(depth);
+ if (node.type === schema.nodes.paragraph) {
+ const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === 25 ? undefined : 25 });
+ const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
+ return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
+ }
+ }
+ return null;
+ }),
+
+ // set the Hanging indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
+ new InputRule(
+ new RegExp(/(%h|h)$/),
+ (state, match, start, end) => {
+ if (!match[0].startsWith("%") && !this.EnteringStyle) return null;
+ const pos = (state.doc.resolve(start) as any);
+ for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
+ const node = pos.node(depth);
+ if (node.type === schema.nodes.paragraph) {
+ const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === -25 ? undefined : -25 });
+ const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
+ return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
+ }
+ }
+ return null;
+ }),
+ // set the Quoted indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
+ new InputRule(
+ new RegExp(/(%q|q)$/),
+ (state, match, start, end) => {
+ if (!match[0].startsWith("%") && !this.EnteringStyle) return null;
+ const pos = (state.doc.resolve(start) as any);
+ if (state.selection instanceof NodeSelection && state.selection.node.type === schema.nodes.ordered_list) {
+ const node = state.selection.node;
+ return state.tr.setNodeMarkup(pos.pos, node.type, { ...node.attrs, indent: node.attrs.indent === 30 ? undefined : 30 });
+ }
+ for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
+ const node = pos.node(depth);
+ if (node.type === schema.nodes.paragraph) {
+ const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, inset: node.attrs.inset === 30 ? undefined : 30 });
+ const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
+ return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
+ }
+ }
+ return null;
+ }),
+
+
+ // center justify text
+ new InputRule(
+ new RegExp(/%\^$/),
+ (state, match, start, end) => {
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || undefined;
+ const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
+ }),
+ // left justify text
+ new InputRule(
+ new RegExp(/%\[$/),
+ (state, match, start, end) => {
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || undefined;
+ const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
+ }),
+ // right justify text
+ new InputRule(
+ new RegExp(/%\]$/),
+ (state, match, start, end) => {
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || undefined;
+ const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
+ }),
+ new InputRule(
+ new RegExp(/%\(/),
+ (state, match, start, end) => {
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || [];
+ const mark = state.schema.marks.summarizeInclusive.create();
+ sm.push(mark);
+ const selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark);
+ const content = selected.selection.content();
+ const replaced = node ? selected.replaceRangeWith(start, end,
+ schema.nodes.summary.create({ visibility: true, text: content, textslice: content.toJSON() })) :
+ state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]);
+ }),
+ new InputRule(
+ new RegExp(/%\)/),
+ (state, match, start, end) => {
+ return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create());
+ }),
+ new InputRule(
+ new RegExp(/%f$/),
+ (state, match, start, end) => {
+ const newNode = schema.nodes.footnote.create({});
+ const tr = state.tr;
+ tr.deleteRange(start, end).replaceSelectionWith(newNode); // replace insertion with a footnote.
+ return tr.setSelection(new NodeSelection( // select the footnote node to open its display
+ tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
+ tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize)));
+ }),
+
+ // activate a style by name using prefix '%'
+ new InputRule(
+ new RegExp(/%[a-z]+$/),
+ (state, match, start, end) => {
+ const color = match[0].substring(1, match[0].length);
+ const marks = RichTextMenu.Instance._brushMap.get(color);
+ if (marks) {
+ const tr = state.tr.deleteRange(start, end);
+ return marks ? Array.from(marks).reduce((tr, m) => tr.addStoredMark(m), tr) : tr;
+ }
+ const isValidColor = (strColor: string) => {
+ const s = new Option().style;
+ s.color = strColor;
+ return s.color === strColor.toLowerCase(); // 'false' if color wasn't assigned
+ };
+ if (isValidColor(color)) {
+ return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontColor.create({ color: color }));
+ }
+ return null;
+ }),
+ ]
+ };
+}
diff --git a/src/client/views/nodes/formattedText/RichTextSchema.tsx b/src/client/views/nodes/formattedText/RichTextSchema.tsx
new file mode 100644
index 000000000..cdb7374f8
--- /dev/null
+++ b/src/client/views/nodes/formattedText/RichTextSchema.tsx
@@ -0,0 +1,537 @@
+import { IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { baseKeymap, toggleMark } from "prosemirror-commands";
+import { redo, undo } from "prosemirror-history";
+import { keymap } from "prosemirror-keymap";
+import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
+import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
+import { EditorState, NodeSelection, Plugin, TextSelection } from "prosemirror-state";
+import { StepMap } from "prosemirror-transform";
+import { EditorView } from "prosemirror-view";
+import * as ReactDOM from 'react-dom';
+import { Doc, DocListCast, Field, HeightSym, WidthSym } from "../../../../new_fields/Doc";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { List } from "../../../../new_fields/List";
+import { ObjectField } from "../../../../new_fields/ObjectField";
+import { listSpec } from "../../../../new_fields/Schema";
+import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField";
+import { ComputedField } from "../../../../new_fields/ScriptField";
+import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../../new_fields/Types";
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils, returnZero } from "../../../../Utils";
+import { DocServer } from "../../../DocServer";
+import { Docs } from "../../../documents/Documents";
+import { CollectionViewType } from "../../collections/CollectionView";
+import { DocumentView } from "../DocumentView";
+import { FormattedTextBox } from "./FormattedTextBox";
+import { DocumentManager } from "../../../util/DocumentManager";
+import { Transform } from "../../../util/Transform";
+import React = require("react");
+
+import { schema } from "./schema_rts";
+
+export class OrderedListView {
+ update(node: any) {
+ return false; // if attr's of an ordered_list (e.g., bulletStyle) change, return false forces the dom node to be recreated which is necessary for the bullet labels to update
+ }
+}
+
+export class ImageResizeView {
+ _handle: HTMLElement;
+ _img: HTMLElement;
+ _outer: HTMLElement;
+ constructor(node: any, view: any, getPos: any, addDocTab: any) {
+ //moved
+ this._handle = document.createElement("span");
+ this._img = document.createElement("img");
+ this._outer = document.createElement("span");
+ this._outer.style.position = "relative";
+ this._outer.style.width = node.attrs.width;
+ this._outer.style.height = node.attrs.height;
+ this._outer.style.display = "inline-block";
+ this._outer.style.overflow = "hidden";
+ (this._outer.style as any).float = node.attrs.float;
+ //moved
+ this._img.setAttribute("src", node.attrs.src);
+ this._img.style.width = "100%";
+ this._handle.style.position = "absolute";
+ this._handle.style.width = "20px";
+ this._handle.style.height = "20px";
+ this._handle.style.backgroundColor = "blue";
+ this._handle.style.borderRadius = "15px";
+ this._handle.style.display = "none";
+ this._handle.style.bottom = "-10px";
+ this._handle.style.right = "-10px";
+ const self = this;
+ //moved
+ this._img.onclick = function (e: any) {
+ e.stopPropagation();
+ e.preventDefault();
+ if (view.state.selection.node && view.state.selection.node.type !== view.state.schema.nodes.image) {
+ view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(view.state.selection.from - 2))));
+ }
+ };
+ //moved
+ this._img.onpointerdown = function (e: any) {
+ if (e.ctrlKey) {
+ e.preventDefault();
+ e.stopPropagation();
+ DocServer.GetRefField(node.attrs.docid).then(async linkDoc =>
+ (linkDoc instanceof Doc) &&
+ DocumentManager.Instance.FollowLink(linkDoc, view.state.schema.Document,
+ document => addDocTab(document, node.attrs.location ? node.attrs.location : "inTab"), false));
+ }
+ };
+ //moved
+ this._handle.onpointerdown = function (e: any) {
+ e.preventDefault();
+ e.stopPropagation();
+ const wid = Number(getComputedStyle(self._img).width.replace(/px/, ""));
+ const hgt = Number(getComputedStyle(self._img).height.replace(/px/, ""));
+ const startX = e.pageX;
+ const startWidth = parseFloat(node.attrs.width);
+ const onpointermove = (e: any) => {
+ const currentX = e.pageX;
+ const diffInPx = currentX - startX;
+ self._outer.style.width = `${startWidth + diffInPx}`;
+ self._outer.style.height = `${(startWidth + diffInPx) * hgt / wid}`;
+ };
+
+ const onpointerup = () => {
+ document.removeEventListener("pointermove", onpointermove);
+ document.removeEventListener("pointerup", onpointerup);
+ const pos = view.state.selection.from;
+ view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: self._outer.style.width, height: self._outer.style.height }));
+ view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(pos))));
+ };
+
+ document.addEventListener("pointermove", onpointermove);
+ document.addEventListener("pointerup", onpointerup);
+ };
+ //Moved
+ this._outer.appendChild(this._img);
+ this._outer.appendChild(this._handle);
+ (this as any).dom = this._outer;
+ }
+
+ selectNode() {
+ this._img.classList.add("ProseMirror-selectednode");
+
+ this._handle.style.display = "";
+ }
+
+ deselectNode() {
+ this._img.classList.remove("ProseMirror-selectednode");
+
+ this._handle.style.display = "none";
+ }
+}
+
+export class DashDocCommentView {
+ _collapsed: HTMLElement;
+ _view: any;
+ constructor(node: any, view: any, getPos: any) {
+ //moved
+ this._collapsed = document.createElement("span");
+ this._collapsed.className = "formattedTextBox-inlineComment";
+ this._collapsed.id = "DashDocCommentView-" + node.attrs.docid;
+ this._view = view;
+ //moved
+ const targetNode = () => { // search forward in the prosemirror doc for the attached dashDocNode that is the target of the comment anchor
+ for (let i = getPos() + 1; i < view.state.doc.content.size; i++) {
+ const m = view.state.doc.nodeAt(i);
+ if (m && m.type === view.state.schema.nodes.dashDoc && m.attrs.docid === node.attrs.docid) {
+ return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any, pos: number, hidden: boolean };
+ }
+ }
+ const dashDoc = view.state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: node.attrs.docid, float: "right" });
+ view.dispatch(view.state.tr.insert(getPos() + 1, dashDoc));
+ setTimeout(() => { try { view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.tr.doc, getPos() + 2))); } catch (e) { } }, 0);
+ return undefined;
+ };
+ //moved
+ this._collapsed.onpointerdown = (e: any) => {
+ e.stopPropagation();
+ };
+ //moved
+ this._collapsed.onpointerup = (e: any) => {
+ const target = targetNode();
+ if (target) {
+ const expand = target.hidden;
+ const tr = view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true });
+ view.dispatch(tr.setSelection(TextSelection.create(tr.doc, getPos() + (expand ? 2 : 1)))); // update the attrs
+ setTimeout(() => {
+ expand && DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
+ try { view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.tr.doc, getPos() + (expand ? 2 : 1)))); } catch (e) { }
+ }, 0);
+ }
+ e.stopPropagation();
+ };
+ //moved
+ this._collapsed.onpointerenter = (e: any) => {
+ DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
+ e.preventDefault();
+ e.stopPropagation();
+ };
+ //moved
+ this._collapsed.onpointerleave = (e: any) => {
+ DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
+ e.preventDefault();
+ e.stopPropagation();
+ };
+
+ (this as any).dom = this._collapsed;
+ }
+ //moved
+ selectNode() { }
+}
+
+export class DashDocView {
+ _dashSpan: HTMLDivElement;
+ _outer: HTMLElement;
+ _dashDoc: Doc | undefined;
+ _reactionDisposer: IReactionDisposer | undefined;
+ _renderDisposer: IReactionDisposer | undefined;
+ _textBox: FormattedTextBox;
+
+ getDocTransform = () => {
+ const { scale, translateX, translateY } = Utils.GetScreenTransform(this._outer);
+ return new Transform(-translateX, -translateY, 1).scale(1 / this.contentScaling() / scale);
+ }
+ contentScaling = () => NumCast(this._dashDoc!._nativeWidth) > 0 ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!._nativeWidth) : 1;
+
+ outerFocus = (target: Doc) => this._textBox.props.focus(this._textBox.props.Document); // ideally, this would scroll to show the focus target
+
+ constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+ this._textBox = tbox;
+ this._dashSpan = document.createElement("div");
+ this._outer = document.createElement("span");
+ this._outer.style.position = "relative";
+ this._outer.style.textIndent = "0";
+ this._outer.style.border = "1px solid " + StrCast(tbox.layoutDoc.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray"));
+ this._outer.style.width = node.attrs.width;
+ this._outer.style.height = node.attrs.height;
+ this._outer.style.display = node.attrs.hidden ? "none" : "inline-block";
+ // this._outer.style.overflow = "hidden"; // bcz: not sure if this is needed. if it's used, then the doc doesn't highlight when you hover over a docComment
+ (this._outer.style as any).float = node.attrs.float;
+
+ this._dashSpan.style.width = node.attrs.width;
+ this._dashSpan.style.height = node.attrs.height;
+ this._dashSpan.style.position = "absolute";
+ this._dashSpan.style.display = "inline-block";
+ this._dashSpan.onpointerleave = () => {
+ const ele = document.getElementById("DashDocCommentView-" + node.attrs.docid);
+ if (ele) {
+ (ele as HTMLDivElement).style.backgroundColor = "";
+ }
+ };
+ this._dashSpan.onpointerenter = () => {
+ const ele = document.getElementById("DashDocCommentView-" + node.attrs.docid);
+ if (ele) {
+ (ele as HTMLDivElement).style.backgroundColor = "orange";
+ }
+ };
+ const removeDoc = () => {
+ const pos = getPos();
+ const ns = new NodeSelection(view.state.doc.resolve(pos));
+ view.dispatch(view.state.tr.setSelection(ns).deleteSelection());
+ return true;
+ };
+ const alias = node.attrs.alias;
+
+ const docid = node.attrs.docid || tbox.props.Document[Id];// tbox.props.DataDoc?.[Id] || tbox.dataDoc?.[Id];
+ DocServer.GetRefField(docid + alias).then(async dashDoc => {
+ if (!(dashDoc instanceof Doc)) {
+ alias && DocServer.GetRefField(docid).then(async dashDocBase => {
+ if (dashDocBase instanceof Doc) {
+ const aliasedDoc = Doc.MakeAlias(dashDocBase, docid + alias);
+ aliasedDoc.layoutKey = "layout";
+ node.attrs.fieldKey && Doc.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, node.attrs.fieldKey, undefined);
+ self.doRender(aliasedDoc, removeDoc, node, view, getPos);
+ }
+ });
+ } else {
+ self.doRender(dashDoc, removeDoc, node, view, getPos);
+ }
+ });
+ const self = this;
+ this._dashSpan.onkeydown = function (e: any) {
+ e.stopPropagation();
+ if (e.key === "Tab" || e.key === "Enter") {
+ e.preventDefault();
+ }
+ };
+ this._dashSpan.onkeypress = function (e: any) { e.stopPropagation(); };
+ this._dashSpan.onwheel = function (e: any) { e.preventDefault(); };
+ this._dashSpan.onkeyup = function (e: any) { e.stopPropagation(); };
+ this._outer.appendChild(this._dashSpan);
+ (this as any).dom = this._outer;
+ }
+
+ doRender(dashDoc: Doc, removeDoc: any, node: any, view: any, getPos: any) {
+ this._dashDoc = dashDoc;
+ const self = this;
+ const dashLayoutDoc = Doc.Layout(dashDoc);
+ const finalLayout = node.attrs.docid ? dashDoc : Doc.expandTemplateLayout(dashLayoutDoc, dashDoc, node.attrs.fieldKey);
+
+ if (!finalLayout) setTimeout(() => self.doRender(dashDoc, removeDoc, node, view, getPos), 0);
+ else {
+ this._reactionDisposer?.();
+ this._reactionDisposer = reaction(() => ({ dim: [finalLayout[WidthSym](), finalLayout[HeightSym]()], color: finalLayout.color }), ({ dim, color }) => {
+ this._dashSpan.style.width = this._outer.style.width = Math.max(20, dim[0]) + "px";
+ this._dashSpan.style.height = this._outer.style.height = Math.max(20, dim[1]) + "px";
+ this._outer.style.border = "1px solid " + StrCast(finalLayout.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray"));
+ }, { fireImmediately: true });
+ const doReactRender = (finalLayout: Doc, resolvedDataDoc: Doc) => {
+ ReactDOM.unmountComponentAtNode(this._dashSpan);
+
+ ReactDOM.render(<DocumentView
+ Document={finalLayout}
+ DataDoc={resolvedDataDoc}
+ LibraryPath={this._textBox.props.LibraryPath}
+ fitToBox={BoolCast(dashDoc._fitToBox)}
+ addDocument={returnFalse}
+ rootSelected={this._textBox.props.isSelected}
+ removeDocument={removeDoc}
+ ScreenToLocalTransform={this.getDocTransform}
+ addDocTab={this._textBox.props.addDocTab}
+ pinToPres={returnFalse}
+ renderDepth={self._textBox.props.renderDepth + 1}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ PanelWidth={finalLayout[WidthSym]}
+ PanelHeight={finalLayout[HeightSym]}
+ focus={this.outerFocus}
+ backgroundColor={returnEmptyString}
+ parentActive={returnFalse}
+ whenActiveChanged={returnFalse}
+ bringToFront={emptyFunction}
+ dontRegisterView={false}
+ ContainingCollectionView={this._textBox.props.ContainingCollectionView}
+ ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc}
+ ContentScaling={this.contentScaling}
+ />, this._dashSpan);
+ if (node.attrs.width !== dashDoc._width + "px" || node.attrs.height !== dashDoc._height + "px") {
+ try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made
+ view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc._width + "px", height: dashDoc._height + "px" }));
+ } catch (e) {
+ console.log(e);
+ }
+ }
+ };
+ this._renderDisposer?.();
+ this._renderDisposer = reaction(() => {
+ // if (!Doc.AreProtosEqual(finalLayout, dashDoc)) {
+ // finalLayout.rootDocument = dashDoc.aliasOf; // bcz: check on this ... why is it here?
+ // }
+ const layoutKey = StrCast(finalLayout.layoutKey);
+ const finalKey = layoutKey && StrCast(finalLayout[layoutKey]).split("'")?.[1];
+ if (finalLayout !== dashDoc && finalKey) {
+ const finalLayoutField = finalLayout[finalKey];
+ if (finalLayoutField instanceof ObjectField) {
+ finalLayout[finalKey + "-textTemplate"] = ComputedField.MakeFunction(`copyField(this.${finalKey})`, { this: Doc.name });
+ }
+ }
+ return { finalLayout, resolvedDataDoc: Cast(finalLayout.resolvedDataDoc, Doc, null) };
+ },
+ (res) => doReactRender(res.finalLayout, res.resolvedDataDoc),
+ { fireImmediately: true });
+ }
+ }
+ destroy() {
+ ReactDOM.unmountComponentAtNode(this._dashSpan);
+ this._reactionDisposer?.();
+ }
+}
+
+export class FootnoteView {
+ innerView: any;
+ outerView: any;
+ node: any;
+ dom: any;
+ getPos: any;
+
+ constructor(node: any, view: any, getPos: any) {
+ // We'll need these later
+ this.node = node;
+ this.outerView = view;
+ this.getPos = getPos;
+
+ // The node's representation in the editor (empty, for now)
+ this.dom = document.createElement("footnote");
+ this.dom.addEventListener("pointerup", this.toggle, true);
+ // These are used when the footnote is selected
+ this.innerView = null;
+ }
+ selectNode() {
+ const attrs = { ...this.node.attrs };
+ attrs.visibility = true;
+ this.dom.classList.add("ProseMirror-selectednode");
+ if (!this.innerView) this.open();
+ }
+
+ deselectNode() {
+ const attrs = { ...this.node.attrs };
+ attrs.visibility = false;
+ this.dom.classList.remove("ProseMirror-selectednode");
+ if (this.innerView) this.close();
+ }
+ open() {
+ // Append a tooltip to the outer node
+ const tooltip = this.dom.appendChild(document.createElement("div"));
+ tooltip.className = "footnote-tooltip";
+ // And put a sub-ProseMirror into that
+ this.innerView = new EditorView(tooltip, {
+ // You can use any node as an editor document
+ state: EditorState.create({
+ doc: this.node,
+ plugins: [keymap(baseKeymap),
+ keymap({
+ "Mod-z": () => undo(this.outerView.state, this.outerView.dispatch),
+ "Mod-y": () => redo(this.outerView.state, this.outerView.dispatch),
+ "Mod-b": toggleMark(schema.marks.strong)
+ }),
+ // new Plugin({
+ // view(newView) {
+ // // TODO -- make this work with RichTextMenu
+ // // return FormattedTextBox.getToolTip(newView);
+ // }
+ // })
+ ],
+
+ }),
+ // This is the magic part
+ dispatchTransaction: this.dispatchInner.bind(this),
+ handleDOMEvents: {
+ pointerdown: ((view: any, e: PointerEvent) => {
+ // Kludge to prevent issues due to the fact that the whole
+ // footnote is node-selected (and thus DOM-selected) when
+ // the parent editor is focused.
+ e.stopPropagation();
+ document.addEventListener("pointerup", this.ignore, true);
+ if (this.outerView.hasFocus()) this.innerView.focus();
+ }) as any
+ }
+
+ });
+ setTimeout(() => this.innerView && this.innerView.docView.setSelection(0, 0, this.innerView.root, true), 0);
+ }
+
+ ignore = (e: PointerEvent) => {
+ e.stopPropagation();
+ document.removeEventListener("pointerup", this.ignore, true);
+ }
+
+ toggle = () => {
+ if (this.innerView) this.close();
+ else {
+ this.open();
+ }
+ }
+ close() {
+ this.innerView && this.innerView.destroy();
+ this.innerView = null;
+ this.dom.textContent = "";
+ }
+
+ dispatchInner(tr: any) {
+ const { state, transactions } = this.innerView.state.applyTransaction(tr);
+ this.innerView.updateState(state);
+
+ if (!tr.getMeta("fromOutside")) {
+ const outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1);
+ for (const transaction of transactions) {
+ const steps = transaction.steps;
+ for (const step of steps) {
+ outerTr.step(step.map(offsetMap));
+ }
+ }
+ if (outerTr.docChanged) this.outerView.dispatch(outerTr);
+ }
+ }
+ update(node: any) {
+ if (!node.sameMarkup(this.node)) return false;
+ this.node = node;
+ if (this.innerView) {
+ const state = this.innerView.state;
+ const start = node.content.findDiffStart(state.doc.content);
+ if (start !== null) {
+ let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content);
+ const overlap = start - Math.min(endA, endB);
+ if (overlap > 0) { endA += overlap; endB += overlap; }
+ this.innerView.dispatch(
+ state.tr
+ .replace(start, endB, node.slice(start, endA))
+ .setMeta("fromOutside", true));
+ }
+ }
+ return true;
+ }
+
+ destroy() {
+ if (this.innerView) this.close();
+ }
+
+ stopEvent(event: any) {
+ return this.innerView && this.innerView.dom.contains(event.target);
+ }
+
+ ignoreMutation() { return true; }
+}
+
+export class SummaryView {
+ _collapsed: HTMLElement;
+ _view: any;
+ constructor(node: any, view: any, getPos: any) {
+ this._collapsed = document.createElement("span");
+ this._collapsed.className = this.className(node.attrs.visibility);
+ this._view = view;
+ const js = node.toJSON;
+ node.toJSON = function () {
+ return js.apply(this, arguments);
+ };
+
+ this._collapsed.onpointerdown = (e: any) => {
+ const visible = !node.attrs.visibility;
+ const attrs = { ...node.attrs, visibility: visible };
+ let textSelection = TextSelection.create(view.state.doc, getPos() + 1);
+ if (!visible) { // update summarized text and save in attrs
+ textSelection = this.updateSummarizedText(getPos() + 1);
+ attrs.text = textSelection.content();
+ attrs.textslice = attrs.text.toJSON();
+ }
+ view.dispatch(view.state.tr.
+ setSelection(textSelection). // select the current summarized text (or where it will be if its collapsed)
+ replaceSelection(!visible ? new Slice(Fragment.fromArray([]), 0, 0) : node.attrs.text). // collapse/expand it
+ setNodeMarkup(getPos(), undefined, attrs)); // update the attrs
+ e.preventDefault();
+ e.stopPropagation();
+ this._collapsed.className = this.className(visible);
+ };
+ (this as any).dom = this._collapsed;
+ }
+ selectNode() { }
+
+ deselectNode() { }
+
+ className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed");
+
+ updateSummarizedText(start?: any) {
+ const mtype = this._view.state.schema.marks.summarize;
+ const mtypeInc = this._view.state.schema.marks.summarizeInclusive;
+ let endPos = start;
+
+ const visited = new Set();
+ for (let i: number = start + 1; i < this._view.state.doc.nodeSize - 1; i++) {
+ let skip = false;
+ this._view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => {
+ if (node.isLeaf && !visited.has(node) && !skip) {
+ if (node.marks.find((m: any) => m.type === mtype || m.type === mtypeInc)) {
+ visited.add(node);
+ endPos = i + node.nodeSize - 1;
+ }
+ else skip = true;
+ }
+ });
+ }
+ return TextSelection.create(this._view.state.doc, start, endPos);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx
new file mode 100644
index 000000000..89908d8ee
--- /dev/null
+++ b/src/client/views/nodes/formattedText/SummaryView.tsx
@@ -0,0 +1,81 @@
+import { TextSelection } from "prosemirror-state";
+import { Fragment, Node, Slice } from "prosemirror-model";
+
+import React = require("react");
+
+interface ISummaryView {
+ node: any;
+ view: any;
+ getPos: any;
+ self: any;
+}
+export class SummaryView extends React.Component<ISummaryView> {
+
+ onPointerDown = (e: any) => {
+ const visible = !this.props.node.attrs.visibility;
+ const attrs = { ...this.props.node.attrs, visibility: visible };
+ let textSelection = TextSelection.create(this.props.view.state.doc, this.props.getPos() + 1);
+ if (!visible) { // update summarized text and save in attrs
+ textSelection = this.updateSummarizedText(this.props.getPos() + 1);
+ attrs.text = textSelection.content();
+ attrs.textslice = attrs.text.toJSON();
+ }
+ this.props.view.dispatch(this.props.view.state.tr.
+ setSelection(textSelection). // select the current summarized text (or where it will be if its collapsed)
+ replaceSelection(!visible ? new Slice(Fragment.fromArray([]), 0, 0) : this.props.node.attrs.text). // collapse/expand it
+ setNodeMarkup(this.props.getPos(), undefined, attrs)); // update the attrs
+ e.preventDefault();
+ e.stopPropagation();
+ const _collapsed = document.getElementById('collapse') as HTMLElement;
+ _collapsed.className = this.className(visible);
+ }
+
+ updateSummarizedText(start?: any) {
+ const mtype = this.props.view.state.schema.marks.summarize;
+ const mtypeInc = this.props.view.state.schema.marks.summarizeInclusive;
+ let endPos = start;
+
+ const visited = new Set();
+ for (let i: number = start + 1; i < this.props.view.state.doc.nodeSize - 1; i++) {
+ let skip = false;
+ this.props.view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => {
+ if (this.props.node.isLeaf && !visited.has(node) && !skip) {
+ if (this.props.node.marks.find((m: any) => m.type === mtype || m.type === mtypeInc)) {
+ visited.add(node);
+ endPos = i + this.props.node.nodeSize - 1;
+ }
+ else skip = true;
+ }
+ });
+ }
+ return TextSelection.create(this.props.view.state.doc, start, endPos);
+ }
+
+ className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed");
+
+ selectNode() { }
+
+ deselectNode() { }
+
+ render() {
+ const _view = this.props.node.view;
+ const js = this.props.node.toJSon;
+
+ this.props.node.toJSON = function () {
+ return js.apply(this, arguments);
+ };
+
+ const spanCollapsedClassName = this.className(this.props.node.attrs.visibility);
+
+ return (
+ <span
+ className={spanCollapsedClassName}
+ id='collapse'
+ onPointerDown={this.onPointerDown}
+ >
+
+ </span>
+ );
+
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/TooltipTextMenu.scss b/src/client/views/nodes/formattedText/TooltipTextMenu.scss
new file mode 100644
index 000000000..e2149e9c1
--- /dev/null
+++ b/src/client/views/nodes/formattedText/TooltipTextMenu.scss
@@ -0,0 +1,372 @@
+@import "../views/globalCssVariables";
+.ProseMirror-menu-dropdown-wrap {
+ display: inline-block;
+ position: relative;
+}
+
+.ProseMirror-menu-dropdown {
+ vertical-align: 1px;
+ cursor: pointer;
+ position: relative;
+ padding: 0 15px 0 4px;
+ background: white;
+ border-radius: 2px;
+ text-align: left;
+ font-size: 12px;
+ white-space: nowrap;
+ margin-right: 4px;
+
+ &:after {
+ content: "";
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 4px solid currentColor;
+ opacity: .6;
+ position: absolute;
+ right: 4px;
+ top: calc(50% - 2px);
+ }
+}
+
+.ProseMirror-menu-submenu-wrap {
+ position: relative;
+ margin-right: -4px;
+}
+
+.ProseMirror-menu-dropdown-menu,
+.ProseMirror-menu-submenu {
+ font-size: 12px;
+ background: white;
+ border: 1px solid rgb(223, 223, 223);
+ min-width: 40px;
+ z-index: 50000;
+ position: absolute;
+ box-sizing: content-box;
+
+ .ProseMirror-menu-dropdown-item {
+ cursor: pointer;
+ z-index: 100000;
+ text-align: left;
+ padding: 3px;
+
+ &:hover {
+ background-color: $light-color-secondary;
+ }
+ }
+}
+
+
+.ProseMirror-menu-submenu-label:after {
+ content: "";
+ border-top: 4px solid transparent;
+ border-bottom: 4px solid transparent;
+ border-left: 4px solid currentColor;
+ opacity: .6;
+ position: absolute;
+ right: 4px;
+ top: calc(50% - 4px);
+}
+
+ .ProseMirror-icon {
+ display: inline-block;
+ // line-height: .8;
+ // vertical-align: -2px; /* Compensate for padding */
+ // padding: 2px 8px;
+ cursor: pointer;
+
+ &.ProseMirror-menu-disabled {
+ cursor: default;
+ }
+
+ svg {
+ fill:white;
+ height: 1em;
+ }
+
+ span {
+ vertical-align: text-top;
+ }
+ }
+
+.wrapper {
+ position: absolute;
+ pointer-events: all;
+ display: flex;
+ align-items: center;
+ transform: translateY(-85px);
+
+ height: 35px;
+ background: #323232;
+ border-radius: 6px;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+
+}
+
+.tooltipMenu, .basic-tools {
+ z-index: 20000;
+ pointer-events: all;
+ padding: 3px;
+ padding-bottom: 5px;
+ display: flex;
+ align-items: center;
+
+ .ProseMirror-example-setup-style hr {
+ padding: 2px 10px;
+ border: none;
+ margin: 1em 0;
+ }
+
+ .ProseMirror-example-setup-style hr:after {
+ content: "";
+ display: block;
+ height: 1px;
+ background-color: silver;
+ line-height: 2px;
+ }
+}
+
+.menuicon {
+ width: 25px;
+ height: 25px;
+ cursor: pointer;
+ text-align: center;
+ line-height: 25px;
+ margin: 0 2px;
+ border-radius: 3px;
+
+ &:hover {
+ background-color: black;
+
+ #link-drag {
+ background-color: black;
+ }
+ }
+
+ &> * {
+ margin-top: 50%;
+ margin-left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ svg {
+ fill: white;
+ width: 18px;
+ height: 18px;
+ }
+}
+
+.menuicon-active {
+ width: 25px;
+ height: 25px;
+ cursor: pointer;
+ text-align: center;
+ line-height: 25px;
+ margin: 0 2px;
+ border-radius: 3px;
+
+ &:hover {
+ background-color: black;
+ }
+
+ &> * {
+ margin-top: 50%;
+ margin-left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ svg {
+ fill: greenyellow;
+ width: 18px;
+ height: 18px;
+ }
+}
+
+#colorPicker {
+ position: relative;
+
+ svg {
+ width: 18px;
+ height: 18px;
+ // margin-top: 11px;
+ }
+
+ .buttonColor {
+ position: absolute;
+ top: 24px;
+ left: 1px;
+ width: 24px;
+ height: 4px;
+ margin-top: 0;
+ }
+}
+
+#link-drag {
+ background-color: #323232;
+}
+
+.underline svg {
+ margin-top: 13px;
+}
+
+ .font-size-indicator {
+ font-size: 12px;
+ padding-right: 0px;
+ }
+ .summarize{
+ color: white;
+ height: 20px;
+ text-align: center;
+ }
+
+
+.brush{
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ stroke-width: 0;
+ stroke: currentColor;
+ fill: currentColor;
+ margin-right: 15px;
+}
+
+.brush-active{
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ stroke-width: 3;
+ fill: greenyellow;
+ margin-right: 15px;
+}
+
+.dragger-wrapper {
+ color: #eee;
+ height: 22px;
+ padding: 0 5px;
+ box-sizing: content-box;
+ cursor: grab;
+
+ .dragger {
+ width: 18px;
+ height: 100%;
+ display: flex;
+ justify-content: space-evenly;
+ }
+
+ .dragger-line {
+ width: 2px;
+ height: 100%;
+ background-color: black;
+ }
+}
+
+.button-dropdown-wrapper {
+ display: flex;
+ align-content: center;
+
+ &:hover {
+ background-color: black;
+ }
+}
+
+.buttonSettings-dropdown {
+
+ &.ProseMirror-menu-dropdown {
+ width: 10px;
+ height: 25px;
+ margin: 0;
+ padding: 0 2px;
+ background-color: #323232;
+ text-align: center;
+
+ &:after {
+ border-top: 4px solid white;
+ right: 2px;
+ }
+
+ &:hover {
+ background-color: black;
+ }
+ }
+
+ &.ProseMirror-menu-dropdown-menu {
+ min-width: 150px;
+ left: -27px;
+ top: 31px;
+ background-color: #323232;
+ border: 1px solid #4d4d4d;
+ color: $light-color-secondary;
+ // border: none;
+ // border: 1px solid $light-color-secondary;
+ border-radius: 0 6px 6px 6px;
+ padding: 3px;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+
+ .ProseMirror-menu-dropdown-item{
+ cursor: default;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &:hover {
+ background-color: #323232;
+ }
+
+ .button-setting, .button-setting-disabled {
+ padding: 2px;
+ border-radius: 2px;
+ }
+
+ .button-setting:hover {
+ cursor: pointer;
+ background-color: black;
+ }
+
+ .separated-button {
+ border-top: 1px solid $light-color-secondary;
+ padding-top: 6px;
+ }
+
+ input {
+ color: black;
+ border: none;
+ border-radius: 1px;
+ padding: 3px;
+ }
+
+ button {
+ padding: 6px;
+ background-color: #323232;
+ border: 1px solid black;
+ border-radius: 1px;
+
+ &:hover {
+ background-color: black;
+ }
+ }
+ }
+
+
+ }
+}
+
+.colorPicker-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ margin-top: 3px;
+ margin-left: -3px;
+ width: calc(100% + 6px);
+}
+
+button.colorPicker {
+ width: 20px;
+ height: 20px;
+ border-radius: 15px !important;
+ margin: 3px;
+ border: none !important;
+
+ &.active {
+ border: 2px solid white !important;
+ }
+}
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
new file mode 100644
index 000000000..46bf481fb
--- /dev/null
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -0,0 +1,296 @@
+import React = require("react");
+import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
+import { Doc } from "../../../../new_fields/Doc";
+
+
+const emDOM: DOMOutputSpecArray = ["em", 0];
+const strongDOM: DOMOutputSpecArray = ["strong", 0];
+const codeDOM: DOMOutputSpecArray = ["code", 0];
+
+// :: Object [Specs](#model.MarkSpec) for the marks in the schema.
+export const marks: { [index: string]: MarkSpec } = {
+ // :: MarkSpec A link. Has `href` and `title` attributes. `title`
+ // defaults to the empty string. Rendered and parsed as an `<a>`
+ // element.
+ link: {
+ attrs: {
+ href: {},
+ targetId: { default: "" },
+ linkId: { default: "" },
+ showPreview: { default: true },
+ location: { default: null },
+ title: { default: null },
+ docref: { default: false } // flags whether the linked text comes from a document within Dash. If so, an attribution label is appended after the text
+ },
+ inclusive: false,
+ parseDOM: [{
+ tag: "a[href]", getAttrs(dom: any) {
+ return { href: dom.getAttribute("href"), location: dom.getAttribute("location"), title: dom.getAttribute("title"), targetId: dom.getAttribute("id") };
+ }
+ }],
+ toDOM(node: any) {
+ return node.attrs.docref && node.attrs.title ?
+ ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, class: "prosemirror-attribution", title: `${node.attrs.title}` }, node.attrs.title], ["br"]] :
+ ["a", { ...node.attrs, id: node.attrs.linkId + node.attrs.targetId, title: `${node.attrs.title}` }, 0];
+ }
+ },
+
+
+ // :: MarkSpec Coloring on text. Has `color` attribute that defined the color of the marked text.
+ pFontColor: {
+ attrs: {
+ color: { default: "#000" }
+ },
+ inclusive: true,
+ parseDOM: [{
+ tag: "span", getAttrs(dom: any) {
+ return { color: dom.getAttribute("color") };
+ }
+ }],
+ toDOM(node: any) {
+ return node.attrs.color ? ['span', { style: 'color:' + node.attrs.color }] : ['span', 0];
+ }
+ },
+
+ marker: {
+ attrs: {
+ highlight: { default: "transparent" }
+ },
+ inclusive: true,
+ parseDOM: [{
+ tag: "span", getAttrs(dom: any) {
+ return { highlight: dom.getAttribute("backgroundColor") };
+ }
+ }],
+ toDOM(node: any) {
+ return node.attrs.highlight ? ['span', { style: 'background-color:' + node.attrs.highlight }] : ['span', { style: 'background-color: transparent' }];
+ }
+ },
+
+ // :: MarkSpec An emphasis mark. Rendered as an `<em>` element.
+ // Has parse rules that also match `<i>` and `font-style: italic`.
+ em: {
+ parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style: italic" }],
+ toDOM() { return emDOM; }
+ },
+
+ // :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules
+ // also match `<b>` and `font-weight: bold`.
+ strong: {
+ parseDOM: [{ tag: "strong" },
+ { tag: "b" },
+ { style: "font-weight" }],
+ toDOM() { return strongDOM; }
+ },
+
+ strikethrough: {
+ parseDOM: [
+ { tag: 'strike' },
+ { style: 'text-decoration=line-through' },
+ { style: 'text-decoration-line=line-through' }
+ ],
+ toDOM: () => ['span', {
+ style: 'text-decoration-line:line-through'
+ }]
+ },
+
+ subscript: {
+ excludes: 'superscript',
+ parseDOM: [
+ { tag: 'sub' },
+ { style: 'vertical-align=sub' }
+ ],
+ toDOM: () => ['sub']
+ },
+
+ superscript: {
+ excludes: 'subscript',
+ parseDOM: [
+ { tag: 'sup' },
+ { style: 'vertical-align=super' }
+ ],
+ toDOM: () => ['sup']
+ },
+
+ mbulletType: {
+ attrs: {
+ bulletType: { default: "decimal" }
+ },
+ toDOM(node: any) {
+ return ['span', {
+ style: `background: ${node.attrs.bulletType === "decimal" ? "yellow" : node.attrs.bulletType === "upper-alpha" ? "blue" : "green"}`
+ }];
+ }
+ },
+
+ metadata: {
+ toDOM() {
+ return ['span', { style: 'font-size:75%; background:rgba(100, 100, 100, 0.2); ' }];
+ }
+ },
+ metadataKey: {
+ toDOM() {
+ return ['span', { style: 'font-style:italic; ' }];
+ }
+ },
+ metadataVal: {
+ toDOM() {
+ return ['span'];
+ }
+ },
+
+ summarizeInclusive: {
+ parseDOM: [
+ {
+ tag: "span",
+ getAttrs: (p: any) => {
+ if (typeof (p) !== "string") {
+ const style = getComputedStyle(p);
+ if (style.textDecoration === "underline") return null;
+ if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 &&
+ p.parentElement.outerHTML.indexOf("text-decoration-style: solid") !== -1) {
+ return null;
+ }
+ }
+ return false;
+ }
+ },
+ ],
+ inclusive: true,
+ toDOM() {
+ return ['span', {
+ style: 'text-decoration: underline; text-decoration-style: solid; text-decoration-color: rgba(204, 206, 210, 0.92)'
+ }];
+ }
+ },
+
+ summarize: {
+ inclusive: false,
+ parseDOM: [
+ {
+ tag: "span",
+ getAttrs: (p: any) => {
+ if (typeof (p) !== "string") {
+ const style = getComputedStyle(p);
+ if (style.textDecoration === "underline") return null;
+ if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 &&
+ p.parentElement.outerHTML.indexOf("text-decoration-style: dotted") !== -1) {
+ return null;
+ }
+ }
+ return false;
+ }
+ },
+ ],
+ toDOM() {
+ return ['span', {
+ style: 'text-decoration: underline; text-decoration-style: dotted; text-decoration-color: rgba(204, 206, 210, 0.92)'
+ }];
+ }
+ },
+
+ underline: {
+ parseDOM: [
+ {
+ tag: "span",
+ getAttrs: (p: any) => {
+ if (typeof (p) !== "string") {
+ const style = getComputedStyle(p);
+ if (style.textDecoration === "underline" || p.parentElement.outerHTML.indexOf("text-decoration-style:line") !== -1) {
+ return null;
+ }
+ }
+ return false;
+ }
+ }
+ // { style: "text-decoration=underline" }
+ ],
+ toDOM: () => ['span', {
+ style: 'text-decoration:underline;text-decoration-style:line'
+ }]
+ },
+
+ search_highlight: {
+ attrs: {
+ selected: { default: false }
+ },
+ parseDOM: [{ style: 'background: yellow' }],
+ toDOM(node: any) {
+ return ['span', {
+ style: `background: ${node.attrs.selected ? "orange" : "yellow"}`
+ }];
+ }
+ },
+
+ // the id of the user who entered the text
+ user_mark: {
+ attrs: {
+ userid: { default: "" },
+ modified: { default: "when?" }, // 1 second intervals since 1970
+ },
+ group: "inline",
+ toDOM(node: any) {
+ const uid = node.attrs.userid.replace(".", "").replace("@", "");
+ const min = Math.round(node.attrs.modified / 12);
+ const hr = Math.round(min / 60);
+ const day = Math.round(hr / 60 / 24);
+ const remote = node.attrs.userid !== Doc.CurrentUserEmail ? " userMark-remote" : "";
+ return ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, 0];
+ }
+ },
+ // the id of the user who entered the text
+ user_tag: {
+ attrs: {
+ userid: { default: "" },
+ modified: { default: "when?" }, // 1 second intervals since 1970
+ tag: { default: "" }
+ },
+ group: "inline",
+ inclusive: false,
+ toDOM(node: any) {
+ const uid = node.attrs.userid.replace(".", "").replace("@", "");
+ return ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, 0];
+ }
+ },
+
+
+ // :: MarkSpec Code font mark. Represented as a `<code>` element.
+ code: {
+ parseDOM: [{ tag: "code" }],
+ toDOM() { return codeDOM; }
+ },
+
+ /* FONTS */
+ pFontFamily: {
+ attrs: {
+ family: { default: "Crimson Text" },
+ },
+ parseDOM: [{
+ tag: "span", getAttrs(dom: any) {
+ const cstyle = getComputedStyle(dom);
+ if (cstyle.font) {
+ if (cstyle.font.indexOf("Times New Roman") !== -1) return { family: "Times New Roman" };
+ if (cstyle.font.indexOf("Arial") !== -1) return { family: "Arial" };
+ if (cstyle.font.indexOf("Georgia") !== -1) return { family: "Georgia" };
+ if (cstyle.font.indexOf("Comic Sans") !== -1) return { family: "Comic Sans MS" };
+ if (cstyle.font.indexOf("Tahoma") !== -1) return { family: "Tahoma" };
+ if (cstyle.font.indexOf("Crimson") !== -1) return { family: "Crimson Text" };
+ }
+ }
+ }],
+ toDOM: (node) => ['span', {
+ style: `font-family: "${node.attrs.family}";`
+ }]
+ },
+
+ /** FONT SIZES */
+ pFontSize: {
+ attrs: {
+ fontSize: { default: 10 }
+ },
+ parseDOM: [{ style: 'font-size: 10px;' }],
+ toDOM: (node) => ['span', {
+ style: `font-size: ${node.attrs.fontSize}px;`
+ }]
+ },
+};
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
new file mode 100644
index 000000000..e7bcf444a
--- /dev/null
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -0,0 +1,264 @@
+import React = require("react");
+import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
+import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
+import ParagraphNodeSpec from "./ParagraphNodeSpec";
+
+const blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
+ preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
+
+// :: Object
+// [Specs](#model.NodeSpec) for the nodes defined in this schema.
+export const nodes: { [index: string]: NodeSpec } = {
+ // :: NodeSpec The top level document node.
+ doc: {
+ content: "block+"
+ },
+
+ footnote: {
+ group: "inline",
+ content: "inline*",
+ inline: true,
+ attrs: {
+ visibility: { default: false }
+ },
+ // This makes the view treat the node as a leaf, even though it
+ // technically has content
+ atom: true,
+ toDOM: () => ["footnote", 0],
+ parseDOM: [{ tag: "footnote" }]
+ },
+
+ paragraph: ParagraphNodeSpec,
+
+ // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
+ blockquote: {
+ content: "block+",
+ group: "block",
+ defining: true,
+ parseDOM: [{ tag: "blockquote" }],
+ toDOM() { return blockquoteDOM; }
+ },
+
+ // :: NodeSpec A horizontal rule (`<hr>`).
+ horizontal_rule: {
+ group: "block",
+ parseDOM: [{ tag: "hr" }],
+ toDOM() { return hrDOM; }
+ },
+
+ // :: NodeSpec A heading textblock, with a `level` attribute that
+ // should hold the number 1 to 6. Parsed and serialized as `<h1>` to
+ // `<h6>` elements.
+ heading: {
+ attrs: { level: { default: 1 } },
+ content: "inline*",
+ group: "block",
+ defining: true,
+ parseDOM: [{ tag: "h1", attrs: { level: 1 } },
+ { tag: "h2", attrs: { level: 2 } },
+ { tag: "h3", attrs: { level: 3 } },
+ { tag: "h4", attrs: { level: 4 } },
+ { tag: "h5", attrs: { level: 5 } },
+ { tag: "h6", attrs: { level: 6 } }],
+ toDOM(node: any) { return ["h" + node.attrs.level, 0]; }
+ },
+
+ // :: NodeSpec A code listing. Disallows marks or non-text inline
+ // nodes by default. Represented as a `<pre>` element with a
+ // `<code>` element inside of it.
+ code_block: {
+ content: "text*",
+ marks: "",
+ group: "block",
+ code: true,
+ defining: true,
+ parseDOM: [{ tag: "pre", preserveWhitespace: "full" }],
+ toDOM() { return preDOM; }
+ },
+
+ // :: NodeSpec The text node.
+ text: {
+ group: "inline"
+ },
+
+ dashComment: {
+ attrs: {
+ docid: { default: "" },
+ },
+ inline: true,
+ group: "inline",
+ toDOM(node) {
+ const attrs = { style: `width: 40px` };
+ return ["span", { ...node.attrs, ...attrs }, "←"];
+ },
+ },
+
+ summary: {
+ inline: true,
+ attrs: {
+ visibility: { default: false },
+ text: { default: undefined },
+ textslice: { default: undefined },
+ },
+ group: "inline",
+ toDOM(node) {
+ const attrs = { style: `width: 40px` };
+ return ["span", { ...node.attrs, ...attrs }];
+ },
+ },
+
+ // :: NodeSpec An inline image (`<img>`) node. Supports `src`,
+ // `alt`, and `href` attributes. The latter two default to the empty
+ // string.
+ image: {
+ inline: true,
+ attrs: {
+ src: {},
+ agnostic: { default: null },
+ width: { default: 100 },
+ alt: { default: null },
+ title: { default: null },
+ float: { default: "left" },
+ location: { default: "onRight" },
+ docid: { default: "" }
+ },
+ group: "inline",
+ draggable: true,
+ parseDOM: [{
+ tag: "img[src]", getAttrs(dom: any) {
+ return {
+ src: dom.getAttribute("src"),
+ title: dom.getAttribute("title"),
+ alt: dom.getAttribute("alt"),
+ width: Math.min(100, Number(dom.getAttribute("width"))),
+ };
+ }
+ }],
+ // TODO if we don't define toDom, dragging the image crashes. Why?
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}` };
+ return ["img", { ...node.attrs, ...attrs }];
+ }
+ },
+
+ dashDoc: {
+ inline: true,
+ attrs: {
+ width: { default: 200 },
+ height: { default: 100 },
+ title: { default: null },
+ float: { default: "right" },
+ location: { default: "onRight" },
+ hidden: { default: false },
+ fieldKey: { default: "" },
+ docid: { default: "" },
+ alias: { default: "" }
+ },
+ group: "inline",
+ draggable: false,
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
+ return ["div", { ...node.attrs, ...attrs }];
+ }
+ },
+
+ dashField: {
+ inline: true,
+ attrs: {
+ fieldKey: { default: "" },
+ docid: { default: "" }
+ },
+ group: "inline",
+ draggable: false,
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
+ return ["div", { ...node.attrs, ...attrs }];
+ }
+ },
+
+ video: {
+ inline: true,
+ attrs: {
+ src: {},
+ width: { default: "100px" },
+ alt: { default: null },
+ title: { default: null }
+ },
+ group: "inline",
+ draggable: true,
+ parseDOM: [{
+ tag: "video[src]", getAttrs(dom: any) {
+ return {
+ src: dom.getAttribute("src"),
+ title: dom.getAttribute("title"),
+ alt: dom.getAttribute("alt"),
+ width: Math.min(100, Number(dom.getAttribute("width"))),
+ };
+ }
+ }],
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}` };
+ return ["video", { ...node.attrs, ...attrs }];
+ }
+ },
+
+ // :: NodeSpec A hard line break, represented in the DOM as `<br>`.
+ hard_break: {
+ inline: true,
+ group: "inline",
+ selectable: false,
+ parseDOM: [{ tag: "br" }],
+ toDOM() { return brDOM; }
+ },
+
+ ordered_list: {
+ ...orderedList,
+ content: 'list_item+',
+ group: 'block',
+ attrs: {
+ bulletStyle: { default: 0 },
+ mapStyle: { default: "decimal" },
+ setFontSize: { default: undefined },
+ setFontFamily: { default: "inherit" },
+ setFontColor: { default: "inherit" },
+ inheritedFontSize: { default: undefined },
+ visibility: { default: true },
+ indent: { default: undefined }
+ },
+ toDOM(node: Node<any>) {
+ if (node.attrs.mapStyle === "bullet") return ['ul', 0];
+ const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : "";
+ const fsize = node.attrs.setFontSize ? node.attrs.setFontSize : node.attrs.inheritedFontSize;
+ const ffam = node.attrs.setFontFamily;
+ const color = node.attrs.setFontColor;
+ return node.attrs.visibility ?
+ ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}; color:${color}; margin-left: ${node.attrs.indent}` }, 0] :
+ ['ol', { class: `${map}-ol`, style: `list-style: none;` }];
+ }
+ },
+
+ bullet_list: {
+ ...bulletList,
+ content: 'list_item+',
+ group: 'block',
+ // parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }],
+ toDOM(node: Node<any>) {
+ return ['ul', 0];
+ }
+ },
+
+ list_item: {
+ attrs: {
+ bulletStyle: { default: 0 },
+ mapStyle: { default: "decimal" },
+ visibility: { default: true }
+ },
+ ...listItem,
+ content: 'paragraph block*',
+ toDOM(node: any) {
+ const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : "";
+ return node.attrs.visibility ? ["li", { class: `${map}` }, 0] : ["li", { class: `${map}` }, "..."];
+ //return ["li", { class: `${map}` }, 0];
+ }
+ },
+}; \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/prosemirrorPatches.js b/src/client/views/nodes/formattedText/prosemirrorPatches.js
new file mode 100644
index 000000000..269423482
--- /dev/null
+++ b/src/client/views/nodes/formattedText/prosemirrorPatches.js
@@ -0,0 +1,139 @@
+'use strict';
+
+Object.defineProperty(exports, '__esModule', { value: true });
+
+var prosemirrorInputRules = require('prosemirror-inputrules');
+var prosemirrorTransform = require('prosemirror-transform');
+var prosemirrorModel = require('prosemirror-model');
+
+exports.liftListItem = liftListItem;
+exports.sinkListItem = sinkListItem;
+exports.wrappingInputRule = wrappingInputRule;
+// :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
+// Create a command to lift the list item around the selection up into
+// a wrapping list.
+function liftListItem(itemType) {
+ return function (tx, dispatch) {
+ var ref = tx.selection;
+ var $from = ref.$from;
+ var $to = ref.$to;
+ var range = $from.blockRange($to, function (node) { return node.childCount && node.firstChild.type == itemType; });
+ if (!range) { return false }
+ if (!dispatch) { return true }
+ if ($from.node(range.depth - 1).type == itemType) // Inside a parent list
+ { return liftToOuterList(tx, dispatch, itemType, range) }
+ else // Outer list node
+ { return liftOutOfList(tx, dispatch, range) }
+ }
+}
+
+function liftToOuterList(tr, dispatch, itemType, range) {
+ var end = range.end, endOfList = range.$to.end(range.depth);
+ if (end < endOfList) {
+ // There are siblings after the lifted items, which must become
+ // children of the last item
+ tr.step(new prosemirrorTransform.ReplaceAroundStep(end - 1, endOfList, end, endOfList,
+ new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(itemType.create(null, range.parent.copy())), 1, 0), 1, true));
+ range = new prosemirrorModel.NodeRange(tr.doc.resolve(range.$from.pos), tr.doc.resolve(endOfList), range.depth);
+ }
+ dispatch(tr.lift(range, prosemirrorTransform.liftTarget(range)).scrollIntoView());
+ return true
+}
+
+function liftOutOfList(tr, dispatch, range) {
+ var list = range.parent;
+ // Merge the list items into a single big item
+ for (var pos = range.end, i = range.endIndex - 1, e = range.startIndex; i > e; i--) {
+ pos -= list.child(i).nodeSize;
+ tr.delete(pos - 1, pos + 1);
+ }
+ var $start = tr.doc.resolve(range.start), item = $start.nodeAfter;
+ var atStart = range.startIndex == 0, atEnd = range.endIndex == list.childCount;
+ var parent = $start.node(-1), indexBefore = $start.index(-1);
+ if (!parent.canReplace(indexBefore + (atStart ? 0 : 1), indexBefore + 1,
+ item.content.append(atEnd ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list)))) { return false }
+ var start = $start.pos, end = start + item.nodeSize;
+ // Strip off the surrounding list. At the sides where we're not at
+ // the end of the list, the existing list is closed. At sides where
+ // this is the end, it is overwritten to its end.
+ tr.step(new prosemirrorTransform.ReplaceAroundStep(start - (atStart ? 1 : 0), end + (atEnd ? 1 : 0), start + 1, end - 1,
+ new prosemirrorModel.Slice((atStart ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list.copy(prosemirrorModel.Fragment.empty)))
+ .append(atEnd ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list.copy(prosemirrorModel.Fragment.empty))),
+ atStart ? 0 : 1, atEnd ? 0 : 1), atStart ? 0 : 1));
+ dispatch(tr.scrollIntoView());
+ return true
+}
+
+// :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
+// Create a command to sink the list item around the selection down
+// into an inner list.
+function sinkListItem(itemType) {
+ return function (state, dispatch) {
+ var ref = state.selection;
+ var $from = ref.$from;
+ var $to = ref.$to;
+ var range = $from.blockRange($to, function (node) { return node.childCount && node.firstChild.type == itemType; });
+ if (!range) { return false }
+ var startIndex = range.startIndex;
+ if (startIndex == 0) { return false }
+ var parent = range.parent, nodeBefore = parent.child(startIndex - 1);
+ if (nodeBefore.type != itemType) { return false; }
+
+ if (dispatch) {
+ var nestedBefore = nodeBefore.lastChild && nodeBefore.lastChild.type == parent.type;
+ var inner = prosemirrorModel.Fragment.from(nestedBefore ? itemType.create() : null);
+ let slice = new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(itemType.create(null, prosemirrorModel.Fragment.from(parent.type.create({ ...parent.attrs, fontSize: parent.attrs.fontSize ? parent.attrs.fontSize - 4 : undefined }, inner)))),
+ nestedBefore ? 3 : 1, 0);
+ var before = range.start, after = range.end;
+ dispatch(state.tr.step(new prosemirrorTransform.ReplaceAroundStep(before - (nestedBefore ? 3 : 1), after,
+ before, after, slice, 1, true))
+ .scrollIntoView());
+ }
+ return true
+ }
+}
+
+function findWrappingOutside(range, type) {
+ var parent = range.parent;
+ var startIndex = range.startIndex;
+ var endIndex = range.endIndex;
+ var around = parent.contentMatchAt(startIndex).findWrapping(type);
+ if (!around) { return null }
+ var outer = around.length ? around[0] : type;
+ return parent.canReplaceWith(startIndex, endIndex, outer) ? around : null
+}
+
+function findWrappingInside(range, type) {
+ var parent = range.parent;
+ var startIndex = range.startIndex;
+ var endIndex = range.endIndex;
+ var inner = parent.child(startIndex);
+ var inside = type.contentMatch.findWrapping(inner.type);
+ if (!inside) { return null }
+ var lastType = inside.length ? inside[inside.length - 1] : type;
+ var innerMatch = lastType.contentMatch;
+ for (var i = startIndex; innerMatch && i < endIndex; i++) { innerMatch = innerMatch.matchType(parent.child(i).type); }
+ if (!innerMatch || !innerMatch.validEnd) { return null }
+ return inside
+}
+function findWrapping(range, nodeType, attrs, innerRange, customWithAttrs = null) {
+ if (innerRange === void 0) innerRange = range;
+ let withAttrs = (type) => ({ type: type, attrs: null });
+ var around = findWrappingOutside(range, nodeType);
+ var inner = around && findWrappingInside(innerRange, nodeType);
+ if (!inner) { return null }
+ return around.map(withAttrs).concat({ type: nodeType, attrs: attrs }).concat(inner.map(customWithAttrs ? customWithAttrs : withAttrs))
+}
+function wrappingInputRule(regexp, nodeType, getAttrs, joinPredicate, customWithAttrs = null) {
+ return new prosemirrorInputRules.InputRule(regexp, function (state, match, start, end) {
+ var attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
+ var tr = state.tr.delete(start, end);
+ var $start = tr.doc.resolve(start), range = $start.blockRange(), wrapping = range && findWrapping(range, nodeType, attrs, undefined, customWithAttrs);
+ if (!wrapping) { return null }
+ tr.wrap(range, wrapping);
+ var before = tr.doc.resolve(start - 1).nodeBefore;
+ if (before && before.type == nodeType && prosemirrorTransform.canJoin(tr.doc, start - 1) &&
+ (!joinPredicate || joinPredicate(match, before))) { tr.join(start - 1); }
+ return tr
+ })
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/schema_rts.ts b/src/client/views/nodes/formattedText/schema_rts.ts
new file mode 100644
index 000000000..83561073c
--- /dev/null
+++ b/src/client/views/nodes/formattedText/schema_rts.ts
@@ -0,0 +1,26 @@
+import { Schema, Slice } from "prosemirror-model";
+
+import { nodes } from "./nodes_rts";
+import { marks } from "./marks_rts";
+
+
+// :: Schema
+// This schema rougly corresponds to the document schema used by
+// [CommonMark](http://commonmark.org/), minus the list elements,
+// which are defined in the [`prosemirror-schema-list`](#schema-list)
+// module.
+//
+// To reuse elements from this schema, extend or read from its
+// `spec.nodes` and `spec.marks` [properties](#model.Schema.spec).
+
+export const schema = new Schema({ nodes, marks });
+
+const fromJson = schema.nodeFromJSON;
+
+schema.nodeFromJSON = (json: any) => {
+ const node = fromJson(json);
+ if (json.type === schema.nodes.summary.name) {
+ node.attrs.text = Slice.fromJSON(schema, node.attrs.textslice);
+ }
+ return node;
+}; \ No newline at end of file
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index 760f64a72..8541a3149 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -56,10 +56,6 @@
left: 0px;
display: inline-block;
width:100%;
- pointer-events: none;
- .collectionFreeFormDocumentView-container {
- pointer-events: all;
- }
}
.pdfViewer-annotationLayer {
diff --git a/src/client/views/presentationview/PresElementBox.scss b/src/client/views/presentationview/PresElementBox.scss
index 8370af490..ccd2e8947 100644
--- a/src/client/views/presentationview/PresElementBox.scss
+++ b/src/client/views/presentationview/PresElementBox.scss
@@ -1,5 +1,4 @@
.presElementBox-item {
- padding: 10px;
display: inline-block;
background-color: #eeeeee;
pointer-events: all;
@@ -16,7 +15,6 @@
user-select: none;
transition: all .1s;
padding: 0px;
- padding-left: 5px;
padding-bottom: 3px;
.documentView-node {
position: absolute;
@@ -38,7 +36,7 @@
border-radius: 6px;
}
-.presElementBox-selected {
+.presElementBox-active {
background: gray;
color: black;
border-radius: 6px;
@@ -54,20 +52,27 @@
padding: 8px;
}
-.presElementBox-interaction {
- color: gray;
- float: left;
- padding: 0px;
- width: 20px;
- height: 20px;
-}
-.presElementBox-interaction-selected {
- color: white;
- float: left;
- padding: 0px;
- width: 22px;
- height: 22px;
+.presElementBox-buttons {
+ display: flow-root;
+ position: relative;
+ width: 100%;
+ height: auto;
+ .presElementBox-interaction {
+ color: gray;
+ float: left;
+ padding: 0px;
+ width: 20px;
+ height: 20px;
+ }
+ .presElementBox-interaction-selected {
+ color: white;
+ float: left;
+ padding: 0px;
+ width: 20px;
+ height: 20px;
+ border: solid 1px darkgray;
+ }
}
.presElementBox-name {
@@ -82,14 +87,10 @@
.presElementBox-embedded {
position: relative;
- margin-top: 22;
display: flex;
width: auto;
justify-content: center;
- .contentFittingDocumentView {
- position: absolute;
- height: 100%;
- }
+ margin:auto;
}
.presElementBox-embeddedMask {
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index dd0cbf929..6d7602268 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -1,15 +1,12 @@
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons';
-import { faArrowDown, faArrowUp, faFile as fileSolid, faFileDownload, faLocationArrow, faSearch } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DataSym } from "../../../new_fields/Doc";
+import { Doc, DataSym, DocListCast } from "../../../new_fields/Doc";
import { documentSchema } from '../../../new_fields/documentSchemas';
import { Id } from "../../../new_fields/FieldSymbols";
import { createSchema, makeInterface } from '../../../new_fields/Schema';
-import { Cast, NumCast } from "../../../new_fields/Types";
-import { emptyFunction, emptyPath, returnFalse, returnTrue } from "../../../Utils";
+import { Cast, NumCast, BoolCast, ScriptCast } from "../../../new_fields/Types";
+import { emptyFunction, emptyPath, returnFalse, returnTrue, returnOne, returnZero } from "../../../Utils";
import { Transform } from "../../util/Transform";
import { CollectionViewType } from '../collections/CollectionView';
import { ViewBoxBaseComponent } from '../DocComponent';
@@ -18,23 +15,16 @@ import { FieldView, FieldViewProps } from '../nodes/FieldView';
import "./PresElementBox.scss";
import React = require("react");
-library.add(faArrowUp);
-library.add(fileSolid);
-library.add(faLocationArrow);
-library.add(fileRegular as any);
-library.add(faSearch);
-library.add(faArrowDown);
-
export const presSchema = createSchema({
presentationTargetDoc: Doc,
presBox: Doc,
- zoomButton: "boolean",
- navButton: "boolean",
- hideTillShownButton: "boolean",
- fadeButton: "boolean",
- hideAfterButton: "boolean",
- groupButton: "boolean",
- expandInlineButton: "boolean"
+ presZoomButton: "boolean",
+ presNavButton: "boolean",
+ presHideTillShownButton: "boolean",
+ presFadeButton: "boolean",
+ presHideAfterButton: "boolean",
+ presGroupButton: "boolean",
+ presExpandInlineButton: "boolean"
});
type PresDocument = makeInterface<[typeof presSchema, typeof documentSchema]>;
@@ -48,16 +38,16 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); }
_heightDisposer: IReactionDisposer | undefined;
- @computed get indexInPres() { return NumCast(this.presElementDoc?.presentationIndex); }
- @computed get presBoxDoc() { return Cast(this.presElementDoc?.presBox, Doc) as Doc; }
- @computed get presElementDoc() { return this.rootDoc; }
- @computed get presLayoutDoc() { return this.layoutDoc; }
- @computed get targetDoc() { return this.presElementDoc?.presentationTargetDoc as Doc; }
- @computed get currentIndex() { return NumCast(this.presBoxDoc?._itemIndex); }
+ // these fields are conditionally computed fields on the layout document that take this document as a parameter
+ @computed get indexInPres() { return Number(this.lookupField("indexInPres")); } // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements)
+ @computed get collapsedHeight() { return Number(this.lookupField("presCollapsedHeight")); } // the collapsed height changes depending on the state of the presBox. We could store this on the presentation elemnt template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up
+ @computed get presStatus() { return BoolCast(this.layoutDoc.presStatus); }
+ @computed get currentIndex() { return NumCast(this.layoutDoc.currentIndex); }
+ @computed get targetDoc() { return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; }
componentDidMount() {
- this._heightDisposer = reaction(() => [this.presElementDoc.expandInlineButton, this.presElementDoc.collapsedHeight],
- params => this.presLayoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true });
+ this._heightDisposer = reaction(() => [this.rootDoc.presExpandInlineButton, this.collapsedHeight],
+ params => this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true });
}
componentWillUnmount() {
this._heightDisposer?.();
@@ -70,13 +60,13 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
@action
onHideDocumentUntilPressClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.presElementDoc.hideTillShownButton = !this.presElementDoc.hideTillShownButton;
- if (!this.presElementDoc.hideTillShownButton) {
+ this.rootDoc.presHideTillShownButton = !this.rootDoc.presHideTillShownButton;
+ if (!this.rootDoc.presHideTillShownButton) {
if (this.indexInPres >= this.currentIndex && this.targetDoc) {
this.targetDoc.opacity = 1;
}
} else {
- if (this.presBoxDoc.presStatus && this.indexInPres > this.currentIndex && this.targetDoc) {
+ if (this.presStatus && this.indexInPres > this.currentIndex && this.targetDoc) {
this.targetDoc.opacity = 0;
}
}
@@ -90,14 +80,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
@action
onHideDocumentAfterPresentedClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.presElementDoc.hideAfterButton = !this.presElementDoc.hideAfterButton;
- if (!this.presElementDoc.hideAfterButton) {
+ this.rootDoc.presHideAfterButton = !this.rootDoc.presHideAfterButton;
+ if (!this.rootDoc.presHideAfterButton) {
if (this.indexInPres <= this.currentIndex && this.targetDoc) {
this.targetDoc.opacity = 1;
}
} else {
- if (this.presElementDoc.fadeButton) this.presElementDoc.fadeButton = false;
- if (this.presBoxDoc.presStatus && this.indexInPres < this.currentIndex && this.targetDoc) {
+ if (this.rootDoc.presFadeButton) this.rootDoc.presFadeButton = false;
+ if (this.presStatus && this.indexInPres < this.currentIndex && this.targetDoc) {
this.targetDoc.opacity = 0;
}
}
@@ -111,14 +101,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
@action
onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.presElementDoc.fadeButton = !this.presElementDoc.fadeButton;
- if (!this.presElementDoc.fadeButton) {
+ this.rootDoc.presFadeButton = !this.rootDoc.presFadeButton;
+ if (!this.rootDoc.presFadeButton) {
if (this.indexInPres <= this.currentIndex && this.targetDoc) {
this.targetDoc.opacity = 1;
}
} else {
- this.presElementDoc.hideAfterButton = false;
- if (this.presBoxDoc.presStatus && (this.indexInPres < this.currentIndex) && this.targetDoc) {
+ this.rootDoc.presHideAfterButton = false;
+ if (this.presStatus && (this.indexInPres < this.currentIndex) && this.targetDoc) {
this.targetDoc.opacity = 0.5;
}
}
@@ -130,11 +120,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
@action
onNavigateDocumentClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.presElementDoc.navButton = !this.presElementDoc.navButton;
- if (this.presElementDoc.navButton) {
- this.presElementDoc.zoomButton = false;
+ this.rootDoc.presNavButton = !this.rootDoc.presNavButton;
+ if (this.rootDoc.presNavButton) {
+ this.rootDoc.presZoomButton = false;
if (this.currentIndex === this.indexInPres) {
- this.props.focus(this.presElementDoc);
+ this.props.focus(this.rootDoc);
}
}
}
@@ -146,13 +136,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
onZoomDocumentClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.presElementDoc.zoomButton = !this.presElementDoc.zoomButton;
- if (!this.presElementDoc.zoomButton) {
- this.presElementDoc.viewScale = 1;
- } else {
- this.presElementDoc.navButton = false;
+ this.rootDoc.presZoomButton = !this.rootDoc.presZoomButton;
+ if (this.rootDoc.presZoomButton) {
+ this.rootDoc.presNavButton = false;
if (this.currentIndex === this.indexInPres) {
- this.props.focus(this.presElementDoc);
+ this.props.focus(this.rootDoc);
}
}
}
@@ -161,18 +149,18 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
*/
ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord];
- embedHeight = () => this.props.PanelHeight() - NumCast(this.presElementDoc.collapsedHeight);
+ embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight);
embedWidth = () => this.props.PanelWidth() - 20;
/**
* The function that is responsible for rendering the a preview or not for this
* presentation element.
*/
- renderEmbeddedInline = () => {
- return !this.presElementDoc.expandInlineButton || !this.targetDoc ? (null) :
- <div className="presElementBox-embedded" style={{ height: this.embedHeight() }}>
+ @computed get renderEmbeddedInline() {
+ return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? (null) :
+ <div className="presElementBox-embedded" style={{ height: this.embedHeight(), width: this.embedWidth() }}>
<ContentFittingDocumentView
Document={this.targetDoc}
- DataDocument={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
+ DataDoc={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
LibraryPath={emptyPath}
fitToBox={true}
rootSelected={returnTrue}
@@ -182,12 +170,18 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
pinToPres={returnFalse}
PanelWidth={this.embedWidth}
PanelHeight={this.embedHeight}
- getTransform={Transform.Identity}
- active={this.props.active}
+ ScreenToLocalTransform={Transform.Identity}
+ parentActive={this.props.active}
moveDocument={this.props.moveDocument!}
renderDepth={this.props.renderDepth + 1}
focus={emptyFunction}
whenActiveChanged={returnFalse}
+ bringToFront={returnFalse}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ ContentScaling={returnOne}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
/>
<div className="presElementBox-embeddedMask" />
</div>;
@@ -195,29 +189,29 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
render() {
const treecontainer = this.props.ContainingCollectionDoc?._viewType === CollectionViewType.Tree;
- const className = "presElementBox-item" + (this.currentIndex === this.indexInPres ? " presElementBox-selected" : "");
+ const className = "presElementBox-item" + (this.currentIndex === this.indexInPres ? " presElementBox-active" : "");
const pbi = "presElementBox-interaction";
- return !(this.presElementDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : (
+ return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : (
<div className={className} key={this.props.Document[Id] + this.indexInPres}
style={{ outlineWidth: Doc.IsBrushed(this.targetDoc) ? `1px` : "0px", }}
- onClick={e => { this.props.focus(this.presElementDoc); e.stopPropagation(); }}>
+ onClick={e => { this.props.focus(this.rootDoc); e.stopPropagation(); }}>
{treecontainer ? (null) : <>
<strong className="presElementBox-name">
{`${this.indexInPres + 1}. ${this.targetDoc?.title}`}
</strong>
- <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument?.(this.presElementDoc)}>X</button>
+ <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument?.(this.rootDoc)}>X</button>
<br />
</>}
- <button title="Zoom" className={pbi + (this.presElementDoc.zoomButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
- <button title="Navigate" className={pbi + (this.presElementDoc.navButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
- <button title="Hide Before" className={pbi + (this.presElementDoc.hideTillShownButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
- <button title="Fade After" className={pbi + (this.presElementDoc.fadeButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
- <button title="Hide After" className={pbi + (this.presElementDoc.hideAfterButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
- <button title="Group With Up" className={pbi + (this.presElementDoc.groupButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); this.presElementDoc.groupButton = !this.presElementDoc.groupButton; }}><FontAwesomeIcon icon={"arrow-up"} /></button>
- <button title="Expand Inline" className={pbi + (this.presElementDoc.expandInlineButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); this.presElementDoc.expandInlineButton = !this.presElementDoc.expandInlineButton; }}><FontAwesomeIcon icon={"arrow-down"} /></button>
-
- <br style={{ lineHeight: 0.1 }} />
- {this.renderEmbeddedInline()}
+ <div className="presElementBox-buttons">
+ <button title="Zoom" className={pbi + (this.rootDoc.presZoomButton ? "-selected" : "")} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} onPointerDown={e => e.stopPropagation()} /></button>
+ <button title="Navigate" className={pbi + (this.rootDoc.presNavButton ? "-selected" : "")} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} onPointerDown={e => e.stopPropagation()} /></button>
+ <button title="Hide Before" className={pbi + (this.rootDoc.presHideTillShownButton ? "-selected" : "")} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={"file"} onPointerDown={e => e.stopPropagation()} /></button>
+ <button title="Fade After" className={pbi + (this.rootDoc.presFadeButton ? "-selected" : "")} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={"file-download"} onPointerDown={e => e.stopPropagation()} /></button>
+ <button title="Hide After" className={pbi + (this.rootDoc.presHideAfterButton ? "-selected" : "")} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={"file-download"} onPointerDown={e => e.stopPropagation()} /></button>
+ <button title="Group With Up" className={pbi + (this.rootDoc.presGroupButton ? "-selected" : "")} onClick={e => { e.stopPropagation(); this.rootDoc.presGroupButton = !this.rootDoc.presGroupButton; }}><FontAwesomeIcon icon={"arrow-up"} onPointerDown={e => e.stopPropagation()} /></button>
+ <button title="Expand Inline" className={pbi + (this.rootDoc.presExpandInlineButton ? "-selected" : "")} onClick={e => { e.stopPropagation(); this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton; }}><FontAwesomeIcon icon={"arrow-down"} onPointerDown={e => e.stopPropagation()} /></button>
+ </div>
+ {this.renderEmbeddedInline}
</div>
);
}
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index fe2000700..96f43e931 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -7,7 +7,7 @@ import { observer } from "mobx-react";
import { Doc } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { emptyFunction, emptyPath, returnFalse, Utils, returnTrue } from "../../../Utils";
+import { emptyFunction, emptyPath, returnFalse, Utils, returnTrue, returnOne, returnZero } from "../../../Utils";
import { DocumentType } from "../../documents/DocumentTypes";
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager, SetupDrag } from "../../util/DragManager";
@@ -164,14 +164,20 @@ export class SearchItem extends React.Component<SearchItemProps> {
removeDocument={returnFalse}
addDocTab={returnFalse}
pinToPres={returnFalse}
- getTransform={Transform.Identity}
+ ContainingCollectionDoc={undefined}
+ ContainingCollectionView={undefined}
+ ScreenToLocalTransform={Transform.Identity}
renderDepth={1}
PanelWidth={returnXDimension}
PanelHeight={returnYDimension}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
focus={emptyFunction}
moveDocument={returnFalse}
- active={returnFalse}
+ parentActive={returnFalse}
whenActiveChanged={returnFalse}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne}
/>
</div>;
return docview;