aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes
diff options
context:
space:
mode:
authorMelissa Zhang <mzhang19096@gmail.com>2020-05-15 19:10:07 -0700
committerMelissa Zhang <mzhang19096@gmail.com>2020-05-15 19:10:07 -0700
commit253d5efbbafe4a44c350fef7dda6d553a70a94c9 (patch)
treeb43a55e1efddc86e25df4bb7d90a99f3c7b52a17 /src/client/views/nodes
parent36c01920e3874e1484222a7012745b581405f67d (diff)
parent08b6bf8b51ab631c8cfe9c3e12bfb0ae2dd7b4c7 (diff)
merge with master
Diffstat (limited to 'src/client/views/nodes')
-rw-r--r--src/client/views/nodes/AudioBox.tsx30
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx96
-rw-r--r--src/client/views/nodes/ColorBox.tsx14
-rw-r--r--src/client/views/nodes/ContentFittingDocumentView.tsx63
-rw-r--r--src/client/views/nodes/DocHolderBox.scss (renamed from src/client/views/nodes/DocumentBox.scss)3
-rw-r--r--src/client/views/nodes/DocHolderBox.tsx213
-rw-r--r--src/client/views/nodes/DocumentBox.tsx160
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx106
-rw-r--r--src/client/views/nodes/DocumentIcon.tsx2
-rw-r--r--src/client/views/nodes/DocumentView.scss1
-rw-r--r--src/client/views/nodes/DocumentView.tsx265
-rw-r--r--src/client/views/nodes/FaceRectangles.tsx6
-rw-r--r--src/client/views/nodes/FieldView.tsx25
-rw-r--r--src/client/views/nodes/FontIconBox.tsx25
-rw-r--r--src/client/views/nodes/ImageBox.tsx62
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx16
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx2
-rw-r--r--src/client/views/nodes/LabelBox.scss14
-rw-r--r--src/client/views/nodes/LabelBox.tsx20
-rw-r--r--src/client/views/nodes/LinkAnchorBox.tsx24
-rw-r--r--src/client/views/nodes/LinkBox.tsx6
-rw-r--r--src/client/views/nodes/PDFBox.scss1
-rw-r--r--src/client/views/nodes/PDFBox.tsx12
-rw-r--r--src/client/views/nodes/PresBox.scss29
-rw-r--r--src/client/views/nodes/PresBox.tsx244
-rw-r--r--src/client/views/nodes/QueryBox.tsx14
-rw-r--r--src/client/views/nodes/RadialMenu.scss13
-rw-r--r--src/client/views/nodes/RadialMenu.tsx9
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx12
-rw-r--r--src/client/views/nodes/ScriptingBox.tsx10
-rw-r--r--src/client/views/nodes/SliderBox.tsx10
-rw-r--r--src/client/views/nodes/VideoBox.tsx27
-rw-r--r--src/client/views/nodes/WebBox.scss56
-rw-r--r--src/client/views/nodes/WebBox.tsx68
-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.tsx215
-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)529
-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)37
-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.tsx876
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts321
-rw-r--r--src/client/views/nodes/formattedText/RichTextSchema.tsx538
-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.ts265
-rw-r--r--src/client/views/nodes/formattedText/prosemirrorPatches.js139
-rw-r--r--src/client/views/nodes/formattedText/schema_rts.ts26
56 files changed, 5545 insertions, 1018 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 6ff6d1b42..1a935d9b0 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -2,25 +2,25 @@ import React = require("react");
import { FieldViewProps, FieldView } from './FieldView';
import { observer } from "mobx-react";
import "./AudioBox.scss";
-import { Cast, DateCast, NumCast } from "../../../new_fields/Types";
-import { AudioField, nullAudio } from "../../../new_fields/URLField";
+import { Cast, DateCast, NumCast } from "../../../fields/Types";
+import { AudioField, nullAudio } from "../../../fields/URLField";
import { ViewBoxBaseComponent } from "../DocComponent";
-import { makeInterface, createSchema } from "../../../new_fields/Schema";
-import { documentSchema } from "../../../new_fields/documentSchemas";
+import { makeInterface, createSchema } from "../../../fields/Schema";
+import { documentSchema } from "../../../fields/documentSchemas";
import { Utils, returnTrue, emptyFunction, returnOne, returnTransparent, returnFalse, returnZero } from "../../../Utils";
import { runInAction, observable, reaction, IReactionDisposer, computed, action } from "mobx";
-import { DateField } from "../../../new_fields/DateField";
+import { DateField } from "../../../fields/DateField";
import { SelectionManager } from "../../util/SelectionManager";
-import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Doc, DocListCast } from "../../../fields/Doc";
import { ContextMenuProps } from "../ContextMenuItem";
import { ContextMenu } from "../ContextMenu";
-import { Id } from "../../../new_fields/FieldSymbols";
+import { Id } from "../../../fields/FieldSymbols";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DocumentView } from "./DocumentView";
-import { Docs } from "../../documents/Documents";
-import { ComputedField } from "../../../new_fields/ScriptField";
+import { Docs, DocUtils } from "../../documents/Documents";
+import { ComputedField } from "../../../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..5d109a5f2 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,26 +1,26 @@
import { computed, IReactionDisposer, observable, reaction, trace } from "mobx";
import { observer } from "mobx-react";
-import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
-import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { Doc, HeightSym, WidthSym } from "../../../fields/Doc";
+import { Cast, NumCast, StrCast } from "../../../fields/Types";
import { Transform } from "../../util/Transform";
import { DocComponent } from "../DocComponent";
import "./CollectionFreeFormDocumentView.scss";
import { DocumentView, DocumentViewProps } from "./DocumentView";
import React = require("react");
-import { PositionDocument } from "../../../new_fields/documentSchemas";
-import { TraceMobx } from "../../../new_fields/util";
+import { Document } from "../../../fields/documentSchemas";
+import { TraceMobx } from "../../../fields/util";
import { ContentFittingDocumentView } from "./ContentFittingDocumentView";
+import { List } from "../../../fields/List";
+import { numberRange } from "../../../Utils";
+import { ComputedField } from "../../../fields/ScriptField";
+import { listSpec } from "../../../fields/Schema";
+import { docs } from "googleapis/build/src/apis/docs";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
- dataProvider?: (doc: Doc, replica: string) => { x: number, y: number, zIndex?: number, highlight?: boolean, z: number, transition?: string } | undefined;
+ dataProvider?: (doc: Doc, replica: string) => { x: number, y: number, zIndex?: number, opacity?: 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 +28,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 +38,14 @@ 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 Opacity() { return this.dataProvider ? this.dataProvider.opacity : (this.Document.opacity || 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; }
@@ -67,6 +68,53 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}
return undefined;
}
+
+ public static getValues(doc: Doc, time: number) {
+ return ({
+ x: Cast(doc["x-indexed"], listSpec("number"), []).reduce((p, x, i) => (i <= time && x !== undefined) || p === undefined ? x : p, undefined as any as number),
+ y: Cast(doc["y-indexed"], listSpec("number"), []).reduce((p, y, i) => (i <= time && y !== undefined) || p === undefined ? y : p, undefined as any as number),
+ opacity: Cast(doc["opacity-indexed"], listSpec("number"), []).reduce((p, o, i) => i <= time || p === undefined ? o : p, undefined as any as number),
+ });
+ }
+
+ public static setValues(timecode: number, d: Doc, x?: number, y?: number, opacity?: number) {
+ Cast(d["x-indexed"], listSpec("number"), [])[timecode] = x as any as number;
+ Cast(d["y-indexed"], listSpec("number"), null)[timecode] = y as any as number;
+ Cast(d["opacity-indexed"], listSpec("number"), null)[timecode] = opacity as any as number;
+ }
+ public static updateKeyframe(docs: Doc[], timecode: number) {
+ docs.forEach(doc => {
+ const xindexed = Cast(doc['x-indexed'], listSpec("number"), null);
+ const yindexed = Cast(doc['y-indexed'], listSpec("number"), null);
+ const opacityindexed = Cast(doc['opacity-indexed'], listSpec("number"), null);
+ xindexed.length <= timecode + 1 && xindexed.push(undefined as any as number);
+ yindexed.length <= timecode + 1 && yindexed.push(undefined as any as number);
+ opacityindexed.length <= timecode + 1 && opacityindexed.push(undefined as any as number);
+ doc.transition = "all 1s";
+ });
+ setTimeout(() => docs.forEach(doc => doc.transition = undefined), 1010);
+ }
+
+ public static gotoKeyframe(docs: Doc[]) {
+ docs.forEach(doc => doc.transition = "all 1s");
+ setTimeout(() => docs.forEach(doc => doc.transition = undefined), 1010);
+ }
+
+ public static setupKeyframes(docs: Doc[], timecode: number, collection: Doc) {
+ docs.forEach(doc => {
+ doc["x-indexed"] = new List<number>(numberRange(timecode).map(i => undefined) as any as number[]);
+ doc["y-indexed"] = new List<number>(numberRange(timecode).map(i => undefined) as any as number[]);
+ doc["opacity-indexed"] = new List<number>(numberRange(timecode).map(i => 0));
+ (doc["x-indexed"] as any).push(NumCast(doc.x));
+ (doc["y-indexed"] as any).push(NumCast(doc.y));
+ (doc["opacity-indexed"] as any).push(NumCast(doc.opacity, 1));
+ doc.timecode = ComputedField.MakeFunction("collection.timecode", {}, { collection });
+ doc.x = ComputedField.MakeInterpolated("x", "timecode");
+ doc.y = ComputedField.MakeInterpolated("y", "timecode");
+ doc.opacity = ComputedField.MakeInterpolated("opacity", "timecode");
+ });
+ }
+
nudge = (x: number, y: number) => {
this.props.Document.x = NumCast(this.props.Document.x) + x;
this.props.Document.y = NumCast(this.props.Document.y) + y;
@@ -75,10 +123,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;
@@ -87,7 +132,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
return <div className="collectionFreeFormDocumentView-container"
style={{
boxShadow:
- this.layoutDoc.opacity === 0 ? undefined : // if it's not visible, then no shadow
+ this.Opacity === 0 ? undefined : // if it's not visible, then no shadow
this.layoutDoc.z ? `#9c9396 ${StrCast(this.layoutDoc.boxShadow, "10px 10px 0.9vw")}` : // if it's a floating doc, give it a big shadow
this.props.backgroundHalo?.() ? (`${this.props.backgroundColor?.(this.props.Document)} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent
this.layoutDoc.isBackground ? undefined : // if it's a background & has a cluster color, make the shadow spread really big
@@ -98,9 +143,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
transition: this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition),
width: this.width,
height: this.height,
+ opacity: this.Opacity,
zIndex: this.ZInd,
display: this.ZInd === -99 ? "none" : undefined,
- pointerEvents: this.props.Document.isBackground ? "none" : this.props.pointerEvents ? "all" : undefined
+ pointerEvents: this.props.Document.isBackground || this.Opacity === 0 ? "none" : this.props.pointerEvents ? "all" : undefined
}} >
{!this.props.fitToBox ?
@@ -115,13 +161,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/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index 6e4341b27..6d53915ea 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -1,10 +1,10 @@
import React = require("react");
import { observer } from "mobx-react";
import { SketchPicker } from 'react-color';
-import { documentSchema } from "../../../new_fields/documentSchemas";
-import { makeInterface } from "../../../new_fields/Schema";
-import { StrCast } from "../../../new_fields/Types";
-import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { documentSchema } from "../../../fields/documentSchemas";
+import { makeInterface } from "../../../fields/Schema";
+import { StrCast } from "../../../fields/Types";
+import { CurrentUserUtils } from "../../util/CurrentUserUtils";
import { SelectionManager } from "../../util/SelectionManager";
import { ViewBoxBaseComponent } from "../DocComponent";
import { InkingControl } from "../InkingControl";
@@ -24,9 +24,13 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument
onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}
style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} >
- <SketchPicker onChange={InkingControl.Instance.switchColor}
+ <SketchPicker onChange={InkingControl.Instance.switchColor} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
color={StrCast(CurrentUserUtils.ActivePen ? CurrentUserUtils.ActivePen.backgroundColor : undefined,
StrCast(selDoc?._backgroundColor, StrCast(selDoc?.backgroundColor, "black")))} />
+ <div style={{ display: "grid", gridTemplateColumns: "20% 80%", paddingTop: "10px" }}>
+ <div>{InkingControl.Instance.selectedWidth ?? 2}</div>
+ <input type="range" value={InkingControl.Instance.selectedWidth ?? 2} defaultValue={2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchWidth(e.target.value)} />
+ </div>
</div>;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx
index 641797cac..a90b4668e 100644
--- a/src/client/views/nodes/ContentFittingDocumentView.tsx
+++ b/src/client/views/nodes/ContentFittingDocumentView.tsx
@@ -2,53 +2,23 @@ import React = require("react");
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 { TraceMobx } from "../../../new_fields/util";
+import { Doc, Opt, WidthSym, HeightSym } from "../../../fields/Doc";
+import { NumCast, StrCast, Cast } from "../../../fields/Types";
+import { TraceMobx } from "../../../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";
-interface ContentFittingDocumentViewProps {
- Document: Doc;
- DataDocument?: Doc;
- LayoutDoc?: () => Opt<Doc>;
- NativeWidth?: () => number;
- NativeHeight?: () => number;
- FreezeDimensions?: boolean;
- LibraryPath: Doc[];
- renderDepth: number;
- fitToBox?: boolean;
- layoutKey?: string;
- dropAction?: dropActionType;
- PanelWidth: () => number;
- PanelHeight: () => number;
- focus?: (doc: Doc) => void;
- CollectionView?: CollectionView;
- CollectionDoc?: Doc;
- onClick?: ScriptField;
- backgroundColor?: (doc: Doc) => string | undefined;
- getTransform: () => Transform;
- addDocument?: (document: Doc) => boolean;
- moveDocument?: (document: Doc, target: Doc | undefined, addDoc: ((doc: Doc) => boolean)) => boolean;
- removeDocument?: (document: Doc) => boolean;
- active: (outsideReaction: boolean) => boolean;
- whenActiveChanged: (isActive: boolean) => void;
- addDocTab: (document: Doc, where: string) => boolean;
- pinToPres: (document: Doc) => void;
- dontRegisterView?: boolean;
- rootSelected: (outsideReaction?: boolean) => boolean;
-}
@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()));
@@ -67,7 +37,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; }
@@ -89,8 +59,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}
@@ -106,11 +77,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/DocHolderBox.scss
index ce21391ce..6a9ef0b6f 100644
--- a/src/client/views/nodes/DocumentBox.scss
+++ b/src/client/views/nodes/DocHolderBox.scss
@@ -2,11 +2,12 @@
width: 100%;
height: 100%;
pointer-events: all;
- background: gray;
+ position: absolute;
.documentBox-lock {
margin: auto;
color: white;
position: absolute;
+ padding: 3px;
}
.contentFittingDocumentView {
position: absolute;
diff --git a/src/client/views/nodes/DocHolderBox.tsx b/src/client/views/nodes/DocHolderBox.tsx
new file mode 100644
index 000000000..0c5239d66
--- /dev/null
+++ b/src/client/views/nodes/DocHolderBox.tsx
@@ -0,0 +1,213 @@
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, IReactionDisposer, reaction } from "mobx";
+import { observer } from "mobx-react";
+import { Doc, Field } from "../../../fields/Doc";
+import { collectionSchema, documentSchema } from "../../../fields/documentSchemas";
+import { makeInterface, listSpec } from "../../../fields/Schema";
+import { ComputedField } from "../../../fields/ScriptField";
+import { Cast, NumCast, StrCast } from "../../../fields/Types";
+import { TraceMobx } from "../../../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 "./DocHolderBox.scss";
+import { DocumentView } from "./DocumentView";
+import { FieldView, FieldViewProps } from "./FieldView";
+import React = require("react");
+
+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.dataDoc[this.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);
+ this._curSelection = this._selections.length - 1;
+ }
+ });
+ }
+ componentWillUnmount() {
+ this._prevSelectionDisposer?.();
+ }
+ 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.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" });
+ }
+ lockSelection = () => {
+ this.dataDoc[this.fieldKey] = this.dataDoc[this.fieldKey];
+ }
+ showSelection = () => {
+ this.dataDoc[this.fieldKey] = ComputedField.MakeFunction(`selectedDocs(self,this.excludeCollections,[_last_])?.[0]`);
+ }
+ isSelectionLocked = () => {
+ const kvpstring = Field.toKeyValueString(this.dataDoc, this.fieldKey);
+ return !kvpstring || kvpstring.includes("DOC");
+ }
+ toggleLockSelection = () => {
+ !this.isSelectionLocked() ? this.lockSelection() : this.showSelection();
+ return true;
+ }
+ prevSelection = () => {
+ this.lockSelection();
+ if (this._curSelection > 0) {
+ this.dataDoc[this.fieldKey] = this._selections[--this._curSelection];
+ return true;
+ }
+ }
+ nextSelection = () => {
+ if (this._curSelection < this._selections.length - 1 && this._selections.length) {
+ this.dataDoc[this.fieldKey] = this._selections[++this._curSelection];
+ return true;
+ }
+ }
+ onPointerDown = (e: React.PointerEvent) => {
+ if (this.active() && e.button === 0 && !e.ctrlKey) {
+ e.stopPropagation();
+ }
+ }
+ onLockClick = (e: React.MouseEvent) => {
+ this.toggleLockSelection();
+ (e.nativeEvent as any).formattedHandled = true;
+ e.stopPropagation();
+ }
+ get xPad() { return NumCast(this.rootDoc._xPadding); }
+ get yPad() { return NumCast(this.rootDoc._yPadding); }
+ onClick = (e: React.MouseEvent) => {
+ let hitWidget: boolean | undefined = false;
+ if (this._contRef.current!.getBoundingClientRect().top + this.yPad > e.clientY) hitWidget = (() => { this.props.select(false); return true; })();
+ else if (this._contRef.current!.getBoundingClientRect().bottom - this.yPad < e.clientY) hitWidget = (() => { this.props.select(false); return true; })();
+ else {
+ if (this._contRef.current!.getBoundingClientRect().left + this.xPad > e.clientX) hitWidget = this.prevSelection();
+ if (this._contRef.current!.getBoundingClientRect().right - this.xPad < e.clientX) hitWidget = this.nextSelection();
+ }
+ if (hitWidget) {
+ (e.nativeEvent as any).formattedHandled = true;
+ e.stopPropagation();
+ }
+ }
+ pwidth = () => this.props.PanelWidth() - 2 * this.xPad;
+ pheight = () => this.props.PanelHeight() - 2 * this.yPad;
+ getTransform = () => this.props.ScreenToLocalTransform().translate(-this.xPad, -this.yPad);
+ isActive = (outsideReaction: boolean) => this.active(outsideReaction) || this.props.renderDepth <= 1;
+ layoutTemplateDoc = () => Cast(this.layoutDoc.childLayoutTemplate, Doc, null);
+ get renderContents() {
+ const containedDoc = Cast(this.dataDoc[this.fieldKey], Doc, null);
+ const layoutTemplate = StrCast(this.layoutDoc.childLayoutString);
+ const contents = !(containedDoc instanceof Doc) ||
+ Cast(containedDoc[Doc.LayoutFieldKey(containedDoc)], listSpec(Doc), null)?.includes(this.rootDoc)
+ ? (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}
+ backgroundColor={this.props.backgroundColor}
+ 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={containedDoc.type !== DocumentType.DOCHOLDER && !this.props.renderDepth ? 0 : 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}
+ backgroundColor={this.props.backgroundColor}
+ ignoreAutoHeight={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={containedDoc.type !== DocumentType.DOCHOLDER && !this.props.renderDepth ? 0 : 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() {
+ const containedDoc = Cast(this.dataDoc[this.fieldKey], Doc, null);
+ TraceMobx();
+ return <div className="documentBox-container" ref={this._contRef}
+ onContextMenu={this.specificContextMenu}
+ onPointerDown={this.onPointerDown} onClick={this.onClick}
+ style={{
+ background: this.props.backgroundColor?.(containedDoc),
+ 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} ref={this.createDropTarget}
+ style={{ marginTop: - this.yPad, background: "black" }}>
+ <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.layoutDoc.childLayoutTemplate = doc;
+ }
+ }
+ }
+ protected createDropTarget = (ele: HTMLDivElement) => {
+ this._dropDisposer?.();
+ ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.rootDoc));
+ }
+
+}
diff --git a/src/client/views/nodes/DocumentBox.tsx b/src/client/views/nodes/DocumentBox.tsx
deleted file mode 100644
index 0d18baaed..000000000
--- a/src/client/views/nodes/DocumentBox.tsx
+++ /dev/null
@@ -1,160 +0,0 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { IReactionDisposer, reaction, computed } from "mobx";
-import { observer } from "mobx-react";
-import { Doc, Field } from "../../../new_fields/Doc";
-import { 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 { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import { ViewBoxAnnotatableComponent } from "../DocComponent";
-import { ContentFittingDocumentView } from "./ContentFittingDocumentView";
-import "./DocumentBox.scss";
-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);
-
-@observer
-export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, DocHolderBoxSchema>(DocHolderBoxDocument) {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DocHolderBox, fieldKey); }
- _prevSelectionDisposer: IReactionDisposer | undefined;
- _selections: Doc[] = [];
- _curSelection = -1;
- componentDidMount() {
- this._prevSelectionDisposer = reaction(() => this.contentDoc[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);
- this._curSelection = this._selections.length - 1;
- }
- });
- }
- componentWillUnmount() {
- this._prevSelectionDisposer?.();
- }
- 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" });
-
- 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];
- }
- showSelection = () => {
- this.contentDoc[this.props.fieldKey] = ComputedField.MakeFunction(`selectedDocs(self,this.excludeCollections,[_last_])?.[0]`);
- }
- isSelectionLocked = () => {
- const kvpstring = Field.toKeyValueString(this.contentDoc, this.props.fieldKey);
- return !kvpstring || kvpstring.includes("DOC");
- }
- toggleLockSelection = () => {
- !this.isSelectionLocked() ? this.lockSelection() : this.showSelection();
- return true;
- }
- prevSelection = () => {
- this.lockSelection();
- if (this._curSelection > 0) {
- this.contentDoc[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];
- return true;
- }
- }
- onPointerDown = (e: React.PointerEvent) => {
- if (this.active() && e.button === 0 && !e.ctrlKey) {
- e.stopPropagation();
- }
- }
- onLockClick = (e: React.MouseEvent) => {
- this.toggleLockSelection();
- (e.nativeEvent as any).formattedHandled = true;
- e.stopPropagation();
- }
- get xPad() { return NumCast(this.props.Document._xPadding); }
- get yPad() { return NumCast(this.props.Document._yPadding); }
- onClick = (e: React.MouseEvent) => {
- let hitWidget: boolean | undefined = false;
- if (this._contRef.current!.getBoundingClientRect().top + this.yPad > e.clientY) hitWidget = (() => { this.props.select(false); return true; })();
- else if (this._contRef.current!.getBoundingClientRect().bottom - this.yPad < e.clientY) hitWidget = (() => { this.props.select(false); return true; })();
- else {
- if (this._contRef.current!.getBoundingClientRect().left + this.xPad > e.clientX) hitWidget = this.prevSelection();
- if (this._contRef.current!.getBoundingClientRect().right - this.xPad < e.clientX) hitWidget = this.nextSelection();
- }
- if (hitWidget) {
- (e.nativeEvent as any).formattedHandled = true;
- 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);
- 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}
- />;
- return contents;
- }
- render() {
- TraceMobx();
- return <div className="documentBox-container" ref={this._contRef}
- onContextMenu={this.specificContextMenu}
- onPointerDown={this.onPointerDown} onClick={this.onClick}
- style={{
- background: StrCast(this.props.Document.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}
- style={{ marginTop: - this.yPad }}>
- <FontAwesomeIcon icon={this.isSelectionLocked() ? "lock" : "unlock"} size="sm" />
- </div>
- </div >;
- }
-}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 90d2d4936..c9db1ea01 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,7 +1,7 @@
import { computed } from "mobx";
import { observer } from "mobx-react";
-import { Doc, Opt, Field } from "../../../new_fields/Doc";
-import { Cast, StrCast, NumCast } from "../../../new_fields/Types";
+import { Doc, Opt, Field } from "../../../fields/Doc";
+import { Cast, StrCast, NumCast } from "../../../fields/Types";
import { OmitKeys, Without, emptyPath } from "../../../Utils";
import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox";
import { CollectionDockingView } from "../collections/CollectionDockingView";
@@ -14,12 +14,12 @@ import { LabelBox } from "./LabelBox";
import { SliderBox } from "./SliderBox";
import { LinkBox } from "./LinkBox";
import { ScriptingBox } from "./ScriptingBox";
-import { DocHolderBox } from "./DocumentBox";
+import { DocHolderBox } from "./DocHolderBox";
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";
@@ -36,8 +36,8 @@ import { WebBox } from "./WebBox";
import { InkingStroke } from "../InkingStroke";
import React = require("react");
import { RecommendationsBox } from "../RecommendationsBox";
-import { TraceMobx } from "../../../new_fields/util";
-import { ScriptField } from "../../../new_fields/ScriptField";
+import { TraceMobx } from "../../../fields/util";
+import { ScriptField } from "../../../fields/ScriptField";
import XRegExp = require("xregexp");
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -59,35 +59,34 @@ const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
interface HTMLtagProps {
Document: Doc;
+ RootDoc: Doc;
htmltag: string;
onClick?: ScriptField;
+ onInput?: ScriptField;
}
//"<HTMLdiv borderRadius='100px' onClick={this.bannerColor=this.bannerColor==='red'?'green':'red'} width='100%' height='100%' transform='rotate({2*this.x+this.y}deg)'><ImageBox {...props} fieldKey={'data'}/><HTMLspan width='100%' marginTop='50%' height='10%' position='absolute' backgroundColor='{this.bannerColor===`green`?`dark`:`light`}grey'>{this.title}</HTMLspan></HTMLdiv>"@observer
@observer
export class HTMLtag extends React.Component<HTMLtagProps> {
click = (e: React.MouseEvent) => {
const clickScript = (this.props as any).onClick as Opt<ScriptField>;
- clickScript?.script.run({ this: this.props.Document });
+ clickScript?.script.run({ this: this.props.Document, self: this.props.RootDoc });
+ }
+ onInput = (e: React.FormEvent<HTMLDivElement>) => {
+ const onInputScript = (this.props as any).onInput as Opt<ScriptField>;
+ onInputScript?.script.run({ this: this.props.Document, self: this.props.RootDoc, value: (e.target as any).textContent });
}
render() {
const style: { [key: string]: any } = {};
- const divKeys = OmitKeys(this.props, ["children", "htmltag", "Document", "key", "onClick", "__proto__"]).omit;
+ 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) => {
- let p = (this.props as any)[prop] as string;
- const replacer = (match: any, expr: string, offset: any, string: any) => { // bcz: extend this to support expression -- is this really a script?
- return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ this: this.props.Document }).result as string || "";
- };
- p = p?.replace(/{([^.'][^}']+)}/g, replacer);
-
- const replacer2 = (match: any, key: string, offset: any, string: any) => { // bcz: extend this to support expression -- is this really a script?
- const n = Cast(this.props.Document[key], "number", null);
- return n ? n.toString() : StrCast(this.props.Document[key], p);
- };
- style[prop] = p?.replace(/@([a-zA-Z0-9-_]+)/g, replacer2);
-
+ const p = (this.props as any)[prop] as string;
+ style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer);
});
const Tag = this.props.htmltag as keyof JSX.IntrinsicElements;
- return <Tag style={style} onClick={this.click}>
+ return <Tag style={style} onClick={this.click} onInput={this.onInput as any}>
{this.props.children}
</Tag>;
}
@@ -98,15 +97,15 @@ 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
}> {
@computed get layout(): string {
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");
+ // 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"));
} else
@@ -127,16 +126,22 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
}
get layoutDoc() {
const params = StrCast(this.props.Document.PARAMS);
- const template: Doc = this.props.LayoutDoc?.() || Doc.Layout(this.props.Document, this.props.layoutKey ? Cast(this.props.Document[this.props.layoutKey], Doc, null) : undefined);
+ // 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.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);
}
- CreateBindings(onClick: Opt<ScriptField>): JsxBindings {
+ CreateBindings(onClick: Opt<ScriptField>, onInput: Opt<ScriptField>): JsxBindings {
const list = {
- ...OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit,
+ ...OmitKeys(this.props, ['parentActive'], "", (obj: any) => obj.active = this.props.parentActive).omit,
+ RootDoc: Cast(this.layoutDoc?.rootDocument, Doc, null) || this.layoutDoc,
Document: this.layoutDoc,
DataDoc: this.dataDoc,
- onClick: onClick
+ onClick: onClick,
+ onInput: onInput
};
return { props: list };
}
@@ -153,7 +158,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
// replace HTML<tag> with corresponding HTML tag as in: <HTMLdiv> becomes <HTMLtag Document={props.Document} htmltag='div'>
const replacer2 = (match: any, p1: string, offset: any, string: any) => {
- return `<HTMLtag Document={props.Document} htmltag='${p1}'`;
+ return `<HTMLtag RootDoc={props.RootDoc} Document={props.Document} htmltag='${p1}'`;
};
layoutFrame = layoutFrame.replace(/<HTML([a-zA-Z0-9_-]+)/g, replacer2);
@@ -164,18 +169,24 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
layoutFrame = layoutFrame.replace(/<\/HTML([a-zA-Z0-9_-]+)/g, replacer3);
// add onClick function to props
- const splits = layoutFrame.split("onClick=");
- let onClick: Opt<ScriptField>;
- if (splits.length > 1) {
- const code = XRegExp.matchRecursive(splits[1], "{", "}", "", { valueNames: ["between", "left", "match", "right", "between"] });
- layoutFrame = splits[0] + " onClick={props.onClick} " + splits[1].substring(code[1].end + 1);
- onClick = ScriptField.MakeScript(code[1].value, { this: Doc.name, self: Doc.name });
- }
-
- const bindings = this.CreateBindings(onClick);
+ const makeFuncProp = (func: string) => {
+ const splits = layoutFrame.split(`func=`);
+ if (splits.length > 1) {
+ const code = XRegExp.matchRecursive(splits[1], "{", "}", "", { valueNames: ["between", "left", "match", "right", "between"] });
+ layoutFrame = splits[0] + ` ${func}={props.onClick} ` + splits[1].substring(code[1].end + 1);
+ return ScriptField.MakeScript(code[1].value, { this: Doc.name, self: Doc.name, value: "string" });
+ }
+ return undefined;
+ // add input function to props
+ };
+ const onClick = makeFuncProp("onClick");
+ const onInput = makeFuncProp("onInput");
+
+ const bindings = this.CreateBindings(onClick, onInput);
// 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) :
+<<<<<<< HEAD
this.props.forceLayout === "FormattedTextBox" && this.props.forceFieldKey ?
<FormattedTextBox {...bindings.props} fieldKey={this.props.forceFieldKey} />
:
@@ -196,5 +207,24 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
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); }}
+ />;
+>>>>>>> 08b6bf8b51ab631c8cfe9c3e12bfb0ae2dd7b4c7
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx
index f56f5e829..fb54f18e8 100644
--- a/src/client/views/nodes/DocumentIcon.tsx
+++ b/src/client/views/nodes/DocumentIcon.tsx
@@ -3,7 +3,7 @@ import * as React from "react";
import { DocumentView } from "./DocumentView";
import { DocumentManager } from "../../util/DocumentManager";
import { Transformer, Scripting, ts } from "../../util/Scripting";
-import { Field } from "../../../new_fields/Doc";
+import { Field } from "../../../fields/Doc";
@observer
export class DocumentIcon extends React.Component<{ view: DocumentView, index: number }> {
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 f8bf9bf64..340fa06a8 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -4,26 +4,26 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
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 { 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 { TraceMobx } from '../../../new_fields/util';
+import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym } from "../../../fields/Doc";
+import { Document } from '../../../fields/documentSchemas';
+import { Id } from '../../../fields/FieldSymbols';
+import { InkTool } from '../../../fields/InkField';
+import { listSpec } from "../../../fields/Schema";
+import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
+import { ScriptField } from '../../../fields/ScriptField';
+import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types";
+import { ImageField } from '../../../fields/URLField';
+import { TraceMobx } from '../../../fields/util';
import { GestureUtils } from '../../../pen-gestures/GestureUtils';
-import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils } from "../../../Utils";
+import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils, emptyPath } 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";
+import { SnappingManager } from '../../util/SnappingManager';
import { DragManager, dropActionType } from "../../util/DragManager";
import { InteractionUtils } from '../../util/InteractionUtils';
import { Scripting } from '../../util/Scripting';
@@ -42,6 +42,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,21 +59,26 @@ export interface DocumentViewProps {
NativeHeight: () => number;
Document: Doc;
DataDoc?: Doc;
- LayoutDoc?: () => Opt<Doc>;
+ LayoutTemplateString?: string;
+ LayoutTemplate?: () => Opt<Doc>;
LibraryPath: Doc[];
fitToBox?: boolean;
+ ignoreAutoHeight?: 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;
+ treeViewId?: string;
dropAction?: dropActionType;
dragDivName?: string;
nudge?: (x: number, y: number) => void;
- addDocument?: (doc: Doc) => boolean;
- removeDocument?: (doc: Doc) => boolean;
- moveDocument?: (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean;
+ addDocument?: (doc: Doc | Doc[]) => boolean;
+ removeDocument?: (doc: Doc | Doc[]) => boolean;
+ moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
+ setupDragLines?: () => void;
renderDepth: number;
ContentScaling: () => number;
PanelWidth: () => number;
@@ -116,6 +122,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;
@@ -237,6 +244,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
dragData.removeDocument = this.props.removeDocument;
dragData.moveDocument = this.props.moveDocument;// this.Document.onDragStart ? undefined : this.props.moveDocument;
dragData.dragDivName = this.props.dragDivName;
+ dragData.treeViewId = this.props.treeViewId;
DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: !dropAction && !this.Document.onDragStart });
}
}
@@ -289,13 +297,26 @@ 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 {
+ UndoManager.RunInBatch(() => {
+ if (StrCast(this.props.Document.layoutKey) !== "layout_fullScreen" && this.props.Document.layout_fullScreen) {
+ const fullScreenAlias = Doc.MakeAlias(this.props.Document);
+ fullScreenAlias.layoutKey = "layout_fullScreen";
+ this.props.addDocTab(fullScreenAlias, "inTab");
+ } else {
+ this.props.addDocTab(this.props.Document, "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();
@@ -308,7 +329,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
UndoManager.RunInBatch(func, "on click");
} else func();
} else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself
- UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick");
+ const alias = Doc.MakeAlias(this.props.Document);
+ Doc.makeCustomViewClicked(alias, undefined, "onClick");
+ this.props.addDocTab(alias, "onRight");
+ // UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick");
//ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY), "on button click");
} else if (this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
DocListCast(this.props.Document.links).length && this.followLinkClick(e.altKey, e.ctrlKey, e.shiftKey);
@@ -441,8 +465,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);
@@ -501,7 +525,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if ((!e.nativeEvent.cancelBubble || this.onClickHandler || this.Document.onDragStart) &&
// if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking
!((this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0))) {
- if ((this.active || this.Document.onDragStart || this.onClickHandler) &&
+ if ((this.active || this.Document.onDragStart) &&
!e.ctrlKey &&
(e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) &&
!this.Document.inOverlay) {
@@ -524,12 +548,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (e.cancelBubble && this.active) {
document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView)
}
- else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.onClickHandler) && !this.Document.lockedPosition && !this.Document.inOverlay) {
+ else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart) && !this.Document.lockedPosition && !this.Document.inOverlay) {
if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
- this.startDragging(this._downX, this._downY, this.props.dropAction ? this.props.dropAction : this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined);
+ this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && "alias") || (this.props.dropAction || this.Document.dropAction || undefined) as dropActionType);
}
}
e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
@@ -624,18 +648,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@undoBatch
@action
- public static unfreezeNativeDimensions(layoutDoc: Doc) {
- layoutDoc._nativeWidth = undefined;
- layoutDoc._nativeHeight = undefined;
- }
-
toggleNativeDimensions = () => {
- if (this.Document._nativeWidth || this.Document._nativeHeight) {
- DocumentView.unfreezeNativeDimensions(this.layoutDoc);
- }
- else {
- Doc.freezeNativeDimensions(this.layoutDoc, this.props.PanelWidth(), this.props.PanelHeight());
- }
+ Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.PanelWidth(), this.props.PanelHeight());
}
@undoBatch
@@ -653,9 +667,13 @@ 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);
+ if (this.Document.isBackground) {
+ this.props.bringToFront(this.props.Document, true);
+ this.props.Document[DataSym][Doc.LayoutFieldKey(this.Document) + "-nativeWidth"] = this.Document[WidthSym]();
+ this.props.Document[DataSym][Doc.LayoutFieldKey(this.Document) + "-nativeHeight"] = this.Document[HeightSym]();
+ }
}
@undoBatch
@@ -690,7 +708,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
const cm = ContextMenu.Instance;
- const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null);
const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []);
Cast(this.props.Document.contextMenuLabels, listSpec("string"), []).forEach((label, i) =>
@@ -699,28 +716,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" }));
- let open = cm.findByDescription("Add a Perspective...");
- const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : [];
- openItems.push({ description: "Open Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" });
- templateDoc && openItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" });
- if (!open) {
- open = { description: "Add a Perspective....", subitems: openItems, icon: "external-link-alt" };
- cm.addItem(open);
- }
-
let options = cm.findByDescription("Options...");
const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : [];
- optionItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
- optionItems.push({ description: `${this.Document._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" });
- optionItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" });
- optionItems.push({ description: this.Document.lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" });
+ const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null);
+ optionItems.push({ description: "Open Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" });
+ templateDoc && optionItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" });
if (!options) {
options = { description: "Options...", subitems: optionItems, icon: "compass" };
cm.addItem(options);
}
- cm.moveAfter(options, open);
-
const existingOnClick = cm.findByDescription("OnClick...");
const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" });
@@ -732,6 +737,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "edit" });
!existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
+
const funcs: ContextMenuProps[] = [];
if (this.Document.onDragStart) {
funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) });
@@ -743,16 +749,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const more = cm.findByDescription("More...");
const moreItems: ContextMenuProps[] = more && "subitems" in more ? more.subitems : [];
moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" });
- moreItems.push({ description: !this.Document._nativeWidth || !this.Document._nativeHeight ? "Freeze" : "Unfreeze", event: this.toggleNativeDimensions, icon: "snowflake" });
+ moreItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
+ moreItems.push({ description: this.Document.lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" });
+ moreItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" });
if (!ClientUtils.RELEASE) {
// let copies: ContextMenuProps[] = [];
moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" });
// cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" });
}
- if (Cast(this.props.Document.data, ImageField)) {
- moreItems.push({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" });
- }
if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) {
moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" });
moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" });
@@ -769,6 +774,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
// a.download = `DocExport-${this.props.Document[Id]}.zip`;
// a.click();
});
+
const recommender_subitems: ContextMenuProps[] = [];
recommender_subitems.push({
@@ -801,53 +807,52 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
moreItems.push({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" });
moreItems.push({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" });
!more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" });
+
+ cm.moveAfter(cm.findByDescription("More...")!, cm.findByDescription("OnClick...")!);
+
runInAction(() => {
- if (!ClientUtils.RELEASE) {
- const setWriteMode = (mode: DocServer.WriteMode) => {
- DocServer.AclsMode = mode;
- const mode1 = mode;
- const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground;
- DocServer.setFieldWriteMode("x", mode1);
- DocServer.setFieldWriteMode("y", mode1);
- DocServer.setFieldWriteMode("_width", mode1);
- DocServer.setFieldWriteMode("_height", mode1);
-
- DocServer.setFieldWriteMode("_panX", mode2);
- DocServer.setFieldWriteMode("_panY", mode2);
- DocServer.setFieldWriteMode("scale", mode2);
- DocServer.setFieldWriteMode("_viewType", mode2);
- };
- const aclsMenu: ContextMenuProps[] = [];
- aclsMenu.push({ description: "Default (write/read all)", event: () => setWriteMode(DocServer.WriteMode.Default), icon: DocServer.AclsMode === DocServer.WriteMode.Default ? "check" : "exclamation" });
- aclsMenu.push({ description: "Playground (write own/no read)", event: () => setWriteMode(DocServer.WriteMode.Playground), icon: DocServer.AclsMode === DocServer.WriteMode.Playground ? "check" : "exclamation" });
- aclsMenu.push({ description: "Live Playground (write own/read others)", event: () => setWriteMode(DocServer.WriteMode.LivePlayground), icon: DocServer.AclsMode === DocServer.WriteMode.LivePlayground ? "check" : "exclamation" });
- aclsMenu.push({ description: "Live Readonly (no write/read others)", event: () => setWriteMode(DocServer.WriteMode.LiveReadonly), icon: DocServer.AclsMode === DocServer.WriteMode.LiveReadonly ? "check" : "exclamation" });
- cm.addItem({ description: "Collaboration ACLs...", subitems: aclsMenu, icon: "share" });
- }
+ const setWriteMode = (mode: DocServer.WriteMode) => {
+ DocServer.AclsMode = mode;
+ const mode1 = mode;
+ const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground;
+ DocServer.setFieldWriteMode("x", mode1);
+ DocServer.setFieldWriteMode("y", mode1);
+ DocServer.setFieldWriteMode("_width", mode1);
+ DocServer.setFieldWriteMode("_height", mode1);
+
+ DocServer.setFieldWriteMode("_panX", mode2);
+ DocServer.setFieldWriteMode("_panY", mode2);
+ DocServer.setFieldWriteMode("scale", mode2);
+ DocServer.setFieldWriteMode("_viewType", mode2);
+ };
+ const aclsMenu: ContextMenuProps[] = [];
+ aclsMenu.push({ description: "Share", event: () => SharingManager.Instance.open(this), icon: "external-link-alt" });
+ aclsMenu.push({ description: "Default (write/read all)", event: () => setWriteMode(DocServer.WriteMode.Default), icon: DocServer.AclsMode === DocServer.WriteMode.Default ? "check" : "exclamation" });
+ aclsMenu.push({ description: "Playground (write own/no read)", event: () => setWriteMode(DocServer.WriteMode.Playground), icon: DocServer.AclsMode === DocServer.WriteMode.Playground ? "check" : "exclamation" });
+ aclsMenu.push({ description: "Live Playground (write own/read others)", event: () => setWriteMode(DocServer.WriteMode.LivePlayground), icon: DocServer.AclsMode === DocServer.WriteMode.LivePlayground ? "check" : "exclamation" });
+ aclsMenu.push({ description: "Live Readonly (no write/read others)", event: () => setWriteMode(DocServer.WriteMode.LiveReadonly), icon: DocServer.AclsMode === DocServer.WriteMode.LiveReadonly ? "check" : "exclamation" });
+ cm.addItem({ description: "Collaboration ...", subitems: aclsMenu, icon: "share" });
});
runInAction(() => {
- cm.addItem({
- description: "Share",
- event: () => SharingManager.Instance.open(this),
- icon: "external-link-alt"
- });
-
if (!this.topMost && !(e instanceof Touch)) {
// DocumentViews should stop propagation of this event
e.stopPropagation();
}
ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
- if (!SelectionManager.IsSelected(this, true)) {
- SelectionManager.SelectDoc(this, false);
- }
+ !SelectionManager.IsSelected(this, true) && SelectionManager.SelectDoc(this, false);
});
const path = this.props.LibraryPath.reduce((p: string, d: Doc) => p + "/" + (Doc.AreProtosEqual(d, (Doc.UserDoc()["tabs-button-library"] as Doc).sourcePanel as Doc) ? "" : d.title), "");
- cm.addItem({
+ const item = ({
description: `path: ${path}`, event: () => {
- this.props.LibraryPath.map(lp => Doc.GetProto(lp).treeViewOpen = lp.treeViewOpen = true);
- Doc.linkFollowHighlight(this.props.Document);
+ if (this.props.LibraryPath !== emptyPath) {
+ this.props.LibraryPath.map(lp => Doc.GetProto(lp).treeViewOpen = lp.treeViewOpen = true);
+ Doc.linkFollowHighlight(this.props.Document);
+ } else {
+ Doc.AddDocToList(Doc.GetProto(Doc.UserDoc().myCatalog as Doc), "data", this.props.Document[DataSym]);
+ }
}, icon: "check"
});
+ //cm.addItem(item);
}
recommender = async () => {
@@ -938,9 +943,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 +976,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}
@@ -993,6 +997,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
renderDepth={this.props.renderDepth}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
+ ignoreAutoHeight={this.props.ignoreAutoHeight}
focus={this.props.focus}
parentActive={this.props.parentActive}
whenActiveChanged={this.props.whenActiveChanged}
@@ -1010,7 +1015,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.
@@ -1026,7 +1030,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
makeLink = () => this._link; // pass the link placeholde to child views so they can react to make a specialized anchor. This is essentially a function call to the descendants since the value of the _link variable will immediately get set back to undefined.
@undoBatch
- hideLinkAnchor = (doc: Doc) => doc.hidden = true
+ hideLinkAnchor = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && (doc.hidden = true), true)
anchorPanelWidth = () => this.props.PanelWidth() || 1;
anchorPanelHeight = () => this.props.PanelHeight() || 1;
@computed get anchors() {
@@ -1038,12 +1042,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,12 +1063,11 @@ 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}
- ContentScaling={this.childScaling}
+ LayoutTemplateString={`<FormattedTextBox {...props} fieldKey={'${showCaption}'}/>`}
+ ContentScaling={returnOne}
ChromeHeight={this.chromeHeight}
isSelected={this.isSelected}
select={this.select}
@@ -1092,7 +1095,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() && !SnappingManager.GetIsDragging()) ||
(this.Document.type === DocumentType.INK && InkingControl.Instance.selectedTool !== InkTool.None);
}
@undoBatch
@@ -1103,17 +1106,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);
});
@@ -1131,20 +1130,34 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"];
let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc._viewType !== CollectionViewType.Linear;
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}
+ return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
+ id={this.props.Document[Id]}
+ 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={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",
border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined,
boxShadow: this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined,
background: finalColor,
- opacity: this.Document.opacity
+ opacity: this.Document.opacity,
+ fontFamily: StrCast(this.Document._fontFamily, "inherit"),
+ fontSize: Cast(this.Document._fontSize, "number", null)
}}>
{this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <>
{this.innards}
@@ -1152,7 +1165,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
</> :
this.innards}
{(this.Document.isBackground !== undefined || this.isSelected(false)) && this.props.renderDepth > 0 && this.props.PanelWidth() > 0 ?
- <div className="documentView-lock" onClick={() => this.toggleBackground(true)}> <FontAwesomeIcon icon={this.Document.isBackground ? "unlock" : "lock"} size="lg" /> </div>
+ <div className="documentView-lock" onClick={() => this.toggleBackground(true)}>
+ <FontAwesomeIcon icon={this.Document.isBackground ? "unlock" : "lock"} style={{ color: this.Document.isBackground ? "red" : undefined }} size="lg" />
+ </div>
: (null)}
</div>;
{ this._showKPQuery ? <KeyphraseQueryView keyphrases={this._queries}></KeyphraseQueryView> : undefined; }
diff --git a/src/client/views/nodes/FaceRectangles.tsx b/src/client/views/nodes/FaceRectangles.tsx
index 3c7f1f206..92ca276cb 100644
--- a/src/client/views/nodes/FaceRectangles.tsx
+++ b/src/client/views/nodes/FaceRectangles.tsx
@@ -1,8 +1,8 @@
import React = require("react");
-import { Doc, DocListCast } from "../../../new_fields/Doc";
-import { Cast, NumCast } from "../../../new_fields/Types";
+import { Doc, DocListCast } from "../../../fields/Doc";
+import { Cast, NumCast } from "../../../fields/Types";
import { observer } from "mobx-react";
-import { Id } from "../../../new_fields/FieldSymbols";
+import { Id } from "../../../fields/FieldSymbols";
import FaceRectangle from "./FaceRectangle";
interface FaceRectanglesProps {
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index a3790d38b..e9dc43bd8 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -1,11 +1,11 @@
import React = require("react");
import { computed } from "mobx";
import { observer } from "mobx-react";
-import { DateField } from "../../../new_fields/DateField";
-import { Doc, FieldResult, Opt, Field } from "../../../new_fields/Doc";
-import { List } from "../../../new_fields/List";
-import { ScriptField } from "../../../new_fields/ScriptField";
-import { AudioField, VideoField } from "../../../new_fields/URLField";
+import { DateField } from "../../../fields/DateField";
+import { Doc, FieldResult, Opt, Field } from "../../../fields/Doc";
+import { List } from "../../../fields/List";
+import { ScriptField } from "../../../fields/ScriptField";
+import { AudioField, VideoField } from "../../../fields/URLField";
import { Transform } from "../../util/Transform";
import { CollectionView } from "../collections/CollectionView";
import { AudioBox } from "./AudioBox";
@@ -31,11 +31,11 @@ export interface FieldViewProps {
select: (isCtrlPressed: boolean) => void;
rootSelected: (outsideReaction?: boolean) => boolean;
renderDepth: number;
- addDocument?: (document: Doc) => boolean;
+ addDocument?: (document: Doc | Doc[]) => boolean;
addDocTab: (document: Doc, where: string) => boolean;
pinToPres: (document: Doc) => void;
- removeDocument?: (document: Doc) => boolean;
- moveDocument?: (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean;
+ removeDocument?: (document: Doc | Doc[]) => boolean;
+ moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
backgroundColor?: (document: Doc) => string | undefined;
ScreenToLocalTransform: () => Transform;
bringToFront: (doc: Doc, sendToBack?: boolean) => void;
@@ -43,6 +43,7 @@ export interface FieldViewProps {
whenActiveChanged: (isActive: boolean) => void;
dontRegisterView?: boolean;
focus: (doc: Doc) => void;
+ ignoreAutoHeight?: boolean;
PanelWidth: () => number;
PanelHeight: () => number;
NativeHeight: () => number;
@@ -50,7 +51,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/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
index c6ea6a882..cf0b16c7c 100644
--- a/src/client/views/nodes/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -1,15 +1,16 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { createSchema, makeInterface } from '../../../new_fields/Schema';
+import { createSchema, makeInterface } from '../../../fields/Schema';
import { DocComponent } from '../DocComponent';
import './FontIconBox.scss';
import { FieldView, FieldViewProps } from './FieldView';
-import { StrCast, Cast } from '../../../new_fields/Types';
+import { StrCast, Cast } from '../../../fields/Types';
import { Utils } from "../../../Utils";
import { runInAction, observable, reaction, IReactionDisposer } from 'mobx';
-import { Doc } from '../../../new_fields/Doc';
+import { Doc } from '../../../fields/Doc';
import { ContextMenu } from '../ContextMenu';
+import { ScriptField } from '../../../fields/ScriptField';
const FontIconSchema = createSchema({
icon: "string"
});
@@ -23,7 +24,7 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(
_ref: React.RefObject<HTMLButtonElement> = React.createRef();
_backgroundReaction: IReactionDisposer | undefined;
componentDidMount() {
- this._backgroundReaction = reaction(() => this.props.Document.backgroundColor,
+ this._backgroundReaction = reaction(() => this.layoutDoc.backgroundColor,
() => {
if (this._ref && this._ref.current) {
const col = Utils.fromRGBAstr(getComputedStyle(this._ref.current).backgroundColor);
@@ -35,13 +36,21 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(
}
showTemplate = (): void => {
- const dragFactory = Cast(this.props.Document.dragFactory, Doc, null);
+ const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null);
dragFactory && this.props.addDocTab(dragFactory, "onRight");
}
+ dragAsTemplate = (): void => {
+ this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
+ }
+ useAsPrototype = (): void => {
+ this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)');
+ }
specificContextMenu = (): void => {
const cm = ContextMenu.Instance;
cm.addItem({ description: "Show Template", event: this.showTemplate, icon: "tag" });
+ cm.addItem({ description: "Use as Render Template", event: this.dragAsTemplate, icon: "tag" });
+ cm.addItem({ description: "Use as Prototype", event: this.useAsPrototype, icon: "tag" });
}
componentWillUnmount() {
@@ -49,12 +58,12 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(
}
render() {
- const referenceDoc = (this.props.Document.dragFactory instanceof Doc ? this.props.Document.dragFactory : this.props.Document);
+ const referenceDoc = (this.layoutDoc.dragFactory instanceof Doc ? this.layoutDoc.dragFactory : this.layoutDoc);
const referenceLayout = Doc.Layout(referenceDoc);
- return <button className="fontIconBox-outerDiv" title={StrCast(this.props.Document.title)} ref={this._ref} onContextMenu={this.specificContextMenu}
+ return <button className="fontIconBox-outerDiv" title={StrCast(this.layoutDoc.title)} ref={this._ref} onContextMenu={this.specificContextMenu}
style={{
background: StrCast(referenceLayout.backgroundColor),
- boxShadow: this.props.Document.ischecked ? `4px 4px 12px black` : undefined
+ boxShadow: this.layoutDoc.ischecked ? `4px 4px 12px black` : undefined
}}>
<FontAwesomeIcon className="fontIconBox-icon" icon={this.dataDoc.icon as any} color={this._foregroundColor} size="sm" />
{!this.rootDoc.label ? (null) : <div className="fontIconBox-label"> {StrCast(this.rootDoc.label).substring(0, 5)} </div>}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 08917d281..47e7607d6 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -4,16 +4,16 @@ import { faAsterisk, faBrain, faFileAudio, faImage, faPaintBrush } from '@fortaw
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, observable, runInAction } from 'mobx';
import { observer } from "mobx-react";
-import { DataSym, Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc';
-import { documentSchema } from '../../../new_fields/documentSchemas';
-import { Id } from '../../../new_fields/FieldSymbols';
-import { List } from '../../../new_fields/List';
-import { ObjectField } from '../../../new_fields/ObjectField';
-import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema';
-import { ComputedField } from '../../../new_fields/ScriptField';
-import { Cast, NumCast, StrCast } from '../../../new_fields/Types';
-import { AudioField, ImageField } from '../../../new_fields/URLField';
-import { TraceMobx } from '../../../new_fields/util';
+import { DataSym, Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc';
+import { documentSchema } from '../../../fields/documentSchemas';
+import { Id } from '../../../fields/FieldSymbols';
+import { List } from '../../../fields/List';
+import { ObjectField } from '../../../fields/ObjectField';
+import { createSchema, listSpec, makeInterface } from '../../../fields/Schema';
+import { ComputedField } from '../../../fields/ScriptField';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { AudioField, ImageField } from '../../../fields/URLField';
+import { TraceMobx } from '../../../fields/util';
import { emptyFunction, returnOne, Utils, returnZero } from '../../../Utils';
import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
import { Docs } from '../../documents/Documents';
@@ -29,6 +29,7 @@ import FaceRectangles from './FaceRectangles';
import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
import React = require("react");
+import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
const requestImageSize = require('../../util/request-image-size');
const path = require('path');
const { Howl } = require('howler');
@@ -157,21 +158,22 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
const field = Cast(this.dataDoc[this.fieldKey], ImageField);
if (field) {
const funcs: ContextMenuProps[] = [];
- funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" });
funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" });
- funcs.push({
- description: "Reset Native Dimensions", event: action(async () => {
- const curNW = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]);
- const curNH = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]);
- if (this.props.PanelWidth() / this.props.PanelHeight() > curNW / curNH) {
- this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelHeight() * curNW / curNH;
- this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelHeight();
- } else {
- this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelWidth();
- this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelWidth() * curNH / curNW;
- }
- }), icon: "expand-arrows-alt"
- });
+ funcs.push({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" });
+ funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" });
+ // funcs.push({
+ // description: "Reset Native Dimensions", event: action(async () => {
+ // const curNW = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]);
+ // const curNH = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]);
+ // if (this.props.PanelWidth() / this.props.PanelHeight() > curNW / curNH) {
+ // this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelHeight() * curNW / curNH;
+ // this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelHeight();
+ // } else {
+ // this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelWidth();
+ // this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelWidth() * curNH / curNW;
+ // }
+ // }), icon: "expand-arrows-alt"
+ // });
const existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers...");
const modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : [];
@@ -181,6 +183,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
!existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" });
ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
+
+
+ const existingMore = ContextMenu.Instance.findByDescription("More...");
+ const mores: ContextMenuProps[] = existingMore && "subitems" in existingMore ? existingMore.subitems : [];
+ !existingMore && ContextMenu.Instance.addItem({ description: "More...", subitems: mores, icon: "hand-point-right" });
}
}
@@ -249,9 +256,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
_curSuffix = "_m";
resize = (imgPath: string) => {
+ const basePath = imgPath.replace(/_[oms]./, "");
const cachedNativeSize = {
- width: imgPath === this.dataDoc[this.fieldKey + "-path"] ? NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]) : 0,
- height: imgPath === this.dataDoc[this.fieldKey + "-path"] ? NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]) : 0,
+ width: basePath === this.dataDoc[this.fieldKey + "-path"] ? NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]) : 0,
+ height: basePath === this.dataDoc[this.fieldKey + "-path"] ? NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]) : 0,
};
const docAspect = this.layoutDoc[HeightSym]() / this.layoutDoc[WidthSym]();
const cachedAspect = cachedNativeSize.height / cachedNativeSize.width;
@@ -265,7 +273,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
this.layoutDoc._height = this.layoutDoc[WidthSym]() * rotatedAspect;
this.dataDoc[this.fieldKey + "-nativeWidth"] = this.layoutDoc._nativeWidth = this.layoutDoc._width;
this.dataDoc[this.fieldKey + "-nativeHeight"] = this.layoutDoc._nativeHeight = this.layoutDoc._height;
- this.dataDoc[this.fieldKey + "-path"] = imgPath;
+ this.dataDoc[this.fieldKey + "-path"] = basePath;
}
})).catch(console.log);
} else if (Math.abs(1 - docAspect / cachedAspect) > 0.1) {
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 2970674a2..e983852ea 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -1,13 +1,13 @@
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { Doc, Field, FieldResult } from "../../../new_fields/Doc";
-import { List } from "../../../new_fields/List";
-import { RichTextField } from "../../../new_fields/RichTextField";
-import { listSpec } from "../../../new_fields/Schema";
-import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
-import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
-import { ImageField } from "../../../new_fields/URLField";
+import { Doc, Field, FieldResult } from "../../../fields/Doc";
+import { List } from "../../../fields/List";
+import { RichTextField } from "../../../fields/RichTextField";
+import { listSpec } from "../../../fields/Schema";
+import { ComputedField, ScriptField } from "../../../fields/ScriptField";
+import { Cast, FieldValue, NumCast } from "../../../fields/Types";
+import { ImageField } from "../../../fields/URLField";
import { Docs } from "../../documents/Documents";
import { SetupDrag } from "../../util/DragManager";
import { CompiledScript, CompileScript, ScriptOptions } from "../../util/Scripting";
@@ -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/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 6dc4ae578..956d6556b 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,6 +1,6 @@
import { action, observable } from 'mobx';
import { observer } from "mobx-react";
-import { Doc, Field, Opt } from '../../../new_fields/Doc';
+import { Doc, Field, Opt } from '../../../fields/Doc';
import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { Transform } from '../../util/Transform';
diff --git a/src/client/views/nodes/LabelBox.scss b/src/client/views/nodes/LabelBox.scss
index 7c7e92379..b605df262 100644
--- a/src/client/views/nodes/LabelBox.scss
+++ b/src/client/views/nodes/LabelBox.scss
@@ -4,23 +4,19 @@
border-radius: inherit;
display: flex;
flex-direction: column;
+ position: absolute;
}
.labelBox-mainButton {
- width: 100%;
- height: 100%;
+ width: fit-content;
+ height: max-content;
border-radius: inherit;
letter-spacing: 2px;
text-transform: uppercase;
overflow: hidden;
- display:flex;
-}
-
-.labelBox-mainButtonCenter {
- overflow: hidden;
- display: inline;
- align-items: center;
+ display: inline-block;
margin: auto;
+ text-overflow: ellipsis;
}
.labelBox-params {
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 59c695f21..2d27ec441 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -3,11 +3,11 @@ import { faEdit } from '@fortawesome/free-regular-svg-icons';
import { action } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, DocListCast } from '../../../new_fields/Doc';
-import { documentSchema } from '../../../new_fields/documentSchemas';
-import { List } from '../../../new_fields/List';
-import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema';
-import { Cast, NumCast, StrCast } from '../../../new_fields/Types';
+import { Doc, DocListCast } from '../../../fields/Doc';
+import { documentSchema } from '../../../fields/documentSchemas';
+import { List } from '../../../fields/List';
+import { createSchema, listSpec, makeInterface } from '../../../fields/Schema';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
@@ -72,19 +72,17 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument
<div className="labelBox-mainButton" style={{
background: StrCast(this.layoutDoc.backgroundColor),
color: StrCast(this.layoutDoc.color, "inherit"),
- fontSize: NumCast(this.layoutDoc.fontSize) || "inherit",
+ fontSize: NumCast(this.layoutDoc._fontSize) || "inherit",
+ fontFamily: StrCast(this.layoutDoc._fontFamily) || "inherit",
letterSpacing: StrCast(this.layoutDoc.letterSpacing),
textTransform: StrCast(this.layoutDoc.textTransform) as any,
paddingLeft: NumCast(this.layoutDoc._xPadding),
paddingRight: NumCast(this.layoutDoc._xPadding),
paddingTop: NumCast(this.layoutDoc._yPadding),
paddingBottom: NumCast(this.layoutDoc._yPadding),
- textOverflow: this.layoutDoc._singleLine ? "ellipsis" : undefined,
- whiteSpace: this.layoutDoc._singleLine ? "nowrap" : "pre-wrap"
+ whiteSpace: this.layoutDoc._singleLine ? "pre" : "pre-wrap"
}} >
- <div className="labelBox-mainButtonCenter">
- {StrCast(this.rootDoc.text, StrCast(this.rootDoc.title))}
- </div>
+ {StrCast(this.rootDoc.text, StrCast(this.rootDoc.title))}
</div>
<div className="labelBox-fieldKeyParams" >
{!missingParams?.length ? (null) : missingParams.map(m => <div key={m} className="labelBox-missingParam">{m}</div>)}
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index 707b9d12a..098aa58e9 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -1,10 +1,10 @@
import { action, observable } from "mobx";
import { observer } from "mobx-react";
-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 { Doc, DocListCast } from "../../../fields/Doc";
+import { documentSchema } from "../../../fields/documentSchemas";
+import { makeInterface } from "../../../fields/Schema";
+import { Cast, NumCast, StrCast } from "../../../fields/Types";
+import { Utils, setupMoveUpEvents, emptyFunction } from '../../../Utils';
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager } from "../../util/DragManager";
import { ViewBoxBaseComponent } from "../DocComponent";
@@ -16,8 +16,7 @@ import { ContextMenu } from "../ContextMenu";
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";
+import { TraceMobx } from "../../../fields/util";
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/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 740f2ef04..3f942e87b 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -1,13 +1,13 @@
import React = require("react");
import { observer } from "mobx-react";
-import { documentSchema } from "../../../new_fields/documentSchemas";
-import { makeInterface, listSpec } from "../../../new_fields/Schema";
+import { documentSchema } from "../../../fields/documentSchemas";
+import { makeInterface, listSpec } from "../../../fields/Schema";
import { returnFalse, returnZero } from "../../../Utils";
import { CollectionTreeView } from "../collections/CollectionTreeView";
import { ViewBoxBaseComponent } from "../DocComponent";
import { FieldView, FieldViewProps } from './FieldView';
import "./LinkBox.scss";
-import { Cast } from "../../../new_fields/Types";
+import { Cast } from "../../../fields/Types";
type LinkDocument = makeInterface<[typeof documentSchema]>;
const LinkDocument = makeInterface(documentSchema);
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/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 3712c648e..493f23dc4 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -3,11 +3,11 @@ import { action, observable, runInAction, reaction, IReactionDisposer, trace, un
import { observer } from "mobx-react";
import * as Pdfjs from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
-import { Opt, WidthSym, Doc, HeightSym } from "../../../new_fields/Doc";
-import { makeInterface } from "../../../new_fields/Schema";
-import { ScriptField } from '../../../new_fields/ScriptField';
-import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { PdfField, URLField } from "../../../new_fields/URLField";
+import { Opt, WidthSym, Doc, HeightSym } from "../../../fields/Doc";
+import { makeInterface } from "../../../fields/Schema";
+import { ScriptField } from '../../../fields/ScriptField';
+import { Cast, NumCast, StrCast } from "../../../fields/Types";
+import { PdfField, URLField } from "../../../fields/URLField";
import { Utils } from '../../../Utils';
import { undoBatch } from '../../util/UndoManager';
import { panZoomSchema } from '../collections/collectionFreeForm/CollectionFreeFormView';
@@ -20,7 +20,7 @@ import { pageSchema } from "./ImageBox";
import { KeyCodes } from '../../util/KeyCodes';
import "./PDFBox.scss";
import React = require("react");
-import { documentSchema } from '../../../new_fields/documentSchemas';
+import { documentSchema } from '../../../fields/documentSchemas';
type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>;
const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema);
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..342a8a215 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -1,14 +1,12 @@
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 { Doc, DocListCast, DocCastAsync } from "../../../fields/Doc";
+import { InkTool } from "../../../fields/InkField";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types";
import { returnFalse } from "../../../Utils";
-import { documentSchema } from "../../../new_fields/documentSchemas";
+import { documentSchema } from "../../../fields/documentSchemas";
import { DocumentManager } from "../../util/DocumentManager";
import { undoBatch } from "../../util/UndoManager";
import { CollectionDockingView } from "../collections/CollectionDockingView";
@@ -17,17 +15,12 @@ import { InkingControl } from "../InkingControl";
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 { makeInterface } from "../../../fields/Schema";
+import { List } from "../../../fields/List";
+import { Docs } from "../../documents/Documents";
+import { PrefetchProxy } from "../../../fields/Proxy";
+import { ScriptField } from "../../../fields/ScriptField";
+import { Scripting } from "../../util/Scripting";
type PresBoxSchema = makeInterface<[typeof documentSchema]>;
const PresBoxDocument = makeInterface(documentSchema);
@@ -35,58 +28,70 @@ 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 itemIndex() { return NumCast(this.rootDoc._itemIndex); }
+ @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.MakeFunction("lookupPresBoxField(container, field, data)",
+ { 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) {
- let nextSelected = this.currentIndex + 1;
- this.gotoDocument(nextSelected, this.currentIndex);
+ if (this.childDocs[this.itemIndex + 1] !== undefined) {
+ let nextSelected = this.itemIndex + 1;
+ this.gotoDocument(nextSelected, this.itemIndex);
for (nextSelected = nextSelected + 1; nextSelected < this.childDocs.length; nextSelected++) {
if (!this.childDocs[nextSelected].groupButton) {
break;
} else {
- this.gotoDocument(nextSelected, this.currentIndex);
+ this.gotoDocument(nextSelected, this.itemIndex);
}
}
}
}
+
+ @undoBatch
+ @action
back = () => {
this.updateCurrentPresentation();
- const docAtCurrent = this.childDocs[this.currentIndex];
+ const docAtCurrent = this.childDocs[this.itemIndex];
if (docAtCurrent) {
//check if any of the group members had used zooming in including the current document
//If so making sure to zoom out, which goes back to state before zooming action
- let prevSelected = this.currentIndex;
+ let prevSelected = this.itemIndex;
let didZoom = docAtCurrent.zoomButton;
for (; !didZoom && prevSelected > 0 && this.childDocs[prevSelected].groupButton; prevSelected--) {
didZoom = this.childDocs[prevSelected].zoomButton;
}
prevSelected = Math.max(0, prevSelected - 1);
- this.gotoDocument(prevSelected, this.currentIndex);
+ this.gotoDocument(prevSelected, this.itemIndex);
}
}
- 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 +100,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 +123,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 +157,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 +172,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,19 +186,13 @@ 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.rootDoc._itemIndex = index;
if (!this.layoutDoc.presStatus) {
this.layoutDoc.presStatus = true;
@@ -213,23 +213,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
} else {
this.layoutDoc.presStatus = true;
this.startPresentation(0);
- this.gotoDocument(0, this.currentIndex);
+ this.gotoDocument(0, this.itemIndex);
}
}
- 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.rootDoc._itemIndex = 0;
this.layoutDoc.presStatus = false;
}
@@ -238,89 +231,95 @@ 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[]) => {
+ const docs = doc instanceof Doc ? [doc] : doc;
+ docs.forEach(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.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}
+ dontRegisterView={true}
focus={this.selectElement}
ScreenToLocalTransform={this.getTransform} />
: (null)
@@ -328,4 +327,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
</div>
</div>;
}
-} \ No newline at end of file
+}
+Scripting.addGlobal(function lookupPresBoxField(container: Doc, field: string, data: Doc) {
+ if (field === 'indexInPres') return DocListCast(container[StrCast(container.presentationFieldKey)]).indexOf(data);
+ if (field === 'presCollapsedHeight') return container._viewType === CollectionViewType.Stacking ? 50 : 46;
+ if (field === 'presStatus') return container.presStatus;
+ if (field === '_itemIndex') return container._itemIndex;
+ return undefined;
+});
diff --git a/src/client/views/nodes/QueryBox.tsx b/src/client/views/nodes/QueryBox.tsx
index 76885eada..0fff0b57f 100644
--- a/src/client/views/nodes/QueryBox.tsx
+++ b/src/client/views/nodes/QueryBox.tsx
@@ -1,16 +1,16 @@
import React = require("react");
import { IReactionDisposer } from "mobx";
import { observer } from "mobx-react";
-import { documentSchema } from "../../../new_fields/documentSchemas";
-import { Id } from '../../../new_fields/FieldSymbols';
-import { makeInterface, listSpec } from "../../../new_fields/Schema";
-import { StrCast, Cast } from "../../../new_fields/Types";
-import { SelectionManager } from "../../util/SelectionManager";
+import { documentSchema } from "../../../fields/documentSchemas";
+import { Id } from '../../../fields/FieldSymbols';
+import { makeInterface, listSpec } from "../../../fields/Schema";
+import { StrCast, Cast } from "../../../fields/Types";
import { ViewBoxAnnotatableComponent } from '../DocComponent';
import { SearchBox } from "../search/SearchBox";
import { FieldView, FieldViewProps } from './FieldView';
import "./QueryBox.scss";
-import { List } from "../../../new_fields/List";
+import { List } from "../../../fields/List";
+import { SnappingManager } from "../../util/SnappingManager";
type QueryDocument = makeInterface<[typeof documentSchema]>;
const QueryDocument = makeInterface(documentSchema);
@@ -27,7 +27,7 @@ export class QueryBox extends ViewBoxAnnotatableComponent<FieldViewProps, QueryD
}
render() {
- const dragging = !SelectionManager.GetIsDragging() ? "" : "-dragging";
+ const dragging = !SnappingManager.GetIsDragging() ? "" : "-dragging";
return <div className={`queryBox${dragging}`} onWheel={(e) => e.stopPropagation()} >
<SearchBox
id={this.props.Document[Id]}
diff --git a/src/client/views/nodes/RadialMenu.scss b/src/client/views/nodes/RadialMenu.scss
index ce0c263ef..daa620d12 100644
--- a/src/client/views/nodes/RadialMenu.scss
+++ b/src/client/views/nodes/RadialMenu.scss
@@ -67,17 +67,4 @@ s
margin-left: 5px;
text-align: left;
display: inline; //need this?
-}
-
-
-
-.icon-background {
- pointer-events: all;
- height:100%;
- margin-top: 15px;
- background-color: transparent;
- width: 35px;
- text-align: center;
- font-size: 20px;
- margin-left: 5px;
} \ No newline at end of file
diff --git a/src/client/views/nodes/RadialMenu.tsx b/src/client/views/nodes/RadialMenu.tsx
index 0ffed78de..ddfdb67b4 100644
--- a/src/client/views/nodes/RadialMenu.tsx
+++ b/src/client/views/nodes/RadialMenu.tsx
@@ -1,12 +1,9 @@
import React = require("react");
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { action, observable, computed, IReactionDisposer, reaction, runInAction } from "mobx";
-import { RadialMenuItem, RadialMenuProps } from "./RadialMenuItem";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import Measure from "react-measure";
-import "./RadialMenu.scss";
-import MobileInkOverlay from "../../../mobile/MobileInkOverlay";
import MobileInterface from "../../../mobile/MobileInterface";
+import "./RadialMenu.scss";
+import { RadialMenuItem, RadialMenuProps } from "./RadialMenuItem";
@observer
export class RadialMenu extends React.Component {
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 125690dc7..5d4af2d77 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -5,10 +5,10 @@ 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 { makeInterface } from "../../../new_fields/Schema";
-import { Cast, NumCast } from "../../../new_fields/Types";
-import { VideoField } from "../../../new_fields/URLField";
+import { documentSchema } from "../../../fields/documentSchemas";
+import { makeInterface } from "../../../fields/Schema";
+import { Cast, NumCast } from "../../../fields/Types";
+import { VideoField } from "../../../fields/URLField";
import { emptyFunction, returnFalse, returnOne, Utils, returnZero } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
@@ -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/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index c607d6614..0944edf60 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -1,10 +1,10 @@
import { action, observable, computed } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
-import { documentSchema } from "../../../new_fields/documentSchemas";
-import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema";
-import { ScriptField } from "../../../new_fields/ScriptField";
-import { StrCast, ScriptCast, Cast } from "../../../new_fields/Types";
+import { documentSchema } from "../../../fields/documentSchemas";
+import { createSchema, makeInterface, listSpec } from "../../../fields/Schema";
+import { ScriptField } from "../../../fields/ScriptField";
+import { StrCast, ScriptCast, Cast } from "../../../fields/Types";
import { InteractionUtils } from "../../util/InteractionUtils";
import { CompileScript, isCompileError, ScriptParam } from "../../util/Scripting";
import { ViewBoxAnnotatableComponent } from "../DocComponent";
@@ -13,7 +13,7 @@ import { FieldView, FieldViewProps } from "../nodes/FieldView";
import "./ScriptingBox.scss";
import { OverlayView } from "../OverlayView";
import { DocumentIconContainer } from "./DocumentIcon";
-import { List } from "../../../new_fields/List";
+import { List } from "../../../fields/List";
const ScriptingSchema = createSchema({});
type ScriptingDocument = makeInterface<[typeof ScriptingSchema, typeof documentSchema]>;
diff --git a/src/client/views/nodes/SliderBox.tsx b/src/client/views/nodes/SliderBox.tsx
index b2d451ea8..9a1aefba9 100644
--- a/src/client/views/nodes/SliderBox.tsx
+++ b/src/client/views/nodes/SliderBox.tsx
@@ -4,10 +4,10 @@ import { runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Handles, Rail, Slider, Ticks, Tracks } from 'react-compound-slider';
-import { documentSchema } from '../../../new_fields/documentSchemas';
-import { createSchema, makeInterface } from '../../../new_fields/Schema';
-import { ScriptField } from '../../../new_fields/ScriptField';
-import { Cast, NumCast, StrCast } from '../../../new_fields/Types';
+import { documentSchema } from '../../../fields/documentSchemas';
+import { createSchema, makeInterface } from '../../../fields/Schema';
+import { ScriptField } from '../../../fields/ScriptField';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxBaseComponent } from '../DocComponent';
@@ -56,7 +56,7 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps, SliderDocume
style={{ boxShadow: this.layoutDoc.opacity === 0 ? undefined : StrCast(this.layoutDoc.boxShadow, "") }}>
<div className="sliderBox-mainButton" onContextMenu={this.specificContextMenu} style={{
background: StrCast(this.layoutDoc.backgroundColor), color: StrCast(this.layoutDoc.color, "black"),
- fontSize: NumCast(this.layoutDoc.fontSize), letterSpacing: StrCast(this.layoutDoc.letterSpacing)
+ fontSize: NumCast(this.layoutDoc._fontSize), letterSpacing: StrCast(this.layoutDoc.letterSpacing)
}} >
<Slider
mode={2}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 613929bca..ccf1f5588 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -5,12 +5,12 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked } from "mobx";
import { observer } from "mobx-react";
import * as rp from 'request-promise';
-import { Doc } from "../../../new_fields/Doc";
-import { InkTool } from "../../../new_fields/InkField";
-import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { ScriptField } from "../../../new_fields/ScriptField";
-import { Cast, StrCast } from "../../../new_fields/Types";
-import { VideoField } from "../../../new_fields/URLField";
+import { Doc } from "../../../fields/Doc";
+import { InkTool } from "../../../fields/InkField";
+import { createSchema, makeInterface } from "../../../fields/Schema";
+import { ScriptField } from "../../../fields/ScriptField";
+import { Cast, StrCast } from "../../../fields/Types";
+import { VideoField } from "../../../fields/URLField";
import { Utils, emptyFunction, returnOne, returnZero } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
@@ -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 "../../../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);
@@ -337,9 +337,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD
}
@action.bound
- addDocumentWithTimestamp(doc: Doc): boolean {
- const curTime = (this.layoutDoc.currentTimecode || -1);
- curTime !== -1 && (doc.displayTimecode = curTime);
+ addDocumentWithTimestamp(doc: Doc | Doc[]): boolean {
+ const docs = doc instanceof Doc ? [doc] : doc;
+ docs.forEach(doc => {
+ const curTime = (this.layoutDoc.currentTimecode || -1);
+ curTime !== -1 && (doc.displayTimecode = curTime);
+ });
return this.addDocument(doc);
}
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index af84a7d95..4623444b9 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -3,6 +3,38 @@
.webBox-container, .webBox-container-dragging {
transform-origin: top left;
+ width: 100%;
+ height: 100%;
+
+ .webBox-htmlSpan {
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+ .webBox-cont {
+ pointer-events: none;
+ }
+ .webBox-cont, .webBox-cont-interactive {
+ padding: 0vw;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ transform-origin: top left;
+ overflow: auto;
+ .webBox-iframe {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top:0;
+ }
+ }
+ .webBox-cont-interactive {
+ span {
+ user-select: text !important;
+ }
+ }
.webBox-outerContent {
width: 100%;
height: 100%;
@@ -18,29 +50,7 @@
display:none;
}
}
-.webBox-cont {
- padding: 0vw;
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- transform-origin: top left;
- overflow: auto;
- pointer-events: none;
-}
-
-.webBox-cont-interactive {
- span {
- user-select: text !important;
- }
-}
-#webBox-htmlSpan {
- position: absolute;
- top: 0;
- left: 0;
-}
.webBox-overlay {
width: 100%;
@@ -66,8 +76,6 @@
opacity: 0.9;
z-index: 9001;
transition: top .5s;
- padding: 10px;
-
.urlEditor {
display: grid;
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 4e383e468..82f05012a 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -2,13 +2,13 @@ import { library } from "@fortawesome/fontawesome-svg-core";
import { faStickyNote, faPen, faMousePointer } from '@fortawesome/free-solid-svg-icons';
import { action, computed, observable, trace, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
-import { Doc, FieldResult } from "../../../new_fields/Doc";
-import { documentSchema } from "../../../new_fields/documentSchemas";
-import { HtmlField } from "../../../new_fields/HtmlField";
-import { InkTool } from "../../../new_fields/InkField";
-import { makeInterface } from "../../../new_fields/Schema";
-import { Cast, NumCast, BoolCast, StrCast } from "../../../new_fields/Types";
-import { WebField } from "../../../new_fields/URLField";
+import { Doc, FieldResult } from "../../../fields/Doc";
+import { documentSchema } from "../../../fields/documentSchemas";
+import { HtmlField } from "../../../fields/HtmlField";
+import { InkTool } from "../../../fields/InkField";
+import { makeInterface } from "../../../fields/Schema";
+import { Cast, NumCast, BoolCast, StrCast } from "../../../fields/Types";
+import { WebField } from "../../../fields/URLField";
import { Utils, returnOne, emptyFunction, returnZero } from "../../../Utils";
import { Docs } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
@@ -48,10 +48,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void);
iframeLoaded = action((e: any) => {
- this._iframeRef.current!.contentDocument?.addEventListener('pointerdown', this.iframedown, false);
- this._iframeRef.current!.contentDocument?.addEventListener('scroll', this.iframeScrolled, false);
- this.layoutDoc.scrollHeight = this._iframeRef.current!.contentDocument?.children?.[0].scrollHeight || 1000;
- this._iframeRef.current!.contentDocument!.children[0].scrollTop = NumCast(this.layoutDoc.scrollTop);
+ if (this._iframeRef.current?.contentDocument) {
+ this._iframeRef.current.contentDocument.addEventListener('pointerdown', this.iframedown, false);
+ this._iframeRef.current.contentDocument.addEventListener('scroll', this.iframeScrolled, false);
+ this.layoutDoc.scrollHeight = this._iframeRef.current.contentDocument.children?.[0].scrollHeight || 1000;
+ this._iframeRef.current.contentDocument.children[0].scrollTop = NumCast(this.layoutDoc.scrollTop);
+ }
this._reactionDisposer?.();
this._reactionDisposer = reaction(() => this.layoutDoc.scrollY,
(scrollY) => {
@@ -127,28 +129,25 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
}
}
- toggleNativeDimensions = () => {
+ toggleAnnotationMode = () => {
if (!this.layoutDoc.isAnnotating) {
- //DocumentView.unfreezeNativeDimensions(this.layoutDoc);
this.layoutDoc.lockedTransform = false;
this.layoutDoc.isAnnotating = true;
}
else {
- //Doc.freezeNativeDimensions(this.layoutDoc, this.props.PanelWidth(), this.props.PanelHeight());
this.layoutDoc.lockedTransform = true;
this.layoutDoc.isAnnotating = false;
}
}
urlEditor() {
- const frozen = this.layoutDoc._nativeWidth && this.layoutDoc.isAnnotating;
return (
<div className="webBox-urlEditor" style={{ top: this._collapsed ? -70 : 0 }}>
<div className="urlEditor">
<div className="editorBase">
<button className="editor-collapse"
style={{
- top: this._collapsed ? 70 : 10,
+ top: this._collapsed ? 70 : 0,
transform: `rotate(${this._collapsed ? 180 : 0}deg) scale(${this._collapsed ? 0.5 : 1}) translate(${this._collapsed ? "-100%, -100%" : "0, 0"})`,
opacity: (this._collapsed && !this.props.isSelected()) ? 0 : 0.9,
left: (this._collapsed ? 0 : "unset"),
@@ -157,10 +156,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
<FontAwesomeIcon icon="caret-up" size="2x" />
</button>
<div className="webBox-buttons" style={{ display: this._collapsed ? "none" : "flex" }}>
- <div className="webBox-freeze" title={"Annotate"} style={{ background: frozen ? "lightBlue" : "gray" }} onClick={this.toggleNativeDimensions} >
+ <div className="webBox-freeze" title={"Annotate"} style={{ background: this.layoutDoc.isAnnotating ? "lightBlue" : "gray" }} onClick={this.toggleAnnotationMode} >
<FontAwesomeIcon icon={faPen} size={"2x"} />
</div>
- <div className="webBox-freeze" title={"Select"} style={{ background: !frozen ? "lightBlue" : "gray" }} onClick={this.toggleNativeDimensions} >
+ <div className="webBox-freeze" title={"Select"} style={{ background: !this.layoutDoc.isAnnotating ? "lightBlue" : "gray" }} onClick={this.toggleAnnotationMode} >
<FontAwesomeIcon icon={faMousePointer} size={"2x"} />
</div>
<input className="webpage-urlInput"
@@ -309,33 +308,35 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
}
}
+ //const href = "https://brown365-my.sharepoint.com/personal/bcz_ad_brown_edu/_layouts/15/Doc.aspx?sourcedoc={31aa3178-4c21-4474-b367-877d0a7135e4}&action=embedview&wdStartOn=1";
@computed
- get content() {
+ get urlContent() {
+
const field = this.dataDoc[this.props.fieldKey];
let view;
if (field instanceof HtmlField) {
- view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
+ view = <span className="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
} else if (field instanceof WebField) {
const url = this.layoutDoc.UseCors ? Utils.CorsProxy(field.url.href) : field.url.href;
- view = <iframe ref={this._iframeRef} onLoad={this.iframeLoaded} src={url} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />;
+ view = <iframe className="webBox-iframe" ref={this._iframeRef} src={url} onLoad={this.iframeLoaded} />;
} else {
- view = <iframe ref={this._iframeRef} src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />;
+ view = <iframe className="webBox-iframe" ref={this._iframeRef} src={"https://crossorigin.me/https://cs.brown.edu"} />;
}
- const content =
- <div style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
- {this.urlEditor()}
- {view}
- </div>;
-
+ return view;
+ }
+ @computed
+ get content() {
+ const view = this.urlContent;
const decInteracting = DocumentDecorations.Instance?.Interacting;
const frozen = !this.props.isSelected() || decInteracting;
return (<>
- <div className={"webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !decInteracting ? "-interactive" : "")} >
- {content}
- </div>
+ <div className={"webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !decInteracting ? "-interactive" : "")}
+ onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
+ {view}
+ </div>;
{!frozen ? (null) :
<div className="webBox-overlay" style={{ pointerEvents: this.layoutDoc.isBackground ? undefined : "all" }}
onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer}>
@@ -344,6 +345,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
<div className="dragger" ref={this._iframeDragRef}></div>
</div>
</div>}
+ {this.urlEditor()}
</>);
}
scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.props.Document.scrollTop));
@@ -351,8 +353,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
return (<div className={`webBox-container`}
style={{
transform: `scale(${this.props.ContentScaling()})`,
- width: `${100 / this.props.ContentScaling()}%`,
- height: `${100 / this.props.ContentScaling()}%`,
+ width: Number.isFinite(this.props.ContentScaling()) ? `${100 / this.props.ContentScaling()}%` : "100%",
+ height: Number.isFinite(this.props.ContentScaling()) ? `${100 / this.props.ContentScaling()}%` : "100%",
pointerEvents: this.layoutDoc.isBackground ? "none" : undefined
}} >
{this.content}
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
new file mode 100644
index 000000000..d56b87ae5
--- /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 "../../../../fields/Doc";
+import { Id } from "../../../../fields/FieldSymbols";
+import { List } from "../../../../fields/List";
+import { ObjectField } from "../../../../fields/ObjectField";
+import { listSpec } from "../../../../fields/Schema";
+import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField";
+import { ComputedField } from "../../../../fields/ScriptField";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../../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..05e6a5959
--- /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 "../../../../fields/Doc";
+import { Id } from "../../../../fields/FieldSymbols";
+import { ObjectField } from "../../../../fields/ObjectField";
+import { ComputedField } from "../../../../fields/ScriptField";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../../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..d05e8f1ea
--- /dev/null
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -0,0 +1,215 @@
+import { IReactionDisposer, observable, runInAction, computed, action } from "mobx";
+import { Doc, DocListCast, Field } from "../../../../fields/Doc";
+import { List } from "../../../../fields/List";
+import { listSpec } from "../../../../fields/Schema";
+import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField";
+import { ComputedField } from "../../../../fields/ScriptField";
+import { Cast, StrCast } from "../../../../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}
+ hideKey={node.attrs.hideKey}
+ tbox={tbox}
+ />, this._fieldWrapper);
+ (this as any).dom = this._fieldWrapper;
+ }
+ destroy() {
+ ReactDOM.unmountComponentAtNode(this._fieldWrapper);
+ }
+ selectNode() { }
+
+}
+interface IDashFieldViewInternal {
+ fieldKey: string;
+ docid: string;
+ hideKey: boolean;
+ 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?.();
+ }
+
+ multiValueDelimeter = ";";
+
+ // 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] || (this._fieldKey === "PARAMS" ? this._textBoxDoc[this._fieldKey] : "");
+ const fval = dashVal instanceof List ? dashVal.join(this.multiValueDelimeter) : 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 {
+ const splits = newText.split(this.multiValueDelimeter);
+ if (this._fieldKey !== "PARAMS" || !this._textBoxDoc[this._fieldKey] || this._dashDoc?.PARAMS) {
+ this._dashDoc![this._fieldKey] = splits.length > 1 ? new List<string>(splits) : 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,
+ }}>
+ {this.props.hideKey ? (null) :
+ <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 425af8ccf..5e33e7e70 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -12,53 +12,67 @@ 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 '../../../../fields/DateField';
+import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../../../fields/Doc";
+import { documentSchema } from '../../../../fields/documentSchemas';
+import { Id } from '../../../../fields/FieldSymbols';
+import { InkTool } from '../../../../fields/InkField';
+import { PrefetchProxy } from '../../../../fields/Proxy';
+import { RichTextField } from "../../../../fields/RichTextField";
+import { RichTextUtils } from '../../../../fields/RichTextUtils';
+import { createSchema, makeInterface } from "../../../../fields/Schema";
+import { Cast, DateCast, NumCast, StrCast } from "../../../../fields/Types";
+import { TraceMobx } from '../../../../fields/util';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils, setupMoveUpEvents } 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 '../../../../fields/ScriptField';
+import GoogleAuthenticationManager from '../../../apis/GoogleAuthenticationManager';
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({
- documentText: "string"
+ documentText: "string",
});
export const GoogleRef = "googleDocId";
@@ -79,21 +93,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
private _editorView: Opt<EditorView>;
private _applyingChange: boolean = false;
private _searchIndex = 0;
- private _sidebarMovement = 0;
- private _lastX = 0;
- private _lastY = 0;
private _undoTyping?: UndoManager.Batch;
- private _searchReactionDisposer?: Lambda;
- private _recordReactionDisposer: Opt<IReactionDisposer>;
- private _scrollToRegionReactionDisposer: Opt<IReactionDisposer>;
- private _reactionDisposer: Opt<IReactionDisposer>;
- private _heightReactionDisposer: Opt<IReactionDisposer>;
- private _proxyReactionDisposer: Opt<IReactionDisposer>;
- private _pullReactionDisposer: Opt<IReactionDisposer>;
- private _pushReactionDisposer: Opt<IReactionDisposer>;
- private _buttonBarReactionDisposer: Opt<IReactionDisposer>;
- private _linkMakerDisposer: Opt<IReactionDisposer>;
- private _scrollDisposer: Opt<IReactionDisposer>;
+ private _disposers: { [name: string]: IReactionDisposer } = {};
private dropDisposer?: DragManager.DragDropDisposer;
@computed get _recording() { return this.dataDoc.audioState === "recording"; }
@@ -157,7 +158,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, _width: 500, _height: 500 }, value);
DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument);
if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; }
- else DocUtils.MakeLink({ doc: this.props.Document }, { doc: this.dataDoc[key] as Doc }, "link to named target", id);
+ else DocUtils.MakeLink({ doc: this.rootDoc }, { doc: this.dataDoc[key] as Doc }, "link to named target", id);
});
});
});
@@ -195,15 +196,22 @@ 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.layoutDoc[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 || curLayout?.Data.includes("dash")) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
+ if (json !== curLayout?.Data) {
+ !curText && tx.storedMarks?.map(m => m.type.name === "pFontSize" && (Doc.UserDoc().fontSize = this.layoutDoc._fontSize = m.attrs.fontSize));
+ 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;
@@ -222,6 +230,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
+ // needs a better API for taking in a set of words with target documents instead of just one target
+ 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));
+ 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);
+ this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
+ const alink = DocUtils.MakeLink({ doc: this.rootDoc }, { doc: target }, "automatic")!;
+ const link = this._editorView.state.schema.marks.link.create({
+ href: Utils.prepend("/doc/" + alink[Id]),
+ title: "a link", location: location, linkId: alink[Id], targetId: target[Id]
+ });
+ this._editorView.dispatch(tr.addMark(flattened[lastSel].from, flattened[lastSel].to, link));
+ }
+ }
public highlightSearchTerms = (terms: string[]) => {
if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) {
const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
@@ -253,7 +278,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
protected createDropTarget = (ele: HTMLDivElement) => {
this.ProseRef = ele;
this.dropDisposer?.();
- ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document));
+ ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc));
}
@undoBatch
@@ -341,10 +366,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
updateHighlights = () => {
clearStyleSheetRules(FormattedTextBox._userStyleSheet);
if (FormattedTextBox._highlights.indexOf("Text from Others") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-remote", { background: "yellow" });
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-remote", { background: "yellow" });
}
if (FormattedTextBox._highlights.indexOf("My Text") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "moccasin" });
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "moccasin" });
}
if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "todo", { outline: "black solid 1px" });
@@ -359,39 +384,33 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "ignore", { "font-size": "1" });
}
if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
const min = Math.round(Date.now() / 1000 / 60);
- numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-min-" + (min - i), { opacity: ((10 - i - 1) / 10).toString() }));
+ numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-min-" + (min - i), { opacity: ((10 - i - 1) / 10).toString() }));
setTimeout(() => this.updateHighlights());
}
if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
const hr = Math.round(Date.now() / 1000 / 60 / 60);
- numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-hr-" + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }));
+ numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-hr-" + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }));
}
}
sidebarDown = (e: React.PointerEvent) => {
- this._lastX = e.clientX;
- this._lastY = e.clientY;
- this._sidebarMovement = 0;
- document.addEventListener("pointermove", this.sidebarMove);
- document.addEventListener("pointerup", this.sidebarUp);
- e.stopPropagation();
- e.preventDefault(); // prevents text from being selected during drag
+ setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction,
+ () => (this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%"));
}
- sidebarMove = (e: PointerEvent) => {
+ sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => {
const bounds = this.CurrentDiv.getBoundingClientRect();
- this._sidebarMovement += Math.sqrt((e.clientX - this._lastX) * (e.clientX - this._lastX) + (e.clientY - this._lastY) * (e.clientY - this._lastY));
- this.props.Document.sidebarWidthPercent = "" + 100 * (1 - (e.clientX - bounds.left) / bounds.width) + "%";
+ this.layoutDoc._sidebarWidthPercent = "" + 100 * (1 - (e.clientX - bounds.left) / bounds.width) + "%";
+ return false;
}
- sidebarUp = (e: PointerEvent) => {
- document.removeEventListener("pointermove", this.sidebarMove);
- document.removeEventListener("pointerup", this.sidebarUp);
+ @undoBatch
+ @action
+ toggleNativeDimensions = () => {
+ Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.NativeWidth(), this.props.NativeHeight());
}
- toggleSidebar = () => this._sidebarMovement < 5 && (this.props.Document.sidebarWidthPercent = StrCast(this.props.Document.sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%");
-
public static get DefaultLayout(): Doc | string | undefined {
return Cast(Doc.UserDoc().defaultTextLayout, Doc, null) || StrCast(Doc.UserDoc().defaultTextLayout, null);
}
@@ -399,18 +418,40 @@ 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" });
- funcs.push({ description: "Reset Default Layout", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" });
- !this.props.Document.rootDocument && funcs.push({
- description: "Make Template", event: () => {
- this.props.Document.isTemplateDoc = makeTemplate(this.props.Document, true);
- Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.props.Document);
+ this.rootDoc.isTemplateDoc && funcs.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc), icon: "eye" });
+ !this.layoutDoc.isTemplateDoc && funcs.push({
+ description: "Convert to use as a style", event: () => {
+ this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc);
+ 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" });
- funcs.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" });
+ 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: `${this.Document._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" });
+ funcs.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" });
+ funcs.push({ description: "Toggle Single Line", event: () => this.layoutDoc._singleLine = !this.layoutDoc._singleLine, icon: "expand-arrows-alt" });
+
+ const uicontrols: ContextMenuProps[] = [];
+ uicontrols.push({ description: "Toggle Sidebar", event: () => this.layoutDoc._showSidebar = !this.layoutDoc._showSidebar, icon: "expand-arrows-alt" });
+ uicontrols.push({ description: "Toggle Dictation Icon", event: () => this.layoutDoc._showAudio = !this.layoutDoc._showAudio, icon: "expand-arrows-alt" });
+ uicontrols.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" });
+
+ funcs.push({ description: "UI Controls...", subitems: uicontrols, icon: "asterisk" });
const highlighting: ContextMenuProps[] = [];
["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
@@ -436,26 +477,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
DocListCast(noteTypesDoc?.data).forEach(note => {
changeItems.push({
description: StrCast(note.title), event: undoBatch(() => {
- Doc.setNativeView(this.props.Document);
+ Doc.setNativeView(this.rootDoc);
Doc.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note);
}), icon: "eye"
});
});
changeItems.push({ description: "FreeForm", event: undoBatch(() => Doc.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), "change view"), icon: "eye" });
!change && cm.addItem({ description: "Change Perspective...", subitems: changeItems, icon: "external-link-alt" });
-
- const open = cm.findByDescription("Add a Perspective...");
- const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : [];
-
- openItems.push({
- description: "FreeForm", event: undoBatch(() => {
- const alias = Doc.MakeAlias(this.rootDoc);
- Doc.makeCustomViewClicked(alias, Docs.Create.FreeformDocument, "freeform");
- this.props.addDocTab(alias, "onRight");
- }), icon: "eye"
- });
- !open && cm.addItem({ description: "Add a Perspective...", subitems: openItems, icon: "external-link-alt" });
-
}
recordDictation = () => {
@@ -473,7 +501,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
@action
toggleMenubar = () => {
- this.props.Document._chromeStatus = this.props.Document._chromeStatus === "disabled" ? "enabled" : "disabled";
+ this.layoutDoc._chromeStatus = this.layoutDoc._chromeStatus === "disabled" ? "enabled" : "disabled";
}
recordBullet = async () => {
@@ -564,7 +592,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
componentDidMount() {
- this._buttonBarReactionDisposer = reaction(
+ this._disposers.buttonBar = reaction(
() => DocumentButtonBar.Instance,
instance => {
if (instance) {
@@ -573,7 +601,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
);
- this._linkMakerDisposer = reaction(
+ this._disposers.linkMaker = reaction(
() => this.props.makeLink?.(),
(linkDoc: Opt<Doc>) => {
if (linkDoc) {
@@ -584,13 +612,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
},
{ fireImmediately: true }
);
-
- this._reactionDisposer = reaction(
+ this._disposers.editorState = reaction(
() => {
- if (this.dataDoc[this.props.fieldKey + "-noTemplate"] || !this.props.Document[this.props.fieldKey + "-textTemplate"]) {
+ if (this.dataDoc[this.props.fieldKey + "-noTemplate"] || !this.layoutDoc[this.props.fieldKey + "-textTemplate"]) {
return Cast(this.dataDoc[this.props.fieldKey], RichTextField, null)?.Data;
}
- return Cast(this.props.Document[this.props.fieldKey + "-textTemplate"], RichTextField, null)?.Data;
+ return Cast(this.layoutDoc[this.props.fieldKey + "-textTemplate"], RichTextField, null)?.Data;
},
incomingValue => {
if (incomingValue !== undefined && this._editorView && !this._applyingChange) {
@@ -600,8 +627,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
);
-
- this._pullReactionDisposer = reaction(
+ this._disposers.pullDoc = reaction(
() => this.props.Document[Pulls],
() => {
if (!DocumentButtonBar.hasPulledHack) {
@@ -611,8 +637,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
);
-
- this._pushReactionDisposer = reaction(
+ this._disposers.pushDoc = reaction(
() => this.props.Document[Pushes],
() => {
if (!DocumentButtonBar.hasPushedHack) {
@@ -621,19 +646,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
);
-
- this._heightReactionDisposer = reaction(
+ this._disposers.autoHeight = reaction(
() => [this.layoutDoc[WidthSym](), this.layoutDoc._autoHeight],
- () => this.tryUpdateHeight()
+ () => setTimeout(() => this.tryUpdateHeight(), 0)
+ );
+ this._disposers.height = reaction(
+ () => this.layoutDoc[HeightSym](),
+ action(height => {
+ if (height <= 20 && height < NumCast(this.layoutDoc._delayAutoHeight, 20)) {
+ this.layoutDoc._delayAutoHeight = height;
+ }
+ })
);
this.setupEditor(this.config, this.props.fieldKey);
- this._searchReactionDisposer = reaction(() => this.rootDoc.searchMatch,
+ this._disposers.search = reaction(() => this.rootDoc.searchMatch,
search => search ? this.highlightSearchTerms([Doc.SearchQuery()]) : this.unhighlightSearchTerms(),
{ fireImmediately: true });
- this._recordReactionDisposer = reaction(() => this._recording,
+ this._disposers.record = reaction(() => this._recording,
() => {
if (this._recording) {
setTimeout(action(() => {
@@ -643,15 +675,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
} else setTimeout(() => this.stopDictation(true), 0);
}
);
-
- this._scrollToRegionReactionDisposer = reaction(
+ this._disposers.scrollToRegion = reaction(
() => StrCast(this.layoutDoc.scrollToLinkID),
async (scrollToLinkID) => {
const findLinkFrag = (frag: Fragment, editor: EditorView) => {
const nodes: Node[] = [];
frag.forEach((node, index) => {
const examinedNode = findLinkNode(node, editor);
- if (examinedNode && examinedNode.textContent) {
+ if (examinedNode?.textContent) {
nodes.push(examinedNode);
start += index;
}
@@ -668,7 +699,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
return linkIndex !== -1 && scrollToLinkID === marks[linkIndex].attrs.href.replace(/.*\/doc\//, "") ? node : undefined;
};
- let start = -1;
+ let start = 0;
if (this._editorView && scrollToLinkID) {
const editor = this._editorView;
const ret = findLinkFrag(editor.state.doc.content, editor);
@@ -689,8 +720,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
},
{ fireImmediately: true }
);
-
- this._scrollDisposer = reaction(() => NumCast(this.props.Document.scrollPos),
+ this._disposers.scroll = reaction(() => NumCast(this.layoutDoc.scrollPos),
pos => this._scrollRef.current && this._scrollRef.current.scrollTo({ top: pos }), { fireImmediately: true }
);
@@ -747,7 +777,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
let pullSuccess = false;
if (exportState !== undefined) {
pullSuccess = true;
- dataDoc.data = new RichTextField(JSON.stringify(exportState.state.toJSON()));
+ dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(exportState.state.toJSON()));
setTimeout(() => {
if (this._editorView) {
const state = this._editorView.state;
@@ -809,7 +839,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
targetAnnotations?.push(pdfRegion);
});
- const link = DocUtils.MakeLink({ doc: this.props.Document }, { doc: pdfRegion }, "PDF pasted");
+ const link = DocUtils.MakeLink({ doc: this.rootDoc }, { doc: pdfRegion }, "PDF pasted");
if (link) {
cbe.clipboardData!.setData("dash/linkDoc", link[Id]);
const linkId = link[Id];
@@ -846,8 +876,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
private setupEditor(config: any, fieldKey: string) {
const curText = Cast(this.dataDoc[this.props.fieldKey], RichTextField, null);
- const useTemplate = !curText?.Text && this.props.Document[this.props.fieldKey + "-textTemplate"];
- const rtfField = Cast((useTemplate && this.props.Document[this.props.fieldKey + "-textTemplate"]) || this.dataDoc[fieldKey], RichTextField);
+ const useTemplate = !curText?.Text && this.layoutDoc[this.props.fieldKey + "-textTemplate"];
+ const rtfField = Cast((useTemplate && this.layoutDoc[this.props.fieldKey + "-textTemplate"]) || this.dataDoc[fieldKey], RichTextField);
if (this.ProseRef) {
const self = this;
this._editorView?.destroy();
@@ -866,7 +896,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); }
@@ -876,7 +914,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
});
const startupText = !rtfField && this._editorView && Field.toString(this.dataDoc[fieldKey] as Field);
if (startupText) {
- this._editorView.dispatch(this._editorView.state.tr.insertText(startupText));
+ const { state: { tr }, dispatch } = this._editorView;
+ dispatch(tr.insertText(startupText));
}
}
@@ -906,17 +945,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
componentWillUnmount() {
- this._scrollDisposer?.();
- this._scrollToRegionReactionDisposer?.();
- this._reactionDisposer?.();
- this._proxyReactionDisposer?.();
- this._pushReactionDisposer?.();
- this._pullReactionDisposer?.();
- this._heightReactionDisposer?.();
- this._searchReactionDisposer?.();
- this._recordReactionDisposer?.();
- this._buttonBarReactionDisposer?.();
- this._linkMakerDisposer?.();
+ Object.values(this._disposers).forEach(disposer => disposer?.());
this._editorView?.destroy();
}
@@ -944,9 +973,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
if (this.props.onClick && e.button === 0 && !this.props.isSelected(false)) {
e.preventDefault();
}
- if (e.button === 0 && this.active(true) && !e.altKey && !e.ctrlKey && !e.metaKey) {
- if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // don't stop propagation if clicking in the sidebar
- e.stopPropagation();
+ if (e.button === 0 && this.props.isSelected(true) && !e.altKey && !e.ctrlKey && !e.metaKey) {
+ if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // stop propagation if not in sidebar
+ e.stopPropagation(); // if the text box is selected, then it consumes all down events
}
}
if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
@@ -984,7 +1013,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
// jump rich text menu to this textbox
const bounds = this._ref.current?.getBoundingClientRect();
- if (bounds && this.props.Document._chromeStatus !== "disabled") {
+ if (bounds && this.layoutDoc._chromeStatus !== "disabled") {
const x = Math.min(Math.max(bounds.left, 0), window.innerWidth - RichTextMenu.Instance.width);
let y = Math.min(Math.max(0, bounds.top - RichTextMenu.Instance.height - 50), window.innerHeight - RichTextMenu.Instance.height);
if (coords && coords.left > x && coords.left < x + RichTextMenu.Instance.width && coords.top > y && coords.top < y + RichTextMenu.Instance.height + 50) {
@@ -1020,43 +1049,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; }
(e.nativeEvent as any).formattedHandled = true;
- // if (e.button === 0 && ((!this.props.isSelected(true) && !e.ctrlKey) || (this.props.isSelected(true) && e.ctrlKey)) && !e.metaKey && e.target) {
- // let href = (e.target as any).href;
- // let location: string;
- // if ((e.target as any).attributes.location) {
- // location = (e.target as any).attributes.location.value;
- // }
- // let pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
- // let node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos);
- // if (node) {
- // let link = node.marks.find(m => m.type === this._editorView!.state.schema.marks.link);
- // if (link && !(link.attrs.docref && link.attrs.title)) { // bcz: getting hacky. this indicates that we clicked on a PDF excerpt quotation. In this case, we don't want to follow the link (we follow only the actual hyperlink for the quotation which is handled above).
- // href = link && link.attrs.href;
- // location = link && link.attrs.location;
- // }
- // }
- // if (href) {
- // if (href.indexOf(Utils.prepend("/doc/")) === 0) {
- // let linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
- // if (linkClicked) {
- // DocServer.GetRefField(linkClicked).then(async linkDoc => {
- // (linkDoc instanceof Doc) &&
- // DocumentManager.Instance.FollowLink(linkDoc, this.props.Document, document => this.props.addDocTab(document, location ? location : "inTab"), false);
- // });
- // }
- // } else {
- // let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.layoutDoc.x, 0) + NumCast(this.layoutDoc.width, 0), y: NumCast(this.layoutDoc.y) });
- // this.props.addDocument && this.props.addDocument(webDoc);
- // }
- // e.stopPropagation();
- // e.preventDefault();
- // }
- // }
if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientX - this._downX) < 4) {
this.props.select(e.ctrlKey);
this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false);
}
+ if (this.props.isSelected(true)) { // if text box is selected, then it consumes all click events
+ e.stopPropagation();
+ }
}
// this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them.
@@ -1173,34 +1173,44 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
onscrolled = (ev: React.UIEvent) => {
- this.props.Document.scrollPos = this._scrollRef.current!.scrollTop;
+ this.layoutDoc.scrollPos = this._scrollRef.current!.scrollTop;
}
@action
tryUpdateHeight(limitHeight?: number) {
let scrollHeight = this._ref.current?.scrollHeight;
- if (this.layoutDoc._autoHeight && scrollHeight &&
- getComputedStyle(this._ref.current!.parentElement!).top === "0px") { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
+ if (this.layoutDoc._autoHeight && !this.props.ignoreAutoHeight && scrollHeight) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
+ scrollHeight = scrollHeight * NumCast(this.layoutDoc.scale, 1);
if (limitHeight && scrollHeight > limitHeight) {
scrollHeight = limitHeight;
this.layoutDoc.limitHeight = undefined;
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.doc && !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;
+ }
}
}
}
- @computed get sidebarWidthPercent() { return StrCast(this.props.Document.sidebarWidthPercent, "0%"); }
+ @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); }
sidebarWidth = () => Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth();
sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth()), 0);
@computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); }
render() {
TraceMobx();
+ const scale = this.props.ContentScaling() * NumCast(this.layoutDoc.scale, 1);
const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
const interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground;
if (this.props.isSelected()) {
@@ -1209,73 +1219,90 @@ 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"]),
- opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1,
- color: this.props.hideOnLeave ? "white" : "inherit",
- pointerEvents: interactive ? "none" : undefined,
- fontSize: NumCast(this.layoutDoc.fontSize, 13),
- fontFamily: StrCast(this.layoutDoc.fontFamily, "Crimson Text"),
- }}
- onContextMenu={this.specificContextMenu}
- onKeyDown={this.onKeyPress}
- onFocus={this.onFocused}
- onClick={this.onClick}
- onPointerMove={e => this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, true)}
- onBlur={this.onBlur}
- onPointerUp={this.onPointerUp}
- onPointerDown={this.onPointerDown}
- onMouseUp={this.onMouseUp}
- onWheel={this.onPointerWheel}
- onPointerEnter={action(() => this._entered = true)}
- onPointerLeave={action(() => this._entered = false)}
- >
- <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._xMargin, 0)}px ${NumCast(this.layoutDoc._yMargin, 0)}px`,
- pointerEvents: ((this.layoutDoc.isLinkButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined
- }} />
+ <div className={"formattedTextBox-cont"} style={{
+ transform: `scale(${scale})`,
+ transformOrigin: "top left",
+ width: `${100 / scale}%`,
+ height: `${100 / scale}%`,
+ ...this.styleFromLayoutString(scale)
+ }}>
+ <div className={`formattedTextBox-cont`} ref={this._ref}
+ style={{
+ width: "100%",
+ 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.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")
+ }}
+ onContextMenu={this.specificContextMenu}
+ onKeyDown={this.onKeyPress}
+ onFocus={this.onFocused}
+ onClick={this.onClick}
+ onPointerMove={e => this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, true)}
+ onBlur={this.onBlur}
+ onPointerUp={this.onPointerUp}
+ onPointerDown={this.onPointerDown}
+ onMouseUp={this.onMouseUp}
+ onWheel={this.onPointerWheel}
+ onPointerEnter={action(() => this._entered = true)}
+ 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, 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>
+ {!this.layoutDoc._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
+ <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} /> :
+ <div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")}
+ style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
+ <CollectionFreeFormView {...this.props}
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.sidebarWidth}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ annotationsKey={this.annotationKey}
+ isAnnotationOverlay={false}
+ focus={this.props.focus}
+ isSelected={this.props.isSelected}
+ select={emptyFunction}
+ active={this.annotationsActive}
+ ContentScaling={returnOne}
+ whenActiveChanged={this.whenActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocument}
+ CollectionView={undefined}
+ ScreenToLocalTransform={this.sidebarScreenToLocal}
+ renderDepth={this.props.renderDepth + 1}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
+ </CollectionFreeFormView>
+ <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} />
+ </div>}
+ {!this.layoutDoc._showAudio ? (null) :
+ <div className="formattedTextBox-dictation"
+ onPointerDown={e => {
+ runInAction(() => this._recording = !this._recording);
+ setTimeout(() => this._editorView!.focus(), 500);
+ e.stopPropagation();
+ }} >
+ <FontAwesomeIcon className="formattedTExtBox-audioFont"
+ style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.5, display: this.props.isSelected() ? "" : "none" }} icon={"microphone"} size="sm" />
+ </div>}
</div>
- {!this.props.Document._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
- <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> :
- <div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")}
- style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
- <CollectionFreeFormView {...this.props}
- PanelHeight={this.props.PanelHeight}
- PanelWidth={this.sidebarWidth}
- NativeHeight={returnZero}
- NativeWidth={returnZero}
- annotationsKey={this.annotationKey}
- isAnnotationOverlay={false}
- focus={this.props.focus}
- isSelected={this.props.isSelected}
- select={emptyFunction}
- active={this.annotationsActive}
- ContentScaling={returnOne}
- whenActiveChanged={this.whenActiveChanged}
- removeDocument={this.removeDocument}
- moveDocument={this.moveDocument}
- addDocument={this.addDocument}
- CollectionView={undefined}
- ScreenToLocalTransform={this.sidebarScreenToLocal}
- renderDepth={this.props.renderDepth + 1}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
- </CollectionFreeFormView>
- <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} />
- </div>}
- {!this.props.Document._showAudio ? (null) :
- <div className="formattedTextBox-dictation"
- onPointerDown={e => {
- runInAction(() => this._recording = !this._recording);
- setTimeout(() => this._editorView!.focus(), 500);
- e.stopPropagation();
- }} >
- <FontAwesomeIcon className="formattedTExtBox-audioFont"
- style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.5, display: this.props.isSelected() ? "" : "none" }} icon={"microphone"} size="sm" />
- </div>}
</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 a9f76ae8f..d47ae63af 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -2,21 +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 "../../../../fields/Doc";
+import { Cast, FieldValue, NumCast } from "../../../../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 { DocumentView } from "./DocumentView";
+import { DocumentType } from "../../../documents/DocumentTypes";
export let formattedTextBoxCommentPlugin = new Plugin({
view(editorView) { return new FormattedTextBoxComment(editorView); }
@@ -92,7 +91,7 @@ export class FormattedTextBoxComment {
(doc: Doc, followLinkLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation));
}
} else if (textBox && (FormattedTextBoxComment.tooltipText as any).href) {
- textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400 }), "onRight");
+ textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400, UseCors: true }), "onRight");
}
keep && textBox && FormattedTextBoxComment.start !== undefined && textBox.adoptAnnotation(
FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark);
@@ -193,21 +192,27 @@ 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%";
+ FormattedTextBoxComment.tooltip.style.width = NumCast(target._width) ? `${NumCast(target._width)}` : "100%";
+ FormattedTextBoxComment.tooltip.style.height = NumCast(target._height) ? `${NumCast(target._height)}` : "100%";
}
// let ext = (target && target.type !== DocumentType.PDFANNO && Doc.fieldExtensionDoc(target, "data")) || target; // try guessing that the target doc's data is in the 'data' field. probably need an 'overviewLayout' and then just display the target Document ....
// let text = ext && StrCast(ext.text);
diff --git a/src/client/views/nodes/formattedText/ImageResizeView.tsx b/src/client/views/nodes/formattedText/ImageResizeView.tsx
new file mode 100644
index 000000000..401ecd7e6
--- /dev/null
+++ b/src/client/views/nodes/formattedText/ImageResizeView.tsx
@@ -0,0 +1,138 @@
+import { NodeSelection } from "prosemirror-state";
+import { Doc } from "../../../../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..2f7d23021
--- /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 "../../../../fields/Types";
+import { Doc } from "../../../../fields/Doc";
+import { FormattedTextBox } from "./FormattedTextBox";
+import { Id } from "../../../../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..fd1b26208
--- /dev/null
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -0,0 +1,876 @@
+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 "../../../../fields/Types";
+import { FormattedTextBoxProps } from "./FormattedTextBox";
+import { unimplementedFunction, Utils } from "../../../../Utils";
+import { wrapInList } from "prosemirror-schema-list";
+import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../../../fields/SchemaHeaderField';
+import "./RichTextMenu.scss";
+import { DocServer } from "../../../DocServer";
+import { Doc } from "../../../../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..fbd6c87bb
--- /dev/null
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -0,0 +1,321 @@
+import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from "prosemirror-inputrules";
+import { NodeSelection, TextSelection } from "prosemirror-state";
+import { DataSym, Doc } from "../../../../fields/Doc";
+import { Id } from "../../../../fields/FieldSymbols";
+import { ComputedField } from "../../../../fields/ScriptField";
+import { Cast, NumCast } from "../../../../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";
+import { List } from "../../../../fields/List";
+
+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;
+ const multiple = tag.split(";");
+ this.Document[DataSym]["#"] = multiple.length > 1 ? new List(multiple) : 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..91280dea4
--- /dev/null
+++ b/src/client/views/nodes/formattedText/RichTextSchema.tsx
@@ -0,0 +1,538 @@
+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 "../../../../fields/Doc";
+import { Id } from "../../../../fields/FieldSymbols";
+import { List } from "../../../../fields/List";
+import { ObjectField } from "../../../../fields/ObjectField";
+import { listSpec } from "../../../../fields/Schema";
+import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField";
+import { ComputedField } from "../../../../fields/ScriptField";
+import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../../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.style.whiteSpace = "normal";
+ 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..ebaa23e99
--- /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 "../../../../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..af39ef9c7
--- /dev/null
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -0,0 +1,265 @@
+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: "" },
+ hideKey: { default: false }
+ },
+ 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