aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes')
-rw-r--r--src/client/views/nodes/ButtonBox.scss19
-rw-r--r--src/client/views/nodes/ButtonBox.tsx74
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx60
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx7
-rw-r--r--src/client/views/nodes/DocumentView.tsx461
-rw-r--r--src/client/views/nodes/FieldView.tsx8
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss112
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx602
-rw-r--r--src/client/views/nodes/FormattedTextBoxComment.scss34
-rw-r--r--src/client/views/nodes/FormattedTextBoxComment.tsx152
-rw-r--r--src/client/views/nodes/ImageBox.scss1
-rw-r--r--src/client/views/nodes/ImageBox.tsx19
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx31
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx6
-rw-r--r--src/client/views/nodes/LinkEditor.scss145
-rw-r--r--src/client/views/nodes/LinkEditor.tsx400
-rw-r--r--src/client/views/nodes/LinkMenu.scss137
-rw-r--r--src/client/views/nodes/LinkMenu.tsx78
-rw-r--r--src/client/views/nodes/LinkMenuGroup.tsx105
-rw-r--r--src/client/views/nodes/LinkMenuItem.tsx139
-rw-r--r--src/client/views/nodes/PDFBox.tsx17
-rw-r--r--src/client/views/nodes/PresBox.tsx529
-rw-r--r--src/client/views/nodes/VideoBox.tsx17
-rw-r--r--src/client/views/nodes/WebBox.scss11
-rw-r--r--src/client/views/nodes/WebBox.tsx64
25 files changed, 1828 insertions, 1400 deletions
diff --git a/src/client/views/nodes/ButtonBox.scss b/src/client/views/nodes/ButtonBox.scss
index 5ed435505..75a790667 100644
--- a/src/client/views/nodes/ButtonBox.scss
+++ b/src/client/views/nodes/ButtonBox.scss
@@ -3,14 +3,29 @@
height: 100%;
pointer-events: all;
border-radius: inherit;
- display:table;
+ display:flex;
+ flex-direction: column;
}
.buttonBox-mainButton {
width: 100%;
height: 100%;
border-radius: inherit;
+ text-align: center;
+ display: table;
+}
+.buttonBox-mainButtonCenter {
+ height: 100%;
display:table-cell;
vertical-align: middle;
- text-align: center;
+}
+
+.buttonBox-params {
+ display:flex;
+ flex-direction: row;
+}
+
+.buttonBox-missingParam {
+ width:100%;
+ background: lightgray;
} \ No newline at end of file
diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx
index 8b6f11aac..68d3b8ae1 100644
--- a/src/client/views/nodes/ButtonBox.tsx
+++ b/src/client/views/nodes/ButtonBox.tsx
@@ -1,21 +1,21 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faEdit } from '@fortawesome/free-regular-svg-icons';
+import { action, computed } from 'mobx';
+import { observer } from 'mobx-react';
import * as React from 'react';
-import { FieldViewProps, FieldView } from './FieldView';
-import { createSchema, makeInterface } from '../../../new_fields/Schema';
+import { Doc, DocListCastAsync } from '../../../new_fields/Doc';
+import { List } from '../../../new_fields/List';
+import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema';
import { ScriptField } from '../../../new_fields/ScriptField';
+import { BoolCast, StrCast, Cast } from '../../../new_fields/Types';
+import { DragManager } from '../../util/DragManager';
+import { undoBatch } from '../../util/UndoManager';
import { DocComponent } from '../DocComponent';
+import './ButtonBox.scss';
+import { FieldView, FieldViewProps } from './FieldView';
+import { ContextMenuProps } from '../ContextMenuItem';
import { ContextMenu } from '../ContextMenu';
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEdit } from '@fortawesome/free-regular-svg-icons';
-import { emptyFunction } from '../../../Utils';
-import { ScriptBox } from '../ScriptBox';
-import { CompileScript } from '../../util/Scripting';
-import { OverlayView } from '../OverlayView';
-import { Doc } from '../../../new_fields/Doc';
-import './ButtonBox.scss';
-import { observer } from 'mobx-react';
-import { DocumentIconContainer } from './DocumentIcon';
-import { StrCast } from '../../../new_fields/Types';
library.add(faEdit as any);
@@ -30,11 +30,55 @@ const ButtonDocument = makeInterface(ButtonSchema);
@observer
export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(ButtonDocument) {
public static LayoutString() { return FieldView.LayoutString(ButtonBox); }
+ private dropDisposer?: DragManager.DragDropDisposer;
+ @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+
+
+ protected createDropTarget = (ele: HTMLDivElement) => {
+ if (this.dropDisposer) {
+ this.dropDisposer();
+ }
+ if (ele) {
+ this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ }
+ }
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ let funcs: ContextMenuProps[] = [];
+ funcs.push({
+ description: "Clear Script Params", event: () => {
+ let params = Cast(this.props.Document.buttonParams, listSpec("string"));
+ params && params.map(p => this.props.Document[p] = undefined)
+ }, icon: "trash"
+ });
+
+ ContextMenu.Instance.addItem({ description: "OnClick...", subitems: funcs, icon: "asterisk" });
+ }
+
+ @undoBatch
+ @action
+ drop = (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.DocumentDragData && e.target) {
+ this.props.Document[(e.target as any).textContent] = new List<Doc>(de.data.droppedDocuments);
+ e.stopPropagation();
+ }
+ }
+ // (!missingParams || !missingParams.length ? "" : "(" + missingParams.map(m => m + ":").join(" ") + ")")
render() {
+ let params = Cast(this.props.Document.buttonParams, listSpec("string"));
+ let missingParams = params && params.filter(p => this.props.Document[p] === undefined);
+ params && params.map(async p => await DocListCastAsync(this.props.Document[p])); // bcz: really hacky form of prefetching ...
return (
- <div className="buttonBox-outerDiv" >
- <div className="buttonBox-mainButton" style={{ background: StrCast(this.props.Document.backgroundColor), color: StrCast(this.props.Document.color, "black") }} >{this.Document.text || this.Document.title}</div>
+ <div className="buttonBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
+ <div className="buttonBox-mainButton" style={{ background: StrCast(this.props.Document.backgroundColor), color: StrCast(this.props.Document.color, "black") }} >
+ <div className="buttonBox-mainButtonCenter">
+ {(this.Document.text || this.Document.title)}
+ </div>
+ </div>
+ <div className="buttonBox-params" >
+ {!missingParams || !missingParams.length ? (null) : missingParams.map(m => <div key={m} className="buttonBox-missingParam">{m}</div>)}
+ </div>
</div>
);
}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index ee596c841..4872a7aa1 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,24 +1,24 @@
import { computed } from "mobx";
import { observer } from "mobx-react";
import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { BoolCast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
+import { BoolCast, FieldValue, NumCast, StrCast, Cast } from "../../../new_fields/Types";
import { Transform } from "../../util/Transform";
import { DocComponent } from "../DocComponent";
import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView";
import "./DocumentView.scss";
import React = require("react");
import { Doc } from "../../../new_fields/Doc";
-import { returnEmptyString } from "../../../Utils";
+import { random } from "animejs";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
x?: number;
y?: number;
width?: number;
height?: number;
+ jitterRotation: number;
}
const schema = createSchema({
- zoomBasis: "number",
zIndex: "number",
});
@@ -28,22 +28,36 @@ const FreeformDocument = makeInterface(schema, positionSchema);
@observer
export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, FreeformDocument>(FreeformDocument) {
- @computed get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}) `; }
- @computed get X() { return this.props.x !== undefined ? this.props.x : this.Document.x || 0; }
- @computed get Y() { return this.props.y !== undefined ? this.props.y : this.Document.y || 0; }
- @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.props.width !== undefined ? this.props.width : this.Document.width || 0; }
- @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.props.height !== undefined ? this.props.height : this.Document.height || 0; }
- @computed get zoom(): number { return 1 / FieldValue(this.Document.zoomBasis, 1); }
+ @computed get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${random(-1, 1) * this.props.jitterRotation}deg)`; }
+ @computed get X() { return this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.Document.x || 0; }
+ @computed get Y() { return this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.Document.y || 0; }
+ @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.Document.width || 0; }
+ @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.Document.height || 0; }
@computed get nativeWidth(): number { return FieldValue(this.Document.nativeWidth, 0); }
@computed get nativeHeight(): number { return FieldValue(this.Document.nativeHeight, 0); }
@computed get scaleToOverridingWidth() { return this.width / NumCast(this.props.Document.width, this.width); }
+ @computed get renderScriptDim() {
+ if (this.Document.renderScript) {
+ let someView = Cast(this.Document.someView, Doc);
+ let minimap = Cast(this.Document.minimap, Doc);
+ if (someView instanceof Doc && minimap instanceof Doc) {
+ let x = (NumCast(someView.panX) - NumCast(someView.width) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap.width) - NumCast(minimap.width) / 2;
+ let y = (NumCast(someView.panY) - NumCast(someView.height) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap.height) - NumCast(minimap.height) / 2;
+ let w = NumCast(someView.width) / NumCast(someView.scale) / NumCast(minimap.fitW) * NumCast(minimap.width);
+ let h = NumCast(someView.height) / NumCast(someView.scale) / NumCast(minimap.fitH) * NumCast(minimap.height);
+ return { x: x, y: y, width: w, height: h };
+ }
+ }
+ return undefined;
+ }
+
contentScaling = () => this.nativeWidth > 0 && !BoolCast(this.props.Document.ignoreAspect) ? this.width / this.nativeWidth : 1;
panelWidth = () => this.props.PanelWidth();
panelHeight = () => this.props.PanelHeight();
getTransform = (): Transform => this.props.ScreenToLocalTransform()
.translate(-this.X, -this.Y)
- .scale(1 / this.contentScaling()).scale(1 / this.zoom / this.scaleToOverridingWidth)
+ .scale(1 / this.contentScaling()).scale(1 / this.scaleToOverridingWidth)
animateBetweenIcon = (icon: number[], stime: number, maximizing: boolean) => {
this.props.bringToFront(this.props.Document);
@@ -60,10 +74,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}
borderRounding = () => {
- let br = StrCast(this.props.Document.layout instanceof Doc ? this.props.Document.layout.borderRounding : this.props.Document.borderRounding);
+ let ruleProvider = this.props.ruleProvider;
+ let ruleRounding = ruleProvider ? StrCast(Doc.GetProto(ruleProvider)["ruleRounding_" + NumCast(this.props.Document.heading)]) : undefined;
+ let br = StrCast(this.layoutDoc.layout instanceof Doc ? this.layoutDoc.layout.borderRounding : this.props.Document.borderRounding);
+ br = !br && ruleRounding ? ruleRounding : br;
if (br.endsWith("%")) {
let percent = Number(br.substr(0, br.length - 1)) / 100;
- let nativeDim = Math.min(NumCast(this.props.Document.nativeWidth), NumCast(this.props.Document.nativeHeight));
+ let nativeDim = Math.min(NumCast(this.layoutDoc.nativeWidth), NumCast(this.layoutDoc.nativeHeight));
let minDim = percent * (nativeDim ? nativeDim : Math.min(this.props.PanelWidth(), this.props.PanelHeight()));
return minDim;
}
@@ -75,6 +92,12 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
clusterColorFunc = (doc: Doc) => this.clusterColor;
+ get layoutDoc() {
+ // if this document's layout field contains a document (ie, a rendering template), then we will use that
+ // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string.
+ return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
+ }
+
render() {
const hasPosition = this.props.x !== undefined || this.props.y !== undefined;
return (
@@ -83,13 +106,16 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
transformOrigin: "left top",
position: "absolute",
backgroundColor: "transparent",
- boxShadow: this.props.Document.z ? `#9c9396 ${StrCast(this.props.Document.boxShadow, "10px 10px 0.9vw")}` :
- this.clusterColor ? (
- this.props.Document.isBackground ? `0px 0px 50px 50px ${this.clusterColor}` :
- `${this.clusterColor} ${StrCast(this.props.Document.boxShadow, `0vw 0vw ${50 / this.props.ContentScaling()}px`)}`) : undefined,
+ boxShadow:
+ this.layoutDoc.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.layoutDoc.isBackground ? `0px 0px 50px 50px ${this.clusterColor}` : // if it's a background & has a cluster color, make the shadow spread really big
+ this.clusterColor ? (
+ `${this.clusterColor} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${50 / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent
+ StrCast(this.layoutDoc.boxShadow, ""),
borderRadius: this.borderRounding(),
transform: this.transform,
- transition: hasPosition ? "transform 1s" : StrCast(this.props.Document.transition),
+ transition: hasPosition ? "transform 1s" : StrCast(this.layoutDoc.transition),
width: this.width,
height: this.height,
zIndex: this.Document.zIndex || 0,
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index c73e960d8..d0e117fe4 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -13,6 +13,8 @@ import { FormattedTextBox } from "./FormattedTextBox";
import { ImageBox } from "./ImageBox";
import { DragBox } from "./DragBox";
import { ButtonBox } from "./ButtonBox";
+import { PresBox } from "./PresBox";
+import { LinkFollowBox } from "../linking/LinkFollowBox";
import { IconBox } from "./IconBox";
import { KeyValueBox } from "./KeyValueBox";
import { PDFBox } from "./PDFBox";
@@ -29,6 +31,7 @@ import { List } from "../../../new_fields/List";
import { Doc } from "../../../new_fields/Doc";
import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox";
import { ScriptField } from "../../../new_fields/ScriptField";
+import { fromPromise } from "mobx-utils";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
type BindingProps = Without<FieldViewProps, 'fieldKey'>;
@@ -67,7 +70,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
}
get dataDoc() {
- if (this.props.DataDoc === undefined && this.props.Document.layout instanceof Doc) {
+ if (this.props.DataDoc === undefined && (this.props.Document.layout instanceof Doc || this.props.Document instanceof Promise)) {
// if there is no dataDoc (ie, we're not rendering a template layout), but this document
// has a template layout document, then we will render the template layout but use
// this document as the data document for the layout.
@@ -107,7 +110,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
return <ObserverJsxParser
blacklistedAttrs={[]}
- components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, YoutubeBox }}
+ components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox }}
bindings={this.CreateBindings()}
jsx={this.finalLayout}
showWarnings={true}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 4f1ec1cdb..2f0dbf0c9 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,6 +1,6 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import * as fa from '@fortawesome/free-solid-svg-icons';
-import { action, computed, IReactionDisposer, reaction, runInAction, trace, observable } from "mobx";
+import { action, computed, IReactionDisposer, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import * as rp from "request-promise";
import { Doc, DocListCast, DocListCastAsync, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
@@ -9,12 +9,13 @@ import { List } from "../../../new_fields/List";
import { ObjectField } from "../../../new_fields/ObjectField";
import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
import { ScriptField } from '../../../new_fields/ScriptField';
-import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
+import { BoolCast, Cast, FieldValue, NumCast, PromiseValue, StrCast } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { RouteStore } from '../../../server/RouteStore';
import { emptyFunction, returnTrue, Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
import { Docs, DocUtils } from "../../documents/Documents";
+import { DocumentType } from '../../documents/DocumentTypes';
import { ClientUtils } from '../../util/ClientUtils';
import { DictationManager } from '../../util/DictationManager';
import { DocumentManager } from "../../util/DocumentManager";
@@ -33,15 +34,13 @@ import { DocComponent } from "../DocComponent";
import { EditableView } from '../EditableView';
import { MainView } from '../MainView';
import { OverlayView } from '../OverlayView';
-import { PresentationView } from "../presentationview/PresentationView";
import { ScriptBox } from '../ScriptBox';
import { ScriptingRepl } from '../ScriptingRepl';
-import { Template } from "./../Templates";
import { DocumentContentsView } from "./DocumentContentsView";
import "./DocumentView.scss";
import { FormattedTextBox } from './FormattedTextBox';
import React = require("react");
-import { IDisposable } from '../../northstar/utils/IDisposable';
+import { CompileScript, Scripting } from '../../util/Scripting';
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
library.add(fa.faTrash);
@@ -90,14 +89,15 @@ export interface DocumentViewProps {
renderDepth: number;
showOverlays?: (doc: Doc) => { title?: string, caption?: string };
ContentScaling: () => number;
+ ruleProvider: Doc | undefined;
PanelWidth: () => number;
PanelHeight: () => number;
focus: (doc: Doc, willZoom: boolean, scale?: number) => void;
- selectOnLoad: boolean;
parentActive: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
bringToFront: (doc: Doc, sendToBack?: boolean) => void;
addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void;
+ pinToPres: (document: Doc) => void;
collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void;
zoomToScale: (scale: number) => void;
backgroundColor: (doc: Doc) => string | undefined;
@@ -139,6 +139,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
private _lastTap: number = 0;
private _doubleTap = false;
private _hitExpander = false;
+ private _hitTemplateDrag = false;
private _mainCont = React.createRef<HTMLDivElement>();
private _dropDisposer?: DragManager.DragDropDisposer;
_animateToIconDisposer?: IReactionDisposer;
@@ -147,14 +148,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
public get ContentDiv() { return this._mainCont.current; }
@computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); }
@computed get topMost(): boolean { return this.props.renderDepth === 0; }
- @computed get templates(): List<string> {
- let field = this.props.Document.templates;
- if (field && field instanceof List) {
- return field;
- }
- return new List<string>();
- }
- set templates(templates: List<string>) { this.props.Document.templates = templates; }
screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect();
@action
@@ -228,7 +221,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
get dataDoc() {
- if (this.props.DataDoc === undefined && this.props.Document.layout instanceof Doc) {
+ if (this.props.DataDoc === undefined && (this.props.Document.layout instanceof Doc || this.props.Document instanceof Promise)) {
// if there is no dataDoc (ie, we're not rendering a temlplate layout), but this document
// has a template layout document, then we will render the template layout but use
// this document as the data document for the layout.
@@ -236,7 +229,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
return this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined;
}
- startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean) {
+ startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean, applyAsTemplate?: boolean) {
if (this._mainCont.current) {
let allConnected = [this.props.Document, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
let alldataConnected = [this.dataDoc, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
@@ -247,6 +240,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
dragData.xOffset = xoff;
dragData.yOffset = yoff;
dragData.moveDocument = this.props.moveDocument;
+ dragData.applyAsTemplate = applyAsTemplate;
DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, {
handlers: {
dragComplete: action(emptyFunction)
@@ -292,9 +286,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
onClick = async (e: React.MouseEvent) => {
- if (e.nativeEvent.cancelBubble) return; // needed because EditableView may stopPropagation which won't apparently stop this event from firing.
- e.stopPropagation();
+ if (e.nativeEvent.cancelBubble || SelectionManager.IsSelected(this)) return; // needed because EditableView may stopPropagation which won't apparently stop this event from firing.
if (this.onClickHandler && this.onClickHandler.script) {
+ e.stopPropagation();
this.onClickHandler.script.run({ this: this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document });
e.preventDefault();
return;
@@ -302,6 +296,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let altKey = e.altKey;
let ctrlKey = e.ctrlKey;
if (this._doubleTap && this.props.renderDepth) {
+ e.stopPropagation();
let fullScreenAlias = Doc.MakeAlias(this.props.Document);
fullScreenAlias.templates = new List<string>();
Doc.UseDetailLayout(fullScreenAlias);
@@ -313,10 +308,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
else if (CurrentUserUtils.MainDocId !== this.props.Document[Id] &&
(Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
+ if (BoolCast(this.props.Document.ignoreClick)) {
+ return;
+ }
+ e.stopPropagation();
SelectionManager.SelectDoc(this, e.ctrlKey);
let isExpander = (e.target as any).id === "isExpander";
- if (BoolCast(this.props.Document.isButton) || isExpander) {
- SelectionManager.DeselectAll();
+ if (BoolCast(this.props.Document.isButton) || this.props.Document.type === DocumentType.BUTTON || isExpander) {
let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs);
let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs);
@@ -327,6 +325,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs;
// let expandedDocs = [...(subBulletDocs ? subBulletDocs : []), ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),];
if (expandedDocs.length) { // bcz: need a better way to associate behaviors with click events on widget-documents
+ SelectionManager.DeselectAll();
let maxLocation = StrCast(this.props.Document.maximizeLocation, "inPlace");
let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target);
if (altKey || ctrlKey) {
@@ -355,7 +354,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
else if (linkedDocs.length) {
- let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document));
+ SelectionManager.DeselectAll();
+ let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document) && !d.anchor1anchored);
+ let firstUnshown = first.filter(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0);
+ if (firstUnshown.length) first = [firstUnshown[0]];
let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : [expandedDocs[0], expandedDocs[0]];
// @TODO: shouldn't always follow target context
@@ -368,8 +370,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined;
DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false,
document => { // open up target if it's not already in view ...
+ let cv = this.props.ContainingCollectionView; // bcz: ugh --- maybe need to have a props.unfocus() method so that we leave things in the state we found them??
+ let px = cv && cv.props.Document.panX;
+ let py = cv && cv.props.Document.panY;
+ let s = cv && cv.props.Document.scale;
this.props.focus(this.props.Document, true, 1); // by zooming into the button document first
- setTimeout(() => this.props.addDocTab(document, undefined, maxLocation), 1000); // then after the 1sec animation, open up the target in a new tab
+ setTimeout(() => {
+ this.props.addDocTab(document, undefined, maxLocation);
+ cv && (cv.props.Document.panX = px);
+ cv && (cv.props.Document.panY = py);
+ cv && (cv.props.Document.scale = s);
+ }, 1000); // then after the 1sec animation, open up the target in a new tab
},
linkedFwdPage[altKey ? 1 : 0], targetContext);
}
@@ -377,29 +388,35 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
}
+
+
onPointerDown = (e: React.PointerEvent): void => {
if (e.nativeEvent.cancelBubble) return;
this._downX = e.clientX;
this._downY = e.clientY;
this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0;
- // if (e.shiftKey && e.buttons === 1 && CollectionDockingView.Instance) {
- // CollectionDockingView.Instance.StartOtherDrag(e, [Doc.MakeAlias(this.props.Document)], [this.dataDoc]);
- // e.stopPropagation();
- // } else {
+ this._hitTemplateDrag = false;
+ for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) {
+ if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") {
+ this._hitTemplateDrag = true;
+ }
+ }
if (this.active) e.stopPropagation(); // events stop at the lowest document that is active.
document.removeEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointerup", this.onPointerUp);
- // }
}
onPointerMove = (e: PointerEvent): void => {
- if (!e.cancelBubble && this.active) {
+ if (e.cancelBubble && this.active) {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ }
+ else if (!e.cancelBubble && this.active) {
if (!this.props.Document.excludeFromLibrary && (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3)) {
if (!e.altKey && !this.topMost && e.buttons === 1 && !BoolCast(this.props.Document.lockedPosition)) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
- this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander);
+ this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander, this._hitTemplateDrag);
}
}
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
@@ -423,17 +440,76 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
@undoBatch
+ makeNativeViewClicked = (): void => {
+ makeNativeView(this.props.Document);
+ }
+ @undoBatch
+ makeCustomViewClicked = (): void => {
+ this.props.Document.nativeLayout = this.props.Document.layout;
+ this.props.Document.nativeType = this.props.Document.type;
+ this.props.Document.nonCustomAutoHeight = this.props.Document.autoHeight;
+ this.props.Document.nonCustomWidth = this.props.Document.nativeWidth;
+ this.props.Document.nonCustomHeight = this.props.Document.nativeHeight;
+ this.props.Document.nonCustomNativeWidth = this.props.Document.nativeWidth;
+ this.props.Document.nonCustomNativeHeight = this.props.Document.nativeHeight;
+ this.props.Document.nonCustomIgnoreAspect = this.props.Document.ignoreAspect;
+ PromiseValue(Cast(this.props.Document.customLayout, Doc)).then(custom => {
+ if (custom) {
+ this.props.Document.type = DocumentType.TEMPLATE;
+ this.props.Document.layout = custom;
+ !custom.nativeWidth && (this.props.Document.nativeWidth = 0);
+ !custom.nativeHeight && (this.props.Document.nativeHeight = 0);
+ !custom.nativeWidth && (this.props.Document.ignoreAspect = true);
+ this.props.Document.autoHeight = this.props.Document.autoHeight;
+ this.props.Document.width = this.props.Document.customWidth;
+ this.props.Document.height = this.props.Document.customHeight;
+ this.props.Document.nativeWidth = this.props.Document.customNativeWidth;
+ this.props.Document.nativeHeight = this.props.Document.customNativeHeight;
+ this.props.Document.ignoreAspect = this.props.Document.ignoreAspect;
+ this.props.Document.customAutoHeight = undefined;
+ this.props.Document.customWidth = undefined;
+ this.props.Document.customHeight = undefined;
+ this.props.Document.customNativeWidth = undefined;
+ this.props.Document.customNativeHeight = undefined;
+ this.props.Document.customIgnoreAspect = undefined;
+ } else {
+ let options = { title: "data", width: NumCast(this.props.Document.width), x: -NumCast(this.props.Document.width) / 2, y: -NumCast(this.props.Document.height) / 2, };
+ let fieldTemplate = this.props.Document.type === DocumentType.TEXT ? Docs.Create.TextDocument(options) :
+ this.props.Document.type === DocumentType.VID ? Docs.Create.VideoDocument("http://www.cs.brown.edu", options) :
+ Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
+
+ fieldTemplate.backgroundColor = StrCast(this.props.Document.backgroundColor);
+ fieldTemplate.heading = 1;
+ fieldTemplate.autoHeight = true;
+
+ let docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: StrCast(this.Document.title) + "layout", width: NumCast(this.props.Document.width) + 20, height: Math.max(100, NumCast(this.props.Document.height) + 45) });
+ let proto = Doc.GetProto(docTemplate);
+ Doc.MakeMetadataFieldTemplate(fieldTemplate, proto, true);
+
+ Doc.ApplyTemplateTo(docTemplate, this.props.Document, undefined, false);
+ Doc.GetProto(this.dataDoc || this.props.Document).customLayout = this.props.Document.layout;
+ }
+ });
+ }
+
+ @undoBatch
makeBtnClicked = (): void => {
let doc = Doc.GetProto(this.props.Document);
- doc.isButton = !BoolCast(doc.isButton);
- if (doc.isButton) {
- if (!doc.nativeWidth) {
- doc.nativeWidth = this.props.Document[WidthSym]();
- doc.nativeHeight = this.props.Document[HeightSym]();
- }
+ if (doc.isButton || doc.onClick) {
+ doc.isButton = false;
+ doc.onClick = undefined;
} else {
- doc.nativeWidth = doc.nativeHeight = undefined;
+ doc.isButton = true;
}
+
+ // if (doc.isButton) {
+ // if (!doc.nativeWidth) {
+ // doc.nativeWidth = this.props.Document[WidthSym]();
+ // doc.nativeHeight = this.props.Document[HeightSym]();
+ // }
+ // } else {
+ // doc.nativeWidth = doc.nativeHeight = undefined;
+ // }
}
@undoBatch
@@ -457,6 +533,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
DocUtils.MakeLink(annotationDoc, targetDoc, this.props.ContainingCollectionView!.props.Document, `Link from ${StrCast(annotationDoc.title)}`);
}
+ if (de.data instanceof DragManager.DocumentDragData && de.data.applyAsTemplate) {
+ Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document, this.props.DataDoc);
+ e.stopPropagation();
+ }
if (de.data instanceof DragManager.LinkDragData) {
let sourceDoc = de.data.linkSourceDocument;
let destDoc = this.props.Document;
@@ -493,28 +573,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
- @action
- addTemplate = (template: Template) => {
- this.templates.push(template.Layout);
- this.templates = this.templates;
- }
-
- @action
- removeTemplate = (template: Template) => {
- for (let i = 0; i < this.templates.length; i++) {
- if (this.templates[i] === template.Layout) {
- this.templates.splice(i, 1);
- break;
- }
- }
- this.templates = this.templates;
- }
- @action
- clearTemplates = () => {
- this.templates.length = 0;
- this.templates = this.templates;
- }
-
@undoBatch
@action
freezeNativeDimensions = (): void => {
@@ -528,15 +586,44 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
@undoBatch
@action
+ makeIntoPortal = (): void => {
+ if (!DocListCast(this.props.Document.links).find(doc => {
+ if (Cast(doc.anchor2, Doc) instanceof Doc && (Cast(doc.anchor2, Doc) as Doc)!.title === this.props.Document.title + ".portal") return true;
+ return false;
+ })) {
+ let portalID = (this.props.Document.title + ".portal").replace(/^-/, "").replace(/\([0-9]*\)$/, "");
+ DocServer.GetRefField(portalID).then(existingPortal => {
+ let portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: portalID });
+ DocUtils.MakeLink(this.props.Document, portal, undefined, portalID);
+ Doc.GetProto(this.props.Document).isButton = true;
+ })
+ }
+ }
+ @undoBatch
+ @action
+ toggleCustomView = (): void => {
+ if (this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.DataDoc) {
+ Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.DataDoc)
+ } else {
+ if (this.Document.type !== DocumentType.COL && this.Document.type !== DocumentType.TEMPLATE) {
+ this.makeCustomViewClicked();
+ } else if (this.Document.nativeLayout) {
+ this.makeNativeViewClicked();
+ }
+ }
+ }
+
+ @undoBatch
+ @action
makeBackground = (): void => {
- this.props.Document.isBackground = !this.props.Document.isBackground;
- this.props.Document.isBackground && this.props.bringToFront(this.props.Document, true);
+ this.layoutDoc.isBackground = !this.layoutDoc.isBackground;
+ this.layoutDoc.isBackground && this.props.bringToFront(this.layoutDoc, true);
}
@undoBatch
@action
toggleLockPosition = (): void => {
- this.props.Document.lockedPosition = BoolCast(this.props.Document.lockedPosition) ? undefined : true;
+ this.layoutDoc.lockedPosition = BoolCast(this.layoutDoc.lockedPosition) ? undefined : true;
}
listen = async () => {
@@ -571,38 +658,54 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "onRight"), icon: "caret-square-right" });
subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" });
cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" });
- let existingMake = ContextMenu.Instance.findByDescription("Make...");
- let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : [];
- makes.push({ description: this.props.Document.isBackground ? "Remove Background" : "Make Background", event: this.makeBackground, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" });
- makes.push({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeBtnClicked, icon: "concierge-bell" });
- makes.push({ description: "Edit OnClick script", icon: "edit", event: () => ScriptBox.EditClickScript(this.props.Document, "onClick") });
- makes.push({
- description: "Make Portal", event: () => {
- let portal = Docs.Create.FreeformDocument([], { width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".portal" });
- //Doc.GetProto(this.props.Document).subBulletDocs = new List<Doc>([portal]);
- //summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight"
- //Doc.GetProto(this.props.Document).templates = new List<string>([Templates.Bullet.Layout]);
- //let coll = Docs.Create.StackingDocument([this.props.Document, portal], { x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y), width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".cont" });
- //this.props.addDocument && this.props.addDocument(coll);
- //this.props.removeDocument && this.props.removeDocument(this.props.Document);
- DocUtils.MakeLink(this.props.Document, portal, undefined, this.props.Document.title + ".portal");
- this.makeBtnClicked();
+
+ let existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
+ let onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
+ onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" });
+ onClicks.push({
+ description: "Toggle Detail", event: () => {
+ let compiled = CompileScript("toggleDetail(this)", {
+ params: { this: "Doc" },
+ typecheck: false,
+ editable: true,
+ });
+ if (compiled.compiled) {
+ this.Document.onClick = new ScriptField(compiled);
+ }
}, icon: "window-restore"
});
- !existingMake && cm.addItem({ description: "Make...", subitems: makes, icon: "hand-point-right" });
+ onClicks.push({ description: this.layoutDoc.ignoreClick ? "Select" : "Do Nothing", event: () => this.layoutDoc.ignoreClick = !this.layoutDoc.ignoreClick, icon: this.layoutDoc.ignoreClick ? "unlock" : "lock" });
+ onClicks.push({ description: this.props.Document.isButton || this.props.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.makeBtnClicked, icon: "concierge-bell" });
+ onClicks.push({ description: "Edit onClick Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", obj.x, obj.y) });
+ onClicks.push({
+ description: "Edit onClick Foreach Doc Script", icon: "edit", event: (obj: any) => {
+ this.props.Document.collectionContext = this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document;
+ ScriptBox.EditButtonScript("Foreach Collection Doc (d) => ", this.props.Document, "onClick", obj.x, obj.y, "docList(this.collectionContext.data).map(d => {", "});\n");
+ }
+ });
+ !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
+
let existing = ContextMenu.Instance.findByDescription("Layout...");
let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
-
- layoutItems.push({ description: `${this.props.Document.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.props.Document.chromeStatus = (this.props.Document.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
- layoutItems.push({ description: `${this.props.Document.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.props.Document.autoHeight = !this.props.Document.autoHeight, icon: "plus" });
- layoutItems.push({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" });
- layoutItems.push({ description: BoolCast(this.props.Document.lockedPosition) ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" });
+ layoutItems.push({ description: this.props.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.props.Document.lockedPosition ? "unlock" : "lock" });
+ if (this.props.DataDoc) {
+ layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc!), icon: "concierge-bell" })
+ }
+ layoutItems.push({ description: `${this.layoutDoc.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.layoutDoc.chromeStatus = (this.layoutDoc.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
+ layoutItems.push({ description: `${this.layoutDoc.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc.autoHeight = !this.layoutDoc.autoHeight, icon: "plus" });
+ layoutItems.push({ description: this.props.Document.ignoreAspect || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" });
+ layoutItems.push({ description: this.layoutDoc.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.layoutDoc.lockedPosition) ? "unlock" : "lock" });
layoutItems.push({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" });
layoutItems.push({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" });
if (this.props.Document.detailedLayout && !this.props.Document.isTemplate) {
layoutItems.push({ description: "Toggle detail", event: () => Doc.ToggleDetailLayout(this.props.Document), icon: "image" });
}
+ if (this.props.Document.type !== DocumentType.COL && this.props.Document.type !== DocumentType.TEMPLATE) {
+ layoutItems.push({ description: "Use Custom Layout", event: this.makeCustomViewClicked, icon: "concierge-bell" });
+ } else if (this.props.Document.nativeLayout) {
+ layoutItems.push({ description: "Use Native Layout", event: this.makeNativeViewClicked, icon: "concierge-bell" });
+ }
!existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" });
if (!ClientUtils.RELEASE) {
let copies: ContextMenuProps[] = [];
@@ -614,54 +717,30 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let analyzers: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : [];
analyzers.push({ description: "Transcribe Speech", event: this.listen, icon: "microphone" });
!existingAnalyze && cm.addItem({ description: "Analyzers...", subitems: analyzers, icon: "hand-point-right" });
- cm.addItem({ description: "Pin to Presentation", event: () => PresentationView.Instance.PinDoc(this.props.Document), icon: "map-pin" });
+ cm.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin" }); //I think this should work... and it does! A miracle!
cm.addItem({ description: "Add Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) });
cm.addItem({
- description: "Download document", icon: "download", event: () => {
- const a = document.createElement("a");
- const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
- a.href = url;
- a.download = `DocExport-${this.props.Document[Id]}.zip`;
- a.click();
+ description: "Download document", icon: "download", event: async () => {
+ let y = JSON.parse(await rp.get(Utils.CorsProxy("http://localhost:8983/solr/dash/select"), {
+ qs: { q: 'world', fq: 'NOT baseProto_b:true AND NOT deleted:true', start: '0', rows: '100', hl: true, 'hl.fl': '*' }
+ }));
+ console.log(y);
+ // const a = document.createElement("a");
+ // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
+ // a.href = url;
+ // a.download = `DocExport-${this.props.Document[Id]}.zip`;
+ // a.click();
}
});
- cm.addItem({
- description: "Import document", icon: "upload", event: () => {
- const input = document.createElement("input");
- input.type = "file";
- input.accept = ".zip";
- input.onchange = async _e => {
- const files = input.files;
- if (!files) return;
- const file = files[0];
- let formData = new FormData();
- formData.append('file', file);
- formData.append('remap', "true");
- const upload = Utils.prepend("/uploadDoc");
- const response = await fetch(upload, { method: "POST", body: formData });
- const json = await response.json();
- if (json === "error") {
- return;
- }
- const doc = await DocServer.GetRefField(json);
- if (!doc || !(doc instanceof Doc)) {
- return;
- }
- const [x, y] = this.props.ScreenToLocalTransform().transformPoint(e.pageX, e.pageY);
- doc.x = x, doc.y = y;
- this.props.addDocument && this.props.addDocument(doc, false);
- };
- input.click();
- }
- });
+ cm.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, StrCast(this.props.Document.title), this.props.addDocument, this.props.removeDocument), icon: "file" });
cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" });
type User = { email: string, userDocumentId: string };
let usersMenu: ContextMenuProps[] = [];
try {
let stuff = await rp.get(Utils.prepend(RouteStore.getUsers));
const users: User[] = JSON.parse(stuff);
- usersMenu = users.filter(({ email }) => email !== CurrentUserUtils.email).map(({ email, userDocumentId }) => ({
+ usersMenu = users.filter(({ email }) => email !== Doc.CurrentUserEmail).map(({ email, userDocumentId }) => ({
description: email, event: async () => {
const userDocument = await Cast(DocServer.GetRefField(userDocumentId), Doc);
if (!userDocument) {
@@ -684,6 +763,30 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
runInAction(() => {
cm.addItem({ description: "Share...", subitems: usersMenu, icon: "share" });
+ if (!ClientUtils.RELEASE) {
+ let 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);
+ };
+ let 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" });
+ cm.addItem({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" });
+ }
+
if (!this.topMost) {
// DocumentViews should stop propagation of this event
e.stopPropagation();
@@ -709,7 +812,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
isSelected={this.isSelected}
select={this.select}
onClick={this.onClickHandler}
- selectOnLoad={this.props.selectOnLoad}
layoutKey={"layout"}
fitToBox={BoolCast(this.props.Document.fitToBox) ? true : this.props.fitToBox}
DataDoc={this.dataDoc} />);
@@ -718,7 +820,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
chromeHeight = () => {
let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.layoutDoc) : undefined;
let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle);
- return showTitle ? 25 : 0;
+ let templates = Cast(this.layoutDoc.templates, listSpec("string"));
+ if (!showOverlays && templates instanceof List) {
+ templates.map(str => {
+ if (!showTitle && str.indexOf("{props.Document.title}") !== -1) showTitle = "title";
+ });
+ }
+ return (showTitle ? 25 : 0) + 1;// bcz: why 8??
}
get layoutDoc() {
@@ -729,9 +837,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
render() {
- let backgroundColor = this.layoutDoc.isBackground || (this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document.clusterOverridesDefaultBackground && this.layoutDoc.backgroundColor === this.layoutDoc.defaultBackgroundColor) ?
+ let ruleProvider = this.props.ruleProvider;
+ let ruleColor = ruleProvider ? StrCast(Doc.GetProto(ruleProvider)["ruleColor_" + NumCast(this.props.Document.heading)]) : undefined;
+ let ruleRounding = ruleProvider ? StrCast(Doc.GetProto(ruleProvider)["ruleRounding_" + NumCast(this.props.Document.heading)]) : undefined;
+ let colorSet = this.layoutDoc.backgroundColor !== this.layoutDoc.defaultBackgroundColor;
+ let clusterCol = this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document.clusterOverridesDefaultBackground;
+
+ let backgroundColor = this.layoutDoc.isBackground || (clusterCol && !colorSet) ?
this.props.backgroundColor(this.layoutDoc) || StrCast(this.layoutDoc.backgroundColor) :
- StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.layoutDoc);
+ ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.layoutDoc);
let foregroundColor = StrCast(this.layoutDoc.color);
var nativeWidth = this.nativeWidth > 0 && !BoolCast(this.props.Document.ignoreAspect) ? `${this.nativeWidth}px` : "100%";
var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
@@ -746,19 +860,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
});
}
let showTextTitle = showTitle && StrCast(this.layoutDoc.layout).startsWith("<FormattedTextBox") ? showTitle : undefined;
- let brushDegree = Doc.IsBrushedDegree(this.props.Document);
- let borderRounding = StrCast(Doc.GetProto(this.props.Document).borderRounding);
- let localScale = this.props.ScreenToLocalTransform().Scale * brushDegree;
+ let fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document);
+ let borderRounding = StrCast(Doc.GetProto(this.props.Document).borderRounding, ruleRounding);
+ let localScale = this.props.ScreenToLocalTransform().Scale * fullDegree;
+ let searchHighlight = (!this.props.Document.search_fields ? (null) :
+ <div key="search" style={{ position: "absolute", background: "yellow", bottom: "-20px", borderRadius: "5px", transformOrigin: "bottom left", width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}>
+ {StrCast(this.props.Document.search_fields)}
+ </div>);
return (
<div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
ref={this._mainCont}
style={{
pointerEvents: this.layoutDoc.isBackground && !this.isSelected() ? "none" : "all",
color: foregroundColor,
- outlineColor: ["transparent", "maroon", "maroon"][brushDegree],
- outlineStyle: ["none", "dashed", "solid"][brushDegree],
- outlineWidth: brushDegree && !borderRounding ? `${localScale}px` : "0px",
- border: brushDegree && borderRounding ? `${["none", "dashed", "solid"][brushDegree]} ${["transparent", "maroon", "maroon"][brushDegree]} ${localScale}px` : undefined,
+ outlineColor: ["transparent", "maroon", "maroon", "yellow"][fullDegree],
+ outlineStyle: ["none", "dashed", "solid", "solid"][fullDegree],
+ outlineWidth: fullDegree && !borderRounding ? `${localScale}px` : "0px",
+ border: fullDegree && borderRounding ? `${["none", "dashed", "solid", "solid"][fullDegree]} ${["transparent", "maroon", "maroon", "yellow"][fullDegree]} ${localScale}px` : undefined,
borderRadius: "inherit",
background: backgroundColor,
width: nativeWidth,
@@ -769,10 +887,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
>
- {!showTitle && !showCaption ? this.contents :
+ {!showTitle && !showCaption ?
+ this.props.Document.search_fields ? <div>
+ {this.contents}
+ {searchHighlight}
+ </div> :
+ this.contents :
<div style={{ position: "absolute", display: "inline-block", width: "100%", height: "100%", pointerEvents: "none" }}>
-
- <div style={{ width: "100%", height: showTextTitle ? "calc(100% - 33px)" : "100%", display: "inline-block", position: "absolute", top: showTextTitle ? "29px" : undefined }}>
+ <div style={{ width: "100%", height: showTextTitle ? "calc(100% - 29px)" : "100%", display: "inline-block", position: "absolute", top: showTextTitle ? "29px" : undefined }}>
{this.contents}
</div>
{!showTitle ? (null) :
@@ -794,12 +916,77 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
{!showCaption ? (null) :
<div style={{ position: "absolute", bottom: 0, transformOrigin: "bottom left", width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}>
- <FormattedTextBox {...this.props} onClick={this.onClickHandler} DataDoc={this.dataDoc} active={returnTrue} isSelected={this.isSelected} focus={emptyFunction} select={this.select} selectOnLoad={this.props.selectOnLoad} fieldExt={""} hideOnLeave={true} fieldKey={showCaption} />
+ <FormattedTextBox {...this.props} onClick={this.onClickHandler} DataDoc={this.dataDoc} active={returnTrue} isSelected={this.isSelected} focus={emptyFunction} select={this.select} fieldExt={""} hideOnLeave={true} fieldKey={showCaption} />
</div>
}
+ {searchHighlight}
</div>
}
</div>
);
}
-} \ No newline at end of file
+}
+
+
+let makeNativeView = (doc: any): void => {
+ doc.layout = doc.nativeLayout;
+ doc.nativeLayout = undefined;
+ doc.type = doc.nativeType;
+
+ doc.customAutoHeight = doc.autoHeight;
+ doc.customWidth = doc.width;
+ doc.customHeight = doc.height;
+ doc.customNativeWidth = doc.nativeWidth;
+ doc.customNativeHeight = doc.nativeHeight;
+ doc.customIgnoreAspect = doc.ignoreAspect;
+
+ doc.autoHeight = doc.nonCustomAutoHeight;
+ doc.width = doc.nonCustomWidth;
+ doc.height = doc.nonCustomHeight;
+ doc.nativeWidth = doc.nonCustomNativeWidth;
+ doc.nativeHeight = doc.nonCustomNativeHeight;
+ doc.ignoreAspect = doc.nonCustomIgnoreAspect;
+ doc.nonCustomAutoHeight = undefined;
+ doc.nonCustomWidth = undefined;
+ doc.nonCustomHeight = undefined;
+ doc.nonCustomNativeWidth = undefined;
+ doc.nonCustomNativeHeight = undefined;
+ doc.nonCustomIgnoreAspect = undefined;
+}
+let makeCustomView = (doc: any): void => {
+ doc.nativeLayout = doc.layout;
+ doc.nativeType = doc.type;
+ doc.nonCustomAutoHeight = doc.autoHeight;
+ doc.nonCustomWidth = doc.nativeWidth;
+ doc.nonCustomHeight = doc.nativeHeight;
+ doc.nonCustomNativeWidth = doc.nativeWidth;
+ doc.nonCustomNativeHeight = doc.nativeHeight;
+ doc.nonCustomIgnoreAspect = doc.ignoreAspect;
+ let custom = doc.customLayout as Doc;
+ if (custom instanceof Doc) {
+ doc.type = DocumentType.TEMPLATE;
+ doc.layout = custom;
+ !custom.nativeWidth && (doc.nativeWidth = 0);
+ !custom.nativeHeight && (doc.nativeHeight = 0);
+ !custom.nativeWidth && (doc.ignoreAspect = true);
+ doc.autoHeight = doc.autoHeight;
+ doc.width = doc.customWidth;
+ doc.height = doc.customHeight;
+ doc.nativeWidth = doc.customNativeWidth;
+ doc.nativeHeight = doc.customNativeHeight;
+ doc.ignoreAspect = doc.ignoreAspect;
+ doc.customAutoHeight = undefined;
+ doc.customWidth = undefined;
+ doc.customHeight = undefined;
+ doc.customNativeWidth = undefined;
+ doc.customNativeHeight = undefined;
+ doc.customIgnoreAspect = undefined;
+ }
+}
+Scripting.addGlobal(function toggleDetail(doc: any) {
+ if (doc.type !== DocumentType.COL && doc.type !== DocumentType.TEMPLATE) {
+ makeCustomView(doc);
+ } else if (doc.nativeLayout) {
+ makeNativeView(doc);
+ }
+}); \ No newline at end of file
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 3287949e2..943d181d6 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -30,15 +30,16 @@ export interface FieldViewProps {
leaveNativeSize?: boolean;
fitToBox?: boolean;
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ ruleProvider: Doc | undefined;
Document: Doc;
DataDoc?: Doc;
onClick?: ScriptField;
isSelected: () => boolean;
select: (isCtrlPressed: boolean) => void;
renderDepth: number;
- selectOnLoad: boolean;
addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean;
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ pinToPres: (document: Doc) => void;
removeDocument?: (document: Doc) => boolean;
moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
@@ -57,6 +58,7 @@ export interface FieldViewProps {
export class FieldView extends React.Component<FieldViewProps> {
public static LayoutString(fieldType: { name: string }, fieldStr: string = "data", fieldExt: string = "") {
return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"} fieldExt={"${fieldExt}"} />`;
+ //"<ImageBox {...props} />"
}
@computed
@@ -78,6 +80,9 @@ export class FieldView extends React.Component<FieldViewProps> {
else if (field instanceof ImageField) {
return <ImageBox {...this.props} leaveNativeSize={true} />;
}
+ // else if (field instaceof PresBox) {
+ // return <PresBox {...this.props} />;
+ // }
else if (field instanceof IconField) {
return <IconBox {...this.props} />;
}
@@ -103,7 +108,6 @@ export class FieldView extends React.Component<FieldViewProps> {
// PanelWidth={returnHundred}
// PanelHeight={returnHundred}
// renderDepth={0} //TODO Why is renderDepth reset?
- // selectOnLoad={false}
// focus={emptyFunction}
// isSelected={this.props.isSelected}
// select={returnFalse}
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 1b537cc52..0d7277cbe 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -4,7 +4,6 @@
width: 100%;
height: 100%;
min-height: 100%;
- font-family: $serif;
}
.ProseMirror:focus {
@@ -65,4 +64,113 @@
.em {
font-style: italic;
-} \ No newline at end of file
+}
+
+.userMarkOpen {
+ background: rgba(255, 255, 0, 0.267);
+ display: inline;
+}
+.userMark {
+ background: rgba(255, 255, 0, 0.267);
+ font-size: 2px;
+ display: inline-grid;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width:10px;
+ min-height:10px;
+ text-align:center;
+ align-content: center;
+}
+
+footnote {
+ display: inline-block;
+ position: relative;
+ cursor: pointer;
+ div {
+ padding : 0 !important;
+ }
+}
+
+footnote::after {
+ content: counter(prosemirror-footnote);
+ vertical-align: super;
+ font-size: 75%;
+ counter-increment: prosemirror-footnote;
+}
+
+.ProseMirror {
+ counter-reset: prosemirror-footnote;
+ }
+
+.footnote-tooltip {
+ cursor: auto;
+ font-size: 75%;
+ position: absolute;
+ left: -30px;
+ top: calc(100% + 10px);
+ background: silver;
+ padding: 3px;
+ border-radius: 2px;
+ max-width: 100px;
+ min-width: 50px;
+ width: max-content;
+}
+
+.prosemirror-attribution {
+ font-size: 8px;
+}
+.footnote-tooltip::before {
+ border: 5px solid silver;
+ border-top-width: 0px;
+ border-left-color: transparent;
+ border-right-color: transparent;
+ position: absolute;
+ top: -5px;
+ left: 27px;
+ content: " ";
+ height: 0;
+ width: 0;
+}
+
+.formattedTextBox-summarizer {
+ opacity :0.5;
+ position: relative;
+ width:40px;
+ height:20px;
+}
+.formattedTextBox-summarizer::after{
+ content: "←" ;
+}
+
+.formattedTextBox-summarizer-collapsed {
+ opacity :0.5;
+ position: relative;
+ width:40px;
+ height:20px;
+}
+.formattedTextBox-summarizer-collapsed::after {
+ content: "...";
+}
+
+ol { counter-reset: deci1 0;}
+.decimal1-ol {counter-reset: deci1; p { display: inline }; font-size: 24 }
+.decimal2-ol {counter-reset: deci2; p { display: inline }; font-size: 18 }
+.decimal3-ol {counter-reset: deci3; p { display: inline }; font-size: 14 }
+.decimal4-ol {counter-reset: deci4; p { display: inline }; font-size: 10 }
+.decimal5-ol {counter-reset: deci5; p { display: inline }; font-size: 10 }
+.decimal6-ol {counter-reset: deci6; p { display: inline }; font-size: 10 }
+.decimal7-ol {counter-reset: deci7; p { display: inline }; font-size: 10 }
+.upper-alpha-ol {counter-reset: ualph; p { display: inline }; font-size: 18 }
+.lower-roman-ol {counter-reset: lroman; p { display: inline }; font-size: 14; }
+.lower-alpha-ol {counter-reset: lalpha; p { display: inline }; font-size: 10;}
+.decimal1:before { content: counter(deci1) ")"; counter-increment: deci1; display:inline-block; width: 30}
+.decimal2:before { content: counter(deci1) "." counter(deci2) ")"; counter-increment: deci2; display:inline-block; width: 35}
+.decimal3:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) ")"; counter-increment: deci3; display:inline-block; width: 35}
+.decimal4:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) ")"; counter-increment: deci4; display:inline-block; width: 40}
+.decimal5:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) ")"; counter-increment: deci5; display:inline-block; width: 40}
+.decimal6:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) ")"; counter-increment: deci6; display:inline-block; width: 45}
+.decimal7:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) "." counter(deci7) ")"; counter-increment: deci7; display:inline-block; width: 50}
+.upper-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) ")"; counter-increment: ualph; display:inline-block; width: 35 }
+.lower-roman:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) ")"; counter-increment: lroman;display:inline-block; width: 50 }
+.lower-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) "." counter(lalpha, lower-alpha) ")"; counter-increment: lalpha; display:inline-block; width: 35}
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index b04b041a9..77e29632e 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,44 +1,49 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEdit, faSmile, faTextHeight } from '@fortawesome/free-solid-svg-icons';
-import { action, computed, IReactionDisposer, Lambda, observable, reaction } from "mobx";
+import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons';
+import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { baseKeymap } from "prosemirror-commands";
import { history } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
-import { Fragment, Node, Node as ProsNode, NodeType, Slice } from "prosemirror-model";
-import { EditorState, Plugin, Transaction } from "prosemirror-state";
+import { Fragment, Node, Node as ProsNode, NodeType, Slice, Mark, ResolvedPos } from "prosemirror-model";
+import { EditorState, Plugin, Transaction, TextSelection, NodeSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { DateField } from '../../../new_fields/DateField';
import { Doc, DocListCast, Opt, WidthSym } from "../../../new_fields/Doc";
import { Copy, Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
-import { RichTextField } from "../../../new_fields/RichTextField";
+import { RichTextField, ToPlainText, FromPlainText } from "../../../new_fields/RichTextField";
+import { BoolCast, Cast, NumCast, StrCast, DateCast, PromiseValue } from "../../../new_fields/Types";
import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../new_fields/Types";
-import { Utils } from '../../../Utils';
+import { Utils, numberRange, timenow } from '../../../Utils';
import { DocServer } from "../../DocServer";
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from "../../util/DragManager";
import buildKeymap from "../../util/ProsemirrorExampleTransfer";
import { inpRules } from "../../util/RichTextRules";
-import { ImageResizeView, schema, SummarizedView } from "../../util/RichTextSchema";
+import { ImageResizeView, schema, SummarizedView, OrderedListView, FootnoteView } from "../../util/RichTextSchema";
import { SelectionManager } from "../../util/SelectionManager";
import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
import { TooltipTextMenu } from "../../util/TooltipTextMenu";
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { DocComponent } from "../DocComponent";
import { InkingControl } from "../InkingControl";
-import { MainOverlayTextBox } from '../MainOverlayTextBox';
import { FieldView, FieldViewProps } from "./FieldView";
import "./FormattedTextBox.scss";
import React = require("react");
+import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils';
+import { DocumentDecorations } from '../DocumentDecorations';
+import { DictationManager } from '../../util/DictationManager';
+import { ReplaceStep } from 'prosemirror-transform';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment';
+import { inputRules } from 'prosemirror-inputrules';
library.add(faEdit);
-library.add(faSmile, faTextHeight);
+library.add(faSmile, faTextHeight, faUpload);
-// FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document
-//
+export const Blank = `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`;
export interface FormattedTextBoxProps {
isOverlay?: boolean;
@@ -53,31 +58,42 @@ const richTextSchema = createSchema({
documentText: "string"
});
+export const GoogleRef = "googleDocId";
+
type RichTextDocument = makeInterface<[typeof richTextSchema]>;
const RichTextDocument = makeInterface(richTextSchema);
+type PullHandler = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => void;
+
@observer
export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
public static LayoutString(fieldStr: string = "data") {
return FieldView.LayoutString(FormattedTextBox, fieldStr);
}
- public static Instance: FormattedTextBox;
- private _ref: React.RefObject<HTMLDivElement>;
+ private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
+ private _ref: React.RefObject<HTMLDivElement> = React.createRef();
private _proseRef?: HTMLDivElement;
private _editorView: Opt<EditorView>;
- private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
private _applyingChange: boolean = false;
private _linkClicked = "";
- private _reactionDisposer: Opt<IReactionDisposer>;
+ private _nodeClicked: any;
+ private _undoTyping?: UndoManager.Batch;
private _searchReactionDisposer?: Lambda;
+ private _reactionDisposer: Opt<IReactionDisposer>;
private _textReactionDisposer: Opt<IReactionDisposer>;
private _heightReactionDisposer: Opt<IReactionDisposer>;
+ private _rulesReactionDisposer: Opt<IReactionDisposer>;
private _proxyReactionDisposer: Opt<IReactionDisposer>;
+ private _pullReactionDisposer: Opt<IReactionDisposer>;
+ private _pushReactionDisposer: Opt<IReactionDisposer>;
private dropDisposer?: DragManager.DragDropDisposer;
- public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
- @observable _entered = false;
+ @observable private _fontSize = 13;
+ @observable private _fontFamily = "Arial";
+ @observable private _fontAlign = "";
+ @observable private _entered = false;
@observable public static InputBoxOverlay?: FormattedTextBox = undefined;
+ public static SelectOnLoad = "";
public static InputBoxOverlayScroll: number = 0;
public static IsFragment(html: string) {
return html.indexOf("data-pm-slice") !== -1;
@@ -106,88 +122,123 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
@undoBatch
public setFontColor(color: string) {
- let self = this;
- if (this._editorView!.state.selection.from === this._editorView!.state.selection.to) return false;
- if (this._editorView!.state.selection.to - this._editorView!.state.selection.from > this._editorView!.state.doc.nodeSize - 3) {
+ let view = this._editorView!;
+ if (view.state.selection.from === view.state.selection.to) return false;
+ if (view.state.selection.to - view.state.selection.from > view.state.doc.nodeSize - 3) {
this.props.Document.color = color;
}
- let colorMark = this._editorView!.state.schema.mark(this._editorView!.state.schema.marks.pFontColor, { color: color });
- this._editorView!.dispatch(this._editorView!.state.tr.addMark(this._editorView!.state.selection.from,
- this._editorView!.state.selection.to, colorMark));
+ let colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color: color });
+ view.dispatch(view.state.tr.addMark(view.state.selection.from, view.state.selection.to, colorMark));
return true;
}
constructor(props: FieldViewProps) {
super(props);
- FormattedTextBox.Instance = this;
- this._ref = React.createRef();
if (this.props.isOverlay) {
DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
}
-
- document.addEventListener("paste", this.paste);
}
+ public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
+
@computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.dataDoc, this.props.fieldKey, "dummy"); }
@computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
- paste = (e: ClipboardEvent) => {
- if (e.clipboardData && this._editorView) {
- let pdfPasteText = `${Utils.GenerateDeterministicGuid("pdf paste")}`;
- for (let i = 0; i < e.clipboardData.items.length; i++) {
- let item = e.clipboardData.items.item(i);
- if (item.type === "text/plain") {
- item.getAsString((text) => {
- let pdfPasteIndex = text.indexOf(pdfPasteText);
- if (pdfPasteIndex > -1) {
- let insertText = text.substr(0, pdfPasteIndex);
- const tx = this._editorView!.state.tr.insertText(insertText);
- // tx.setSelection(new Selection(tx.))
- const state = this._editorView!.state;
- this._editorView!.dispatch(tx);
- if (FormattedTextBox._toolTipTextMenu) {
- // this._toolTipTextMenu.makeLinkWithState(state)
- }
- e.stopPropagation();
- e.preventDefault();
- }
- });
+ // this should be internal to prosemirror, but is needed
+ // here to make sure that footnote view nodes in the overlay editor
+ // get removed when they're not selected.
+
+ syncNodeSelection(view: any, sel: any) {
+ if (sel instanceof NodeSelection) {
+ var desc = view.docView.descAt(sel.from);
+ if (desc !== view.lastSelectedViewDesc) {
+ if (view.lastSelectedViewDesc) {
+ view.lastSelectedViewDesc.deselectNode();
+ view.lastSelectedViewDesc = null;
}
+ if (desc) { desc.selectNode(); }
+ view.lastSelectedViewDesc = desc;
+ }
+ } else {
+ if (view.lastSelectedViewDesc) {
+ view.lastSelectedViewDesc.deselectNode();
+ view.lastSelectedViewDesc = null;
}
}
}
+ linkOnDeselect: Map<string, string> = new Map();
+
+ doLinkOnDeselect() {
+ Array.from(this.linkOnDeselect.entries()).map(entry => {
+ let key = entry[0];
+ let value = entry[1];
+ let id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
+ DocServer.GetRefField(value).then(doc => {
+ DocServer.GetRefField(id).then(linkDoc => {
+ 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(this.dataDoc, this.dataDoc[key] as Doc, undefined, "Ref:" + value, undefined, undefined, id, true);
+ })
+ });
+ })
+ this.linkOnDeselect.clear();
+ }
+
dispatchTransaction = (tx: Transaction) => {
if (this._editorView) {
+ let metadata = tx.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata);
+ if (metadata) {
+ let range = tx.selection.$from.blockRange(tx.selection.$to);
+ let text = range ? tx.doc.textBetween(range.start, range.end) : "";
+ let textEndSelection = tx.selection.to;
+ for (; textEndSelection < range!.end && text[textEndSelection - range!.start] != " "; textEndSelection++) { }
+ text = text.substr(0, textEndSelection - range!.start);
+ text = text.split(" ")[text.split(" ").length - 1];
+ let split = text.split("::");
+ if (split.length > 1 && split[1]) {
+ let key = split[0];
+ let value = split[split.length - 1];
+ this.linkOnDeselect.set(key, value);
+
+ let id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
+ const link = this._editorView!.state.schema.marks.link.create({ href: `http://localhost:1050/doc/${id}`, location: "onRight", title: value });
+ const mval = this._editorView!.state.schema.marks.metadataVal.create();
+ let offset = (tx.selection.to === range!.end - 1 ? -1 : 0);
+ tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval);
+ this.dataDoc[key] = value;
+ }
+ }
const state = this._editorView.state.apply(tx);
- FormattedTextBox._toolTipTextMenu && (FormattedTextBox._toolTipTextMenu.HackToFixTextSelectionGlitch = true);
this._editorView.updateState(state);
- FormattedTextBox._toolTipTextMenu && (FormattedTextBox._toolTipTextMenu.HackToFixTextSelectionGlitch = false);
- if (state.selection.empty && FormattedTextBox._toolTipTextMenu) {
- const marks = tx.storedMarks;
- console.log(marks);
- if (marks) { FormattedTextBox._toolTipTextMenu.mark_key_pressed(marks); }
+ this.syncNodeSelection(this._editorView, this._editorView.state.selection); // bcz: ugh -- shouldn't be needed but without this the overlay view's footnote popup doesn't get deselected
+ if (state.selection.empty && FormattedTextBox._toolTipTextMenu && tx.storedMarks) {
+ FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks);
}
+
this._applyingChange = true;
- const fieldkey = "preview";
- if (this.extensionDoc) this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n");
- if (this.extensionDoc) this.extensionDoc.lastModified = new DateField(new Date(Date.now()));
- this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()));
+ this.extensionDoc && (this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n"));
+ this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now())));
+ this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()));
this._applyingChange = false;
- let title = StrCast(this.dataDoc.title);
- if (title && title.startsWith("-") && this._editorView && !this.Document.customTitle) {
- let str = this._editorView.state.doc.textContent;
- let titlestr = str.substr(0, Math.min(40, str.length));
- this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
- }
+ this.updateTitle();
+ this.tryUpdateHeight();
+ }
+ }
+
+ updateTitle = () => {
+ if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.Document.customTitle) {
+ let str = this._editorView.state.doc.textContent;
+ let titlestr = str.substr(0, Math.min(40, str.length));
+ this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
}
}
public highlightSearchTerms = (terms: String[]) => {
if (this._editorView && (this._editorView as any).docView) {
- const fieldkey = "preview";
const doc = this._editorView.state.doc;
const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => {
@@ -217,23 +268,18 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
});
- // const fieldkey = 'search_string';
- // if (Object.keys(this.props.Document).indexOf(fieldkey) !== -1) {
- // this.props.Document[fieldkey] = undefined;
- // }
- // else this.props.Document.proto![fieldkey] = undefined;
- // }
}
}
-
+ setAnnotation = (start: number, end: number, mark: Mark, opened: boolean, keep: boolean = false) => {
+ let view = this._editorView!;
+ let mid = view.state.doc.resolve(Math.round((start + end) / 2));
+ let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: keep ? Doc.CurrentUserEmail : mark.attrs.userid, opened: opened });
+ view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark).setSelection(new TextSelection(mid)));
+ }
protected createDropTarget = (ele: HTMLDivElement) => {
this._proseRef = ele;
- if (this.dropDisposer) {
- this.dropDisposer();
- }
- if (ele) {
- this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
- }
+ this.dropDisposer && this.dropDisposer();
+ ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }));
}
@undoBatch
@@ -241,41 +287,112 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
drop = async (e: Event, de: DragManager.DropEvent) => {
// We're dealing with a link to a document
if (de.data instanceof DragManager.EmbedDragData && de.data.urlField) {
+ let target = de.data.embeddableSourceDoc;
// We're dealing with an internal document drop
let url = de.data.urlField.url.href;
let model: NodeType = (url.includes(".mov") || url.includes(".mp4")) ? schema.nodes.video : schema.nodes.image;
- this._editorView!.dispatch(this._editorView!.state.tr.insert(0, model.create({ src: url })));
+ let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y });
+ this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, model.create({ src: url, docid: target[Id] })));
+ DocUtils.MakeLink(this.dataDoc, target, undefined, "ImgRef:" + target.title, undefined, undefined, target[Id]);
e.stopPropagation();
- } else {
- if (de.data instanceof DragManager.DocumentDragData) {
- this.props.Document.layout = de.data.draggedDocuments[0];
- de.data.draggedDocuments[0].isTemplate = true;
+ } else if (de.data instanceof DragManager.DocumentDragData) {
+ const draggedDoc = de.data.draggedDocuments.length && de.data.draggedDocuments[0];
+ if (draggedDoc && draggedDoc.type === DocumentType.TEXT) {
+ if (!Doc.AreProtosEqual(draggedDoc, this.props.Document)) {
+ draggedDoc.isTemplate = true;
+ if (typeof (draggedDoc.layout) === "string") {
+ let layoutDelegateToOverrideFieldKey = Doc.MakeDelegate(draggedDoc);
+ layoutDelegateToOverrideFieldKey.layout = StrCast(layoutDelegateToOverrideFieldKey.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${this.props.fieldKey}"}`);
+ this.props.Document.layout = layoutDelegateToOverrideFieldKey;
+ } else {
+ this.props.Document.layout = draggedDoc.layout instanceof Doc ? draggedDoc.layout : draggedDoc;
+ }
+ }
e.stopPropagation();
}
}
}
- componentDidMount() {
- const config = {
+ recordKeyHandler = (e: KeyboardEvent) => {
+ if (SelectionManager.SelectedDocuments().length && this.props.Document === SelectionManager.SelectedDocuments()[0].props.Document) {
+ if (e.key === "R" && e.altKey) {
+ e.stopPropagation();
+ e.preventDefault();
+ this.recordBullet();
+ }
+ }
+ }
+
+ recordBullet = async () => {
+ let completedCue = "end session";
+ let results = await DictationManager.Controls.listen({
+ interimHandler: this.setCurrentBulletContent,
+ continuous: { indefinite: false },
+ terminators: [completedCue, "bullet", "next"]
+ });
+ if (results && [DictationManager.Controls.Infringed, completedCue].includes(results)) {
+ DictationManager.Controls.stop();
+ return;
+ }
+ this.nextBullet(this._editorView!.state.selection.to);
+ setTimeout(this.recordBullet, 2000);
+ }
+
+ setCurrentBulletContent = (value: string) => {
+ if (this._editorView) {
+ let state = this._editorView.state;
+ let from = state.selection.from;
+ let to = state.selection.to;
+ this._editorView.dispatch(state.tr.insertText(value, from, to));
+ state = this._editorView.state;
+ let updated = TextSelection.create(state.doc, from, from + value.length);
+ this._editorView.dispatch(state.tr.setSelection(updated));
+ }
+ }
+
+ nextBullet = (pos: number) => {
+ if (this._editorView) {
+ let frag = Fragment.fromArray(this.newListItems(2));
+ let slice = new Slice(frag, 2, 2);
+ let state = this._editorView.state;
+ this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice)));
+ pos += 4;
+ state = this._editorView.state;
+ this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos)));
+ }
+ }
+
+ private newListItems = (count: number) => {
+ return numberRange(count).map(x => schema.nodes.list_item.create(undefined, schema.nodes.paragraph.create()));
+ }
+
+ _keymap: any = undefined;
+ @computed get config() {
+ this._keymap = buildKeymap(schema);
+ return {
schema,
- inpRules, //these currently don't do anything, but could eventually be helpful
plugins: this.props.isOverlay ? [
+ inputRules(inpRules),
this.tooltipTextMenuPlugin(),
history(),
- keymap(buildKeymap(schema)),
+ keymap(this._keymap),
keymap(baseKeymap),
// this.tooltipLinkingMenuPlugin(),
new Plugin({
props: {
attributes: { class: "ProseMirror-example-setup-style" }
}
- })
+ }),
+ formattedTextBoxCommentPlugin
] : [
history(),
- keymap(buildKeymap(schema)),
+ keymap(this._keymap),
keymap(baseKeymap),
]
};
+ }
+
+ componentDidMount() {
if (!this.props.isOverlay) {
this._proxyReactionDisposer = reaction(() => this.props.isSelected(),
@@ -287,19 +404,48 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}, { fireImmediately: true });
}
+ this.pullFromGoogleDoc(this.checkState);
+ this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => DocumentDecorations.Instance.isAnimatingFetch = true);
+
this._reactionDisposer = reaction(
() => {
const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined;
- return field ? field.Data : `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`;
+ return field ? field.Data : Blank;
},
- field2 => this._editorView && !this._applyingChange &&
- this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field2)))
+ incomingValue => {
+ if (this._editorView && !this._applyingChange) {
+ let updatedState = JSON.parse(incomingValue);
+ this._editorView.updateState(EditorState.fromJSON(this.config, updatedState));
+ this.tryUpdateHeight();
+ }
+ }
);
- this.props.isOverlay && (this._heightReactionDisposer = reaction(
+ this._pullReactionDisposer = reaction(
+ () => this.props.Document[Pulls],
+ () => {
+ if (!DocumentDecorations.hasPulledHack) {
+ DocumentDecorations.hasPulledHack = true;
+ let unchanged = this.dataDoc.unchanged;
+ this.pullFromGoogleDoc(unchanged ? this.checkState : this.updateState);
+ }
+ }
+ );
+
+ this._pushReactionDisposer = reaction(
+ () => this.props.Document[Pushes],
+ () => {
+ if (!DocumentDecorations.hasPushedHack) {
+ DocumentDecorations.hasPushedHack = true;
+ this.pushToGoogleDoc();
+ }
+ }
+ );
+
+ this._heightReactionDisposer = reaction(
() => this.props.Document[WidthSym](),
() => this.tryUpdateHeight()
- ));
+ );
this._textReactionDisposer = reaction(
() => this.extensionDoc,
@@ -311,7 +457,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this.dataDoc.lastModified = undefined;
}
}, { fireImmediately: true });
- this.setupEditor(config, this.dataDoc, this.props.fieldKey);
+
+
+ this.setupEditor(this.config, this.dataDoc, this.props.fieldKey);
this._searchReactionDisposer = reaction(() => {
return StrCast(this.props.Document.search_string);
@@ -328,6 +476,120 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this.unhighlightSearchTerms();
}
}, { fireImmediately: true });
+
+
+ this._rulesReactionDisposer = reaction(() => {
+ let ruleProvider = this.props.ruleProvider;
+ let heading = NumCast(this.props.Document.heading);
+ if (ruleProvider instanceof Doc) {
+ return {
+ align: StrCast(ruleProvider["ruleAlign_" + heading], ""),
+ font: StrCast(ruleProvider["ruleFont_" + heading], "Arial"),
+ size: NumCast(ruleProvider["ruleSize_" + heading], 13)
+ };
+ }
+ return undefined;
+ },
+ action((rules: any) => {
+ this._fontFamily = rules ? rules.font : "Arial";
+ this._fontSize = rules ? rules.size : 13;
+ rules && setTimeout(() => {
+ const view = this._editorView!;
+ if (this._proseRef) {
+ let n = new NodeSelection(view.state.doc.resolve(0));
+ if (this._editorView!.state.doc.textContent === "") {
+ view.dispatch(view.state.tr.setSelection(new TextSelection(view.state.doc.resolve(0), view.state.doc.resolve(2))).
+ replaceSelectionWith(this._editorView!.state.schema.nodes.paragraph.create({ align: rules.align }), true));
+ } else if (n.node && n.node.type === view.state.schema.nodes.paragraph) {
+ view.dispatch(view.state.tr.setNodeMarkup(0, n.node.type, { ...n.node.attrs, align: rules.align }));
+ }
+ this.tryUpdateHeight();
+ }
+ }, 0);
+ }), { fireImmediately: true }
+ );
+
+ setTimeout(() => this.tryUpdateHeight(), 0);
+ }
+
+ pushToGoogleDoc = async () => {
+ this.pullFromGoogleDoc(async (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => {
+ let modes = GoogleApiClientUtils.WriteMode;
+ let mode = modes.Replace;
+ let reference: Opt<GoogleApiClientUtils.Reference> = Cast(this.dataDoc[GoogleRef], "string");
+ if (!reference) {
+ mode = modes.Insert;
+ reference = { service: GoogleApiClientUtils.Service.Documents, title: StrCast(this.dataDoc.title) };
+ }
+ let redo = async () => {
+ let data = Cast(this.dataDoc.data, RichTextField);
+ if (this._editorView && reference && data) {
+ let content = data[ToPlainText]();
+ let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode });
+ response && (this.dataDoc[GoogleRef] = response.documentId);
+ let pushSuccess = response !== undefined && !("errors" in response);
+ dataDoc.unchanged = pushSuccess;
+ DocumentDecorations.Instance.startPushOutcome(pushSuccess);
+ }
+ };
+ let undo = () => {
+ let content = exportState.body;
+ if (reference && content) {
+ GoogleApiClientUtils.Docs.write({ reference, content, mode });
+ }
+ };
+ UndoManager.AddEvent({ undo, redo });
+ redo();
+ });
+ }
+
+ pullFromGoogleDoc = async (handler: PullHandler) => {
+ let dataDoc = this.dataDoc;
+ let documentId = StrCast(dataDoc[GoogleRef]);
+ let exportState: GoogleApiClientUtils.ReadResult = {};
+ if (documentId) {
+ exportState = await GoogleApiClientUtils.Docs.read({ identifier: documentId });
+ }
+ UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls);
+ }
+
+ updateState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => {
+ let pullSuccess = false;
+ if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) {
+ const data = Cast(dataDoc.data, RichTextField);
+ if (data instanceof RichTextField) {
+ pullSuccess = true;
+ dataDoc.data = new RichTextField(data[FromPlainText](exportState.body));
+ setTimeout(() => {
+ if (this._editorView) {
+ let state = this._editorView.state;
+ let end = state.doc.content.size - 1;
+ this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end)));
+ }
+ }, 0);
+ dataDoc.title = exportState.title;
+ this.Document.customTitle = true;
+ dataDoc.unchanged = true;
+ }
+ } else {
+ delete dataDoc[GoogleRef];
+ }
+ DocumentDecorations.Instance.startPullOutcome(pullSuccess);
+ }
+
+ checkState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => {
+ if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) {
+ let data = Cast(dataDoc.data, RichTextField);
+ if (data) {
+ let storedPlainText = data[ToPlainText]() + "\n";
+ let receivedPlainText = exportState.body;
+ let storedTitle = dataDoc.title;
+ let receivedTitle = exportState.title;
+ let unchanged = storedPlainText === receivedPlainText && storedTitle === receivedTitle;
+ dataDoc.unchanged = unchanged;
+ DocumentDecorations.Instance.setPullState(unchanged);
+ }
+ }
}
clipboardTextSerializer = (slice: Slice): string => {
@@ -384,7 +646,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (link) {
cbe.clipboardData!.setData("dash/linkDoc", link[Id]);
linkId = link[Id];
- let frag = addMarkToFrag(slice.content);
+ let frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(doc.title)));
slice = new Slice(frag, slice.openStart, slice.openEnd);
var tr = view.state.tr.replaceSelection(slice);
view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste"));
@@ -394,24 +656,20 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return true;
- function addMarkToFrag(frag: Fragment) {
+ function addMarkToFrag(frag: Fragment, marker: (node: Node) => Node) {
const nodes: Node[] = [];
- frag.forEach(node => nodes.push(addLinkMark(node)));
+ frag.forEach(node => nodes.push(marker(node)));
return Fragment.fromArray(nodes);
}
- function addLinkMark(node: Node) {
+ function addLinkMark(node: Node, title: string) {
if (!node.isText) {
- const content = addMarkToFrag(node.content);
+ const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title));
return node.copy(content);
}
const marks = [...node.marks];
const linkIndex = marks.findIndex(mark => mark.type.name === "link");
- const link = view.state.schema.mark(view.state.schema.marks.link, { href: `http://localhost:1050/doc/${linkId}`, location: "onRight" });
- if (linkIndex !== -1) {
- marks.splice(linkIndex, 1, link);
- } else {
- marks.push(link);
- }
+ const link = view.state.schema.mark(view.state.schema.marks.link, { href: `http://localhost:1050/doc/${linkId}`, location: "onRight", title: title, docref: true });
+ marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link);
return node.mark(marks);
}
}
@@ -429,47 +687,70 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
if (this._proseRef) {
+ let self = this;
+ this._editorView && this._editorView.destroy();
this._editorView = new EditorView(this._proseRef, {
state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config),
dispatchTransaction: this.dispatchTransaction,
nodeViews: {
- image(node, view, getPos) { return new ImageResizeView(node, view, getPos); },
+ image(node, view, getPos) { return new ImageResizeView(node, view, getPos, self.props.addDocTab); },
star(node, view, getPos) { return new SummarizedView(node, view, getPos); },
+ ordered_list(node, view, getPos) { return new OrderedListView(); },
+ footnote(node, view, getPos) { return new FootnoteView(node, view, getPos); }
},
clipboardTextSerializer: this.clipboardTextSerializer,
handlePaste: this.handlePaste,
});
+ (this._editorView as any).isOverlay = this.props.isOverlay;
if (startup) {
Doc.GetProto(doc).documentText = undefined;
this._editorView.dispatch(this._editorView.state.tr.insertText(startup));
}
}
- if (this.props.selectOnLoad) {
- if (!this.props.isOverlay) this.props.select(false);
- else this._editorView!.focus();
- this.tryUpdateHeight();
+ let selectOnLoad = this.props.Document[Id] === FormattedTextBox.SelectOnLoad;
+ if (selectOnLoad) {
+ FormattedTextBox.SelectOnLoad = "";
+ this.props.select(false);
+ }
+ else if (this.props.isOverlay) this._editorView!.focus();
+ // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
+ this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })];
+ }
+ getFont(font: string) {
+ switch (font) {
+ case "Arial": return schema.marks.arial.create();
+ case "Times New Roman": return schema.marks.timesNewRoman.create();
+ case "Georgia": return schema.marks.georgia.create();
+ case "Comic Sans MS": return schema.marks.comicSans.create();
+ case "Tahoma": return schema.marks.tahoma.create();
+ case "Impact": return schema.marks.impact.create();
+ case "ACrimson Textrial": return schema.marks.crimson.create();
}
+ return schema.marks.arial.create();
}
componentWillUnmount() {
- this._editorView && this._editorView.destroy();
+ this._rulesReactionDisposer && this._rulesReactionDisposer();
this._reactionDisposer && this._reactionDisposer();
this._proxyReactionDisposer && this._proxyReactionDisposer();
this._textReactionDisposer && this._textReactionDisposer();
+ this._pushReactionDisposer && this._pushReactionDisposer();
+ this._pullReactionDisposer && this._pullReactionDisposer();
this._heightReactionDisposer && this._heightReactionDisposer();
this._searchReactionDisposer && this._searchReactionDisposer();
+ this._editorView && this._editorView.destroy();
}
+
onPointerDown = (e: React.PointerEvent): void => {
+ let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
+ pos && (this._nodeClicked = this._editorView!.state.doc.nodeAt(pos.pos));
if (this.props.onClick && e.button === 0) {
e.preventDefault();
}
if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) {
e.stopPropagation();
- if (FormattedTextBox._toolTipTextMenu && FormattedTextBox._toolTipTextMenu.tooltip) {
- //this._toolTipTextMenu.tooltip.style.opacity = "0";
- }
}
let ctrlKey = e.ctrlKey;
if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) {
@@ -481,6 +762,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
for (let parent = (e.target as any).parentNode; !href && parent; parent = parent.parentNode) {
href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href;
}
+ 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);
+ href = link && link.attrs.href;
+ location = link && link.attrs.location;
+ }
if (href) {
if (href.indexOf(Utils.prepend("/doc/")) === 0) {
this._linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
@@ -501,7 +789,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
DocumentManager.Instance.jumpToDocument(targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
} else if (jumpToDoc) {
DocumentManager.Instance.jumpToDocument(jumpToDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
-
+ } else {
+ DocumentManager.Instance.jumpToDocument(linkDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
}
}
});
@@ -522,10 +811,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
e.preventDefault();
}
}
+
onPointerUp = (e: React.PointerEvent): void => {
- if (FormattedTextBox._toolTipTextMenu && FormattedTextBox._toolTipTextMenu.tooltip) {
- //this._toolTipTextMenu.tooltip.style.opacity = "1";
- }
+ FormattedTextBoxComment.textBox = this;
if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
e.stopPropagation();
}
@@ -533,6 +821,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
@action
onFocused = (e: React.FocusEvent): void => {
+ document.removeEventListener("keypress", this.recordKeyHandler);
+ document.addEventListener("keypress", this.recordKeyHandler);
+ this.tryUpdateHeight();
if (!this.props.isOverlay) {
FormattedTextBox.InputBoxOverlay = this;
} else {
@@ -549,6 +840,17 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
onClick = (e: React.MouseEvent): void => {
+ // 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.
+ if (this.props.isSelected() && e.nativeEvent.offsetX < 40) {
+ let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
+ if (pos && pos.pos > 0) {
+ let node = this._editorView!.state.doc.nodeAt(pos.pos);
+ let node2 = node && node.type === schema.nodes.paragraph ? this._editorView!.state.doc.nodeAt(pos.pos - 1) : undefined;
+ if (node === this._nodeClicked && node2 && (node2.type === schema.nodes.ordered_list || node2.type === schema.nodes.list_item)) {
+ this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.pos - 1, node2.type, { ...node2.attrs, visibility: !node2.attrs.visibility }));
+ }
+ }
+ }
this._proseRef!.focus();
if (this._linkClicked) {
this._linkClicked = "";
@@ -570,7 +872,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops);
}
});
- //this.props.Document.tooltip = self._toolTipTextMenu;
}
tooltipLinkingMenuPlugin() {
@@ -582,94 +883,71 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
});
}
onBlur = (e: any) => {
+ document.removeEventListener("keypress", this.recordKeyHandler);
if (this._undoTyping) {
this._undoTyping.end();
this._undoTyping = undefined;
}
+ this.doLinkOnDeselect();
}
- public _undoTyping?: UndoManager.Batch;
onKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
SelectionManager.DeselectAll();
}
e.stopPropagation();
- if (e.key === "Tab") e.preventDefault();
- // stop propagation doesn't seem to stop propagation of native keyboard events.
- // so we set a flag on the native event that marks that the event's been handled.
- (e.nativeEvent as any).DASHFormattedTextBoxHandled = true;
- if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.Document.customTitle) {
- let str = this._editorView.state.doc.textContent;
- let titlestr = str.substr(0, Math.min(40, str.length));
- this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
+ if (e.key === "Tab" || e.key === "Enter") {
+ e.preventDefault();
}
+ this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() }));
+
if (!this._undoTyping) {
this._undoTyping = UndoManager.StartBatch("undoTyping");
}
- this.tryUpdateHeight();
}
@action
tryUpdateHeight() {
- if (this.props.Document.autoHeight && this.props.isOverlay) {
- let xf = this._ref.current!.getBoundingClientRect();
- let scrBounds = this.props.ScreenToLocalTransform().transformBounds(0, 0, xf.width, xf.height);
+ const ChromeHeight = this.props.ChromeHeight;
+ let sh = this._ref.current ? this._ref.current.scrollHeight : 0;
+ if (!this.props.isOverlay && this.props.Document.autoHeight && sh !== 0) {
let nh = this.props.Document.isTemplate ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
let dh = NumCast(this.props.Document.height, 0);
- let sh = scrBounds.height;
- const ChromeHeight = MainOverlayTextBox.Instance.ChromeHeight;
this.props.Document.height = Math.max(10, (nh ? dh / nh * sh : sh) + (ChromeHeight ? ChromeHeight() : 0));
this.dataDoc.nativeHeight = nh ? sh : undefined;
}
}
- @action
- onPointerEnter = (e: React.PointerEvent) => {
- this._entered = true;
- }
- @action
- onPointerLeave = (e: React.PointerEvent) => {
- this._entered = false;
- }
-
- specificContextMenu = (e: React.MouseEvent): void => {
- // let subitems: ContextMenuProps[] = [];
- // subitems.push({
- // description: BoolCast(this.props.Document.autoHeight) ? "Manual Height" : "Auto Height",
- // event: action(() => Doc.GetProto(this.props.Document).autoHeight = !BoolCast(this.props.Document.autoHeight)), icon: "expand-arrows-alt"
- // });
- // ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems, icon: "text-height" });
- }
render() {
- let self = this;
let style = this.props.isOverlay ? "scroll" : "hidden";
let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : "";
- let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground ||
- (this.props.Document.isButton && !this.props.isSelected()) ? "none" : "all";
+ let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground
+ //|| (this.props.Document.isButton && !this.props.isSelected())
+ ? "none" : "all";
Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
return (
<div className={`formattedTextBox-cont-${style}`} ref={this._ref}
style={{
overflowY: this.props.Document.autoHeight ? "hidden" : "auto",
- height: this.props.height ? this.props.height : undefined,
+ height: this.props.Document.autoHeight ? "max-content" : this.props.height ? this.props.height : undefined,
background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : undefined,
opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || Doc.IsBrushed(this.props.Document) ? 1 : 0.1) : 1,
color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit",
pointerEvents: interactive,
- fontSize: "13px"
+ fontSize: this._fontSize,
+ fontFamily: this._fontFamily,
}}
onKeyDown={this.onKeyPress}
onFocus={this.onFocused}
onClick={this.onClick}
- onContextMenu={this.specificContextMenu}
onBlur={this.onBlur}
onPointerUp={this.onPointerUp}
onPointerDown={this.onPointerDown}
onMouseDown={this.onMouseDown}
onWheel={this.onPointerWheel}
- onPointerEnter={this.onPointerEnter}
- onPointerLeave={this.onPointerLeave}
+ onPointerEnter={action(() => this._entered = true)}
+ onPointerLeave={action(() => this._entered = false)}
>
- <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} style={{ whiteSpace: "pre-wrap" }} />
+ <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: (this.props.Document.isButton && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} />
</div>
);
}
diff --git a/src/client/views/nodes/FormattedTextBoxComment.scss b/src/client/views/nodes/FormattedTextBoxComment.scss
new file mode 100644
index 000000000..792cee182
--- /dev/null
+++ b/src/client/views/nodes/FormattedTextBoxComment.scss
@@ -0,0 +1,34 @@
+.FormattedTextBox-tooltip {
+ position: absolute;
+ pointer-events: none;
+ z-index: 20;
+ background: white;
+ border: 1px solid silver;
+ border-radius: 2px;
+ padding: 2px 10px;
+ margin-bottom: 7px;
+ -webkit-transform: translateX(-50%);
+ transform: translateX(-50%);
+ }
+ .FormattedTextBox-tooltip:before {
+ content: "";
+ height: 0; width: 0;
+ position: absolute;
+ left: 50%;
+ margin-left: -5px;
+ bottom: -6px;
+ border: 5px solid transparent;
+ border-bottom-width: 0;
+ border-top-color: silver;
+ }
+ .FormattedTextBox-tooltip:after {
+ content: "";
+ height: 0; width: 0;
+ position: absolute;
+ left: 50%;
+ margin-left: -5px;
+ bottom: -4.5px;
+ border: 5px solid transparent;
+ border-bottom-width: 0;
+ border-top-color: white;
+ } \ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
new file mode 100644
index 000000000..2d2fff06d
--- /dev/null
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -0,0 +1,152 @@
+import { Plugin, EditorState } from "prosemirror-state";
+import './FormattedTextBoxComment.scss';
+import { ResolvedPos, Mark } from "prosemirror-model";
+import { EditorView } from "prosemirror-view";
+import { Doc } from "../../../new_fields/Doc";
+import { schema } from "../../util/RichTextSchema";
+import { DocServer } from "../../DocServer";
+import { Utils } from "../../../Utils";
+import { StrCast } from "../../../new_fields/Types";
+
+export let formattedTextBoxCommentPlugin = new Plugin({
+ view(editorView) { return new FormattedTextBoxComment(editorView); }
+});
+export function findOtherUserMark(marks: Mark[]): Mark | undefined {
+ return marks.find(m => m.attrs.userid && m.attrs.userid !== Doc.CurrentUserEmail);
+}
+export function findUserMark(marks: Mark[]): Mark | undefined {
+ return marks.find(m => m.attrs.userid);
+}
+export function findLinkMark(marks: Mark[]): Mark | undefined {
+ return marks.find(m => m.type === schema.marks.link);
+}
+export function findStartOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) {
+ let before = 0;
+ let nbef = rpos.nodeBefore;
+ while (nbef && finder(nbef.marks)) {
+ before += nbef.nodeSize;
+ rpos = view.state.doc.resolve(rpos.pos - nbef.nodeSize);
+ rpos && (nbef = rpos.nodeBefore);
+ }
+ return before;
+}
+export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) {
+ let after = 0;
+ let naft = rpos.nodeAfter;
+ while (naft && finder(naft.marks)) {
+ after += naft.nodeSize;
+ rpos = view.state.doc.resolve(rpos.pos + naft.nodeSize);
+ rpos && (naft = rpos.nodeAfter);
+ }
+ return after;
+}
+
+
+export class FormattedTextBoxComment {
+ static tooltip: HTMLElement;
+ static tooltipText: HTMLElement;
+ static start: number;
+ static end: number;
+ static mark: Mark;
+ static opened: boolean;
+ static textBox: any;
+ constructor(view: any) {
+ if (!FormattedTextBoxComment.tooltip) {
+ const root = document.getElementById("root");
+ let input = document.createElement("input");
+ input.type = "checkbox";
+ FormattedTextBoxComment.tooltip = document.createElement("div");
+ FormattedTextBoxComment.tooltipText = document.createElement("div");
+ FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipText);
+ FormattedTextBoxComment.tooltip.className = "FormattedTextBox-tooltip";
+ FormattedTextBoxComment.tooltip.style.pointerEvents = "all";
+ FormattedTextBoxComment.tooltip.appendChild(input);
+ FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => {
+ let keep = e.target && (e.target as any).type === "checkbox";
+ FormattedTextBoxComment.opened = keep || !FormattedTextBoxComment.opened;
+ FormattedTextBoxComment.textBox && FormattedTextBoxComment.textBox.setAnnotation(
+ FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark,
+ FormattedTextBoxComment.opened, keep);
+ };
+ root && root.appendChild(FormattedTextBoxComment.tooltip);
+ }
+ this.update(view, undefined);
+ }
+
+ public static Hide() {
+ FormattedTextBoxComment.textBox = undefined;
+ FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "none");
+ }
+ public static SetState(textBox: any, opened: boolean, start: number, end: number, mark: Mark) {
+ FormattedTextBoxComment.textBox = textBox;
+ FormattedTextBoxComment.start = start;
+ FormattedTextBoxComment.end = end;
+ FormattedTextBoxComment.mark = mark;
+ FormattedTextBoxComment.opened = opened;
+ FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "");
+ }
+
+ update(view: EditorView, lastState?: EditorState) {
+ let state = view.state;
+ // Don't do anything if the document/selection didn't change
+ if (lastState && lastState.doc.eq(state.doc) &&
+ lastState.selection.eq(state.selection)) return;
+
+ let set = "none";
+ if (state.selection.$from) {
+ let nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark);
+ let naft = findEndOfMark(state.selection.$from, view, findOtherUserMark);
+ const spos = state.selection.$from.pos - nbef;
+ const epos = state.selection.$from.pos + naft;
+ let child = state.selection.$from.nodeBefore;
+ let mark = child && findOtherUserMark(child.marks);
+ let noselection = view.state.selection.$from === view.state.selection.$to;
+ if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) {
+ FormattedTextBoxComment.SetState(this, mark.attrs.opened, spos, epos, mark);
+ }
+ if (mark && child && nbef && naft) {
+ FormattedTextBoxComment.tooltipText.textContent = mark.attrs.userid + " " + mark.attrs.modified;
+ // These are in screen coordinates
+ // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to);
+ let start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef);
+ // The box in which the tooltip is positioned, to use as base
+ let box = (document.getElementById("main-div") as any).getBoundingClientRect();
+ // Find a center-ish x position from the selection endpoints (when
+ // crossing lines, end may be more to the left)
+ let left = Math.max((start.left + end.left) / 2, start.left + 3);
+ FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px";
+ FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px";
+ set = "";
+ }
+ }
+ if (set === "none" && state.selection.$from) {
+ FormattedTextBoxComment.textBox = undefined;
+ let nbef = findStartOfMark(state.selection.$from, view, findLinkMark);
+ let naft = findEndOfMark(state.selection.$from, view, findLinkMark);
+ let child = state.selection.$from.nodeBefore;
+ let mark = child && findLinkMark(child.marks);
+ if (mark && child && nbef && naft) {
+ FormattedTextBoxComment.tooltipText.textContent = "link : " + (mark.attrs.title || mark.attrs.href);
+ if (mark.attrs.href.indexOf(Utils.prepend("/doc/")) === 0) {
+ let docTarget = mark.attrs.href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ docTarget && DocServer.GetRefField(docTarget).then(linkDoc =>
+ (linkDoc as Doc) && (FormattedTextBoxComment.tooltipText.textContent = "link :" + StrCast((linkDoc as Doc).title)));
+ }
+ // These are in screen coordinates
+ // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to);
+ let start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef);
+ // The box in which the tooltip is positioned, to use as base
+ let box = (document.getElementById("main-div") as any).getBoundingClientRect();
+ // Find a center-ish x position from the selection endpoints (when
+ // crossing lines, end may be more to the left)
+ let left = Math.max((start.left + end.left) / 2, start.left + 3);
+ FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px";
+ FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px";
+ set = "";
+ }
+ }
+ FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = set);
+ }
+
+ destroy() { FormattedTextBoxComment.tooltip.style.display = "none"; }
+}
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index b1afa3f7d..00c069e1f 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -65,6 +65,7 @@
display:flex;
align-items: center;
height:100%;
+ overflow:hidden;
.imageBox-fadeBlocker {
width:100%;
height:100%;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 708e00576..95f304641 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -89,10 +89,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.data instanceof DragManager.DocumentDragData) {
de.data.droppedDocuments.forEach(action((drop: Doc) => {
- if (de.mods === "CtrlKey") {
- Doc.ApplyTemplateTo(drop, this.props.Document, this.props.DataDoc);
- e.stopPropagation();
- } else if (de.mods === "AltKey" && /*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) {
+ if (de.mods === "AltKey" && /*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) {
Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(drop.data.url);
e.stopPropagation();
} else if (de.mods === "MetaKey") {
@@ -219,12 +216,13 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
funcs.push({ description: "Record 1sec audio", event: this.recordAudioAnnotation, icon: "expand-arrows-alt" });
funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" });
- let modes: ContextMenuProps[] = [];
+ let existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers...");
+ let modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : [];
modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" });
modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" });
+ !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" })
ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs, icon: "asterisk" });
- ContextMenu.Instance.addItem({ description: "Analyze...", subitems: modes, icon: "eye" });
}
}
@@ -264,13 +262,8 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
onDotDown(index: number) {
this.Document.curPage = index;
}
-
- @computed get fieldExtensionDoc() {
- return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true");
- }
-
@computed private get url() {
- let data = Cast(Doc.GetProto(this.props.Document).data, ImageField);
+ let data = Cast(Doc.GetProto(this.props.Document)[this.props.fieldKey], ImageField);
return data ? data.url.href : undefined;
}
@@ -321,7 +314,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
let rotation = NumCast(this.dataDoc.rotation) % 180;
let realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size;
let aspect = realsize.height / realsize.width;
- if (Math.abs(NumCast(layoutdoc.height) - realsize.height) > 1 || Math.abs(NumCast(layoutdoc.width) - realsize.width) > 1) {
+ if (layoutdoc.nativeHeight !== 0 && layoutdoc.nativeWidth !== 0 && (Math.abs(NumCast(layoutdoc.height) - realsize.height) > 1 || Math.abs(NumCast(layoutdoc.width) - realsize.width) > 1)) {
setTimeout(action(() => {
layoutdoc.height = layoutdoc[WidthSym]() * aspect;
layoutdoc.nativeHeight = realsize.height;
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 0d4b377dd..ee70942de 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -2,26 +2,21 @@
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
-import { CompileScript, ScriptOptions, CompiledScript } from "../../util/Scripting";
-import { FieldView, FieldViewProps } from './FieldView';
-import "./KeyValueBox.scss";
-import { KeyValuePair } from "./KeyValuePair";
-import React = require("react");
-import { NumCast, Cast, FieldValue, StrCast } from "../../../new_fields/Types";
-import { Doc, Field, FieldResult, DocListCastAsync } from "../../../new_fields/Doc";
-import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
-import { SetupDrag } from "../../util/DragManager";
-import { Docs } from "../../documents/Documents";
-import { RawDataOperationParameters } from "../../northstar/model/idea/idea";
-import { Templates } from "../Templates";
+import { Doc, Field, FieldResult } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
-import { TextField } from "../../util/ProsemirrorCopy/prompt";
import { RichTextField } from "../../../new_fields/RichTextField";
-import { ImageField } from "../../../new_fields/URLField";
-import { SelectionManager } from "../../util/SelectionManager";
import { listSpec } from "../../../new_fields/Schema";
-import { CollectionViewType } from "../collections/CollectionBaseView";
+import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
+import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { ImageField } from "../../../new_fields/URLField";
+import { Docs } from "../../documents/Documents";
+import { SetupDrag } from "../../util/DragManager";
+import { CompiledScript, CompileScript, ScriptOptions } from "../../util/Scripting";
import { undoBatch } from "../../util/UndoManager";
+import { FieldView, FieldViewProps } from './FieldView';
+import "./KeyValueBox.scss";
+import { KeyValuePair } from "./KeyValuePair";
+import React = require("react");
export type KVPScript = {
script: CompiledScript;
@@ -128,7 +123,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let rows: JSX.Element[] = [];
let i = 0;
const self = this;
- for (let key of Object.keys(ids).sort()) {
+ for (let key of Object.keys(ids).slice().sort()) {
rows.push(<KeyValuePair doc={realDoc} ref={(function () {
let oldEl: KeyValuePair | undefined;
return (el: KeyValuePair) => {
@@ -203,7 +198,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
return;
}
let previousViewType = fieldTemplate.viewType;
- Doc.MakeTemplate(fieldTemplate, metaKey, Doc.GetProto(parentStackingDoc));
+ Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(parentStackingDoc));
previousViewType && (fieldTemplate.viewType = previousViewType);
Cast(parentStackingDoc.data, listSpec(Doc))!.push(fieldTemplate);
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 534a42efc..7e0f3735d 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -55,12 +55,12 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
Document: this.props.doc,
DataDoc: this.props.doc,
ContainingCollectionView: undefined,
+ ruleProvider: undefined,
fieldKey: this.props.keyName,
fieldExt: "",
isSelected: returnFalse,
select: emptyFunction,
renderDepth: 1,
- selectOnLoad: false,
active: returnFalse,
whenActiveChanged: emptyFunction,
ScreenToLocalTransform: Transform.Identity,
@@ -68,6 +68,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
PanelWidth: returnZero,
PanelHeight: returnZero,
addDocTab: returnZero,
+ pinToPres: returnZero,
ContentScaling: returnOne
};
let contents = <FieldView {...props} />;
@@ -112,7 +113,8 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
<div className="keyValuePair-td-value-container">
<EditableView
contents={contents}
- height={36}
+ maxHeight={36}
+ height={"auto"}
GetValue={() => {
return Field.toKeyValueString(props.Document, props.fieldKey);
}}
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
deleted file mode 100644
index fc5f2410c..000000000
--- a/src/client/views/nodes/LinkEditor.scss
+++ /dev/null
@@ -1,145 +0,0 @@
-@import "../globalCssVariables";
-
-.linkEditor {
- width: 100%;
- height: auto;
- font-size: 12px; // TODO
-}
-
-.linkEditor-back {
- margin-bottom: 6px;
-}
-
-.linkEditor-info {
- border-bottom: 0.5px solid $light-color-secondary;
- padding-bottom: 6px;
- margin-bottom: 6px;
- display: flex;
- justify-content: space-between;
-
- .linkEditor-linkedTo {
- width: calc(100% - 26px);
- }
-}
-
-.linkEditor-button {
- width: 20px;
- height: 20px;
- margin-left: 6px;
- padding: 0;
- // font-size: 12px;
- border-radius: 10px;
-
- &:disabled {
- background-color: gray;
- }
-}
-
-.linkEditor-groupsLabel {
- display: flex;
- justify-content: space-between;
-}
-
-.linkEditor-group {
- background-color: $light-color-secondary;
- padding: 6px;
- margin: 3px 0;
- border-radius: 3px;
-
- .linkEditor-group-row {
- display: flex;
- margin-bottom: 3px;
-
- .linkEditor-group-row-label {
- margin-right: 6px;
- }
- }
-
- .linkEditor-metadata-row {
- display: flex;
- justify-content: space-between;
- margin-bottom: 6px;
-
- .linkEditor-error {
- border-color: red;
- }
-
- input {
- width: calc(50% - 16px);
- height: 20px;
- }
-
- button {
- width: 20px;
- height: 20px;
- margin-left: 3px;
- padding: 0;
- font-size: 10px;
- }
- }
-}
-
-
-.linkEditor-dropdown {
- width: 100%;
- position: relative;
- z-index: 999;
-
- input {
- width: 100%;
- }
-
- .linkEditor-options-wrapper {
- width: 100%;
- position: absolute;
- top: 19px;
- left: 0;
- display: flex;
- flex-direction: column;
- }
-
- .linkEditor-option {
- background-color: $light-color-secondary;
- border: 1px solid $intermediate-color;
- border-top: 0;
- padding: 3px;
- cursor: pointer;
-
- &:hover {
- background-color: lightgray;
- }
-
- &.onDown {
- background-color: gray;
- }
- }
-}
-
-.linkEditor-typeButton {
- background-color: transparent;
- color: $dark-color;
- width: 100%;
- height: 20px;
- padding: 0 3px;
- padding-bottom: 2px;
- text-align: left;
- text-transform: none;
- letter-spacing: normal;
- font-size: 12px;
- font-weight: bold;
-
- &:hover {
- background-color: $light-color;
- }
-}
-
-.linkEditor-group-buttons {
- height: 20px;
- display: flex;
- justify-content: flex-end;
- margin-top: 5px;
-
- .linkEditor-button {
- margin-left: 6px;
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
deleted file mode 100644
index ecb3e9db4..000000000
--- a/src/client/views/nodes/LinkEditor.tsx
+++ /dev/null
@@ -1,400 +0,0 @@
-import { observable, computed, action, trace } from "mobx";
-import React = require("react");
-import { observer } from "mobx-react";
-import './LinkEditor.scss';
-import { StrCast, Cast, FieldValue } from "../../../new_fields/Types";
-import { Doc } from "../../../new_fields/Doc";
-import { LinkManager } from "../../util/LinkManager";
-import { Docs } from "../../documents/Documents";
-import { Utils } from "../../../Utils";
-import { faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus } from '@fortawesome/free-solid-svg-icons';
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { SetupDrag } from "../../util/DragManager";
-import { SchemaHeaderField, RandomPastel } from "../../../new_fields/SchemaHeaderField";
-
-library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus);
-
-
-interface GroupTypesDropdownProps {
- groupType: string;
- setGroupType: (group: string) => void;
-}
-// this dropdown could be generalized
-@observer
-class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> {
- @observable private _searchTerm: string = this.props.groupType;
- @observable private _groupType: string = this.props.groupType;
- @observable private _isEditing: boolean = false;
-
- @action
- createGroup = (groupType: string): void => {
- this.props.setGroupType(groupType);
- LinkManager.Instance.addGroupType(groupType);
- }
-
- @action
- onChange = (val: string): void => {
- this._searchTerm = val;
- this._groupType = val;
- this._isEditing = true;
- }
-
- @action
- onKeyDown = (e: React.KeyboardEvent): void => {
- if (e.key === "Enter") {
- let allGroupTypes = Array.from(LinkManager.Instance.getAllGroupTypes());
- let groupOptions = allGroupTypes.filter(groupType => groupType.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
- let exactFound = groupOptions.findIndex(groupType => groupType.toUpperCase() === this._searchTerm.toUpperCase());
-
- if (exactFound > -1) {
- let groupType = groupOptions[exactFound];
- this.props.setGroupType(groupType);
- this._groupType = groupType;
- } else {
- this.createGroup(this._searchTerm);
- this._groupType = this._searchTerm;
- }
-
- this._searchTerm = this._groupType;
- this._isEditing = false;
- }
- }
-
- @action
- onOptionClick = (value: string, createNew: boolean): void => {
- if (createNew) {
- this.createGroup(this._searchTerm);
- this._groupType = this._searchTerm;
-
- } else {
- this.props.setGroupType(value);
- this._groupType = value;
-
- }
- this._searchTerm = this._groupType;
- this._isEditing = false;
- }
-
- @action
- onButtonPointerDown = (): void => {
- this._isEditing = true;
- }
-
- renderOptions = (): JSX.Element[] | JSX.Element => {
- if (this._searchTerm === "") return <></>;
-
- let allGroupTypes = Array.from(LinkManager.Instance.getAllGroupTypes());
- let groupOptions = allGroupTypes.filter(groupType => groupType.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
- let exactFound = groupOptions.findIndex(groupType => groupType.toUpperCase() === this._searchTerm.toUpperCase()) > -1;
-
- let options = groupOptions.map(groupType => {
- let ref = React.createRef<HTMLDivElement>();
- return <div key={groupType} ref={ref} className="linkEditor-option"
- onClick={() => this.onOptionClick(groupType, false)}>{groupType}</div>;
- });
-
- // if search term does not already exist as a group type, give option to create new group type
- if (!exactFound && this._searchTerm !== "") {
- let ref = React.createRef<HTMLDivElement>();
- options.push(<div key={""} ref={ref} className="linkEditor-option"
- onClick={() => this.onOptionClick(this._searchTerm, true)}>Define new "{this._searchTerm}" relationship</div>);
- }
-
- return options;
- }
-
- render() {
- if (this._isEditing || this._groupType === "") {
- return (
- <div className="linkEditor-dropdown">
- <input type="text" value={this._groupType} placeholder="Search for or create a new group"
- onChange={e => this.onChange(e.target.value)} onKeyDown={this.onKeyDown} autoFocus></input>
- <div className="linkEditor-options-wrapper">
- {this.renderOptions()}
- </div>
- </div >
- );
- } else {
- return <button className="linkEditor-typeButton" onClick={() => this.onButtonPointerDown()}>{this._groupType}</button>;
- }
- }
-}
-
-
-interface LinkMetadataEditorProps {
- id: string;
- groupType: string;
- mdDoc: Doc;
- mdKey: string;
- mdValue: string;
- changeMdIdKey: (id: string, newKey: string) => void;
-}
-@observer
-class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
- @observable private _key: string = this.props.mdKey;
- @observable private _value: string = this.props.mdValue;
- @observable private _keyError: boolean = false;
-
- @action
- setMetadataKey = (value: string): void => {
- let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);
-
- // don't allow user to create existing key
- let newIndex = groupMdKeys.findIndex(key => key.toUpperCase() === value.toUpperCase());
- if (newIndex > -1) {
- this._keyError = true;
- this._key = value;
- return;
- } else {
- this._keyError = false;
- }
-
- // set new value for key
- let currIndex = groupMdKeys.findIndex(key => {
- return StrCast(key).toUpperCase() === this._key.toUpperCase();
- });
- if (currIndex === -1) console.error("LinkMetadataEditor: key was not found");
- groupMdKeys[currIndex] = value;
-
- this.props.changeMdIdKey(this.props.id, value);
- this._key = value;
- LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, [...groupMdKeys]);
- }
-
- @action
- setMetadataValue = (value: string): void => {
- if (!this._keyError) {
- this._value = value;
- this.props.mdDoc[this._key] = value;
- }
- }
-
- @action
- removeMetadata = (): void => {
- let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);
-
- let index = groupMdKeys.findIndex(key => key.toUpperCase() === this._key.toUpperCase());
- if (index === -1) console.error("LinkMetadataEditor: key was not found");
- groupMdKeys.splice(index, 1);
-
- LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, groupMdKeys);
- this._key = "";
- }
-
- render() {
- return (
- <div className="linkEditor-metadata-row">
- <input className={this._keyError ? "linkEditor-error" : ""} type="text" value={this._key === "new key" ? "" : this._key} placeholder="key" onChange={e => this.setMetadataKey(e.target.value)}></input>:
- <input type="text" value={this._value} placeholder="value" onChange={e => this.setMetadataValue(e.target.value)}></input>
- <button onClick={() => this.removeMetadata()}><FontAwesomeIcon icon="times" size="sm" /></button>
- </div>
- );
- }
-}
-
-interface LinkGroupEditorProps {
- sourceDoc: Doc;
- linkDoc: Doc;
- groupDoc: Doc;
-}
-@observer
-export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
-
- private _metadataIds: Map<string, string> = new Map();
-
- constructor(props: LinkGroupEditorProps) {
- super(props);
-
- let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(StrCast(props.groupDoc.type));
- groupMdKeys.forEach(key => {
- this._metadataIds.set(key, Utils.GenerateGuid());
- });
- }
-
- @action
- setGroupType = (groupType: string): void => {
- this.props.groupDoc.type = groupType;
- }
-
- removeGroupFromLink = (groupType: string): void => {
- LinkManager.Instance.removeGroupFromAnchor(this.props.linkDoc, this.props.sourceDoc, groupType);
- }
-
- deleteGroup = (groupType: string): void => {
- LinkManager.Instance.deleteGroupType(groupType);
- }
-
- copyGroup = async (groupType: string): Promise<void> => {
- let sourceGroupDoc = this.props.groupDoc;
- const sourceMdDoc = await Cast(sourceGroupDoc.metadata, Doc);
- if (!sourceMdDoc) return;
-
- let destDoc = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
- // let destGroupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, destDoc);
- let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
-
- // create new metadata doc with copied kvp
- let destMdDoc = new Doc();
- destMdDoc.anchor1 = StrCast(sourceMdDoc.anchor2);
- destMdDoc.anchor2 = StrCast(sourceMdDoc.anchor1);
- keys.forEach(key => {
- let val = sourceMdDoc[key] === undefined ? "" : StrCast(sourceMdDoc[key]);
- destMdDoc[key] = val;
- });
-
- // create new group doc with new metadata doc
- let destGroupDoc = new Doc();
- destGroupDoc.type = groupType;
- destGroupDoc.metadata = destMdDoc;
-
- if (destDoc) {
- LinkManager.Instance.addGroupToAnchor(this.props.linkDoc, destDoc, destGroupDoc, true);
- }
- }
-
- @action
- addMetadata = (groupType: string): void => {
- this._metadataIds.set("new key", Utils.GenerateGuid());
- let mdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
- // only add "new key" if there is no other key with value "new key"; prevents spamming
- if (mdKeys.indexOf("new key") === -1) mdKeys.push("new key");
- LinkManager.Instance.setMetadataKeysForGroup(groupType, mdKeys);
- }
-
- // for key rendering purposes
- changeMdIdKey = (id: string, newKey: string) => {
- this._metadataIds.set(newKey, id);
- }
-
- renderMetadata = (): JSX.Element[] => {
- let metadata: Array<JSX.Element> = [];
- let groupDoc = this.props.groupDoc;
- const mdDoc = FieldValue(Cast(groupDoc.metadata, Doc));
- if (!mdDoc) {
- return [];
- }
- let groupType = StrCast(groupDoc.type);
- let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
-
- groupMdKeys.forEach((key) => {
- let val = StrCast(mdDoc[key]);
- metadata.push(
- <LinkMetadataEditor key={"mded-" + this._metadataIds.get(key)} id={this._metadataIds.get(key)!} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={val} changeMdIdKey={this.changeMdIdKey} />
- );
- });
- return metadata;
- }
-
- viewGroupAsTable = (groupType: string): JSX.Element => {
- let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
- let index = keys.indexOf("");
- if (index > -1) keys.splice(index, 1);
- let cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb"));
- let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
- let createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
- let ref = React.createRef<HTMLDivElement>();
- return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>;
- }
-
- render() {
- let groupType = StrCast(this.props.groupDoc.type);
- // if ((groupType && LinkManager.Instance.getMetadataKeysInGroup(groupType).length > 0) || groupType === "") {
- let buttons;
- if (groupType === "") {
- buttons = (
- <>
- <button className="linkEditor-button" disabled={true} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button>
- <button className="linkEditor-button" disabled title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
- <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
- <button className="linkEditor-button" disabled title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
- <button className="linkEditor-button" disabled title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button>
- </>
- );
- } else {
- buttons = (
- <>
- <button className="linkEditor-button" onClick={() => this.addMetadata(groupType)} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button>
- <button className="linkEditor-button" onClick={() => this.copyGroup(groupType)} title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
- <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
- <button className="linkEditor-button" onClick={() => this.deleteGroup(groupType)} title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
- {this.viewGroupAsTable(groupType)}
- </>
- );
- }
- return (
- <div className="linkEditor-group">
- <div className="linkEditor-group-row ">
- <p className="linkEditor-group-row-label">type:</p>
- <GroupTypesDropdown groupType={groupType} setGroupType={this.setGroupType} />
- </div>
- {this.renderMetadata().length > 0 ? <p className="linkEditor-group-row-label">metadata:</p> : <></>}
- {this.renderMetadata()}
- <div className="linkEditor-group-buttons">
- {buttons}
- </div>
- </div>
- );
- }
-}
-
-
-interface LinkEditorProps {
- sourceDoc: Doc;
- linkDoc: Doc;
- showLinks: () => void;
-}
-@observer
-export class LinkEditor extends React.Component<LinkEditorProps> {
-
- @action
- deleteLink = (): void => {
- LinkManager.Instance.deleteLink(this.props.linkDoc);
- this.props.showLinks();
- }
-
- @action
- addGroup = (): void => {
- // create new metadata document for group
- let mdDoc = new Doc();
- mdDoc.anchor1 = this.props.sourceDoc.title;
- let opp = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
- if (opp) {
- mdDoc.anchor2 = opp.title;
- }
-
- // create new group document
- let groupDoc = new Doc();
- groupDoc.type = "";
- groupDoc.metadata = mdDoc;
-
- LinkManager.Instance.addGroupToAnchor(this.props.linkDoc, this.props.sourceDoc, groupDoc);
- }
-
- render() {
- let destination = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
-
- let groupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc);
- let groups = groupList.map(groupDoc => {
- return <LinkGroupEditor key={"gred-" + StrCast(groupDoc.type)} linkDoc={this.props.linkDoc} sourceDoc={this.props.sourceDoc} groupDoc={groupDoc} />;
- });
-
- if (destination) {
- return (
- <div className="linkEditor">
- <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>
- <div className="linkEditor-info">
- <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto!.title}</b></p>
- <button className="linkEditor-button" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button>
- </div>
- <div className="linkEditor-groupsLabel">
- <b>Relationships:</b>
- <button className="linkEditor-button" onClick={() => this.addGroup()} title=" Add Group"><FontAwesomeIcon icon="plus" size="sm" /></button>
- </div>
- {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>}
- </div>
-
- );
- }
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss
deleted file mode 100644
index a4018bd2d..000000000
--- a/src/client/views/nodes/LinkMenu.scss
+++ /dev/null
@@ -1,137 +0,0 @@
-@import "../globalCssVariables";
-
-.linkMenu {
- width: 100%;
- height: auto;
-}
-
-.linkMenu-list {
- max-height: 200px;
- overflow-y: scroll;
-}
-
-.linkMenu-group {
- border-bottom: 0.5px solid lightgray;
- padding: 5px 0;
-
-
- &:last-child {
- border-bottom: none;
- }
-
- .linkMenu-group-name {
- display: flex;
-
- &:hover {
- p {
- background-color: lightgray;
- }
- p.expand-one {
- width: calc(100% - 26px);
- }
- .linkEditor-tableButton {
- display: block;
- }
- }
-
- p {
- width: 100%;
- padding: 4px 6px;
- line-height: 12px;
- border-radius: 5px;
- font-weight: bold;
- }
-
- .linkEditor-tableButton {
- display: none;
- }
- }
-}
-
-.linkMenu-item {
- // border-top: 0.5px solid $main-accent;
- position: relative;
- display: flex;
- font-size: 12px;
-
-
- .link-name {
- position: relative;
-
- p {
- padding: 4px 6px;
- line-height: 12px;
- border-radius: 5px;
- overflow-wrap: break-word;
- }
- }
-
- .linkMenu-item-content {
- width: 100%;
- }
-
- .link-metadata {
- padding: 0 10px 0 16px;
- margin-bottom: 4px;
- color: $main-accent;
- font-style: italic;
- font-size: 10.5px;
- }
-
- &:hover {
- .linkMenu-item-buttons {
- display: flex;
- }
- .linkMenu-item-content {
- &.expand-two p {
- width: calc(100% - 52px);
- background-color: lightgray;
- }
- &.expand-three p {
- width: calc(100% - 84px);
- background-color: lightgray;
- }
- }
- }
-}
-
-.linkMenu-item-buttons {
- display: none;
- position: absolute;
- top: 50%;
- right: 0;
- transform: translateY(-50%);
-
- .button {
- width: 20px;
- height: 20px;
- margin: 0;
- margin-right: 6px;
- border-radius: 50%;
- cursor: pointer;
- pointer-events: auto;
- background-color: $dark-color;
- color: $light-color;
- font-size: 65%;
- transition: transform 0.2s;
- text-align: center;
- position: relative;
-
- .fa-icon {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- }
-
- &:last-child {
- margin-right: 0;
- }
- &:hover {
- background: $main-accent;
- }
- }
-}
-
-
-
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
deleted file mode 100644
index 1a4af04f8..000000000
--- a/src/client/views/nodes/LinkMenu.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import { action, observable } from "mobx";
-import { observer } from "mobx-react";
-import { DocumentView } from "./DocumentView";
-import { LinkEditor } from "./LinkEditor";
-import './LinkMenu.scss';
-import React = require("react");
-import { Doc } from "../../../new_fields/Doc";
-import { LinkManager } from "../../util/LinkManager";
-import { LinkMenuGroup } from "./LinkMenuGroup";
-import { faTrash } from '@fortawesome/free-solid-svg-icons';
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-
-library.add(faTrash);
-import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
-import { Id } from "../../../new_fields/FieldSymbols";
-import { DocumentType } from "../../documents/Documents";
-
-interface Props {
- docView: DocumentView;
- changeFlyout: () => void;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
-}
-
-@observer
-export class LinkMenu extends React.Component<Props> {
-
- @observable private _editingLink?: Doc;
-
- @action
- componentWillReceiveProps() {
- this._editingLink = undefined;
- }
-
- clearAllLinks = () => {
- LinkManager.Instance.deleteAllLinksOnAnchor(this.props.docView.props.Document);
- }
-
- renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => {
- let linkItems: Array<JSX.Element> = [];
- groups.forEach((group, groupType) => {
- linkItems.push(
- <LinkMenuGroup
- key={groupType}
- sourceDoc={this.props.docView.props.Document}
- group={group}
- groupType={groupType}
- showEditor={action((linkDoc: Doc) => this._editingLink = linkDoc)}
- addDocTab={this.props.addDocTab} />
- );
- });
-
- // if source doc has no links push message
- if (linkItems.length === 0) linkItems.push(<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>);
-
- return linkItems;
- }
-
- render() {
- let sourceDoc = this.props.docView.props.Document;
- let groups: Map<string, Doc[]> = LinkManager.Instance.getRelatedGroupedLinks(sourceDoc);
- if (this._editingLink === undefined) {
- return (
- <div className="linkMenu">
- <button className="linkEditor-button linkEditor-clearButton" onClick={() => this.clearAllLinks()} title="Clear all links"><FontAwesomeIcon icon="trash" size="sm" /></button>
- {/* <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> */}
- <div className="linkMenu-list">
- {this.renderAllGroups(groups)}
- </div>
- </div>
- );
- } else {
- return (
- <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor>
- );
- }
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/nodes/LinkMenuGroup.tsx
deleted file mode 100644
index e04044266..000000000
--- a/src/client/views/nodes/LinkMenuGroup.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { action, observable } from "mobx";
-import { observer } from "mobx-react";
-import { DocumentView } from "./DocumentView";
-import { LinkMenuItem } from "./LinkMenuItem";
-import { LinkEditor } from "./LinkEditor";
-import './LinkMenu.scss';
-import React = require("react");
-import { Doc, DocListCast } from "../../../new_fields/Doc";
-import { Id } from "../../../new_fields/FieldSymbols";
-import { LinkManager } from "../../util/LinkManager";
-import { DragLinksAsDocuments, DragManager, SetupDrag } from "../../util/DragManager";
-import { emptyFunction } from "../../../Utils";
-import { Docs } from "../../documents/Documents";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { UndoManager } from "../../util/UndoManager";
-import { StrCast } from "../../../new_fields/Types";
-import { SchemaHeaderField, RandomPastel } from "../../../new_fields/SchemaHeaderField";
-
-interface LinkMenuGroupProps {
- sourceDoc: Doc;
- group: Doc[];
- groupType: string;
- showEditor: (linkDoc: Doc) => void;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
-}
-
-@observer
-export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
-
- private _drag = React.createRef<HTMLDivElement>();
- private _table = React.createRef<HTMLDivElement>();
-
- onLinkButtonDown = (e: React.PointerEvent): void => {
- e.stopPropagation();
- document.removeEventListener("pointermove", this.onLinkButtonMoved);
- document.addEventListener("pointermove", this.onLinkButtonMoved);
- document.removeEventListener("pointerup", this.onLinkButtonUp);
- document.addEventListener("pointerup", this.onLinkButtonUp);
- }
-
- onLinkButtonUp = (e: PointerEvent): void => {
- document.removeEventListener("pointermove", this.onLinkButtonMoved);
- document.removeEventListener("pointerup", this.onLinkButtonUp);
- e.stopPropagation();
- }
-
-
- onLinkButtonMoved = async (e: PointerEvent) => {
- UndoManager.RunInBatch(() => {
- if (this._drag.current !== null && (e.movementX > 1 || e.movementY > 1)) {
- document.removeEventListener("pointermove", this.onLinkButtonMoved);
- document.removeEventListener("pointerup", this.onLinkButtonUp);
-
- let draggedDocs = this.props.group.map(linkDoc => {
- let opp = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
- if (opp) return opp;
- }) as Doc[];
- let dragData = new DragManager.DocumentDragData(draggedDocs, draggedDocs.map(d => undefined));
-
- DragManager.StartLinkedDocumentDrag([this._drag.current], dragData, e.x, e.y, {
- handlers: {
- dragComplete: action(emptyFunction),
- },
- hideSource: false
- });
- }
- }, "drag links");
- e.stopPropagation();
- }
-
- viewGroupAsTable = (groupType: string): JSX.Element => {
- let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
- let index = keys.indexOf("");
- if (index > -1) keys.splice(index, 1);
- let cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb"));
- let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
- let createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
- let ref = React.createRef<HTMLDivElement>();
- return <div ref={ref}><button className="linkEditor-button linkEditor-tableButton" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>;
- }
-
- render() {
- let groupItems = this.props.group.map(linkDoc => {
- let destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
- if (destination && this.props.sourceDoc) {
- return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]} groupType={this.props.groupType}
- addDocTab={this.props.addDocTab}
- linkDoc={linkDoc} sourceDoc={this.props.sourceDoc} destinationDoc={destination} showEditor={this.props.showEditor} />;
- }
- });
-
- return (
- <div className="linkMenu-group">
- <div className="linkMenu-group-name">
- <p ref={this._drag} onPointerDown={this.onLinkButtonDown}
- className={this.props.groupType === "*" || this.props.groupType === "" ? "" : "expand-one"} > {this.props.groupType}:</p>
- {this.props.groupType === "*" || this.props.groupType === "" ? <></> : this.viewGroupAsTable(this.props.groupType)}
- </div>
- <div className="linkMenu-group-wrapper">
- {groupItems}
- </div>
- </div >
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx
deleted file mode 100644
index a119eb39b..000000000
--- a/src/client/views/nodes/LinkMenuItem.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEdit, faEye, faTimes, faArrowRight, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { observer } from "mobx-react";
-import { DocumentManager } from "../../util/DocumentManager";
-import { undoBatch } from "../../util/UndoManager";
-import './LinkMenu.scss';
-import React = require("react");
-import { Doc, DocListCastAsync } from '../../../new_fields/Doc';
-import { StrCast, Cast, FieldValue, NumCast } from '../../../new_fields/Types';
-import { observable, action } from 'mobx';
-import { LinkManager } from '../../util/LinkManager';
-import { DragLinkAsDocument } from '../../util/DragManager';
-import { CollectionDockingView } from '../collections/CollectionDockingView';
-import { SelectionManager } from '../../util/SelectionManager';
-library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp);
-
-
-interface LinkMenuItemProps {
- groupType: string;
- linkDoc: Doc;
- sourceDoc: Doc;
- destinationDoc: Doc;
- showEditor: (linkDoc: Doc) => void;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
-}
-
-@observer
-export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
- private _drag = React.createRef<HTMLDivElement>();
- @observable private _showMore: boolean = false;
- @action toggleShowMore() { this._showMore = !this._showMore; }
-
- @undoBatch
- onFollowLink = async (e: React.PointerEvent): Promise<void> => {
- e.stopPropagation();
- e.persist();
- let jumpToDoc = this.props.destinationDoc;
- let pdfDoc = FieldValue(Cast(this.props.destinationDoc, Doc));
- if (pdfDoc) {
- jumpToDoc = pdfDoc;
- }
- let proto = Doc.GetProto(this.props.linkDoc);
- let targetContext = await Cast(proto.targetContext, Doc);
- let sourceContext = await Cast(proto.sourceContext, Doc);
- let self = this;
-
-
- let dockingFunc = (document: Doc) => { this.props.addDocTab(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
- if (e.ctrlKey) {
- dockingFunc = (document: Doc) => CollectionDockingView.Instance.AddRightSplit(document, undefined);
- }
-
- if (this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, async document => dockingFunc(document), undefined, targetContext!);
- }
- else if (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => dockingFunc(sourceContext!));
- }
- else if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((this.props.destinationDoc === self.props.linkDoc.anchor2 ? self.props.linkDoc.anchor2Page : self.props.linkDoc.anchor1Page)));
- }
- else {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, dockingFunc);
- }
- }
-
- onEdit = (e: React.PointerEvent): void => {
- e.stopPropagation();
- this.props.showEditor(this.props.linkDoc);
- }
-
- renderMetadata = (): JSX.Element => {
- let groups = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc);
- let index = groups.findIndex(groupDoc => StrCast(groupDoc.type).toUpperCase() === this.props.groupType.toUpperCase());
- let groupDoc = index > -1 ? groups[index] : undefined;
-
- let mdRows: Array<JSX.Element> = [];
- if (groupDoc) {
- let mdDoc = Cast(groupDoc.metadata, Doc, null);
- if (mdDoc) {
- let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
- mdRows = keys.map(key => {
- return (<div key={key} className="link-metadata-row"><b>{key}</b>: {StrCast(mdDoc[key])}</div>);
- });
- }
- }
-
- return (<div className="link-metadata">{mdRows}</div>);
- }
-
- onLinkButtonDown = (e: React.PointerEvent): void => {
- e.stopPropagation();
- document.removeEventListener("pointermove", this.onLinkButtonMoved);
- document.addEventListener("pointermove", this.onLinkButtonMoved);
- document.removeEventListener("pointerup", this.onLinkButtonUp);
- document.addEventListener("pointerup", this.onLinkButtonUp);
- }
-
- onLinkButtonUp = (e: PointerEvent): void => {
- document.removeEventListener("pointermove", this.onLinkButtonMoved);
- document.removeEventListener("pointerup", this.onLinkButtonUp);
- e.stopPropagation();
- }
-
- onLinkButtonMoved = async (e: PointerEvent) => {
- if (this._drag.current !== null && (e.movementX > 1 || e.movementY > 1)) {
- document.removeEventListener("pointermove", this.onLinkButtonMoved);
- document.removeEventListener("pointerup", this.onLinkButtonUp);
-
- DragLinkAsDocument(this._drag.current, e.x, e.y, this.props.linkDoc, this.props.sourceDoc);
- }
- e.stopPropagation();
- }
-
- render() {
-
- let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
- let canExpand = keys ? keys.length > 0 : false;
-
- return (
- <div className="linkMenu-item">
- <div className={canExpand ? "linkMenu-item-content expand-three" : "linkMenu-item-content expand-two"}>
- <div className="link-name">
- <p ref={this._drag} onPointerDown={this.onLinkButtonDown}>{StrCast(this.props.destinationDoc.title)}</p>
- <div className="linkMenu-item-buttons">
- {canExpand ? <div title="Show more" className="button" onPointerDown={() => this.toggleShowMore()}>
- <FontAwesomeIcon className="fa-icon" icon={this._showMore ? "chevron-up" : "chevron-down"} size="sm" /></div> : <></>}
- <div title="Edit link" className="button" onPointerDown={this.onEdit}><FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div>
- <div title="Follow link" className="button" onPointerDown={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
- </div>
- </div>
- {this._showMore ? this.renderMetadata() : <></>}
- </div>
-
- </div >
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 6450cb826..df35b603c 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -33,7 +33,9 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
@observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>;
@computed get containingCollectionDocument() { return this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document; }
- @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
+ @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+
+
@computed get fieldExtensionDoc() { return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); }
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
@@ -48,7 +50,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
componentDidMount() {
this.props.setPdfBox && this.props.setPdfBox(this);
- const pdfUrl = Cast(this.props.Document.data, PdfField);
+ const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
if (pdfUrl instanceof PdfField) {
Pdfjs.getDocument(pdfUrl.url.pathname).promise.then(pdf => runInAction(() => this._pdf = pdf));
}
@@ -78,7 +80,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
@action
public GotoPage(p: number) {
- if (p > 0 && p <= NumCast(this.props.Document.numPages)) {
+ if (p > 0 && p <= NumCast(this.dataDoc.numPages)) {
this.props.Document.curPage = p;
this.props.Document.panY = (p - 1) * NumCast(this.dataDoc.nativeHeight);
}
@@ -87,7 +89,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
@action
public ForwardPage() {
let cp = this.GetPage() + 1;
- if (cp <= NumCast(this.props.Document.numPages)) {
+ if (cp <= NumCast(this.dataDoc.numPages)) {
this.props.Document.curPage = cp;
this.props.Document.panY = (cp - 1) * NumCast(this.dataDoc.nativeHeight);
}
@@ -185,11 +187,12 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
}
}
+
render() {
- const pdfUrl = Cast(this.props.Document.data, PdfField);
+ const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
let classname = "pdfBox-cont" + (this.props.active() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : "");
return (!(pdfUrl instanceof PdfField) || !this._pdf ?
- <div>{`pdf, ${this.props.Document.data}, not found`}</div> :
+ <div>{`pdf, ${this.dataDoc[this.props.fieldKey]}, not found`}</div> :
<div className={classname}
onScroll={this.onScroll}
style={{ marginTop: `${this.containingCollectionDocument ? NumCast(this.containingCollectionDocument.panY) : 0}px` }}
@@ -198,7 +201,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
<PDFViewer pdf={this._pdf} url={pdfUrl.url.pathname} active={this.props.active} scrollTo={this.scrollTo} loaded={this.loaded} panY={NumCast(this.props.Document.panY)}
Document={this.props.Document} DataDoc={this.props.DataDoc}
addDocTab={this.props.addDocTab} setPanY={this.setPanY}
- addDocument={this.props.addDocument}
+ pinToPres={this.props.pinToPres} addDocument={this.props.addDocument}
fieldKey={this.props.fieldKey} fieldExtensionDoc={this.fieldExtensionDoc} />
{this.settingsPanel()}
</div>);
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
new file mode 100644
index 000000000..e376fbddb
--- /dev/null
+++ b/src/client/views/nodes/PresBox.tsx
@@ -0,0 +1,529 @@
+import React = require("react");
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faTimes } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, observable, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { List } from "../../../new_fields/List";
+import { listSpec } from "../../../new_fields/Schema";
+import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
+import { Utils } from "../../../Utils";
+import { DocumentManager } from "../../util/DocumentManager";
+import { undoBatch } from "../../util/UndoManager";
+import PresentationElement from "../presentationview/PresentationElement";
+import PresentationViewList from "../presentationview/PresentationList";
+import "../presentationview/PresentationView.scss";
+import { FieldView, FieldViewProps } from './FieldView';
+import { ContextMenu } from "../ContextMenu";
+
+library.add(faArrowLeft);
+library.add(faArrowRight);
+library.add(faPlay);
+library.add(faStop);
+library.add(faPlus);
+library.add(faTimes);
+library.add(faMinus);
+library.add(faEdit);
+
+
+export interface PresViewProps {
+ Documents: List<Doc>;
+}
+
+const expandedWidth = 450;
+
+@observer
+export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
+
+
+ public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(PresBox, fieldKey); }
+
+ public static Instance: PresBox;
+
+ //Keeping track of the doc for the current presentation -- bcz: keeping a list of current presentations shouldn't be needed. Let users create them, store them, as they see fit.
+ @computed get curPresentation() { return this.props.Document; }
+
+ //mapping from docs to their rendered component
+ @observable presElementsMappings: Map<Doc, PresentationElement> = new Map();
+ //variable that holds all the docs in the presentation
+ @observable childrenDocs: Doc[] = [];
+ //variable to hold if presentation is started
+ @observable presStatus: boolean = false;
+ //Mapping of guids to presentations.
+ @observable presentationsMapping: Map<String, Doc> = new Map();
+ //Mapping of presentations to guid, so that select option values can be given.
+ @observable presentationsKeyMapping: Map<Doc, String> = new Map();
+ //Variable to keep track of guid of the current presentation
+ @observable currentSelectedPresValue: string | undefined;
+ //A flag to keep track if title input is open, which is used in rendering.
+ @observable PresTitleInputOpen: boolean = false;
+ //Variable that holds reference to title input, so that new presentations get titles assigned.
+ @observable titleInputElement: HTMLInputElement | undefined;
+ @observable PresTitleChangeOpen: boolean = false;
+
+ @observable opacity = 1;
+ @observable persistOpacity = true;
+ @observable labelOpacity = 0;
+ @observable presMode = false;
+
+ @observable public static CurrentPresentation: PresBox;
+
+ //initilize class variables
+ constructor(props: FieldViewProps) {
+ super(props);
+ runInAction(() => PresBox.CurrentPresentation = this);
+ }
+
+ @action
+ toggle = (forcedValue: boolean | undefined) => {
+ if (forcedValue !== undefined) {
+ this.curPresentation.width = forcedValue ? expandedWidth : 0;
+ } else {
+ this.curPresentation.width = this.curPresentation.width === expandedWidth ? 0 : expandedWidth;
+ }
+ }
+
+ //Second lifecycle function that gets called when component mounts. It makes sure toS
+ //get the back-up information from previous session for the current presentation.
+ async componentDidMount() {
+ this.setPresentationBackUps();
+ }
+
+
+ /**
+ * The function that retrieves the backUps for the current Presentation if present,
+ * otherwise initializes.
+ */
+ setPresentationBackUps = async () => {
+ //storing the presentation status,ie. whether it was stopped or playing
+ let presStatusBackUp = BoolCast(this.curPresentation.presStatus);
+ runInAction(() => this.presStatus = presStatusBackUp);
+ }
+
+ //observable means render is re-called every time variable is changed
+ @observable
+ collapsed: boolean = false;
+ next = async () => {
+ const current = NumCast(this.curPresentation.selectedDoc);
+ //asking to get document at current index
+ let docAtCurrentNext = await this.getDocAtIndex(current + 1);
+ if (docAtCurrentNext === undefined) {
+ return;
+ }
+ let nextSelected = current + 1;
+
+ let presDocs = DocListCast(this.curPresentation.data);
+ for (; nextSelected < presDocs.length - 1; nextSelected++) {
+ if (!this.presElementsMappings.get(presDocs[nextSelected + 1])!.props.document.groupButton) {
+ break;
+ }
+ }
+
+ this.gotoDocument(nextSelected, current);
+
+ }
+ back = async () => {
+ const current = NumCast(this.curPresentation.selectedDoc);
+ //requesting for the doc at current index
+ let docAtCurrent = await this.getDocAtIndex(current);
+ if (docAtCurrent === undefined) {
+ return;
+ }
+
+ //asking for its presentation id.
+ let curPresId = StrCast(docAtCurrent.presentId);
+ let prevSelected = current;
+ let zoomOut: boolean = false;
+
+ //checking if this presentation id is mapped to a group, if so chosing the first element in group
+ let presDocs = DocListCast(this.curPresentation.data);
+ let currentsArray: Doc[] = [];
+ for (; prevSelected > 0 && presDocs[prevSelected].groupButton; prevSelected--) {
+ currentsArray.push(presDocs[prevSelected]);
+ }
+ prevSelected = Math.max(0, prevSelected - 1);
+
+ //checking if any of the group members had used zooming in
+ currentsArray.forEach((doc: Doc) => {
+ //let presElem: PresentationElement | undefined = this.presElementsMappings.get(doc);
+ if (this.presElementsMappings.get(doc)!.props.document.showButton) {
+ zoomOut = true;
+ return;
+ }
+ });
+
+
+ // if a group set that flag to zero or a single element
+ //If so making sure to zoom out, which goes back to state before zooming action
+ if (current > 0) {
+ if (zoomOut || this.presElementsMappings.get(docAtCurrent)!.showButton) {
+ let prevScale = NumCast(this.childrenDocs[prevSelected].viewScale, null);
+ let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[current]);
+ if (prevScale !== undefined) {
+ if (prevScale !== curScale) {
+ DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale);
+ }
+ }
+ }
+ }
+ this.gotoDocument(prevSelected, current);
+
+ }
+
+ /**
+ * 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:
+ * Hide Until Presented, Hide After Presented, Fade After Presented
+ */
+ showAfterPresented = (index: number) => {
+ this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => {
+ //the order of cases is aligned based on priority
+ if (presElem.props.document.hideTillShownButton) {
+ if (this.childrenDocs.indexOf(key) <= index) {
+ key.opacity = 1;
+ }
+ }
+ if (presElem.props.document.hideAfterButton) {
+ if (this.childrenDocs.indexOf(key) < index) {
+ key.opacity = 0;
+ }
+ }
+ if (presElem.props.document.fadeButton) {
+ if (this.childrenDocs.indexOf(key) < index) {
+ key.opacity = 0.5;
+ }
+ }
+ });
+ }
+
+ /**
+ * This is the method that checks for the actions that need to be performed
+ * before the document has been presented, which involves 3 button options:
+ * Hide Until Presented, Hide After Presented, Fade After Presented
+ */
+ hideIfNotPresented = (index: number) => {
+ this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => {
+ //the order of cases is aligned based on priority
+
+ if (presElem.props.document.hideAfterButton) {
+ if (this.childrenDocs.indexOf(key) >= index) {
+ key.opacity = 1;
+ }
+ }
+ if (presElem.props.document.fadeButton) {
+ if (this.childrenDocs.indexOf(key) >= index) {
+ key.opacity = 1;
+ }
+ }
+ if (presElem.props.document.hideTillShownButton) {
+ if (this.childrenDocs.indexOf(key) > index) {
+ key.opacity = 0;
+ }
+ }
+ });
+ }
+
+ /**
+ * This method makes sure that cursor navigates to the element that
+ * has the option open and last in the group. If not in the group, and it has
+ * te option open, navigates to that element.
+ */
+ navigateToElement = async (curDoc: Doc, fromDoc: number) => {
+ let docToJump: Doc = curDoc;
+ let willZoom: boolean = false;
+
+
+ let presDocs = DocListCast(this.curPresentation.data);
+ let nextSelected = presDocs.indexOf(curDoc);
+ let currentDocGroups: Doc[] = [];
+ for (; nextSelected < presDocs.length - 1; nextSelected++) {
+ if (!this.presElementsMappings.get(presDocs[nextSelected + 1])!.props.document.groupButton) {
+ break;
+ }
+ currentDocGroups.push(presDocs[nextSelected]);
+ }
+
+ currentDocGroups.forEach((doc: Doc, index: number) => {
+ if (this.presElementsMappings.get(doc)!.navButton) {
+ docToJump = doc;
+ willZoom = false;
+ }
+ if (this.presElementsMappings.get(doc)!.showButton) {
+ docToJump = doc;
+ willZoom = true;
+ }
+ });
+
+ //docToJump stayed same meaning, it was not in the group or was the last element in the group
+ if (docToJump === curDoc) {
+ //checking if curDoc has navigation open
+ if (this.presElementsMappings.get(curDoc)!.navButton) {
+ DocumentManager.Instance.jumpToDocument(curDoc, false);
+ } else if (this.presElementsMappings.get(curDoc)!.showButton) {
+ let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]);
+ //awaiting jump so that new scale can be found, since jumping is async
+ await DocumentManager.Instance.jumpToDocument(curDoc, true);
+ let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
+ curDoc.viewScale = newScale;
+
+ //saving the scale user was on before zooming in
+ if (curScale !== 1) {
+ this.childrenDocs[fromDoc].viewScale = curScale;
+ }
+
+ }
+ return;
+ }
+ let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]);
+
+ //awaiting jump so that new scale can be found, since jumping is async
+ await DocumentManager.Instance.jumpToDocument(docToJump, willZoom);
+ let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
+ curDoc.viewScale = newScale;
+ //saving the scale that user was on
+ if (curScale !== 1) {
+ this.childrenDocs[fromDoc].viewScale = curScale;
+ }
+
+ }
+
+ /**
+ * Async function that supposedly return the doc that is located at given index.
+ */
+ getDocAtIndex = async (index: number) => {
+ const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+ if (!list) {
+ return undefined;
+ }
+ if (index < 0 || index >= list.length) {
+ return undefined;
+ }
+
+ this.curPresentation.selectedDoc = index;
+ //awaiting async call to finish to get Doc instance
+ const doc = await list[index];
+ return doc;
+ }
+
+ /**
+ * The function that removes a doc from a presentation. It also makes sure to
+ * do necessary updates to backUps and mappings stored locally.
+ */
+ @action
+ public RemoveDoc = async (index: number) => {
+ const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+ if (value) {
+ let removedDoc = await value.splice(index, 1)[0];
+
+ //removing the Presentation Element stored for it
+ this.presElementsMappings.delete(removedDoc);
+
+ }
+ }
+
+ public removeDocByRef = (doc: Doc) => {
+ let indexOfDoc = this.childrenDocs.indexOf(doc);
+ const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+ if (value) {
+ value.splice(indexOfDoc, 1)[0];
+ }
+ //this.RemoveDoc(indexOfDoc, true);
+ if (indexOfDoc !== - 1) {
+ return true;
+ }
+ return false;
+ }
+
+ //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.
+ @action
+ public gotoDocument = async (index: number, fromDoc: number) => {
+ Doc.UnBrushAllDocs();
+ const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+ if (!list) {
+ return;
+ }
+ if (index < 0 || index >= list.length) {
+ return;
+ }
+ this.curPresentation.selectedDoc = index;
+
+ if (!this.presStatus) {
+ this.presStatus = true;
+ this.startPresentation(index);
+ }
+
+ const doc = await list[index];
+ if (this.presStatus) {
+ this.navigateToElement(doc, fromDoc);
+ this.hideIfNotPresented(index);
+ this.showAfterPresented(index);
+ }
+ }
+ //Function that sets the store of the children docs.
+ @action
+ setChildrenDocs = (docList: Doc[]) => {
+ this.childrenDocs = docList;
+ }
+
+ //The function that is called to render the play or pause button depending on
+ //if presentation is running or not.
+ renderPlayPauseButton = () => {
+ if (this.presStatus) {
+ return <button title="Reset Presentation" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="stop" /></button>;
+ } else {
+ return <button title="Start Presentation From Start" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="play" /></button>;
+ }
+ }
+
+ //The function that starts or resets presentaton functionally, depending on status flag.
+ @action
+ startOrResetPres = () => {
+ if (this.presStatus) {
+ this.resetPresentation();
+ } else {
+ this.presStatus = true;
+ this.startPresentation(0);
+ const current = NumCast(this.curPresentation.selectedDoc);
+ this.gotoDocument(0, current);
+ }
+ this.curPresentation.presStatus = this.presStatus;
+ }
+
+ //The function that resets the presentation by removing every action done by it. It also
+ //stops the presentaton.
+ @action
+ resetPresentation = () => {
+ this.childrenDocs.forEach((doc: Doc) => {
+ doc.opacity = 1;
+ doc.viewScale = 1;
+ });
+ this.curPresentation.selectedDoc = 0;
+ this.presStatus = false;
+ this.curPresentation.presStatus = this.presStatus;
+ if (this.childrenDocs.length === 0) {
+ return;
+ }
+ DocumentManager.Instance.zoomIntoScale(this.childrenDocs[0], 1);
+ }
+
+
+ //The function that starts the presentation, also checking if actions should be applied
+ //directly at start.
+ startPresentation = (startIndex: number) => {
+ this.presElementsMappings.forEach((component: PresentationElement, doc: Doc) => {
+ if (component.props.document.hideTillShownButton) {
+ if (this.childrenDocs.indexOf(doc) > startIndex) {
+ doc.opacity = 0;
+ }
+
+ }
+ if (component.props.document.hideAfterButton) {
+ if (this.childrenDocs.indexOf(doc) < startIndex) {
+ doc.opacity = 0;
+ }
+ }
+ if (component.props.document.fadeButton) {
+ if (this.childrenDocs.indexOf(doc) < startIndex) {
+ doc.opacity = 0.5;
+ }
+ }
+
+ });
+
+ }
+
+
+ /**
+ * The function that is called to render either select for presentations, or title inputting.
+ */
+ renderSelectOrPresSelection = () => {
+ if (this.PresTitleInputOpen || this.PresTitleChangeOpen) {
+ return <input ref={(e) => this.titleInputElement = e!} type="text" className="presentationView-title" placeholder="Enter Name!" onKeyDown={this.submitPresentationTitle} />;
+ } else {
+ return (null);
+ }
+ }
+
+ /**
+ * The function that is called on enter press of title input. It gives the
+ * new presentation the title user entered. If nothing is entered, gives a default title.
+ */
+ @action
+ submitPresentationTitle = (e: React.KeyboardEvent) => {
+ if (e.keyCode === 13) {
+ let presTitle = this.titleInputElement!.value;
+ this.titleInputElement!.value = "";
+ if (this.PresTitleChangeOpen) {
+ this.PresTitleChangeOpen = false;
+ this.changePresentationTitle(presTitle);
+ }
+ }
+ }
+ /**
+ * The function that is called to change title of presentation to what user entered.
+ */
+ @undoBatch
+ changePresentationTitle = (newTitle: string) => {
+ if (newTitle === "") {
+ return;
+ }
+ this.curPresentation.title = newTitle;
+ }
+
+ addPressElem = (keyDoc: Doc, elem: PresentationElement) => {
+ this.presElementsMappings.set(keyDoc, elem);
+ }
+
+ minimize = undoBatch(action(() => {
+ this.presMode = true;
+ this.props.addDocTab && this.props.addDocTab(this.props.Document, this.props.DataDoc, "close");
+ }));
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ ContextMenu.Instance.addItem({ description: "Make Current Presentation", event: action(() => Doc.UserDoc().curPresentation = this.props.Document), icon: "asterisk" });
+ }
+
+ render() {
+
+ let width = "100%"; //NumCast(this.curPresentation.width)
+ return (
+ <div className="presentationView-cont" onPointerEnter={action(() => !this.persistOpacity && (this.opacity = 1))} onContextMenu={this.specificContextMenu}
+ onPointerLeave={action(() => !this.persistOpacity && (this.opacity = 0.4))}
+ style={{ width: width, opacity: this.opacity, }}>
+ <div className="presentation-buttons">
+ <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
+ {this.renderPlayPauseButton()}
+ <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
+ <button title="Minimize" className="presentation-button" onClick={this.minimize}><FontAwesomeIcon icon={"eye"} /></button>
+ </div>
+ <input
+ type="checkbox"
+ onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this.persistOpacity = e.target.checked;
+ this.opacity = this.persistOpacity ? 1 : 0.4;
+ })}
+ checked={this.persistOpacity}
+ style={{ position: "absolute", bottom: 5, left: 5 }}
+ onPointerEnter={action(() => this.labelOpacity = 1)}
+ onPointerLeave={action(() => this.labelOpacity = 0)}
+ />
+ <p style={{ position: "absolute", bottom: 1, left: 22, opacity: this.labelOpacity, transition: "0.7s opacity ease" }}>opacity {this.persistOpacity ? "persistent" : "on focus"}</p>
+ <PresentationViewList
+ mainDocument={this.curPresentation}
+ deleteDocument={this.RemoveDoc}
+ gotoDocument={this.gotoDocument}
+ PresElementsMappings={this.presElementsMappings}
+ setChildrenDocs={this.setChildrenDocs}
+ presStatus={this.presStatus}
+ removeDocByRef={this.removeDocByRef}
+ clearElemMap={() => this.presElementsMappings.clear()}
+ />
+ </div>
+ );
+ }
+
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index a33155fff..1ac2fae39 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -4,7 +4,7 @@ import { observer } from "mobx-react";
import * as rp from 'request-promise';
import { InkTool } from "../../../new_fields/InkField";
import { makeInterface } from "../../../new_fields/Schema";
-import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { Cast, FieldValue, NumCast, BoolCast } from "../../../new_fields/Types";
import { VideoField } from "../../../new_fields/URLField";
import { RouteStore } from "../../../server/RouteStore";
import { Utils } from "../../../Utils";
@@ -34,7 +34,7 @@ library.add(faVideo);
export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) {
private _reactionDisposer?: IReactionDisposer;
private _youtubeReactionDisposer?: IReactionDisposer;
- private _youtubePlayer: any = undefined;
+ private _youtubePlayer: YT.Player | undefined = undefined;
private _videoRef: HTMLVideoElement | null = null;
private _youtubeIframeId: number = -1;
private _youtubeContentCreated = false;
@@ -78,7 +78,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
@action public Pause = (update: boolean = true) => {
this.Playing = false;
update && this.player && this.player.pause();
- update && this._youtubePlayer && this._youtubePlayer.pauseVideo();
+ update && this._youtubePlayer && this._youtubePlayer.pauseVideo && this._youtubePlayer.pauseVideo();
this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
this._playTimer = undefined;
this.updateTimecode();
@@ -206,7 +206,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
}
}
specificContextMenu = (e: React.MouseEvent): void => {
- let field = Cast(this.Document[this.props.fieldKey], VideoField);
+ let field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
if (field) {
let url = field.url.href;
let subitems: ContextMenuProps[] = [];
@@ -218,7 +218,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
}
@computed get content() {
- let field = Cast(this.Document[this.props.fieldKey], VideoField);
+ let field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
let interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
let style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
return !field ? <div>Loading</div> :
@@ -230,7 +230,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
}
@computed get youtubeVideoId() {
- let field = Cast(this.Document[this.props.fieldKey], VideoField);
+ let field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
}
@@ -246,7 +246,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
let onYoutubePlayerStateChange = (event: any) => runInAction(() => {
if (started && event.data === YT.PlayerState.PLAYING) {
started = false;
- this._youtubePlayer.unMute();
+ this._youtubePlayer && this._youtubePlayer.unMute();
this.Pause();
return;
}
@@ -271,6 +271,8 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
}
+ @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+
@computed get youtubeContent() {
this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
@@ -283,6 +285,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
}
render() {
+ Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
return <div style={{ pointerEvents: "all", width: "100%", height: "100%" }} onContextMenu={this.specificContextMenu}>
{this.youtubeVideoId ? this.youtubeContent : this.content}
</div>;
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index 07774263c..fbe9bf063 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -1,3 +1,5 @@
+@import "../globalCssVariables.scss";
+
.webBox-cont,
.webBox-cont-interactive {
padding: 0vw;
@@ -28,6 +30,7 @@
width: 100%;
height: 100%;
position: absolute;
+ pointer-events: all;
}
.webBox-button {
@@ -61,6 +64,14 @@
width: 40px;
transform-origin: top left;
}
+
+ .switchToText {
+ color: $main-accent;
+ }
+
+ .switchToText:hover {
+ color: $dark-color;
+ }
}
button:hover {
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 9b66b2431..29eef27a0 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,18 +1,29 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, observable } from "mobx";
import { observer } from "mobx-react";
-import { FieldResult } from "../../../new_fields/Doc";
+import { FieldResult, Doc, Field } from "../../../new_fields/Doc";
import { HtmlField } from "../../../new_fields/HtmlField";
-import { InkTool } from "../../../new_fields/InkField";
-import { Cast, NumCast } from "../../../new_fields/Types";
import { WebField } from "../../../new_fields/URLField";
-import { Utils } from "../../../Utils";
import { DocumentDecorations } from "../DocumentDecorations";
import { InkingControl } from "../InkingControl";
import { FieldView, FieldViewProps } from './FieldView';
-import { KeyValueBox } from "./KeyValueBox";
import "./WebBox.scss";
import React = require("react");
+import { InkTool } from "../../../new_fields/InkField";
+import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
+import { Utils } from "../../../Utils";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faStickyNote } from '@fortawesome/free-solid-svg-icons';
+import { observable, action, computed } from "mobx";
+import { listSpec } from "../../../new_fields/Schema";
+import { RefField } from "../../../new_fields/RefField";
+import { ObjectField } from "../../../new_fields/ObjectField";
+import { updateSourceFile } from "typescript";
+import { KeyValueBox } from "./KeyValueBox";
+import { setReactionScheduler } from "mobx/lib/internal";
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { SelectionManager } from "../../util/SelectionManager";
+import { Docs } from "../../documents/Documents";
+
+library.add(faStickyNote);
@observer
export class WebBox extends React.Component<FieldViewProps> {
@@ -64,6 +75,29 @@ export class WebBox extends React.Component<FieldViewProps> {
}
}
+
+ switchToText = () => {
+ let url: string = "";
+ let field = Cast(this.props.Document[this.props.fieldKey], WebField);
+ if (field) url = field.url.href;
+
+ let newBox = Docs.Create.TextDocument({
+ x: NumCast(this.props.Document.x),
+ y: NumCast(this.props.Document.y),
+ title: url,
+ width: 200,
+ height: 70,
+ documentText: "@@@" + url
+ });
+
+ SelectionManager.SelectedDocuments().map(dv => {
+ dv.props.addDocument && dv.props.addDocument(newBox, false);
+ dv.props.removeDocument && dv.props.removeDocument(dv.props.Document);
+ });
+
+ Doc.BrushDoc(newBox);
+ }
+
urlEditor() {
return (
<div className="webView-urlEditor" style={{ top: this.collapsed ? -70 : 0 }}>
@@ -86,9 +120,19 @@ export class WebBox extends React.Component<FieldViewProps> {
onChange={this.onURLChange}
onKeyDown={this.onValueKeyDown}
/>
- <button className="submitUrl" onClick={this.submitURL}>
- SUBMIT URL
- </button>
+ <div style={{
+ display: "flex",
+ flexDirection: "row",
+ justifyContent: "space-between",
+ minWidth: "100px",
+ }}>
+ <button className="submitUrl" onClick={this.submitURL}>
+ SUBMIT
+ </button>
+ <div className="switchToText" title="Convert web to text doc" onClick={this.switchToText} style={{ display: "flex", alignItems: "center", justifyContent: "center" }} >
+ <FontAwesomeIcon icon={faStickyNote} size={"lg"} />
+ </div>
+ </div>
</div>
</div>
</div>