aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTyler Schicke <tyler_schicke@brown.edu>2019-05-09 20:32:06 -0400
committerTyler Schicke <tyler_schicke@brown.edu>2019-05-09 20:32:06 -0400
commitea4ab9d57b9c5b66508a3e24268997f70302cdc5 (patch)
treea8d64afc0699a6322004166b6f35cda340779594 /src
parent3b012d7555c0f32b88a2506b1f474262df5a5f2d (diff)
parent30aaf7c496eb4cbebac1e4cf9e0695cd286c63f2 (diff)
Merge branch 'master' of github-tsch-brown:browngraphicslab/Dash-Web
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/Documents.ts4
-rw-r--r--src/client/northstar/dash-nodes/HistogramBox.tsx8
-rw-r--r--src/client/northstar/operations/HistogramOperation.ts5
-rw-r--r--src/client/util/DocumentManager.ts47
-rw-r--r--src/client/util/DragManager.ts36
-rw-r--r--src/client/util/RichTextSchema.tsx87
-rw-r--r--src/client/util/SelectionManager.ts21
-rw-r--r--src/client/util/TooltipTextMenu.tsx109
-rw-r--r--src/client/util/UndoManager.ts5
-rw-r--r--src/client/views/DocumentDecorations.scss7
-rw-r--r--src/client/views/DocumentDecorations.tsx64
-rw-r--r--src/client/views/Main.scss3
-rw-r--r--src/client/views/Main.tsx6
-rw-r--r--src/client/views/MainOverlayTextBox.tsx31
-rw-r--r--src/client/views/PreviewCursor.tsx6
-rw-r--r--src/client/views/SearchBox.scss99
-rw-r--r--src/client/views/SearchBox.tsx133
-rw-r--r--src/client/views/SearchItem.tsx52
-rw-r--r--src/client/views/TemplateMenu.tsx30
-rw-r--r--src/client/views/Templates.tsx40
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx14
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx6
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx5
-rw-r--r--src/client/views/collections/CollectionSubView.tsx22
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx24
-rw-r--r--src/client/views/collections/CollectionVideoView.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss8
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx4
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx64
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss8
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx39
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx112
-rw-r--r--src/client/views/globalCssVariables.scss2
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx43
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx29
-rw-r--r--src/client/views/nodes/DocumentView.tsx38
-rw-r--r--src/client/views/nodes/FieldView.tsx5
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss3
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx137
-rw-r--r--src/client/views/nodes/IconBox.tsx11
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx5
-rw-r--r--src/client/views/nodes/LinkBox.tsx23
-rw-r--r--src/client/views/nodes/LinkMenu.tsx4
-rw-r--r--src/client/views/nodes/PDFBox.tsx5
-rw-r--r--src/debug/Test.tsx7
-rw-r--r--src/fields/TemplateField.ts43
-rw-r--r--src/new_fields/CursorField.ts55
-rw-r--r--src/new_fields/Doc.ts2
-rw-r--r--src/new_fields/InkField.ts5
-rw-r--r--src/new_fields/List.ts49
-rw-r--r--src/new_fields/ObjectField.ts5
-rw-r--r--src/server/Search.ts45
-rw-r--r--src/server/index.ts84
55 files changed, 1297 insertions, 412 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 37d263e75..11929455c 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -19,7 +19,6 @@ import { ColumnAttributeModel } from "../northstar/core/attribute/AttributeModel
import { AttributeTransformationModel } from "../northstar/core/attribute/AttributeTransformationModel";
import { AggregateFunction } from "../northstar/model/idea/idea";
import { Template } from "../views/Templates";
-import { TemplateField } from "../../fields/TemplateField";
import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
import { IconBox } from "../views/nodes/IconBox";
import { Field, Doc, Opt } from "../../new_fields/Doc";
@@ -132,7 +131,7 @@ export namespace Docs {
}
function CreateTextPrototype(): Doc {
let textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 150 });
+ { x: 0, y: 0, width: 300, height: 150, backgroundColor: "#f1efeb" });
return textProto;
}
function CreatePdfPrototype(): Doc {
@@ -175,6 +174,7 @@ export namespace Docs {
if (!("creationDate" in protoProps)) {
protoProps.creationDate = new DateField;
}
+ protoProps.isPrototype = true;
return SetDelegateOptions(SetInstanceOptions(proto, protoProps, data), delegateProps);
}
diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index 765ecf8f0..5e7b867b3 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -117,15 +117,15 @@ export class HistogramBox extends React.Component<FieldViewProps> {
runInAction(() => {
this.HistoOp = histoOp ? histoOp.HistoOp : HistogramOperation.Empty;
if (this.HistoOp !== HistogramOperation.Empty) {
- reaction(() => Cast(this.props.Document.linkedFromDocs, listSpec(Doc), []), (docs) => this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs), { fireImmediately: true });
+ reaction(() => Cast(this.props.Document.linkedFromDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc), (docs) => this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs), { fireImmediately: true });
reaction(() => Cast(this.props.Document.brushingDocs, listSpec(Doc), []).length,
() => {
- let brushingDocs = Cast(this.props.Document.brushingDocs, listSpec(Doc), []);
+ let brushingDocs = Cast(this.props.Document.brushingDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc);
const proto = this.props.Document.proto;
if (proto) {
this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...brushingDocs.map((brush, i) => {
- brush.bckgroundColor = StyleConstants.BRUSH_COLORS[i % StyleConstants.BRUSH_COLORS.length];
- let brushed = Cast(brush.brushingDocs, listSpec(Doc), []);
+ brush.backgroundColor = StyleConstants.BRUSH_COLORS[i % StyleConstants.BRUSH_COLORS.length];
+ let brushed = Cast(brush.brushingDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc);
return { l: brush, b: brushed[0][Id] === proto[Id] ? brushed[1] : brushed[0] };
}));
}
diff --git a/src/client/northstar/operations/HistogramOperation.ts b/src/client/northstar/operations/HistogramOperation.ts
index 5c9c832c0..78b206bdc 100644
--- a/src/client/northstar/operations/HistogramOperation.ts
+++ b/src/client/northstar/operations/HistogramOperation.ts
@@ -65,7 +65,7 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
@computed
public get FilterString(): string {
if (this.OverridingFilters.length > 0) {
- return "(" + this.OverridingFilters.filter(fm => fm != null).map(fm => fm.ToPythonString()).join(" || ") + ")";
+ return "(" + this.OverridingFilters.filter(fm => fm !== null).map(fm => fm.ToPythonString()).join(" || ") + ")";
}
let filterModels: FilterModel[] = [];
return FilterModel.GetFilterModelsRecursive(this, new Set<IBaseFilterProvider>(), filterModels, true);
@@ -89,8 +89,9 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
@action
public DrillDown(up: boolean) {
if (!up) {
- if (!this.BarFilterModels.length)
+ if (!this.BarFilterModels.length) {
return;
+ }
this._stackedFilters.push(this.BarFilterModels.map(f => f));
this.OverridingFilters.length = 0;
this.OverridingFilters.push(...this._stackedFilters[this._stackedFilters.length - 1]);
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 69964e2c9..a8b643d4d 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,8 +1,10 @@
import { computed, observable } from 'mobx';
import { DocumentView } from '../views/nodes/DocumentView';
import { Doc } from '../../new_fields/Doc';
-import { FieldValue, Cast } from '../../new_fields/Types';
+import { FieldValue, Cast, NumCast, BoolCast } from '../../new_fields/Types';
import { listSpec } from '../../new_fields/Schema';
+import { undoBatch } from './UndoManager';
+import { CollectionDockingView } from '../views/collections/CollectionDockingView';
export class DocumentManager {
@@ -70,8 +72,8 @@ export class DocumentManager {
@computed
public get LinkedDocumentViews() {
- return DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => {
- let linksList = Cast(dv.props.Document.linkedToDocs, listSpec(Doc));
+ return DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
+ let linksList = Cast(dv.props.Document.linkedToDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc);
if (linksList && linksList.length) {
pairs.push(...linksList.reduce((pairs, link) => {
if (link) {
@@ -84,7 +86,46 @@ export class DocumentManager {
return pairs;
}, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
}
+ linksList = Cast(dv.props.Document.linkedFromDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc);
+ if (linksList && linksList.length) {
+ pairs.push(...linksList.reduce((pairs, link) => {
+ if (link) {
+ let linkFromDoc = FieldValue(Cast(link.linkedFrom, Doc));
+ if (linkFromDoc) {
+ DocumentManager.Instance.getDocumentViews(linkFromDoc).map(docView1 =>
+ pairs.push({ a: dv, b: docView1, l: link }));
+ }
+ }
+ return pairs;
+ }, pairs));
+ }
return pairs;
}, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
}
+
+ @undoBatch
+ public jumpToDocument = async (doc: Doc): Promise<void> => {
+ let docView = DocumentManager.Instance.getDocumentView(doc);
+ if (docView) {
+ docView.props.focus(docView.props.Document);
+ } else {
+ const contextDoc = await Cast(doc.annotationOn, Doc);
+ if (!contextDoc) {
+ CollectionDockingView.Instance.AddRightSplit(Doc.MakeDelegate(doc));
+ } else {
+ const page = NumCast(doc.page, undefined);
+ const curPage = NumCast(contextDoc.curPage, undefined);
+ if (page !== curPage) {
+ contextDoc.curPage = page;
+ }
+ let contextView = DocumentManager.Instance.getDocumentView(contextDoc);
+ if (contextView) {
+ contextDoc.panTransformType = "Ease";
+ contextView.props.focus(contextDoc);
+ } else {
+ CollectionDockingView.Instance.AddRightSplit(contextDoc);
+ }
+ }
+ }
+ }
} \ No newline at end of file
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index a3dbe6e43..266679c16 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,11 +1,9 @@
-import { action } from "mobx";
+import { action, runInAction } from "mobx";
+import { Doc, DocListCast } from "../../new_fields/Doc";
+import { Cast } from "../../new_fields/Types";
import { emptyFunction } from "../../Utils";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import * as globalCssVariables from "../views/globalCssVariables.scss";
-import { MainOverlayTextBox } from "../views/MainOverlayTextBox";
-import { Doc } from "../../new_fields/Doc";
-import { Cast } from "../../new_fields/Types";
-import { listSpec } from "../../new_fields/Schema";
export type dropActionType = "alias" | "copy" | undefined;
export function SetupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: () => Doc, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType) {
@@ -42,12 +40,14 @@ export function SetupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc:
export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) {
let srcTarg = sourceDoc.proto;
- let draggedDocs = srcTarg ?
- Cast(srcTarg.linkedToDocs, listSpec(Doc), []).map(linkDoc =>
- Cast(linkDoc.linkedTo, Doc) as Doc) : [];
- let draggedFromDocs = srcTarg ?
- Cast(srcTarg.linkedFromDocs, listSpec(Doc), []).map(linkDoc =>
- Cast(linkDoc.linkedFrom, Doc) as Doc) : [];
+ let draggedDocs: Doc[] = [];
+ let draggedFromDocs: Doc[] = []
+ if (srcTarg) {
+ let linkToDocs = await DocListCast(srcTarg.linkedToDocs);
+ let linkFromDocs = await DocListCast(srcTarg.linkedFromDocs);
+ if (linkToDocs) draggedDocs = linkToDocs.map(linkDoc => Cast(linkDoc.linkedTo, Doc) as Doc);
+ if (linkFromDocs) draggedFromDocs = linkFromDocs.map(linkDoc => Cast(linkDoc.linkedFrom, Doc) as Doc);
+ }
draggedDocs.push(...draggedFromDocs);
if (draggedDocs.length) {
let moddrag: Doc[] = [];
@@ -152,12 +152,15 @@ export namespace DragManager {
[id: string]: any;
}
+ export let StartDragFunctions: (() => void)[] = [];
+
export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+ runInAction(() => StartDragFunctions.map(func => func()));
StartDrag(eles, dragData, downX, downY, options,
(dropData: { [id: string]: any }) =>
- (dropData.droppedDocuments = dragData.userDropAction == "alias" || (!dragData.userDropAction && dragData.dropAction == "alias") ?
+ (dropData.droppedDocuments = dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ?
dragData.draggedDocuments.map(d => Doc.MakeAlias(d)) :
- dragData.userDropAction == "copy" || (!dragData.userDropAction && dragData.dropAction == "copy") ?
+ dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ?
dragData.draggedDocuments.map(d => Doc.MakeCopy(d, true)) :
dragData.draggedDocuments));
}
@@ -170,6 +173,7 @@ export namespace DragManager {
droppedDocuments: Doc[] = [];
linkSourceDocument: Doc;
blacklist: Doc[];
+ dontClearTextBox?: boolean;
[id: string]: any;
}
@@ -186,7 +190,6 @@ export namespace DragManager {
dragDiv.style.pointerEvents = "none";
DragManager.Root().appendChild(dragDiv);
}
- MainOverlayTextBox.Instance.SetTextDoc();
let scaleXs: number[] = [];
let scaleYs: number[] = [];
@@ -214,6 +217,7 @@ export namespace DragManager {
dragElement.style.top = "0";
dragElement.style.bottom = "";
dragElement.style.left = "0";
+ dragElement.style.color = "black";
dragElement.style.transformOrigin = "0 0";
dragElement.style.zIndex = globalCssVariables.contextMenuZindex;// "1000";
dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`;
@@ -280,7 +284,7 @@ export namespace DragManager {
};
let hideDragElements = () => {
- dragElements.map(dragElement => dragElement.parentNode == dragDiv && dragDiv.removeChild(dragElement));
+ dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
eles.map(ele => (ele.hidden = false));
};
let endDrag = () => {
@@ -289,7 +293,7 @@ export namespace DragManager {
if (options) {
options.handlers.dragComplete({});
}
- }
+ };
AbortDrag = () => {
hideDragElements();
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 9ef71e305..c0e6f7899 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -84,6 +84,7 @@ export const nodes: { [index: string]: NodeSpec } = {
inline: true,
attrs: {
src: {},
+ width: { default: "100px" },
alt: { default: null },
title: { default: null }
},
@@ -94,11 +95,16 @@ export const nodes: { [index: string]: NodeSpec } = {
return {
src: dom.getAttribute("src"),
title: dom.getAttribute("title"),
- alt: dom.getAttribute("alt")
- };
+ alt: dom.getAttribute("alt"),
+ width: Math.min(100, Number(dom.getAttribute("width"))),
+ }
}
}],
- toDOM(node: any) { return ["img", node.attrs]; }
+ // TODO if we don't define toDom, something weird happens: dragging the image will not move it but clone it. Why?
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}` }
+ return ["img", { ...node.attrs, ...attrs }]
+ }
},
// :: NodeSpec A hard line break, represented in the DOM as `<br>`.
@@ -290,6 +296,13 @@ export const marks: { [index: string]: MarkSpec } = {
}]
},
+ p14: {
+ parseDOM: [{ style: 'font-size: 14px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 14px;'
+ }]
+ },
+
p16: {
parseDOM: [{ style: 'font-size: 16px;' }],
toDOM: () => ['span', {
@@ -325,7 +338,75 @@ export const marks: { [index: string]: MarkSpec } = {
}]
},
};
+function getFontSize(element: any) {
+ return parseFloat((getComputedStyle(element) as any).fontSize);
+}
+
+export class ImageResizeView {
+ _handle: HTMLElement;
+ _img: HTMLElement;
+ _outer: HTMLElement;
+ constructor(node: any, view: any, getPos: any) {
+ this._handle = document.createElement("span");
+ this._img = document.createElement("img");
+ this._outer = document.createElement("span");
+ this._outer.style.position = "relative";
+ this._outer.style.width = node.attrs.width;
+ this._outer.style.display = "inline-block";
+ this._outer.style.overflow = "hidden";
+
+ this._img.setAttribute("src", node.attrs.src);
+ this._img.style.width = "100%";
+ this._handle.style.position = "absolute";
+ this._handle.style.width = "20px";
+ this._handle.style.height = "20px";
+ this._handle.style.backgroundColor = "blue";
+ this._handle.style.borderRadius = "15px";
+ this._handle.style.display = "none";
+ this._handle.style.bottom = "-10px";
+ this._handle.style.right = "-10px";
+ let self = this;
+ this._handle.onpointerdown = function (e: any) {
+ e.preventDefault();
+ e.stopPropagation();
+ const startX = e.pageX;
+ const startWidth = parseFloat(node.attrs.width);
+ const onpointermove = (e: any) => {
+ const currentX = e.pageX;
+ const diffInPx = currentX - startX;
+ self._outer.style.width = `${startWidth + diffInPx}`;
+ }
+
+ const onpointerup = () => {
+ document.removeEventListener("pointermove", onpointermove);
+ document.removeEventListener("pointerup", onpointerup);
+ view.dispatch(
+ view.state.tr.setNodeMarkup(getPos(), null,
+ { src: node.attrs.src, width: self._outer.style.width })
+ .setSelection(view.state.selection));
+ }
+
+ document.addEventListener("pointermove", onpointermove)
+ document.addEventListener("pointerup", onpointerup)
+ }
+ this._outer.appendChild(this._handle);
+ this._outer.appendChild(this._img);
+ (this as any).dom = this._outer;
+ }
+
+ selectNode() {
+ this._img.classList.add("ProseMirror-selectednode");
+
+ this._handle.style.display = "";
+ }
+
+ deselectNode() {
+ this._img.classList.remove("ProseMirror-selectednode");
+
+ this._handle.style.display = "none";
+ }
+}
// :: Schema
// This schema rougly corresponds to the document schema used by
// [CommonMark](http://commonmark.org/), minus the list elements,
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index fe5acf4b4..8c92c2023 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,7 +1,8 @@
import { observable, action } from "mobx";
import { Doc } from "../../new_fields/Doc";
-import { MainOverlayTextBox } from "../views/MainOverlayTextBox";
import { DocumentView } from "../views/nodes/DocumentView";
+import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { NumCast } from "../../new_fields/Types";
export namespace SelectionManager {
class Manager {
@@ -25,7 +26,7 @@ export namespace SelectionManager {
DeselectAll(): void {
manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false));
manager.SelectedDocuments = [];
- MainOverlayTextBox.Instance.SetTextDoc();
+ FormattedTextBox.InputBoxOverlay = undefined;
}
@action
ReselectAll() {
@@ -68,4 +69,20 @@ export namespace SelectionManager {
export function SelectedDocuments(): Array<DocumentView> {
return manager.SelectedDocuments;
}
+ export function ViewsSortedVertically(): DocumentView[] {
+ let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
+ if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1;
+ if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
+ return 0;
+ });
+ return sorted;
+ }
+ export function ViewsSortedHorizontally(): DocumentView[] {
+ let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
+ if (NumCast(doc1.props.Document.y) > NumCast(doc2.props.Document.y)) return 1;
+ if (NumCast(doc1.props.Document.y) < NumCast(doc2.props.Document.y)) return -1;
+ return 0;
+ });
+ return sorted;
+ }
}
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 68a73375e..6eb654319 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -6,19 +6,28 @@ import { keymap } from "prosemirror-keymap";
import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { schema } from "./RichTextSchema";
-import { Schema, NodeType, MarkType } from "prosemirror-model";
+import { Schema, NodeType, MarkType, Mark } from "prosemirror-model";
import React = require("react");
import "./TooltipTextMenu.scss";
const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands");
import { library } from '@fortawesome/fontawesome-svg-core';
import { wrapInList, bulletList, liftListItem, listItem, } from 'prosemirror-schema-list';
-import { liftTarget } from 'prosemirror-transform';
+import { liftTarget, RemoveMarkStep, AddMarkStep } from 'prosemirror-transform';
import {
faListUl,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { FieldViewProps } from "../views/nodes/FieldView";
import { throwStatement } from "babel-types";
+import { View } from "@react-pdf/renderer";
+import { DragManager } from "./DragManager";
+import { Doc, Opt, Field } from "../../new_fields/Doc";
+import { Id } from "../../new_fields/RefField";
+import { Utils } from "../northstar/utils/Utils";
+import { DocServer } from "../DocServer";
+import { CollectionFreeFormDocumentView } from "../views/nodes/CollectionFreeFormDocumentView";
+import { CollectionDockingView } from "../views/collections/CollectionDockingView";
+import { DocumentManager } from "./DocumentManager";
const SVG = "http://www.w3.org/2000/svg";
@@ -37,6 +46,9 @@ export class TooltipTextMenu {
private fontStylesToName: Map<MarkType, string>;
private listTypeToIcon: Map<NodeType, string>;
private fontSizeIndicator: HTMLSpanElement = document.createElement("span");
+ private linkEditor?: HTMLDivElement;
+ private linkText?: HTMLDivElement;
+ private linkDrag?: HTMLImageElement;
//dropdown doms
private fontSizeDom?: Node;
private fontStyleDom?: Node;
@@ -94,6 +106,7 @@ export class TooltipTextMenu {
this.fontSizeToNum = new Map();
this.fontSizeToNum.set(schema.marks.p10, 10);
this.fontSizeToNum.set(schema.marks.p12, 12);
+ this.fontSizeToNum.set(schema.marks.p14, 14);
this.fontSizeToNum.set(schema.marks.p16, 16);
this.fontSizeToNum.set(schema.marks.p24, 24);
this.fontSizeToNum.set(schema.marks.p32, 32);
@@ -149,6 +162,96 @@ export class TooltipTextMenu {
this.tooltip.appendChild(this.fontStyleDom);
}
+ updateLinkMenu() {
+ if (!this.linkEditor || !this.linkText) {
+ this.linkEditor = document.createElement("div");
+ this.linkEditor.style.color = "white";
+ this.linkText = document.createElement("div");
+ this.linkText.style.cssFloat = "left";
+ this.linkText.style.marginRight = "5px";
+ this.linkText.style.marginLeft = "5px";
+ this.linkText.setAttribute("contenteditable", "true");
+ this.linkText.style.whiteSpace = "nowrap";
+ this.linkText.style.width = "150px";
+ this.linkText.style.overflow = "hidden";
+ this.linkText.style.color = "white";
+ this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); }
+ let linkBtn = document.createElement("div");
+ linkBtn.textContent = ">>";
+ linkBtn.style.width = "20px";
+ linkBtn.style.height = "20px";
+ linkBtn.style.color = "white";
+ linkBtn.style.cssFloat = "left";
+ linkBtn.onpointerdown = (e: PointerEvent) => {
+ let node = this.view.state.selection.$from.nodeAfter;
+ let link = node && node.marks.find(m => m.type.name === "link");
+ if (link) {
+ console.log("Link to : " + link.attrs.href);
+ let href: string = link.attrs.href;
+ if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
+ let docid = href.replace(DocServer.prepend("/doc/"), "");
+ DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
+ if (f instanceof Doc) {
+ if (DocumentManager.Instance.getDocumentView(f))
+ DocumentManager.Instance.getDocumentView(f)!.props.focus(f);
+ else CollectionDockingView.Instance.AddRightSplit(f);
+ }
+ }));
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+ this.linkDrag = document.createElement("img");
+ this.linkDrag.src = "https://seogurusnyc.com/wp-content/uploads/2016/12/link-1.png";
+ this.linkDrag.style.width = "20px";
+ this.linkDrag.style.height = "20px";
+ this.linkDrag.style.color = "white";
+ this.linkDrag.style.background = "black";
+ this.linkDrag.style.cssFloat = "left";
+ this.linkDrag.onpointerdown = (e: PointerEvent) => {
+ let dragData = new DragManager.LinkDragData(this.editorProps.Document);
+ dragData.dontClearTextBox = true;
+ DragManager.StartLinkDrag(this.linkDrag!, dragData, e.clientX, e.clientY,
+ {
+ handlers: {
+ dragComplete: action(() => {
+ let m = dragData.droppedDocuments as Doc[];
+ this.makeLink(DocServer.prepend("/doc/" + m[0][Id]));
+ }),
+ },
+ hideSource: false
+ })
+ };
+ this.linkEditor.appendChild(this.linkDrag);
+ this.linkEditor.appendChild(this.linkText);
+ this.linkEditor.appendChild(linkBtn);
+ this.tooltip.appendChild(this.linkEditor);
+ }
+
+ let node = this.view.state.selection.$from.nodeAfter;
+ let link = node && node.marks.find(m => m.type.name === "link");
+ this.linkText.textContent = link ? link.attrs.href : "-empty-";
+
+ this.linkText.onkeydown = (e: KeyboardEvent) => {
+ if (e.key === "Enter") {
+ this.makeLink(this.linkText!.textContent!);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+ this.tooltip.appendChild(this.linkEditor);
+ }
+
+ makeLink = (target: string) => {
+ let node = this.view.state.selection.$from.nodeAfter;
+ let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target });
+ this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link));
+ this.view.dispatch(this.view.state.tr.addMark(this.view.state.selection.from, this.view.state.selection.to, link));
+ node = this.view.state.selection.$from.nodeAfter;
+ link = node && node.marks.find(m => m.type.name === "link");
+ }
+
//will display a remove-list-type button if selection is in list, otherwise will show list type dropdown
updateListItemDropdown(label: string, listTypeBtn: Node) {
//remove old btn
@@ -347,6 +450,8 @@ export class TooltipTextMenu {
} else { //multiple font sizes selected
this.updateFontSizeDropdown("Various");
}
+
+ this.updateLinkMenu();
}
//finds all active marks on selection in given group
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 0b5280c4a..c0ed015bd 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -1,7 +1,6 @@
import { observable, action, runInAction } from "mobx";
import 'source-map-support/register';
import { Without } from "../../Utils";
-import { string } from "prop-types";
function getBatchName(target: any, key: string | symbol): string {
let keyName = key.toString();
@@ -94,6 +93,10 @@ export namespace UndoManager {
return redoStack.length > 0;
}
+ export function PrintBatches(): void {
+ GetOpenBatches().forEach(batch => console.log(batch.batchName));
+ }
+
let openBatches: Batch[] = [];
export function GetOpenBatches(): Without<Batch, 'end'>[] {
return openBatches;
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 158b02b5a..6a2e33836 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -210,14 +210,15 @@ $linkGap : 3px;
position: absolute;
top: 0;
left: 30px;
- width: 150px;
- line-height: 25px;
- max-height: 175px;
+ width: max-content;
font-family: $sans-serif;
font-size: 12px;
background-color: $light-color-secondary;
padding: 2px 12px;
list-style: none;
+ .templateToggle {
+ text-align: left;
+ }
input {
margin-right: 10px;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 693d6ec55..4786b4de6 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -5,7 +5,6 @@ import { DragLinksAsDocuments, DragManager } from "../util/DragManager";
import { SelectionManager } from "../util/SelectionManager";
import { undoBatch } from "../util/UndoManager";
import './DocumentDecorations.scss';
-import { MainOverlayTextBox } from "./MainOverlayTextBox";
import { DocumentView, PositionDocument } from "./nodes/DocumentView";
import { LinkMenu } from "./nodes/LinkMenu";
import { TemplateMenu } from "./TemplateMenu";
@@ -25,9 +24,9 @@ import { faLink } from '@fortawesome/free-solid-svg-icons';
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
-import { CollectionFreeFormView } from "./collections/collectionFreeForm/CollectionFreeFormView";
import { CollectionView } from "./collections/CollectionView";
-import { createCipher } from "crypto";
+import { DocumentManager } from "../util/DocumentManager";
+import { FormattedTextBox } from "./nodes/FormattedTextBox";
import { FieldView } from "./nodes/FieldView";
library.add(faLink);
@@ -78,11 +77,15 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (SelectionManager.SelectedDocuments().length > 0) {
let field = SelectionManager.SelectedDocuments()[0].props.Document[this._fieldKey];
if (typeof field === "number") {
- SelectionManager.SelectedDocuments().forEach(d =>
- d.props.Document[this._fieldKey] = +this._title);
+ SelectionManager.SelectedDocuments().forEach(d => {
+ let doc = d.props.Document.proto ? d.props.Document.proto : d.props.Document;
+ doc[this._fieldKey] = +this._title;
+ });
} else {
- SelectionManager.SelectedDocuments().forEach(d =>
- d.props.Document[this._fieldKey] = this._title);
+ SelectionManager.SelectedDocuments().forEach(d => {
+ let doc = d.props.Document.proto ? d.props.Document.proto : d.props.Document;
+ doc[this._fieldKey] = this._title;
+ });
}
}
}
@@ -246,7 +249,22 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (this._iconDoc && selectedDocs.length === 1 && this._removeIcon) {
selectedDocs[0].props.removeDocument && selectedDocs[0].props.removeDocument(this._iconDoc);
}
- !this._removeIcon && selectedDocs.length === 1 && this.getIconDoc(selectedDocs[0]).then(icon => selectedDocs[0].props.toggleMinimized());
+ if (!this._removeIcon) {
+ if (selectedDocs.length === 1)
+ this.getIconDoc(selectedDocs[0]).then(icon => selectedDocs[0].props.toggleMinimized());
+ else {
+ let docViews = SelectionManager.ViewsSortedVertically();
+ let topDocView = docViews[0];
+ let ind = topDocView.templates.indexOf(Templates.Bullet.Layout);
+ if (ind !== -1) {
+ topDocView.templates.splice(ind, 1);
+ topDocView.props.Document.subBulletDocs = undefined;
+ } else {
+ topDocView.addTemplate(Templates.Bullet);
+ topDocView.props.Document.subBulletDocs = new List<Doc>(docViews.filter(v => v !== topDocView).map(v => v.props.Document));
+ }
+ }
+ }
this._removeIcon = false;
}
runInAction(() => this._minimizedX = this._minimizedY = 0);
@@ -273,13 +291,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
public getIconDoc = async (docView: DocumentView): Promise<Doc | undefined> => {
let doc = docView.props.Document;
let iconDoc: Doc | undefined = await Cast(doc.minimizedDoc, Doc);
- if (!iconDoc) {
+
+ if (!iconDoc || !DocumentManager.Instance.getDocumentView(iconDoc)) {
const layout = StrCast(doc.backgroundLayout, StrCast(doc.layout, FieldView.LayoutString(DocumentView)));
iconDoc = this.createIcon([docView], layout);
}
- if (SelectionManager.SelectedDocuments()[0].props.addDocument !== undefined) {
- SelectionManager.SelectedDocuments()[0].props.addDocument!(iconDoc!);
- }
return iconDoc;
}
moveIconDoc(iconDoc: Doc) {
@@ -318,6 +334,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
e.stopPropagation();
}
+ @action
onLinkerButtonMoved = (e: PointerEvent): void => {
if (this._linkerButton.current !== null) {
document.removeEventListener("pointermove", this.onLinkerButtonMoved);
@@ -325,6 +342,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let selDoc = SelectionManager.SelectedDocuments()[0];
let container = selDoc.props.ContainingCollectionView ? selDoc.props.ContainingCollectionView.props.Document.proto : undefined;
let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []);
+ FormattedTextBox.InputBoxOverlay = undefined;
DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, {
handlers: {
dragComplete: action(emptyFunction),
@@ -407,7 +425,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
break;
}
- MainOverlayTextBox.Instance.SetTextDoc();
+ runInAction(() => FormattedTextBox.InputBoxOverlay = undefined);
SelectionManager.SelectedDocuments().forEach(element => {
const rect = element.ContentDiv ? element.ContentDiv.getBoundingClientRect() : new DOMRect();
@@ -503,9 +521,23 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
}
let templates: Map<Template, boolean> = new Map();
- let doc = SelectionManager.SelectedDocuments()[0];
Array.from(Object.values(Templates.TemplateList)).map(template => {
- let docTemps = doc.templates;
+ let sorted = SelectionManager.ViewsSortedVertically().slice().sort((doc1, doc2) => {
+ if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1;
+ if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
+ return 0;
+ });
+ let docTemps = sorted.reduce((res: string[], doc: DocumentView, i) => {
+ let temps = doc.props.Document.templates;
+ if (temps instanceof List) {
+ temps.map(temp => {
+ if (temp !== Templates.Bullet.Layout || i === 0) {
+ res.push(temp);
+ }
+ })
+ }
+ return res
+ }, [] as string[]);
let checked = false;
docTemps.forEach(temp => {
if (template.Layout === temp) {
@@ -556,7 +588,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
<FontAwesomeIcon className="fa-icon-link" icon="link" size="sm" />
</div>
</div>
- <TemplateMenu doc={doc} templates={templates} />
+ <TemplateMenu docs={SelectionManager.ViewsSortedVertically()} templates={templates} />
</div>
</div >
</div>
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index 5c5c252e9..2430e8f6c 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -47,7 +47,7 @@ h1 {
}
.jsx-parser {
- width:100%;
+ width: 100%;
pointer-events: none;
border-radius: inherit;
}
@@ -184,6 +184,7 @@ button:hover {
overflow: scroll;
z-index: 1;
}
+
#mainContent-div {
width: 100%;
height: 100%;
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 617580332..c9d5c395c 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -31,6 +31,7 @@ import "./Main.scss";
import { MainOverlayTextBox } from './MainOverlayTextBox';
import { DocumentView } from './nodes/DocumentView';
import { PreviewCursor } from './PreviewCursor';
+import { SearchBox } from './SearchBox';
import { SelectionManager } from '../util/SelectionManager';
import { FieldResult, Field, Doc, Opt } from '../../new_fields/Doc';
import { Cast, FieldValue, StrCast } from '../../new_fields/Types';
@@ -38,7 +39,6 @@ import { DocServer } from '../DocServer';
import { listSpec } from '../../new_fields/Schema';
import { Id } from '../../new_fields/RefField';
-
@observer
export class Main extends React.Component {
public static Instance: Main;
@@ -266,9 +266,11 @@ export class Main extends React.Component {
<button className="toolbar-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button>
<button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button>
</div >,
- <div className="main-buttonDiv" key="logout" style={{ top: '34px', right: '1px', position: 'absolute' }} ref={logoutRef}>
+ <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <SearchBox /> </div>,
+ <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}>
<button onClick={() => request.get(DocServer.prepend(RouteStore.logout), emptyFunction)}>Log Out</button></div>
];
+
}
render() {
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index d32e3f21b..91f626737 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -1,16 +1,12 @@
-import { action, observable, trace } from 'mobx';
+import { action, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
-import "normalize.css";
import * as React from 'react';
import { emptyFunction, returnTrue, returnZero } from '../../Utils';
-import '../northstar/model/ModelExtensions';
-import '../northstar/utils/Extensions';
import { DragManager } from '../util/DragManager';
import { Transform } from '../util/Transform';
+import "normalize.css";
import "./MainOverlayTextBox.scss";
import { FormattedTextBox } from './nodes/FormattedTextBox';
-import { Doc } from '../../new_fields/Doc';
-import { NumCast } from '../../new_fields/Types';
interface MainOverlayTextBoxProps {
}
@@ -18,8 +14,6 @@ interface MainOverlayTextBoxProps {
@observer
export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> {
public static Instance: MainOverlayTextBox;
- @observable public TextDoc?: Doc = undefined;
- public TextScroll: number = 0;
@observable _textXf: () => Transform = () => Transform.Identity();
private _textFieldKey: string = "data";
private _textColor: string | null = null;
@@ -30,30 +24,32 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
super(props);
this._textProxyDiv = React.createRef();
MainOverlayTextBox.Instance = this;
+ reaction(() => FormattedTextBox.InputBoxOverlay,
+ (box?: FormattedTextBox) => {
+ if (box) this.setTextDoc(box.props.fieldKey, box.CurrentDiv, box.props.ScreenToLocalTransform);
+ else this.setTextDoc();
+ });
}
@action
- SetTextDoc(textDoc?: Doc, textFieldKey?: string, div?: HTMLDivElement, tx?: () => Transform) {
+ private setTextDoc(textFieldKey?: string, div?: HTMLDivElement, tx?: () => Transform) {
if (this._textTargetDiv) {
this._textTargetDiv.style.color = this._textColor;
}
-
- this.TextDoc = textDoc;
this._textFieldKey = textFieldKey!;
this._textXf = tx ? tx : () => Transform.Identity();
this._textTargetDiv = div;
if (div) {
+ if (div.parentElement && div.parentElement instanceof HTMLDivElement && div.parentElement.id === "screenSpace") this._textXf = () => Transform.Identity();
this._textColor = div.style.color;
div.style.color = "transparent";
- this.TextScroll = div.scrollTop;
}
}
@action
textScroll = (e: React.UIEvent) => {
if (this._textProxyDiv.current && this._textTargetDiv) {
- this.TextScroll = (e as any)._targetInst.stateNode.scrollTop;// this._textProxyDiv.current.children[0].scrollTop;
- this._textTargetDiv.scrollTop = this.TextScroll;
+ this._textTargetDiv.scrollTop = (e as any)._targetInst.stateNode.scrollTop;
}
}
@@ -63,11 +59,12 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
document.addEventListener('pointerup', this.textBoxUp);
}
}
+ @action
textBoxMove = (e: PointerEvent) => {
if (e.movementX > 1 || e.movementY > 1) {
document.removeEventListener("pointermove", this.textBoxMove);
document.removeEventListener('pointerup', this.textBoxUp);
- let dragData = new DragManager.DocumentDragData([this.TextDoc!]);
+ let dragData = new DragManager.DocumentDragData(FormattedTextBox.InputBoxOverlay ? [FormattedTextBox.InputBoxOverlay.props.Document] : []);
const [left, top] = this._textXf().inverse().transformPoint(0, 0);
dragData.xOffset = e.clientX - left;
dragData.yOffset = e.clientY - top;
@@ -85,13 +82,13 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
}
render() {
- if (this.TextDoc && this._textTargetDiv) {
+ if (FormattedTextBox.InputBoxOverlay && this._textTargetDiv) {
let textRect = this._textTargetDiv.getBoundingClientRect();
let s = this._textXf().Scale;
return <div className="mainOverlayTextBox-textInput" style={{ transform: `translate(${textRect.left}px, ${textRect.top}px) scale(${1 / s},${1 / s})`, width: "auto", height: "auto" }} >
<div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll}
style={{ width: `${textRect.width * s}px`, height: `${textRect.height * s}px` }}>
- <FormattedTextBox fieldKey={this._textFieldKey} isOverlay={true} Document={this.TextDoc} isSelected={returnTrue} select={emptyFunction} isTopMost={true}
+ <FormattedTextBox fieldKey={this._textFieldKey} isOverlay={true} Document={FormattedTextBox.InputBoxOverlay.props.Document} isSelected={returnTrue} select={emptyFunction} isTopMost={true}
selectOnLoad={true} ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue}
ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} />
</div>
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 4359ba093..78024a58c 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -17,7 +17,7 @@ export class PreviewCursor extends React.Component<{}> {
constructor(props: any) {
super(props);
- document.addEventListener("keydown", this.onKeyPress)
+ document.addEventListener("keydown", this.onKeyPress);
}
@action
@@ -27,8 +27,8 @@ export class PreviewCursor extends React.Component<{}> {
// the keyPress here.
//if not these keys, make a textbox if preview cursor is active!
if (e.key.startsWith("F") && !e.key.endsWith("F")) {
- } else if (e.key != "Escape" && e.key != "Alt" && e.key != "Shift" && e.key != "Meta" && e.key != "Control" && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) {
- if ((!e.ctrlKey && !e.metaKey) || e.key === "v") {
+ } else if (e.key !== "Escape" && e.key !== "Alt" && e.key !== "Shift" && e.key !== "Meta" && e.key !== "Control" && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) {
+ if ((!e.ctrlKey && !e.metaKey) || e.key === "v" || e.key === "q") {
PreviewCursor.Visible && PreviewCursor._onKeyPress && PreviewCursor._onKeyPress(e);
PreviewCursor.Visible = false;
}
diff --git a/src/client/views/SearchBox.scss b/src/client/views/SearchBox.scss
new file mode 100644
index 000000000..792d6dd3c
--- /dev/null
+++ b/src/client/views/SearchBox.scss
@@ -0,0 +1,99 @@
+@import "globalCssVariables";
+
+.searchBox {
+ height: 32px;
+ //display: flex;
+ //padding: 4px;
+ -webkit-transition: width 0.4s ease-in-out;
+ transition: width 0.4s ease-in-out;
+ align-items: center;
+
+
+
+ input[type=text] {
+ width: 130px;
+ -webkit-transition: width 0.4s;
+ transition: width 0.4s;
+ position: absolute;
+ right: 100px;
+ }
+
+ input[type=text]:focus {
+ width: 500px;
+ outline: 3px solid lightblue;
+ }
+
+ .filter-button {
+ position: absolute;
+ right: 30px;
+ }
+
+ .submit-search {
+ text-align: right;
+ color: $dark-color;
+ -webkit-transition: right 0.4s;
+ transition: right 0.4s;
+ }
+
+ .submit-search:hover {
+ color: $main-accent;
+ transform: scale(1.05);
+ cursor: pointer;
+ }
+}
+
+.filter-form {
+ background: $dark-color;
+ height: 400px;
+ width: 400px;
+ position: relative;
+ right: 1px;
+ color: $light-color;
+ padding: 10px;
+ flex-direction: column;
+}
+
+#header {
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 100%;
+ height: 40px;
+}
+
+#option {
+ height: 20px;
+}
+
+.results {
+ top: 300px;
+ display: flex;
+ flex-direction: column;
+
+ .search-item {
+ width: 500px;
+ height: 50px;
+ background: $light-color-secondary;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ transition: all 0.1s;
+ border-width: 0.11px;
+ border-style: none;
+ border-color: $intermediate-color;
+ border-bottom-style: solid;
+ padding: 10px;
+ white-space: nowrap;
+ font-size: 13px;
+ }
+
+ .search-item:hover {
+ transition: all 0.1s;
+ background: $lighter-alt-accent;
+ }
+
+ .search-title {
+ text-transform: uppercase;
+ text-align: left;
+ width: 8vw;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx
new file mode 100644
index 000000000..7dd1af4e7
--- /dev/null
+++ b/src/client/views/SearchBox.tsx
@@ -0,0 +1,133 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action, runInAction } from 'mobx';
+import { Utils } from '../../Utils';
+import { MessageStore } from '../../server/Message';
+import "./SearchBox.scss";
+import { faSearch } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { library } from '@fortawesome/fontawesome-svg-core';
+// const app = express();
+// import * as express from 'express';
+import { Search } from '../../server/Search';
+import * as rp from 'request-promise';
+import { SearchItem } from './SearchItem';
+import { isString } from 'util';
+import { constant } from 'async';
+import { DocServer } from '../DocServer';
+import { Doc } from '../../new_fields/Doc';
+import { Id } from '../../new_fields/RefField';
+import { DocumentManager } from '../util/DocumentManager';
+
+
+library.add(faSearch);
+
+@observer
+export class SearchBox extends React.Component {
+ @observable
+ searchString: string = "";
+
+ @observable private _open: boolean = false;
+ @observable private _resultsOpen: boolean = false;
+
+ @observable
+ private _results: Doc[] = [];
+
+ @action.bound
+ onChange(e: React.ChangeEvent<HTMLInputElement>) {
+ this.searchString = e.target.value;
+ }
+
+ @action
+ submitSearch = async () => {
+ runInAction(() => this._results = []);
+ let query = this.searchString;
+
+ let response = await rp.get('http://localhost:1050/search', {
+ qs: {
+ query
+ }
+ });
+ let results = JSON.parse(response);
+
+ //gets json result into a list of documents that can be used
+ this.getResults(results);
+
+ runInAction(() => { this._resultsOpen = true; });
+ }
+
+ @action
+ getResults = async (res: string[]) => {
+ res.map(async result => {
+ const doc = await DocServer.GetRefField(result);
+ if (doc instanceof Doc) {
+ runInAction(() => this._results.push(doc));
+ }
+ });
+ }
+
+ @action
+ handleClickFilter = (e: Event): void => {
+ var className = (e.target as any).className;
+ var id = (e.target as any).id;
+ if (className !== "filter-button" && className !== "filter-form") {
+ this._open = false;
+ }
+
+ }
+
+ @action
+ handleClickResults = (e: Event): void => {
+ var className = (e.target as any).className;
+ var id = (e.target as any).id;
+ if (id !== "result") {
+ this._resultsOpen = false;
+ }
+
+ }
+
+ componentWillMount() {
+ document.addEventListener('mousedown', this.handleClickFilter, false);
+ document.addEventListener('mousedown', this.handleClickResults, false);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('mousedown', this.handleClickFilter, false);
+ document.removeEventListener('mousedown', this.handleClickResults, false);
+ }
+
+ @action
+ toggleFilterDisplay = () => {
+ this._open = !this._open;
+ }
+
+ enter = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ if (e.key === "Enter") {
+ this.submitSearch();
+ }
+ }
+
+ render() {
+ return (
+ <div id="outer">
+ <div className="searchBox" id="outer">
+
+ <input value={this.searchString} onChange={this.onChange} type="text" placeholder="Search.." className="search" id="input" onKeyPress={this.enter} />
+ <button className="filter-button" onClick={this.toggleFilterDisplay}> Filter </button>
+ <div className="submit-search" id="submit" onClick={this.submitSearch}><FontAwesomeIcon style={{ height: "100%" }} icon="search" size="lg" /></div>
+ <div className="results" id="results" style={this._resultsOpen ? { display: "flex" } : { display: "none" }}>
+ {this._results.map(result => <SearchItem doc={result} key={result[Id]} />)}
+ </div>
+ </div>
+ <div className="filter-form" id="filter" style={this._open ? { display: "flex" } : { display: "none" }}>
+ <div className="filter-form" id="header">Filter Search Results</div>
+ <div className="filter-form" id="option">
+ filter by collection, key, type of node
+ </div>
+
+ </div>
+ </div>
+
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx
new file mode 100644
index 000000000..d30579907
--- /dev/null
+++ b/src/client/views/SearchItem.tsx
@@ -0,0 +1,52 @@
+import React = require("react");
+import { Doc } from "../../new_fields/Doc";
+import { DocumentManager } from "../util/DocumentManager";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Cast } from "../../new_fields/Types";
+import { FieldView, FieldViewProps } from './nodes/FieldView';
+import { computed } from "mobx";
+import { IconField } from "../../new_fields/IconField";
+
+
+export interface SearchProps {
+ doc: Doc;
+}
+
+library.add(faCaretUp);
+library.add(faObjectGroup);
+library.add(faStickyNote);
+library.add(faFilePdf);
+library.add(faFilm);
+
+export class SearchItem extends React.Component<SearchProps> {
+
+ onClick = () => {
+ DocumentManager.Instance.jumpToDocument(this.props.doc);
+ }
+
+ //needs help
+ // @computed get layout(): string { const field = Cast(this.props.doc[fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; }
+
+
+ public static DocumentIcon(layout: string) {
+ let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf :
+ layout.indexOf("ImageBox") !== -1 ? faImage :
+ layout.indexOf("Formatted") !== -1 ? faStickyNote :
+ layout.indexOf("Video") !== -1 ? faFilm :
+ layout.indexOf("Collection") !== -1 ? faObjectGroup :
+ faCaretUp;
+ return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />;
+ }
+
+ render() {
+ return (
+ <div className="search-item" id="result" onClick={this.onClick}>
+ <div className="search-title" id="result" >title: {this.props.doc.title}</div>
+ {/* <div className="search-type" id="result" >Type: {this.props.doc.layout}</div> */}
+ {/* <div className="search-type" >{SearchItem.DocumentIcon(this.layout)}</div> */}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index f29d9c8a1..e2b3bd07a 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -4,6 +4,9 @@ import { observer } from "mobx-react";
import './DocumentDecorations.scss';
import { Template } from "./Templates";
import { DocumentView } from "./nodes/DocumentView";
+import { List } from "../../new_fields/List";
+import { Doc } from "../../new_fields/Doc";
+import { NumCast } from "../../new_fields/Types";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -13,7 +16,7 @@ class TemplateToggle extends React.Component<{ template: Template, checked: bool
render() {
if (this.props.template) {
return (
- <li>
+ <li className="templateToggle">
<input type="checkbox" checked={this.props.checked} onChange={(event) => this.props.toggle(event, this.props.template)} />
{this.props.template.Name}
</li>
@@ -25,26 +28,39 @@ class TemplateToggle extends React.Component<{ template: Template, checked: bool
}
export interface TemplateMenuProps {
- doc: DocumentView;
+ docs: DocumentView[];
templates: Map<Template, boolean>;
}
@observer
export class TemplateMenu extends React.Component<TemplateMenuProps> {
-
@observable private _hidden: boolean = true;
+ constructor(props: TemplateMenuProps) {
+ super(props);
+ console.log("");
+ }
@action
toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => {
if (event.target.checked) {
- this.props.doc.addTemplate(template);
+ if (template.Name == "Bullet") {
+ let topDocView = this.props.docs[0];
+ topDocView.addTemplate(template);
+ topDocView.props.Document.subBulletDocs = new List<Doc>(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document));
+ } else {
+ this.props.docs.map(d => d.addTemplate(template));
+ }
this.props.templates.set(template, true);
- this.props.templates.forEach((checked, template) => console.log("Set Checked + " + checked + " " + this.props.templates.get(template)));
} else {
- this.props.doc.removeTemplate(template);
+ if (template.Name == "Bullet") {
+ let topDocView = this.props.docs[0];
+ topDocView.removeTemplate(template);
+ topDocView.props.Document.subBulletDocs = undefined;
+ } else {
+ this.props.docs.map(d => d.removeTemplate(template));
+ }
this.props.templates.set(template, false);
- this.props.templates.forEach((checked, template) => console.log("Unset Checked + " + checked + " " + this.props.templates.get(template)));
}
}
diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx
index 5858ee014..02f9aa510 100644
--- a/src/client/views/Templates.tsx
+++ b/src/client/views/Templates.tsx
@@ -39,7 +39,7 @@ export namespace Templates {
// export const BasicLayout = new Template("Basic layout", "{layout}");
export const OuterCaption = new Template("Outer caption", TemplatePosition.OutterBottom,
- `<div><div style="margin:auto; height:calc(100%); width:100%;">{layout}</div><div style="height:(100% + 50px); width:100%; position:absolute"><FormattedTextBox {...props} fieldKey={"caption"} /></div></div>`
+ `<div id="screenSpace" style="margin-top: 100%; font-size:14px; background:yellow; width:100%; position:absolute"><FormattedTextBox {...props} fieldKey={"caption"} /></div>`
);
export const InnerCaption = new Template("Inner caption", TemplatePosition.InnerBottom,
@@ -50,27 +50,29 @@ export namespace Templates {
`<div><div style="margin:auto; height:100%; width:100%;">{layout}</div><div style="height:100%; width:300px; position:absolute; top: 0; right: -300px;"><FormattedTextBox {...props} fieldKey={"caption"}/></div> </div>`
);
+ export const TitleOverlay = new Template("TitleOverlay", TemplatePosition.InnerTop,
+ `<div><div style="height:100%; width:100%;position:absolute;">{layout}</div>
+ <div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; ">
+ <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span>
+ </div></div>`
+ );
export const Title = new Template("Title", TemplatePosition.InnerTop,
- `<div><div style="height:100%; width:100%;position:absolute;">{layout}</div><div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; padding:2px 10px">{props.Document.title}</div></div>`
+ `<div><div style="height:calc(100% - 25px); margin-top: 25px; width:100%;position:absolute;">{layout}</div>
+ <div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; ">
+ <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span>
+ </div></div>`
);
- export const Summary = new Template("Title", TemplatePosition.InnerTop,
- `<div><div style="height:100%; width:100%;position:absolute;">{layout}</div><div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; padding:2px 10px">{props.Document.doc1.title}</div></div>`
+
+ export const Bullet = new Template("Bullet", TemplatePosition.InnerTop,
+ `<div><div style="height:100%; width:100%;position:absolute;">{layout}</div>
+ <div id="isBullet" style="height:15px; width:15px; margin-left:-16px; pointer-events:all; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white;">
+ <img id="isBullet" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAZlBMVEX///8AAABmZmb7+/tYWFhgYGBFRUVSUlL4+Pg/Pz9jY2N5eXmcnJyioqKBgYFzc3NtbW1LS0s3NzfW1taWlpaOjo6IiIgvLy9WVlampqZcXFw5OTlvb28mJiYxMTHe3t7l5eUjIyMY8kIZAAAD2UlEQVR4nO2d61biMBRGW1FBEVHxfp15/5ecOVa5lHxtArmck/Xtn1BotjtNoXQtm4YQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEFIrX6UHEA1gsmrneceRjHm7cj28attKFOf/TRyKIliH4vzbZE+xE2zbZYkxRWX5Y9JT/BW0X3G+NtlR3Ahar7jcMtlS3Ba0XXG+Y7JW3BW0XHHZM/lR7AvaVewL/ijuC1pV3Bf8VnQJ2lR0CYriq/Nxg4puwfa1aZ7dz9yUHnEgN26NZ3luWkPFd7fEtHsWVDwpO+YgTgYKCuYn6tAU7TBecaygcGpZEQie7m5luKJPQQFUvCwx5iAuvQoK4KShvSIoOHVtCz7dnOUecxBn7kG/urc2eCz6T9EOcxXDCgpAUetyAwoOCBqrGF5QMKR4mCA8L+pTBIJwkRl95eifJjPHTDYTFQ8vePyrs3BsBfXLzfFHkvKKMY4j1ctNnCmmuGKslfCQT0RZiPdFVmnFmOcy36sDWYn7DU9hxdifRkKuEGQh/pWW0K/QiUlxtUxVxTTXyhQtN6kuI6mpmO5qpxJFIBjl1yMVimmvV4PfrnIq3iYsKICTRj7F9L84gIq38fYwCCj4HnMfRY/FPL8ZFayYo6BQbLlJeZrYpVDFXAUFcMtKWkUgmOhmnwKKOQsK4NaxdIp5CwqZj8X8gv27jNecJ9nZuXtnie/SzjhRQcHkt6Fnq1imoAAUY1csVVDIUrFcQSGDIhC8jriLQZIrli0oXKdVLF1QSFqxfEEBVLyI8NYXCgoKySaqhinakajimxrBRBX1FBQSVNRyDP4SXVGbYHRFfYJN8xhTESwyj5HHHEjEihoLCqDiXfAb3aksKESqCAoqEIxUUW9BAS03E+93mOhcZDYcXVF3QeHBPcI3v4qo4EPiUQcBKr75vHaiv6AAKt6NV0SCqgoKqOKYovpFZgOo+DmsOHkyUlA4ZKKamaIdQPEJK5oqKKCKM7D9zFZBIayiuYICWm5cFWef7o3vs486CP8VdQIEVRcU7sFE7VecgSmqvKDgVxEJqi8ogIof2xVnH2YLCuMT1fAU7RirOPtrXHCsovmCwlDFCgoKWNH4IrMBTdQ/NUzRjiu3CeCq9HAPAVSspaDgX9FkQcG3ollB34qGBf0UTQv6KBoXHFc0LzimWIFg0ywGBBelBxcHXLGKggKqWElBwV2xIkF3xaoEXYqVCe4rVifYV3wpPZwULOouKLzUXVBY1F1QeKm7oLCoXVAqVi7YNM7/F0YIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCG+/ANh4i1CHdc63QAAAABJRU5ErkJggg=="
+ width="15px" height="15px"/>
+ </div>
+ </div>`
);
- // export const Summary = new Template("Title", TemplatePosition.InnerTop,
- // `<div style="height:100%; width:100%;position:absolute; margin:0; padding: 0">
- // <div style="height:60%; width:100%;position:absolute;background:yellow; padding:0; margin:0">
- // {layout}
- // </div>
- // <div style="bottom:0px; height:40%; width:50%; position:absolute; background-color: rgba(0, 0, 0, .4); color: white;">
- // <FieldView {...props} fieldKey={"doc1"} />
- // </div>
- // <div style="bottom:0; left: 50%; height:40%; width:50%; position:absolute; background-color: rgba(0, 0, 0, .4); color: white;">
- // <FieldView {...props} fieldKey={"doc2"} />
- // </div>
- // </div>`
- // );
-
- export const TemplateList: Template[] = [Title, OuterCaption, InnerCaption, SideCaption];
+
+ export const TemplateList: Template[] = [Title, TitleOverlay, OuterCaption, InnerCaption, SideCaption, Bullet];
export function sortTemplates(a: Template, b: Template) {
if (a.Position < b.Position) { return -1; }
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index cbb568c07..2b1f7bb37 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -63,13 +63,13 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
if (!(documentToAdd instanceof Doc)) {
return false;
}
- let data = Cast(documentToAdd.data, listSpec(Doc), []);
+ let data = Cast(documentToAdd.data, listSpec(Doc), []).filter(d => d).map(d => d as Doc);
for (const doc of data.filter(d => d instanceof Document)) {
if (this.createsCycle(doc, containerDocument)) {
return true;
}
}
- let annots = Cast(documentToAdd.annotations, listSpec(Doc), []);
+ let annots = Cast(documentToAdd.annotations, listSpec(Doc), []).filter(d => d).map(d => d as Doc);
for (const annot of annots) {
if (this.createsCycle(annot, containerDocument)) {
return true;
@@ -95,15 +95,18 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
if (!this.createsCycle(doc, props.Document)) {
//TODO This won't create the field if it doesn't already exist
const value = Cast(props.Document[props.fieldKey], listSpec(Doc));
+ let alreadyAdded = true;
if (value !== undefined) {
- if (allowDuplicates || !value.some(v => v[Id] === doc[Id])) {
+ if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) {
+ alreadyAdded = false;
value.push(doc);
}
} else {
+ alreadyAdded = false;
Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new List([doc]));
}
// set the ZoomBasis only if hasn't already been set -- bcz: maybe set/resetting the ZoomBasis should be a parameter to addDocument?
- if (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid) {
+ if (!alreadyAdded && (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid)) {
let zoom = NumCast(this.props.Document.scale, 1);
Doc.SetOnPrototype(doc, "zoomBasis", zoom);
}
@@ -118,7 +121,8 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
const value = Cast(props.Document[props.fieldKey], listSpec(Doc), []);
let index = -1;
for (let i = 0; i < value.length; i++) {
- if (value[i][Id] === doc[Id]) {
+ let v = value[i];
+ if (v instanceof Doc && v[Id] === doc[Id]) {
index = i;
break;
}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index d894909d0..159815ea5 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -392,13 +392,15 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; }
get content() {
- if (!this._document)
+ if (!this._document) {
return (null);
+ }
return (
<div className="collectionDockingView-content" ref={this._mainCont}
style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}>
- <DocumentView key={this._document![Id]} Document={this._document!}
+ <DocumentView key={this._document[Id]} Document={this._document}
toggleMinimized={emptyFunction}
+ bringToFront={emptyFunction}
addDocument={undefined}
removeDocument={undefined}
ContentScaling={this.contentScaling}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 16818affd..ae949b2ed 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -276,7 +276,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
}
get documentKeysCheckList() {
- const docs = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+ const docs = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).filter(d => d).map(d => d as Doc);
let keys: { [key: string]: boolean } = {};
// bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields.
// then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be
@@ -322,8 +322,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
render() {
library.add(faCog);
library.add(faPlus);
- //This can't just pass FieldValue to filter because filter passes other arguments to the passed in function, which end up as default values in FieldValue
- const children = (this.children || []).filter(doc => FieldValue(doc));
+ const children = this.children;
return (
<div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel}
onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}>
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 828ac880a..0b08e150a 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -10,13 +10,14 @@ import * as rp from 'request-promise';
import { CollectionView } from "./CollectionView";
import { CollectionPDFView } from "./CollectionPDFView";
import { CollectionVideoView } from "./CollectionVideoView";
-import { Doc, Opt } from "../../../new_fields/Doc";
+import { Doc, Opt, FieldResult } from "../../../new_fields/Doc";
import { DocComponent } from "../DocComponent";
import { listSpec } from "../../../new_fields/Schema";
-import { Cast, PromiseValue, FieldValue } from "../../../new_fields/Types";
+import { Cast, PromiseValue, FieldValue, ListSpec } from "../../../new_fields/Types";
import { List } from "../../../new_fields/List";
import { DocServer } from "../../DocServer";
import { ObjectField } from "../../../new_fields/ObjectField";
+import CursorField, { CursorPosition, CursorMetadata } from "../../../new_fields/CursorField";
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -30,8 +31,6 @@ export interface SubCollectionViewProps extends CollectionViewProps {
CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
}
-export type CursorEntry = TupleField<[string, string], [number, number]>;
-
export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) {
private dropDisposer?: DragManager.DragDropDisposer;
@@ -50,30 +49,29 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
get children() {
//TODO tfs: This might not be what we want?
//This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue)
- return Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).filter(doc => FieldValue(doc));
+ return Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).filter(doc => FieldValue(doc)).map(doc => doc as Doc);
}
@action
protected async setCursorPosition(position: [number, number]) {
- return;
let ind;
let doc = this.props.Document;
let id = CurrentUserUtils.id;
let email = CurrentUserUtils.email;
+ let pos = { x: position[0], y: position[1] };
if (id && email) {
- let textInfo: [string, string] = [id, email];
const proto = await doc.proto;
if (!proto) {
return;
}
- let cursors = await Cast(proto.cursors, listSpec(ObjectField));
+ let cursors = Cast(proto.cursors, listSpec(CursorField));
if (!cursors) {
- proto.cursors = cursors = new List<ObjectField>();
+ proto.cursors = cursors = new List<CursorField>();
}
- if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0][0] === id)) > -1) {
- cursors[ind].Data[1] = position;
+ if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.data.metadata.id === id)) > -1) {
+ cursors[ind].setPosition(pos);
} else {
- let entry = new TupleField<[string, string], [number, number]>([textInfo, position]);
+ let entry = new CursorField({ metadata: { id: id, identifier: email }, position: pos });
cursors.push(entry);
}
}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 6fa374464..33787f06b 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -68,8 +68,8 @@ class TreeView extends React.Component<TreeViewProps> {
}
@action
- remove = (document: Document) => {
- let children = Cast(this.props.document.data, listSpec(Doc), []);
+ remove = (document: Document, key: string) => {
+ let children = Cast(this.props.document[key], listSpec(Doc), []);
if (children) {
children.splice(children.indexOf(document), 1);
}
@@ -81,7 +81,7 @@ class TreeView extends React.Component<TreeViewProps> {
return true;
}
//TODO This should check if it was removed
- this.remove(document);
+ this.remove(document, "data");
return addDoc(document);
}
@@ -136,14 +136,18 @@ class TreeView extends React.Component<TreeViewProps> {
if (DocumentManager.Instance.getDocumentViews(this.props.document).length) {
ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) });
}
- ContextMenu.Instance.addItem({ description: "Delete", event: undoBatch(() => this.props.deleteDoc(this.props.document)) });
+ ContextMenu.Instance.addItem({
+ description: "Delete", event: undoBatch(() => {
+ this.props.deleteDoc(this.props.document);
+ })
+ });
ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
e.stopPropagation();
}
}
- onPointerEnter = (e: React.PointerEvent): void => { this.props.document.libraryBrush = true; }
- onPointerLeave = (e: React.PointerEvent): void => { this.props.document.libraryBrush = false; }
+ onPointerEnter = (e: React.PointerEvent): void => { this.props.document.libraryBrush = true; };
+ onPointerLeave = (e: React.PointerEvent): void => { this.props.document.libraryBrush = false; };
render() {
let bulletType = BulletType.List;
@@ -160,7 +164,7 @@ class TreeView extends React.Component<TreeViewProps> {
contentElement.push(<ul key={key + "more"}>
{(key === "data") ? (null) :
<span className="collectionTreeView-keyHeader" key={key}>{key}</span>}
- {TreeView.GetChildElements(docList, key !== "data", this.remove, this.move, this.props.dropAction)}
+ {TreeView.GetChildElements(docList, key !== "data", (doc: Doc) => this.remove(doc, key), this.move, this.props.dropAction)}
</ul >);
} else
bulletType = BulletType.Collapsed;
@@ -175,9 +179,9 @@ class TreeView extends React.Component<TreeViewProps> {
</li>
</div>;
}
- public static GetChildElements(docs: Doc[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) {
- return docs.filter(child => !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).filter(doc => FieldValue(doc)).map(child =>
- <TreeView document={child} key={child[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} />);
+ public static GetChildElements(docs: (Doc | Promise<Doc>)[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) {
+ return docs.filter(child => child instanceof Doc && !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).filter(doc => FieldValue(doc)).map(child =>
+ <TreeView document={child as Doc} key={(child as Doc)[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} />);
}
}
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
index 9dee217cb..cb3fd1ba4 100644
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -90,7 +90,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
}
}
- setVideoBox = (player: VideoBox) => { this._videoBox = player; }
+ setVideoBox = (player: VideoBox) => { this._videoBox = player; };
private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => {
let props = { ...this.props, ...renderProps };
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index 3e8a8a442..737ffba7d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -1,12 +1,12 @@
.collectionfreeformlinkview-linkLine {
stroke: black;
- stroke-width: 3;
transform: translate(10000px,10000px);
+ opacity: 0.5;
pointer-events: all;
}
.collectionfreeformlinkview-linkCircle {
- stroke: black;
- stroke-width: 3;
+ stroke: rgb(0,0,0);
+ opacity: 0.5;
transform: translate(10000px,10000px);
pointer-events: all;
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 3b700b053..63d2f7642 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -47,11 +47,11 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
return (
<>
<line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine"
- style={{ strokeWidth: `${l.length * 5}` }}
+ style={{ strokeWidth: `${l.length / 2}` }}
x1={`${x1}`} y1={`${y1}`}
x2={`${x2}`} y2={`${y2}`} />
- <circle key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine"
- cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={10} onPointerDown={this.onPointerDown} />
+ <circle key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkCircle"
+ cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={5} onPointerDown={this.onPointerDown} />
</>
);
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 1c62db862..62b1456cf 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -1,4 +1,4 @@
-import { computed, IReactionDisposer, reaction } from "mobx";
+import { computed, IReactionDisposer, reaction, trace } from "mobx";
import { observer } from "mobx-react";
import { Utils } from "../../../../Utils";
import { DocumentManager } from "../../../util/DocumentManager";
@@ -84,7 +84,7 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
}
if (view.props.ContainingCollectionView) {
let collid = view.props.ContainingCollectionView.props.Document[Id];
- Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).
+ Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).filter(d => d).map(d => d as Doc).
filter(child =>
child[Id] === collid).map(view =>
DocumentManager.Instance.getDocumentViews(view).map(view =>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
index 036745eca..642118d75 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -1,26 +1,35 @@
import { computed } from "mobx";
import { observer } from "mobx-react";
-import { CollectionViewProps, CursorEntry } from "../CollectionSubView";
+import { CollectionViewProps } from "../CollectionSubView";
import "./CollectionFreeFormView.scss";
import React = require("react");
import v5 = require("uuid/v5");
import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
+import CursorField from "../../../../new_fields/CursorField";
+import { List } from "../../../../new_fields/List";
+import { Cast } from "../../../../new_fields/Types";
+import { listSpec } from "../../../../new_fields/Schema";
@observer
export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> {
- protected getCursors(): CursorEntry[] {
+
+ protected getCursors(): CursorField[] {
let doc = this.props.Document;
+
let id = CurrentUserUtils.id;
- let cursors = doc.GetList(KeyStore.Cursors, [] as CursorEntry[]);
- let notMe = cursors.filter(entry => entry.Data[0][0] !== id);
- return id ? notMe : [];
+ if (!id) {
+ return [];
+ }
+
+ let cursors = Cast(doc.cursors, listSpec(CursorField));
+
+ return (cursors || []).filter(cursor => cursor.data.metadata.id !== id);
}
private crosshairs?: HTMLCanvasElement;
drawCrosshairs = (backgroundColor: string) => {
if (this.crosshairs) {
- let c = this.crosshairs;
- let ctx = c.getContext('2d');
+ let ctx = this.crosshairs.getContext('2d');
if (ctx) {
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, 20, 20);
@@ -49,29 +58,26 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV
}
}
}
- @computed
+
get sharedCursors() {
- return this.getCursors().map(entry => {
- if (entry.Data.length > 0) {
- let id = entry.Data[0][0];
- let email = entry.Data[0][1];
- let point = entry.Data[1];
- this.drawCrosshairs("#" + v5(id, v5.URL).substring(0, 6).toUpperCase() + "22");
- return (
- <div key={id} className="collectionFreeFormRemoteCursors-cont"
- style={{ transform: `translate(${point[0] - 10}px, ${point[1] - 10}px)` }}
- >
- <canvas className="collectionFreeFormRemoteCursors-canvas"
- ref={(el) => { if (el) this.crosshairs = el; }}
- width={20}
- height={20}
- />
- <p className="collectionFreeFormRemoteCursors-symbol">
- {email[0].toUpperCase()}
- </p>
- </div>
- );
- }
+ return this.getCursors().map(c => {
+ let m = c.data.metadata;
+ let l = c.data.position;
+ this.drawCrosshairs("#" + v5(m.id, v5.URL).substring(0, 6).toUpperCase() + "22");
+ return (
+ <div key={m.id} className="collectionFreeFormRemoteCursors-cont"
+ style={{ transform: `translate(${l.x - 10}px, ${l.y - 10}px)` }}
+ >
+ <canvas className="collectionFreeFormRemoteCursors-canvas"
+ ref={(el) => { if (el) this.crosshairs = el; }}
+ width={20}
+ height={20}
+ />
+ <p className="collectionFreeFormRemoteCursors-symbol">
+ {m.identifier[0].toUpperCase()}
+ </p>
+ </div>
+ );
});
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index cb849b325..063c9e2cf 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -37,7 +37,9 @@
border-radius: $border-radius;
box-sizing: border-box;
position: absolute;
- overflow: hidden;
+ .marqueeView {
+ overflow: hidden;
+ }
top: 0;
left: 0;
width: 100%;
@@ -61,7 +63,9 @@
border-radius: $border-radius;
box-sizing: border-box;
position:absolute;
- overflow: hidden;
+ .marqueeView {
+ overflow: hidden;
+ }
top: 0;
left: 0;
width: 100%;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 17c25c9db..6861ce58b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -70,7 +70,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
public getActiveDocuments = () => {
const curPage = FieldValue(this.Document.curPage, -1);
- return FieldValue(this.children, [] as Doc[]).filter(doc => {
+ return this.children.filter(doc => {
var page = NumCast(doc.page, -1);
return page === curPage || page === -1;
});
@@ -110,15 +110,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerDown = (e: React.PointerEvent): void => {
- let childSelected = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), [] as Doc[]).filter(doc => doc).reduce((childSelected, doc) => {
- var dv = DocumentManager.Instance.getDocumentView(doc);
- return childSelected || (dv && SelectionManager.IsSelected(dv) ? true : false);
- }, false);
if ((CollectionFreeFormView.RIGHT_BTN_DRAG &&
(((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) ||
- (e.button === 0 && e.altKey)) && (childSelected || this.props.active()))) ||
+ (e.button === 0 && e.altKey)) && this.props.active())) ||
(!CollectionFreeFormView.RIGHT_BTN_DRAG &&
- ((e.button === 0 && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) && (childSelected || this.props.active())))) {
+ ((e.button === 0 && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) && this.props.active()))) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointermove", this.onPointerMove);
@@ -173,7 +169,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
// if (!this.props.active()) {
// return;
// }
- let childSelected = (this.children || []).filter(doc => doc).some(doc => {
+ let childSelected = this.children.some(doc => {
var dv = DocumentManager.Instance.getDocumentView(doc);
return dv && SelectionManager.IsSelected(dv) ? true : false;
});
@@ -226,14 +222,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
bringToFront = (doc: Doc) => {
- const docs = (this.children || []);
+ const docs = this.children;
docs.slice().sort((doc1, doc2) => {
if (doc1 === doc) return 1;
if (doc2 === doc) return -1;
return NumCast(doc1.zIndex) - NumCast(doc2.zIndex);
}).forEach((doc, index) => doc.zIndex = index + 1);
doc.zIndex = docs.length + 1;
- return doc;
}
focusDocument = (doc: Doc) => {
@@ -265,7 +260,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
ContainingCollectionView: this.props.CollectionView,
focus: this.focusDocument,
parentActive: this.props.active,
- whenActiveChanged: this.props.active,
+ whenActiveChanged: this.props.whenActiveChanged,
bringToFront: this.bringToFront,
};
}
@@ -273,8 +268,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@computed.struct
get views() {
let curPage = FieldValue(this.Document.curPage, -1);
- let docviews = (this.children || []).filter(doc => doc).reduce((prev, doc) => {
- if (!FieldValue(doc)) return prev;
+ let docviews = this.children.reduce((prev, doc) => {
+ if (!(doc instanceof Doc)) return prev;
var page = NumCast(doc.page, -1);
if (page === curPage || page === -1) {
let minim = Cast(doc.isMinimized, "boolean");
@@ -314,22 +309,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
{this.childViews}
</InkingCanvas>
</CollectionFreeFormLinksView>
- {/* <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" /> */}
+ <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
</CollectionFreeFormViewPannableContents>
- <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} {...this.props} />
</MarqueeView>
+ <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} {...this.props} />
</div>
);
}
}
@observer
-class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps> {
+class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
@computed get overlayView() {
- let overlayLayout = Cast(this.props.Document.overlayLayout, "string", "");
- return !overlayLayout ? (null) :
- (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
- isTopMost={this.props.isTopMost} isSelected={returnFalse} select={emptyFunction} />);
+ return (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
+ isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
}
render() {
return this.overlayView;
@@ -339,10 +332,8 @@ class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps> {
@observer
class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
@computed get backgroundView() {
- let backgroundLayout = Cast(this.props.Document.backgroundLayout, "string", "");
- return !backgroundLayout ? (null) :
- (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
- isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
+ return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
+ isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
}
render() {
return this.backgroundView;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index ae0a9fd48..6e8ec8662 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -21,6 +21,6 @@
white-space:nowrap;
}
.marquee-legend::after {
- content: "Press: C (collection), or Delete"
+ content: "Press: c (collection), s (summary), r (replace) or Delete"
}
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 805921ad4..1bf39e335 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -3,7 +3,7 @@ import { observer } from "mobx-react";
import { Docs } from "../../../documents/Documents";
import { SelectionManager } from "../../../util/SelectionManager";
import { Transform } from "../../../util/Transform";
-import { undoBatch } from "../../../util/UndoManager";
+import { undoBatch, UndoManager } from "../../../util/UndoManager";
import { InkingCanvas } from "../../InkingCanvas";
import { PreviewCursor } from "../../PreviewCursor";
import { CollectionFreeFormView } from "./CollectionFreeFormView";
@@ -13,10 +13,7 @@ import { Utils } from "../../../../Utils";
import { Doc } from "../../../../new_fields/Doc";
import { NumCast, Cast } from "../../../../new_fields/Types";
import { InkField, StrokeData } from "../../../../new_fields/InkField";
-import { Templates } from "../../Templates";
import { List } from "../../../../new_fields/List";
-import { emitKeypressEvents } from "readline";
-import { listSpec } from "../../../../new_fields/Schema";
interface MarqueeViewProps {
getContainerTransform: () => Transform;
@@ -50,12 +47,42 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
this._visible = false;
}
+ @undoBatch
@action
onKeyPress = (e: KeyboardEvent) => {
//make textbox and add it to this collection
let [x, y] = this.props.getTransform().transformPoint(this._downX, this._downY);
- let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
- this.props.addLiveTextDocument(newBox);
+ if (e.key === "q" && e.ctrlKey) {
+ e.preventDefault();
+ (async () => {
+ let text = await navigator.clipboard.readText();
+ let ns = text.split("\n").filter(t => t != "\r");
+ for (let i = 0; i < ns.length - 1; i++) {
+ if (ns[i].trim() === "") {
+ ns.splice(i, 1);
+ continue;
+ }
+ while (!(ns[i].trim() === "" || ns[i].endsWith("-\r") || ns[i].endsWith("-") ||
+ ns[i].endsWith(";\r") || ns[i].endsWith(";") ||
+ ns[i].endsWith(".\r") || ns[i].endsWith(".") ||
+ ns[i].endsWith(":\r") || ns[i].endsWith(":")) && i < ns.length - 1) {
+ let sub = ns[i].endsWith("\r") ? 1 : 0;
+ let br = ns[i + 1].trim() === "";
+ ns.splice(i, 2, ns[i].substr(0, ns[i].length - sub) + ns[i + 1].trimLeft());
+ if (br) break;
+ }
+ }
+ ns.map(line => {
+ let indent = line.search(/\S|$/);
+ let newBox = Docs.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line });
+ this.props.addDocument(newBox, false);
+ y += 40 * this.props.getTransform().Scale;
+ })
+ })();
+ } else {
+ let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
+ this.props.addLiveTextDocument(newBox);
+ }
e.stopPropagation();
}
@action
@@ -69,6 +96,8 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
document.addEventListener("pointermove", this.onPointerMove, true);
document.addEventListener("pointerup", this.onPointerUp, true);
document.addEventListener("keydown", this.marqueeCommand, true);
+ // bcz: do we need this? it kills the context menu on the main collection
+ // e.stopPropagation();
}
if (e.altKey) {
e.preventDefault();
@@ -141,22 +170,30 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
if (this._commandExecuted) {
return;
}
- if (e.key === "Backspace" || e.key === "Delete" || e.key == "d") {
+ if (e.key === "Backspace" || e.key === "Delete" || e.key === "d") {
this._commandExecuted = true;
this.marqueeSelect().map(d => this.props.removeDocument(d));
let ink = Cast(this.props.container.props.Document.ink, InkField);
if (ink) {
this.marqueeInkDelete(ink.inkData);
}
+ SelectionManager.DeselectAll();
this.cleanupInteractions(false);
e.stopPropagation();
}
- if (e.key === "c" || e.key === "r" || e.key === "R" || e.key === "e") {
+ if (e.key === "c" || e.key === "r" || e.key === "s" || e.key === "e" || e.key === "p") {
this._commandExecuted = true;
e.stopPropagation();
let bounds = this.Bounds;
let selected = this.marqueeSelect().map(d => {
- if (e.key !== "R") {
+ if (e.key === "s") {
+ let dCopy = Doc.MakeCopy(d);
+ dCopy.x = NumCast(d.x) - bounds.left - bounds.width / 2;
+ dCopy.y = NumCast(d.y) - bounds.top - bounds.height / 2;
+ dCopy.page = -1;
+ return dCopy;
+ }
+ else if (e.key !== "r") {
this.props.removeDocument(d);
d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
@@ -177,55 +214,52 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
width: bounds.width * zoomBasis,
height: bounds.height * zoomBasis,
ink: inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined,
- title: "a nested collection"
+ title: "a nested collection",
});
this.marqueeInkDelete(inkData);
// SelectionManager.DeselectAll();
- if (e.key === "r" || e.key === "R") {
+ if (e.key === "s" || e.key === "r" || e.key === "p") {
e.preventDefault();
let scrpt = this.props.getTransform().inverse().transformPoint(bounds.left, bounds.top);
let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" });
- if (e.key === "r") {
+ if (e.key === "s" || e.key === "p") {
summary.proto!.maximizeOnRight = true;
- let list = Cast(newCollection.data, listSpec(Doc));
- if (list && list.length === 1) {
- selected = list;
- } else {
- selected = [newCollection];
- this.props.addDocument(newCollection, false);
- }
+ newCollection.proto!.summaryDoc = summary;
+ selected = [newCollection];
}
- summary.proto!.maximizedDocs = new List<Doc>(selected);
+ summary.proto!.summarizedDocs = new List<Doc>(selected);
summary.proto!.isButton = true;
- selected.map(maximizedDoc => {
- let maxx = NumCast(maximizedDoc.x, undefined);
- let maxy = NumCast(maximizedDoc.y, undefined);
- let maxw = NumCast(maximizedDoc.width, undefined);
- let maxh = NumCast(maximizedDoc.height, undefined);
- maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), 0])
+ selected.map(summarizedDoc => {
+ let maxx = NumCast(summarizedDoc.x, undefined);
+ let maxy = NumCast(summarizedDoc.y, undefined);
+ let maxw = NumCast(summarizedDoc.width, undefined);
+ let maxh = NumCast(summarizedDoc.height, undefined);
+ summarizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), 0])
});
this.props.addLiveTextDocument(summary);
}
else {
this.props.addDocument(newCollection, false);
+ SelectionManager.DeselectAll();
+ this.props.selectDocuments([newCollection]);
}
this.cleanupInteractions(false);
- }
- if (e.key === "s") {
- this._commandExecuted = true;
- e.stopPropagation();
- e.preventDefault();
- let bounds = this.Bounds;
- let selected = this.marqueeSelect();
- SelectionManager.DeselectAll();
- let summary = Docs.TextDocument({ x: bounds.left + bounds.width + 25, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" });
- this.props.addLiveTextDocument(summary);
- selected.forEach(select => Doc.MakeLink(summary.proto!, select.proto!));
+ } else
+ if (e.key === "s") {
+ // this._commandExecuted = true;
+ // e.stopPropagation();
+ // e.preventDefault();
+ // let bounds = this.Bounds;
+ // let selected = this.marqueeSelect();
+ // SelectionManager.DeselectAll();
+ // let summary = Docs.TextDocument({ x: bounds.left + bounds.width + 25, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" });
+ // this.props.addLiveTextDocument(summary);
+ // selected.forEach(select => Doc.MakeLink(summary.proto!, select.proto!));
- this.cleanupInteractions(false);
- }
+ // this.cleanupInteractions(false);
+ }
}
@action
marqueeInkSelect(ink: Map<any, any>) {
diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss
index 4f68b71b0..cb4d1ad87 100644
--- a/src/client/views/globalCssVariables.scss
+++ b/src/client/views/globalCssVariables.scss
@@ -1,7 +1,7 @@
@import url("https://fonts.googleapis.com/css?family=Noto+Sans:400,700|Crimson+Text:400,400i,700");
// colors
$light-color: #fcfbf7;
-$light-color-secondary: rgb(241, 239, 235);
+$light-color-secondary:#f1efeb;
$main-accent: #61aaa3;
// $alt-accent: #cdd5ec;
// $alt-accent: #cdeceb;
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index f1083f859..b05f2eea2 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -6,12 +6,13 @@ import "./DocumentView.scss";
import React = require("react");
import { DocComponent } from "../DocComponent";
import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema";
-import { FieldValue, Cast, NumCast, BoolCast } from "../../../new_fields/Types";
+import { FieldValue, Cast, NumCast, BoolCast, StrCast } from "../../../new_fields/Types";
import { OmitKeys, Utils } from "../../../Utils";
import { SelectionManager } from "../../util/SelectionManager";
import { Doc, DocListCast, HeightSym } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
}
@@ -64,7 +65,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
contentScaling = () => this.nativeWidth > 0 ? this.width / this.nativeWidth : 1;
panelWidth = () => this.props.PanelWidth();
panelHeight = () => this.props.PanelHeight();
- toggleMinimized = () => this.toggleIcon();
+ toggleMinimized = async () => this.toggleIcon(await DocListCast(this.props.Document.maximizedDocs));
getTransform = (): Transform => this.props.ScreenToLocalTransform()
.translate(-this.X, -this.Y)
.scale(1 / this.contentScaling()).scale(1 / this.zoom)
@@ -95,6 +96,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}
animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, target: Doc, maximizing: boolean) {
+ if (first) {
+ if (maximizing) target.width = target.height = 1;
+ }
setTimeout(() => {
let now = Date.now();
let progress = Math.min(1, (now - stime) / 200);
@@ -122,10 +126,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
2);
}
@action
- public toggleIcon = async (): Promise<void> => {
+ public toggleIcon = async (maximizedDocs: Doc[] | undefined): Promise<void> => {
SelectionManager.DeselectAll();
let isMinimized: boolean | undefined;
- let maximizedDocs = await DocListCast(this.props.Document.maximizedDocs);
let minimizedDoc: Doc | undefined = this.props.Document;
if (!maximizedDocs) {
minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc);
@@ -133,14 +136,17 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}
if (minimizedDoc && maximizedDocs) {
let minimizedTarget = minimizedDoc;
+ if (!CollectionFreeFormDocumentView._undoBatch) {
+ CollectionFreeFormDocumentView._undoBatch = UndoManager.StartBatch("iconAnimating");
+ }
maximizedDocs.forEach(maximizedDoc => {
let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
if (!iconAnimating || (Date.now() - iconAnimating[6] > 1000)) {
if (isMinimized === undefined) {
isMinimized = BoolCast(maximizedDoc.isMinimized, false);
}
- let minx = NumCast(minimizedTarget.x, undefined) + NumCast(minimizedTarget.width, undefined) / 2;
- let miny = NumCast(minimizedTarget.y, undefined) + NumCast(minimizedTarget.height, undefined) / 2;
+ let minx = NumCast(minimizedTarget.x, undefined) + NumCast(minimizedTarget.width, undefined) * this.getTransform().Scale * this.contentScaling() / 2;
+ let miny = NumCast(minimizedTarget.y, undefined) + NumCast(minimizedTarget.height, undefined) * this.getTransform().Scale * this.contentScaling() / 2;
let maxx = NumCast(maximizedDoc.x, undefined);
let maxy = NumCast(maximizedDoc.y, undefined);
let maxw = NumCast(maximizedDoc.width, undefined);
@@ -148,32 +154,39 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
if (minx !== undefined && miny !== undefined && maxx !== undefined && maxy !== undefined &&
maxw !== undefined && maxh !== undefined) {
let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(minx, miny);
- maximizedDoc.width = maximizedDoc.height = 1;
- maximizedDoc.isMinimized = false;
+ if (isMinimized) maximizedDoc.isMinimized = false;
maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), isMinimized ? 1 : 0])
}
}
});
+ setTimeout(() => {
+ CollectionFreeFormDocumentView._undoBatch && CollectionFreeFormDocumentView._undoBatch.end();
+ CollectionFreeFormDocumentView._undoBatch = undefined;
+ }, 500);
}
}
+ static _undoBatch?: UndoManager.Batch = undefined;
onPointerDown = (e: React.PointerEvent): void => {
this._downX = e.clientX;
this._downY = e.clientY;
- e.stopPropagation();
+ // e.stopPropagation();
}
onClick = async (e: React.MouseEvent) => {
e.stopPropagation();
let altKey = e.altKey;
if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
- if (BoolCast(this.props.Document.isButton, false)) {
- let maximizedDocs = await DocListCast(this.props.Document.maximizedDocs);
+ let isBullet = (e.target as any).id === "isBullet";
+ let isIcon = StrCast(this.props.Document.layout).indexOf("IconBox") !== -1;
+ if (BoolCast(this.props.Document.isButton, false) || isBullet) {
+ let maximizedDocs = await DocListCast(isBullet ? this.props.Document.subBulletDocs : isIcon ? this.props.Document.maximizedDocs : this.props.Document.summarizedDocs);
if (maximizedDocs) { // bcz: need a better way to associate behaviors with click events on widget-documents
if ((altKey && !this.props.Document.maximizeOnRight) || (!altKey && this.props.Document.maximizeOnRight)) {
let dataDocs = await DocListCast(CollectionDockingView.Instance.props.Document.data);
if (dataDocs) {
SelectionManager.DeselectAll();
maximizedDocs.forEach(maxDoc => {
+ maxDoc.isMinimized = false;
if (!dataDocs || dataDocs.indexOf(maxDoc) == -1) {
CollectionDockingView.Instance.AddRightSplit(maxDoc);
} else {
@@ -183,15 +196,15 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}
} else {
this.props.addDocument && maximizedDocs.forEach(async maxDoc => this.props.addDocument!(await maxDoc, false));
- this.toggleIcon();
+ this.toggleIcon(maximizedDocs);
}
}
}
}
}
- onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; }
- onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; }
+ onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; };
+ onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; };
borderRounding = () => {
let br = NumCast(this.props.Document.borderRounding);
@@ -218,7 +231,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
return (
<div className="collectionFreeFormDocumentView-container" ref={this._mainCont}
onPointerDown={this.onPointerDown}
- onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
+ onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} onPointerOver={this.onPointerEnter}
onClick={this.onClick}
style={{
outlineColor: "black",
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index bbc927b5a..f404b7bc6 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,4 +1,4 @@
-import { computed } from "mobx";
+import { computed, trace } from "mobx";
import { observer } from "mobx-react";
import { CollectionDockingView } from "../collections/CollectionDockingView";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
@@ -45,7 +45,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
select: (ctrl: boolean) => void,
layoutKey: string
}> {
- @computed get layout(): string { return Cast(this.props.Document[this.props.layoutKey], "string", "<p>Error loading layout data</p>"); }
+ @computed get layout(): string { return Cast(this.props.Document[this.props.layoutKey], "string", this.props.layoutKey === "layout" ? "<p>Error loading layout data</p>" : ""); }
CreateBindings(): JsxBindings {
return { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit };
@@ -58,20 +58,33 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
}
return new List<string>();
}
- set templates(templates: List<string>) { this.props.Document.templates = templates; }
- get finalLayout() {
+ @computed get finalLayout() {
const baseLayout = this.layout;
let base = baseLayout;
let layout = baseLayout;
- this.templates.forEach(template => {
- layout = template.replace("{layout}", base);
- base = layout;
- });
+ // bcz: templates are intended only for a document's primary layout or overlay (not background). However,
+ // a DocumentContentsView is used to render annotation overlays, so we detect that here
+ // by checking the layoutKey. This should probably be moved into
+ // a prop so that the overlay can explicitly turn off templates.
+ if ((this.props.layoutKey === "overlayLayout" && StrCast(this.props.Document.layout).indexOf("CollectionView") !== -1) ||
+ (this.props.layoutKey === "layout" && StrCast(this.props.Document.layout).indexOf("CollectionView") === -1)) {
+ this.templates.forEach(template => {
+ let self = this;
+ function convertConstantsToNative(match: string, offset: number, x: string) {
+ let px = Number(match.replace("px", ""));
+ return `${px * self.props.ScreenToLocalTransform().Scale}px`;
+ }
+ let nativizedTemplate = template.replace(/([0-9]+)px/g, convertConstantsToNative);
+ layout = nativizedTemplate.replace("{layout}", base);
+ base = layout;
+ });
+ }
return layout;
}
render() {
+ if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
return <ObserverJsxParser
components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
bindings={this.CreateBindings()}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a20a8a93b..25a75904b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, runInAction } from "mobx";
+import { action, computed, runInAction, reaction, IReactionDisposer } from "mobx";
import { observer } from "mobx-react";
import { emptyFunction, Utils } from "../../../Utils";
import { Docs } from "../../documents/Documents";
@@ -16,10 +16,10 @@ import { Template, Templates } from "./../Templates";
import { DocumentContentsView } from "./DocumentContentsView";
import "./DocumentView.scss";
import React = require("react");
-import { Opt, Doc, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { Opt, Doc, WidthSym, HeightSym, DocListCast } from "../../../new_fields/Doc";
import { DocComponent } from "../DocComponent";
-import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { FieldValue, StrCast, BoolCast } from "../../../new_fields/Types";
+import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema";
+import { FieldValue, StrCast, BoolCast, Cast } from "../../../new_fields/Types";
import { List } from "../../../new_fields/List";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
@@ -99,6 +99,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
set templates(templates: List<string>) { this.props.Document.templates = templates; }
screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect();
+ _reactionDisposer?: IReactionDisposer;
@action
componentDidMount() {
if (this._mainCont.current) {
@@ -106,6 +107,18 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
handlers: { drop: this.drop.bind(this) }
});
}
+ // bcz: kind of ugly .. setup a reaction to update the title of a summary document's target (maximizedDocs) whenver the summary doc's title changes
+ this._reactionDisposer = reaction(() => [this.props.Document.maximizedDocs, this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""],
+ async () => {
+ let maxDoc = await DocListCast(this.props.Document.maximizedDocs);
+ if (maxDoc && StrCast(this.props.Document.layout).indexOf("IconBox") !== -1) {
+ this.props.Document.title = (maxDoc && maxDoc.length === 1 ? maxDoc[0].title + ".icon" : "");
+ }
+ let sumDoc = Cast(this.props.Document.summaryDoc, Doc);
+ if (sumDoc instanceof Doc) {
+ this.props.Document.title = sumDoc.title + ".expanded";
+ }
+ }, { fireImmediately: true });
DocumentManager.Instance.DocumentViews.push(this);
}
@action
@@ -121,9 +134,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
@action
componentWillUnmount() {
- if (this._dropDisposer) {
- this._dropDisposer();
- }
+ if (this._reactionDisposer) this._reactionDisposer();
+ if (this._dropDisposer) this._dropDisposer();
DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
}
@@ -131,10 +143,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
e.stopPropagation();
}
- startDragging(x: number, y: number, dropAction: dropActionType) {
+ startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean) {
if (this._mainCont.current) {
+ let allConnected = dragSubBullets ? [this.props.Document, ...Cast(this.props.Document.subBulletDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc)] : [this.props.Document];
const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0);
- let dragData = new DragManager.DocumentDragData([this.props.Document]);
+ let dragData = new DragManager.DocumentDragData(allConnected);
const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top);
dragData.dropAction = dropAction;
dragData.xOffset = xoff;
@@ -156,15 +169,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
SelectionManager.SelectDoc(this, e.ctrlKey);
}
}
+ _hitIsBullet = false;
onPointerDown = (e: React.PointerEvent): void => {
this._downX = e.clientX;
this._downY = e.clientY;
if (CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && !this.isSelected()) {
return;
}
+ this._hitIsBullet = (e.target && (e.target as any).id === "isBullet");
if (e.shiftKey && e.buttons === 1) {
if (this.props.isTopMost) {
- this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey ? "alias" : undefined);
+ this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey ? "alias" : undefined, this._hitIsBullet);
} else if (this.props.Document) {
CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e);
}
@@ -182,7 +197,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
if (!e.altKey && !this.topMost && (!CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 1) || (CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 2)) {
- this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined);
+ this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitIsBullet);
}
}
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
@@ -227,6 +242,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const protoDest = destDoc.proto;
const protoSrc = sourceDoc.proto;
Doc.MakeLink(protoSrc ? protoSrc : sourceDoc, protoDest ? protoDest : destDoc);
+ de.data.droppedDocuments.push(destDoc);
e.stopPropagation();
}
}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 613c24fa4..8bdf34181 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -1,6 +1,6 @@
import React = require("react");
import { observer } from "mobx-react";
-import { computed } from "mobx";
+import { computed, observable } from "mobx";
import { FormattedTextBox } from "./FormattedTextBox";
import { ImageBox } from "./ImageBox";
import { VideoBox } from "./VideoBox";
@@ -99,7 +99,8 @@ export class FieldView extends React.Component<FieldViewProps> {
ContainingCollectionView={this.props.ContainingCollectionView}
parentActive={this.props.active}
toggleMinimized={emptyFunction}
- whenActiveChanged={this.props.whenActiveChanged} />
+ whenActiveChanged={this.props.whenActiveChanged}
+ bringToFront={emptyFunction} />
);
}
else if (field instanceof List) {
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index d43aa4e02..458a62c5b 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -11,12 +11,13 @@
}
.formattedTextBox-cont-scroll, .formattedTextBox-cont-hidden {
- background: $light-color-secondary;
+ background: inherit;
padding: 0;
border-width: 0px;
border-radius: inherit;
border-color: $intermediate-color;
box-sizing: border-box;
+ background-color: inherit;
border-style: solid;
overflow-y: scroll;
overflow-x: hidden;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 65b8b805f..37dc05ca5 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,30 +1,31 @@
-import { action, IReactionDisposer, reaction, trace, computed, _allowStateChangesInsideComputed } from "mobx";
+import { action, IReactionDisposer, observable, reaction } from "mobx";
+import { observer } from "mobx-react";
import { baseKeymap } from "prosemirror-commands";
import { history } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
import { EditorState, Plugin, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
+import { Doc, Field, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
+import { RichTextField } from "../../../new_fields/RichTextField";
+import { createSchema, makeInterface } from "../../../new_fields/Schema";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { DocServer } from "../../DocServer";
+import { DocumentManager } from "../../util/DocumentManager";
+import { DragManager } from "../../util/DragManager";
import buildKeymap from "../../util/ProsemirrorKeymap";
import { inpRules } from "../../util/RichTextRules";
-import { schema } from "../../util/RichTextSchema";
+import { ImageResizeView, schema } 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 { ContextMenu } from "../../views/ContextMenu";
-import { MainOverlayTextBox } from "../MainOverlayTextBox";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { DocComponent } from "../DocComponent";
+import { InkingControl } from "../InkingControl";
import { FieldView, FieldViewProps } from "./FieldView";
import "./FormattedTextBox.scss";
import React = require("react");
-import { SelectionManager } from "../../util/SelectionManager";
-import { DocComponent } from "../DocComponent";
-import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { Opt, Doc, WidthSym, HeightSym } from "../../../new_fields/Doc";
-import { observer } from "mobx-react";
-import { InkingControl } from "../InkingControl";
-import { StrCast, Cast, NumCast, BoolCast } from "../../../new_fields/Types";
-import { RichTextField } from "../../../new_fields/RichTextField";
-import { Id } from "../../../new_fields/RefField";
-const { buildMenuItems } = require("prosemirror-example-setup");
-const { menuBar } = require("prosemirror-menu");
// FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document
//
@@ -63,15 +64,23 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
private _proseRef: React.RefObject<HTMLDivElement>;
private _editorView: Opt<EditorView>;
private _gotDown: boolean = false;
+ private _dropDisposer?: DragManager.DragDropDisposer;
private _reactionDisposer: Opt<IReactionDisposer>;
private _inputReactionDisposer: Opt<IReactionDisposer>;
private _proxyReactionDisposer: Opt<IReactionDisposer>;
+ public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
+
+ @observable public static InputBoxOverlay?: FormattedTextBox = undefined;
+ public static InputBoxOverlayScroll: number = 0;
constructor(props: FieldViewProps) {
super(props);
this._ref = React.createRef();
this._proseRef = React.createRef();
+ if (this.props.isOverlay) {
+ DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
+ }
}
_applyingChange: boolean = false;
@@ -91,11 +100,31 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
let titlestr = str.substr(0, Math.min(40, str.length));
let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
target.title = "-" + titlestr + (str.length > 40 ? "..." : "");
- };
+ }
+ }
+ }
+
+ @undoBatch
+ @action
+ drop = async (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.LinkDragData) {
+ let sourceDoc = de.data.linkSourceDocument;
+ let destDoc = this.props.Document;
+
+ const protoDest = destDoc.proto;
+ const protoSrc = sourceDoc.proto;
+ Doc.MakeLink(protoSrc ? protoSrc : sourceDoc, protoDest ? protoDest : destDoc);
+ de.data.droppedDocuments.push(destDoc);
+ e.stopPropagation();
}
}
componentDidMount() {
+ if (this._ref.current) {
+ this._dropDisposer = DragManager.MakeDropTarget(this._ref.current, {
+ handlers: { drop: this.drop.bind(this) }
+ });
+ }
const config = {
schema,
inpRules, //these currently don't do anything, but could eventually be helpful
@@ -118,7 +147,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
};
if (this.props.isOverlay) {
- this._inputReactionDisposer = reaction(() => MainOverlayTextBox.Instance.TextDoc && MainOverlayTextBox.Instance.TextDoc[Id],
+ this._inputReactionDisposer = reaction(() => FormattedTextBox.InputBoxOverlay,
() => {
if (this._editorView) {
this._editorView.destroy();
@@ -128,7 +157,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
);
} else {
this._proxyReactionDisposer = reaction(() => this.props.isSelected(),
- () => this.props.isSelected() && MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform));
+ () => {
+ if (this.props.isSelected()) {
+ FormattedTextBox.InputBoxOverlay = this;
+ FormattedTextBox.InputBoxOverlayScroll = this._ref.current!.scrollTop;
+ }
+ });
}
@@ -148,8 +182,16 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (this._proseRef.current) {
this._editorView = new EditorView(this._proseRef.current, {
state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config),
- dispatchTransaction: this.dispatchTransaction
+ dispatchTransaction: this.dispatchTransaction,
+ nodeViews: {
+ image(node, view, getPos) { return new ImageResizeView(node, view, getPos) }
+ }
});
+ let text = StrCast(this.props.Document.documentText);
+ if (text.startsWith("@@@")) {
+ this.props.Document.proto!.documentText = undefined;
+ this._editorView.dispatch(this._editorView.state.tr.insertText(text.replace("@@@", "")));
+ }
}
if (this.props.selectOnLoad) {
@@ -171,13 +213,35 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (this._proxyReactionDisposer) {
this._proxyReactionDisposer();
}
+ if (this._dropDisposer) {
+ this._dropDisposer();
+ }
}
onPointerDown = (e: React.PointerEvent): void => {
if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) {
e.stopPropagation();
- if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip)
+ if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) {
this._toolTipTextMenu.tooltip.style.opacity = "0";
+ }
+ }
+ if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey) {
+ if (e.target && (e.target as any).href) {
+ let href = (e.target as any).href;
+ if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
+ let docid = href.replace(DocServer.prepend("/doc/"), "");
+ DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
+ if (f instanceof Doc) {
+ if (DocumentManager.Instance.getDocumentView(f))
+ DocumentManager.Instance.getDocumentView(f)!.props.focus(f);
+ else CollectionDockingView.Instance.AddRightSplit(f);
+ }
+ }));
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
}
if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
this._gotDown = true;
@@ -185,19 +249,21 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
onPointerUp = (e: React.PointerEvent): void => {
- if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip)
+ if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) {
this._toolTipTextMenu.tooltip.style.opacity = "1";
+ }
if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
e.stopPropagation();
}
}
+ @action
onFocused = (e: React.FocusEvent): void => {
if (!this.props.isOverlay) {
- MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform);
+ FormattedTextBox.InputBoxOverlay = this;
} else {
if (this._proseRef.current) {
- this._proseRef.current.scrollTop = MainOverlayTextBox.Instance.TextScroll;
+ this._proseRef.current.scrollTop = FormattedTextBox.InputBoxOverlayScroll;
}
}
}
@@ -222,19 +288,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
description: NumCast(this.props.Document.nativeWidth) ? "Unfreeze" : "Freeze",
event: this.textCapability
});
-
- // ContextMenu.Instance.addItem({
- // description: "Submenu",
- // items: [
- // {
- // description: "item 1", event:
- // },
- // {
- // description: "item 2", event:
- // }
- // ]
- // })
- // e.stopPropagation()
}
onPointerWheel = (e: React.WheelEvent): void => {
@@ -271,7 +324,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
});
}
-
+ onBlur = (e: any) => {
+ if (this._undoTyping) {
+ this._undoTyping.end();
+ this._undoTyping = undefined;
+ }
+ }
+ public _undoTyping?: UndoManager.Batch;
onKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
SelectionManager.DeselectAll();
@@ -287,22 +346,24 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
target.title = "-" + titlestr + (str.length > 40 ? "..." : "");
}
+ if (!this._undoTyping) {
+ this._undoTyping = UndoManager.StartBatch("undoTyping");
+ }
}
render() {
let style = this.props.isOverlay ? "scroll" : "hidden";
let rounded = NumCast(this.props.Document.borderRounding) < 0 ? "-rounded" : "";
- let color = StrCast(this.props.Document.backgroundColor);
let interactive = InkingControl.Instance.selectedTool ? "" : "interactive";
return (
<div className={`formattedTextBox-cont-${style}`} ref={this._ref}
style={{
pointerEvents: interactive ? "all" : "none",
- background: color,
}}
onKeyDown={this.onKeyPress}
onKeyPress={this.onKeyPress}
onFocus={this.onFocused}
onClick={this.onClick}
+ onBlur={this.onBlur}
onPointerUp={this.onPointerUp}
onPointerDown={this.onPointerDown}
onMouseDown={this.onMouseDown}
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index 19abec4af..4bcb4c636 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -24,17 +24,6 @@ library.add(faFilm);
@observer
export class IconBox extends React.Component<FieldViewProps> {
public static LayoutString() { return FieldView.LayoutString(IconBox); }
- _reactionDisposer?: IReactionDisposer;
- componentDidMount() {
- this._reactionDisposer = reaction(() => [this.props.Document.maximizedDocs],
- async () => {
- let maxDoc = await DocListCast(this.props.Document.maximizedDocs);
- this.props.Document.title = (maxDoc && maxDoc.length === 1 ? maxDoc[0].title + ".icon" : "");
- }, { fireImmediately: true });
- }
- componentWillUnmount() {
- if (this._reactionDisposer) this._reactionDisposer();
- }
@computed get layout(): string { const field = Cast(this.props.Document[this.props.fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; }
@computed get minimizedIcon() { return IconBox.DocumentIcon(this.layout); }
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 203fb5625..5de660d57 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -46,8 +46,9 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
<td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}>
<div className="keyValuePair-td-key-container">
<button className="keyValuePair-td-key-delete" onClick={() => {
- let field = FieldValue(props.Document[props.fieldKey]);
- field && (props.Document[props.fieldKey] = undefined);
+ if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1)
+ props.Document[props.fieldKey] = undefined;
+ else props.Document.proto![props.fieldKey] = undefined;
}}>
X
</button>
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 08cfa590b..611cb66b6 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -31,28 +31,7 @@ export class LinkBox extends React.Component<Props> {
@undoBatch
onViewButtonPressed = async (e: React.PointerEvent): Promise<void> => {
e.stopPropagation();
- let docView = DocumentManager.Instance.getDocumentView(this.props.pairedDoc);
- if (docView) {
- docView.props.focus(docView.props.Document);
- } else {
- const contextDoc = await Cast(this.props.pairedDoc.annotationOn, Doc);
- if (!contextDoc) {
- CollectionDockingView.Instance.AddRightSplit(Doc.MakeDelegate(this.props.pairedDoc));
- } else {
- const page = NumCast(this.props.pairedDoc.page, undefined);
- const curPage = NumCast(contextDoc.curPage, undefined);
- if (page !== curPage) {
- contextDoc.curPage = page;
- }
- let contextView = DocumentManager.Instance.getDocumentView(contextDoc);
- if (contextView) {
- contextDoc.panTransformType = "Ease";
- contextView.props.focus(contextDoc);
- } else {
- CollectionDockingView.Instance.AddRightSplit(contextDoc);
- }
- }
- }
+ DocumentManager.Instance.jumpToDocument(this.props.pairedDoc);
}
onEditButtonPressed = (e: React.PointerEvent): void => {
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index e21adebbc..24901913d 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -31,8 +31,8 @@ export class LinkMenu extends React.Component<Props> {
render() {
//get list of links from document
- let linkFrom: Doc[] = Cast(this.props.docView.props.Document.linkedFromDocs, listSpec(Doc), []);
- let linkTo: Doc[] = Cast(this.props.docView.props.Document.linkedToDocs, listSpec(Doc), []);
+ let linkFrom = Cast(this.props.docView.props.Document.linkedFromDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc);
+ let linkTo = Cast(this.props.docView.props.Document.linkedToDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc);
if (this._editingLink === undefined) {
return (
<div id="linkMenu-container">
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index eb45ea273..caa66cbeb 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -215,8 +215,9 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
if (e.altKey) {
this._alt = true;
} else {
- if (e.metaKey)
+ if (e.metaKey) {
e.stopPropagation();
+ }
}
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointerup", this.onPointerUp);
@@ -286,7 +287,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
renderHeight = 2400;
@computed
get pdfPage() {
- return <Page height={this.renderHeight} pageNumber={this.curPage} onLoadSuccess={this.onLoaded} />
+ return <Page height={this.renderHeight} pageNumber={this.curPage} onLoadSuccess={this.onLoaded} />;
}
@computed
get pdfContent() {
diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx
index 04ef00722..57221aa39 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -6,6 +6,7 @@ import { ImageField } from '../new_fields/URLField';
import { Doc } from '../new_fields/Doc';
import { List } from '../new_fields/List';
+
const schema1 = createSchema({
hello: "number",
test: "string",
@@ -41,7 +42,7 @@ class Test extends React.Component {
doc.fields = "test";
doc.test = "hello doc";
doc.url = url;
- doc.testDoc = doc2;
+ //doc.testDoc = doc2;
const test1: TestDoc = TestDoc(doc);
@@ -70,7 +71,9 @@ class Test extends React.Component {
}
render() {
- return <button onClick={this.onClick}>Click me</button>;
+ return <div><button onClick={this.onClick}>Click me</button>
+ {/* <input onKeyPress={this.onEnter}></input> */}
+ </div>;
}
}
diff --git a/src/fields/TemplateField.ts b/src/fields/TemplateField.ts
deleted file mode 100644
index 72ae13c2e..000000000
--- a/src/fields/TemplateField.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Types } from "../server/Message";
-import { FieldId } from "./Field";
-import { Template, TemplatePosition } from "../client/views/Templates";
-
-
-export class TemplateField extends BasicField<Array<Template>> {
- constructor(data: Array<Template> = [], id?: FieldId, save: boolean = true) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new TemplateField("${this.Data}")`;
- }
-
- Copy() {
- return new TemplateField(this.Data);
- }
-
- ToJson() {
- let templates: Array<{ name: string, position: TemplatePosition, layout: string }> = [];
- this.Data.forEach(template => {
- templates.push({ name: template.Name, layout: template.Layout, position: template.Position });
- });
- return {
- type: Types.Templates,
- data: templates,
- id: this.Id,
- };
- }
-
- UpdateFromServer(data: any) {
- this.data = new Array(data);
- }
-
- static FromJson(id: string, data: any): TemplateField {
- let templates: Array<Template> = [];
- data.forEach((template: { name: string, position: number, layout: string }) => {
- templates.push(new Template(template.name, template.position, template.layout));
- });
- return new TemplateField(templates, id, false);
- }
-} \ No newline at end of file
diff --git a/src/new_fields/CursorField.ts b/src/new_fields/CursorField.ts
new file mode 100644
index 000000000..7fd326a5f
--- /dev/null
+++ b/src/new_fields/CursorField.ts
@@ -0,0 +1,55 @@
+import { ObjectField, Copy, OnUpdate } from "./ObjectField";
+import { observable } from "mobx";
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, createSimpleSchema, object } from "serializr";
+
+export type CursorPosition = {
+ x: number,
+ y: number
+}
+
+export type CursorMetadata = {
+ id: string,
+ identifier: string
+}
+
+export type CursorData = {
+ metadata: CursorMetadata,
+ position: CursorPosition
+}
+
+const PositionSchema = createSimpleSchema({
+ x: true,
+ y: true
+});
+
+const MetadataSchema = createSimpleSchema({
+ id: true,
+ identifier: true
+});
+
+const CursorSchema = createSimpleSchema({
+ metadata: object(MetadataSchema),
+ position: object(PositionSchema)
+});
+
+@Deserializable("cursor")
+export default class CursorField extends ObjectField {
+
+ @serializable(object(CursorSchema))
+ readonly data: CursorData;
+
+ constructor(data: CursorData) {
+ super();
+ this.data = data;
+ }
+
+ setPosition(position: CursorPosition) {
+ this.data.position = position;
+ this[OnUpdate]();
+ }
+
+ [Copy]() {
+ return new CursorField(this.data);
+ }
+} \ No newline at end of file
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index afcf71fc9..f844dad6e 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -25,7 +25,7 @@ export type FieldResult<T extends Field = Field> = Opt<T> | FieldWaiting<Extract
export const Update = Symbol("Update");
export const Self = Symbol("Self");
-const SelfProxy = Symbol("SelfProxy");
+export const SelfProxy = Symbol("SelfProxy");
export const WidthSym = Symbol("Width");
export const HeightSym = Symbol("Height");
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
index 86a8bd18a..2d75f8a19 100644
--- a/src/new_fields/InkField.ts
+++ b/src/new_fields/InkField.ts
@@ -1,8 +1,6 @@
import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, custom, createSimpleSchema, list, object, map } from "serializr";
import { ObjectField, Copy } from "./ObjectField";
-import { number } from "prop-types";
-import { any } from "bluebird";
import { deepCopy } from "../Utils";
export enum InkTool {
@@ -11,6 +9,7 @@ export enum InkTool {
Highlighter,
Eraser
}
+
export interface StrokeData {
pathData: Array<{ x: number, y: number }>;
color: string;
@@ -39,6 +38,6 @@ export class InkField extends ObjectField {
}
[Copy]() {
- return new InkField(deepCopy(this.inkData))
+ return new InkField(deepCopy(this.inkData));
}
}
diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts
index 5aba64406..88a65eba4 100644
--- a/src/new_fields/List.ts
+++ b/src/new_fields/List.ts
@@ -1,9 +1,9 @@
import { Deserializable, autoObject } from "../client/util/SerializationHelper";
-import { Field, Update, Self, FieldResult } from "./Doc";
-import { setter, getter, deleteProperty } from "./util";
+import { Field, Update, Self, FieldResult, SelfProxy } from "./Doc";
+import { setter, getter, deleteProperty, updateFunction } from "./util";
import { serializable, alias, list } from "serializr";
import { observable, action } from "mobx";
-import { ObjectField, OnUpdate, Copy } from "./ObjectField";
+import { ObjectField, OnUpdate, Copy, Parent } from "./ObjectField";
import { RefField } from "./RefField";
import { ProxyField } from "./Proxy";
@@ -27,7 +27,17 @@ const listHandlers: any = {
},
push: action(function (this: any, ...items: any[]) {
items = items.map(toObjectField);
- const res = this[Self].__fields.push(...items);
+ const list = this[Self];
+ const length = list.__fields.length;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ //TODO Error checking to make sure parent doesn't already exist
+ if (item instanceof ObjectField) {
+ item[Parent] = list;
+ item[OnUpdate] = updateFunction(list, i + length, item, this);
+ }
+ }
+ const res = list.__fields.push(...items);
this[Update]();
return res;
}),
@@ -48,12 +58,33 @@ const listHandlers: any = {
},
splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) {
items = items.map(toObjectField);
- const res = this[Self].__fields.splice(start, deleteCount, ...items);
+ const list = this[Self];
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ //TODO Error checking to make sure parent doesn't already exist
+ //TODO Need to change indices of other fields in array
+ if (item instanceof ObjectField) {
+ item[Parent] = list;
+ item[OnUpdate] = updateFunction(list, i + start, item, this);
+ }
+ }
+ const res = list.__fields.splice(start, deleteCount, ...items);
this[Update]();
return res.map(toRealField);
}),
unshift(...items: any[]) {
items = items.map(toObjectField);
+ const list = this[Self];
+ const length = list.__fields.length;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ //TODO Error checking to make sure parent doesn't already exist
+ //TODO Need to change indices of other fields in array
+ if (item instanceof ObjectField) {
+ item[Parent] = list;
+ item[OnUpdate] = updateFunction(list, i, item, this);
+ }
+ }
const res = this[Self].__fields.unshift(...items);
this[Update]();
return res;
@@ -202,6 +233,7 @@ class ListImpl<T extends Field> extends ObjectField {
deleteProperty: deleteProperty,
defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); },
});
+ this[SelfProxy] = list;
(list as any).push(...fields);
return list;
}
@@ -215,6 +247,12 @@ class ListImpl<T extends Field> extends ObjectField {
private set __fields(value) {
this.___fields = value;
+ for (const key in value) {
+ const field = value[key];
+ if (!(field instanceof ObjectField)) continue;
+ (field as ObjectField)[Parent] = this[Self];
+ (field as ObjectField)[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]);
+ }
}
[Copy]() {
@@ -235,6 +273,7 @@ class ListImpl<T extends Field> extends ObjectField {
}
private [Self] = this;
+ private [SelfProxy]: any;
}
export type List<T extends Field> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[];
export const List: { new <T extends Field>(fields?: T[]): List<T> } = ListImpl as any; \ No newline at end of file
diff --git a/src/new_fields/ObjectField.ts b/src/new_fields/ObjectField.ts
index 0f3777af6..f276bfa67 100644
--- a/src/new_fields/ObjectField.ts
+++ b/src/new_fields/ObjectField.ts
@@ -1,12 +1,13 @@
import { Doc } from "./Doc";
+import { RefField } from "./RefField";
export const OnUpdate = Symbol("OnUpdate");
export const Parent = Symbol("Parent");
export const Copy = Symbol("Copy");
export abstract class ObjectField {
- protected [OnUpdate]?: (diff?: any) => void;
- private [Parent]?: Doc;
+ protected [OnUpdate](diff?: any) { };
+ private [Parent]?: RefField | ObjectField;
abstract [Copy](): ObjectField;
}
diff --git a/src/server/Search.ts b/src/server/Search.ts
new file mode 100644
index 000000000..1d8cfdcf0
--- /dev/null
+++ b/src/server/Search.ts
@@ -0,0 +1,45 @@
+import * as rp from 'request-promise';
+import { Database } from './database';
+import { thisExpression } from 'babel-types';
+
+export class Search {
+ public static Instance = new Search();
+ private url = 'http://localhost:8983/solr/';
+
+ public async updateDocument(document: any) {
+ try {
+ return rp.post(this.url + "dash/update", {
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify([document])
+ });
+ } catch { }
+ }
+
+ public async search(query: string) {
+ try {
+ const searchResults = JSON.parse(await rp.get(this.url + "dash/select", {
+ qs: {
+ q: query
+ }
+ }));
+ const fields = searchResults.response.docs;
+ const ids = fields.map((field: any) => field.id);
+ return ids;
+ } catch {
+ return [];
+ }
+ }
+
+ public async clear() {
+ try {
+ return rp.post(this.url + "dash/update", {
+ body: {
+ delete: {
+ query: "*:*"
+ }
+ },
+ json: true
+ });
+ } catch { }
+ }
+} \ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 6801b3132..93e4cafbf 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -11,6 +11,7 @@ import { ObservableMap } from 'mobx';
import * as passport from 'passport';
import * as path from 'path';
import * as request from 'request';
+import * as rp from 'request-promise';
import * as io from 'socket.io';
import { Socket } from 'socket.io';
import * as webpack from 'webpack';
@@ -21,7 +22,7 @@ import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLo
import { DashUserModel } from './authentication/models/user_model';
import { Client } from './Client';
import { Database } from './database';
-import { MessageStore, Transferable, Diff } from "./Message";
+import { MessageStore, Transferable, Types, Diff } from "./Message";
import { RouteStore } from './RouteStore';
const app = express();
const config = require('../../webpack.config');
@@ -31,6 +32,9 @@ const serverPort = 4321;
import expressFlash = require('express-flash');
import flash = require('connect-flash');
import c = require("crypto");
+import { Search } from './Search';
+import { debug } from 'util';
+import _ = require('lodash');
const MongoStore = require('connect-mongo')(session);
const mongoose = require('mongoose');
@@ -117,8 +121,16 @@ app.get("/pull", (req, res) =>
res.redirect("/");
}));
+// SEARCH
+
// GETTERS
+app.get("/search", async (req, res) => {
+ let query = req.query.query || "hello";
+ let results = await Search.Instance.search(query);
+ res.send(results);
+});
+
// anyone attempting to navigate to localhost at this port will
// first have to login
addSecureRoute(
@@ -240,6 +252,7 @@ server.on("connection", function (socket: Socket) {
async function deleteFields() {
await Database.Instance.deleteAll();
+ await Search.Instance.clear();
await Database.Instance.deleteAll('newDocuments');
}
@@ -248,6 +261,7 @@ async function deleteAll() {
await Database.Instance.deleteAll('newDocuments');
await Database.Instance.deleteAll('sessions');
await Database.Instance.deleteAll('users');
+ await Search.Instance.clear();
}
function barReceived(guid: String) {
@@ -266,6 +280,10 @@ function getFields([ids, callback]: [string[], (result: Transferable[]) => void]
function setField(socket: Socket, newValue: Transferable) {
Database.Instance.update(newValue.id, newValue, () =>
socket.broadcast.emit(MessageStore.SetField.Message, newValue));
+ if (newValue.type === Types.Text) {
+ Search.Instance.updateDocument({ id: newValue.id, data: (newValue as any).data });
+ console.log("set field");
+ }
}
function GetRefField([id, callback]: [string, (result?: Transferable) => void]) {
@@ -276,9 +294,73 @@ function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => v
Database.Instance.getDocuments(ids, callback, "newDocuments");
}
+
+const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
+ "number": "_n",
+ "string": "_t",
+ "image": ["_t", "url"],
+ "video": ["_t", "url"],
+ "pdf": ["_t", "url"],
+ "audio": ["_t", "url"],
+ "web": ["_t", "url"],
+ "date": ["_d", value => new Date(value.date).toISOString()],
+ "proxy": ["_i", "fieldId"],
+ "list": ["_l", list => {
+ const results = [];
+ for (const value of list.fields) {
+ const term = ToSearchTerm(value);
+ if (term) {
+ results.push(term.value);
+ }
+ }
+ return results.length ? results : null;
+ }]
+};
+
+function ToSearchTerm(val: any): { suffix: string, value: any } | undefined {
+ const type = val.__type || typeof val;
+ let suffix = suffixMap[type];
+ if (!suffix) {
+ return;
+ }
+
+ if (Array.isArray(suffix)) {
+ const accessor = suffix[1];
+ if (typeof accessor === "function") {
+ val = accessor(val);
+ } else {
+ val = val[accessor];
+ }
+ suffix = suffix[0];
+ }
+
+ return { suffix, value: val }
+}
+
function UpdateField(socket: Socket, diff: Diff) {
Database.Instance.update(diff.id, diff.diff,
() => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false, "newDocuments");
+ const docfield = diff.diff.$set;
+ if (!docfield) {
+ return;
+ }
+ const update: any = { id: diff.id };
+ let dynfield = false;
+ for (let key in docfield) {
+ if (!key.startsWith("fields.")) continue;
+ let val = docfield[key];
+ let term = ToSearchTerm(val);
+ if (term !== undefined) {
+ let { suffix, value } = term;
+ key = key.substring(7);
+ Object.values(suffixMap).forEach(suf => update[key + suf] = null);
+ update[key + suffix] = { set: value };
+ dynfield = true;
+ }
+ }
+ if (dynfield) {
+ Search.Instance.updateDocument(update);
+ }
}
function CreateField(newValue: any) {