diff options
Diffstat (limited to 'src')
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) { |