diff options
Diffstat (limited to 'src/client/util')
-rw-r--r-- | src/client/util/ClientUtils.ts.temp | 3 | ||||
-rw-r--r-- | src/client/util/DocumentManager.ts | 110 | ||||
-rw-r--r-- | src/client/util/DragManager.ts | 181 | ||||
-rw-r--r-- | src/client/util/History.ts | 136 | ||||
-rw-r--r-- | src/client/util/Import & Export/DirectoryImportBox.tsx | 376 | ||||
-rw-r--r-- | src/client/util/Import & Export/ImportMetadataEntry.tsx | 149 | ||||
-rw-r--r-- | src/client/util/LinkManager.ts | 245 | ||||
-rw-r--r-- | src/client/util/ProsemirrorCopy/prompt.js | 179 | ||||
-rw-r--r-- | src/client/util/ProsemirrorExampleTransfer.ts (renamed from src/client/util/ProsemirrorKeymap.ts) | 10 | ||||
-rw-r--r-- | src/client/util/RichTextSchema.tsx | 205 | ||||
-rw-r--r-- | src/client/util/Scripting.ts | 55 | ||||
-rw-r--r-- | src/client/util/SearchUtil.ts | 64 | ||||
-rw-r--r-- | src/client/util/SelectionManager.ts | 79 | ||||
-rw-r--r-- | src/client/util/SerializationHelper.ts | 24 | ||||
-rw-r--r-- | src/client/util/TooltipTextMenu.scss | 94 | ||||
-rw-r--r-- | src/client/util/TooltipTextMenu.tsx | 498 | ||||
-rw-r--r-- | src/client/util/UndoManager.ts | 1 | ||||
-rw-r--r-- | src/client/util/request-image-size.js | 75 | ||||
-rw-r--r-- | src/client/util/type_decls.d | 52 |
19 files changed, 2194 insertions, 342 deletions
diff --git a/src/client/util/ClientUtils.ts.temp b/src/client/util/ClientUtils.ts.temp new file mode 100644 index 000000000..f9fad5ed9 --- /dev/null +++ b/src/client/util/ClientUtils.ts.temp @@ -0,0 +1,3 @@ +export namespace ClientUtils { + export const RELEASE = "mode"; +}
\ No newline at end of file diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index a613625cb..bb1345044 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,7 +1,7 @@ import { computed, observable, action } from 'mobx'; import { DocumentView } from '../views/nodes/DocumentView'; import { Doc, DocListCast, Opt } from '../../new_fields/Doc'; -import { FieldValue, Cast, NumCast, BoolCast } from '../../new_fields/Types'; +import { FieldValue, Cast, NumCast, BoolCast, StrCast } from '../../new_fields/Types'; import { listSpec } from '../../new_fields/Schema'; import { undoBatch } from './UndoManager'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; @@ -9,6 +9,8 @@ import { CollectionView } from '../views/collections/CollectionView'; import { CollectionPDFView } from '../views/collections/CollectionPDFView'; import { CollectionVideoView } from '../views/collections/CollectionVideoView'; import { Id } from '../../new_fields/FieldSymbols'; +import { LinkManager } from './LinkManager'; +import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; export class DocumentManager { @@ -30,14 +32,37 @@ export class DocumentManager { // this.DocumentViews = new Array<DocumentView>(); } + //gets all views + public getDocumentViewsById(id: string) { + let toReturn: DocumentView[] = []; + DocumentManager.Instance.DocumentViews.map(view => { + if (view.props.Document[Id] === id) { + toReturn.push(view); + } + }); + if (toReturn.length === 0) { + DocumentManager.Instance.DocumentViews.map(view => { + let doc = view.props.Document.proto; + if (doc && doc[Id]) { + if (doc[Id] === id) { toReturn.push(view); } + } + }); + } + return toReturn; + } + + public getAllDocumentViews(doc: Doc) { + return this.getDocumentViewsById(doc[Id]); + } + public getDocumentViewById(id: string, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null { let toReturn: DocumentView | null = null; let passes = preferredCollection ? [preferredCollection, undefined] : [undefined]; - for (let i = 0; i < passes.length; i++) { + for (let pass of passes) { DocumentManager.Instance.DocumentViews.map(view => { - if (view.props.Document[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) { + if (view.props.Document[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) { toReturn = view; return; } @@ -45,7 +70,7 @@ export class DocumentManager { if (!toReturn) { DocumentManager.Instance.DocumentViews.map(view => { let doc = view.props.Document.proto; - if (doc && doc[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) { + if (doc && doc[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) { toReturn = view; } }); @@ -66,13 +91,11 @@ export class DocumentManager { //gets document view that is in a freeform canvas collection DocumentManager.Instance.DocumentViews.map(view => { let doc = view.props.Document; - // if (view.props.ContainingCollectionView instanceof CollectionFreeFormView) { if (doc === toFind) { toReturn.push(view); } else { - let docSrc = FieldValue(doc.proto); - if (docSrc && Object.is(docSrc, toFind)) { + if (Doc.AreProtosEqual(doc, toFind)) { toReturn.push(view); } } @@ -83,39 +106,29 @@ export class DocumentManager { @computed public get LinkedDocumentViews() { - return DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => { - let linksList = DocListCast(dv.props.Document.linkedToDocs); - if (linksList && linksList.length) { - pairs.push(...linksList.reduce((pairs, link) => { - if (link) { - let linkToDoc = FieldValue(Cast(link.linkedTo, Doc)); - if (linkToDoc) { - DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => - pairs.push({ a: dv, b: docView1, l: link })); - } - } - return pairs; - }, [] as { a: DocumentView, b: DocumentView, l: Doc }[])); - } - linksList = DocListCast(dv.props.Document.linkedFromDocs); - 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 })); - } + let pairs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush)).reduce((pairs, dv) => { + let linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document); + pairs.push(...linksList.reduce((pairs, link) => { + if (link) { + let linkToDoc = LinkManager.Instance.getOppositeAnchor(link, dv.props.Document); + if (linkToDoc) { + DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => { + pairs.push({ a: dv, b: docView1, l: link }); + }); } - return pairs; - }, pairs)); - } + } + return pairs; + }, [] as { a: DocumentView, b: DocumentView, l: Doc }[])); + // } return pairs; }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]); + + return pairs; } + @undoBatch - public jumpToDocument = async (docDelegate: Doc, willZoom: boolean, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number): Promise<void> => { + public jumpToDocument = async (docDelegate: Doc, willZoom: boolean, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number, docContext?: Doc): Promise<void> => { let doc = Doc.GetProto(docDelegate); const contextDoc = await Cast(doc.annotationOn, Doc); if (contextDoc) { @@ -123,6 +136,7 @@ export class DocumentManager { const curPage = NumCast(contextDoc.curPage, page); if (page !== curPage) contextDoc.curPage = page; } + let docView: DocumentView | null; // using forceDockFunc as a flag for splitting linked to doc to the right...can change later if needed if (!forceDockFunc && (docView = DocumentManager.Instance.getDocumentView(doc))) { @@ -131,18 +145,34 @@ export class DocumentManager { docView.props.focus(docView.props.Document, willZoom); } else { if (!contextDoc) { - const actualDoc = Doc.MakeAlias(docDelegate); - actualDoc.libraryBrush = true; - if (linkPage !== undefined) actualDoc.curPage = linkPage; - (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc); + if (docContext) { + let targetContextView: DocumentView | null; + if (!forceDockFunc && docContext && (targetContextView = DocumentManager.Instance.getDocumentView(docContext))) { + docContext.panTransformType = "Ease"; + targetContextView.props.focus(docDelegate, willZoom); + } else { + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext, undefined); + setTimeout(() => { + this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage); + }, 10); + } + } else { + const actualDoc = Doc.MakeAlias(docDelegate); + actualDoc.libraryBrush = true; + if (linkPage !== undefined) actualDoc.curPage = linkPage; + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc, undefined); + } } else { let contextView: DocumentView | null; docDelegate.libraryBrush = true; if (!forceDockFunc && (contextView = DocumentManager.Instance.getDocumentView(contextDoc))) { contextDoc.panTransformType = "Ease"; - contextView.props.focus(contextDoc, willZoom); + contextView.props.focus(docDelegate, willZoom); } else { - (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc); + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc, undefined); + setTimeout(() => { + this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage); + }, 10); } } } diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 1e84a0db0..323908302 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,62 +1,109 @@ import { action, runInAction } from "mobx"; -import { Doc, DocListCastAsync } from "../../new_fields/Doc"; +import { Doc } from "../../new_fields/Doc"; import { Cast } from "../../new_fields/Types"; +import { URLField } from "../../new_fields/URLField"; import { emptyFunction } from "../../Utils"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import * as globalCssVariables from "../views/globalCssVariables.scss"; +import { DocumentManager } from "./DocumentManager"; +import { LinkManager } from "./LinkManager"; +import { SelectionManager } from "./SelectionManager"; export type dropActionType = "alias" | "copy" | undefined; -export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc>, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType) { +export function SetupDrag( + _reference: React.RefObject<HTMLElement>, + docFunc: () => Doc | Promise<Doc>, + moveFunc?: DragManager.MoveFunction, + dropAction?: dropActionType, + options?: any, + dontHideOnDrop?: boolean, + dragStarted?: () => void +) { let onRowMove = async (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); document.removeEventListener("pointermove", onRowMove); document.removeEventListener('pointerup', onRowUp); - var dragData = new DragManager.DocumentDragData([await docFunc()]); + let doc = await docFunc(); + var dragData = new DragManager.DocumentDragData([doc], [undefined]); dragData.dropAction = dropAction; dragData.moveDocument = moveFunc; + dragData.options = options; + dragData.dontHideOnDrop = dontHideOnDrop; DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y); + dragStarted && dragStarted(); }; let onRowUp = (): void => { document.removeEventListener("pointermove", onRowMove); document.removeEventListener('pointerup', onRowUp); }; let onItemDown = async (e: React.PointerEvent) => { - // if (this.props.isSelected() || this.props.isTopMost) { if (e.button === 0) { e.stopPropagation(); if (e.shiftKey && CollectionDockingView.Instance) { - CollectionDockingView.Instance.StartOtherDrag([await docFunc()], e); + e.persist(); + CollectionDockingView.Instance.StartOtherDrag({ + pageX: e.pageX, + pageY: e.pageY, + preventDefault: emptyFunction, + button: 0 + }, [await docFunc()]); } else { document.addEventListener("pointermove", onRowMove); document.addEventListener("pointerup", onRowUp); } } - //} }; return onItemDown; } +function moveLinkedDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean { + const document = SelectionManager.SelectedDocuments()[0]; + document.props.removeDocument && document.props.removeDocument(doc); + addDocument(doc); + return true; +} + +export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: number, linkDoc: Doc, sourceDoc: Doc) { + let draggeddoc = LinkManager.Instance.getOppositeAnchor(linkDoc, sourceDoc); + if (draggeddoc) { + let moddrag = await Cast(draggeddoc.annotationOn, Doc); + let dragdocs = moddrag ? [moddrag] : [draggeddoc]; + let dragData = new DragManager.DocumentDragData(dragdocs, dragdocs); + dragData.moveDocument = moveLinkedDocument; + DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, { + handlers: { + dragComplete: action(emptyFunction), + }, + hideSource: false + }); + } +} + export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) { let srcTarg = sourceDoc.proto; let draggedDocs: Doc[] = []; - let draggedFromDocs: Doc[] = []; + if (srcTarg) { - let linkToDocs = await DocListCastAsync(srcTarg.linkedToDocs); - let linkFromDocs = await DocListCastAsync(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); + let linkDocs = LinkManager.Instance.getAllRelatedLinks(srcTarg); + if (linkDocs) { + draggedDocs = linkDocs.map(link => { + let opp = LinkManager.Instance.getOppositeAnchor(link, sourceDoc); + if (opp) return opp; + }) as Doc[]; + } } - draggedDocs.push(...draggedFromDocs); if (draggedDocs.length) { let moddrag: Doc[] = []; for (const draggedDoc of draggedDocs) { let doc = await Cast(draggedDoc.annotationOn, Doc); if (doc) moddrag.push(doc); } - let dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs); - DragManager.StartDocumentDrag([dragEle], dragData, x, y, { + let dragdocs = moddrag.length ? moddrag : draggedDocs; + let dragData = new DragManager.DocumentDragData(dragdocs, dragdocs); + dragData.moveDocument = moveLinkedDocument; + DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, { handlers: { dragComplete: action(emptyFunction), }, @@ -65,6 +112,7 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n } } + export namespace DragManager { export function Root() { const root = document.getElementById("root"); @@ -86,6 +134,10 @@ export namespace DragManager { handlers: DragHandlers; hideSource: boolean | (() => boolean); + + dragHasStarted?: () => void; + + withoutShiftDrag?: boolean; } export interface DragDropDisposer { @@ -137,13 +189,15 @@ export namespace DragManager { export type MoveFunction = (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; export class DocumentDragData { - constructor(dragDoc: Doc[]) { + constructor(dragDoc: Doc[], dragDataDocs: (Doc | undefined)[]) { this.draggedDocuments = dragDoc; + this.draggedDataDocs = dragDataDocs; this.droppedDocuments = dragDoc; this.xOffset = 0; this.yOffset = 0; } draggedDocuments: Doc[]; + draggedDataDocs: (Doc | undefined)[]; droppedDocuments: Doc[]; xOffset: number; yOffset: number; @@ -153,17 +207,63 @@ export namespace DragManager { [id: string]: any; } + export class AnnotationDragData { + constructor(dragDoc: Doc, annotationDoc: Doc, dropDoc: Doc) { + this.dragDocument = dragDoc; + this.dropDocument = dropDoc; + this.annotationDocument = annotationDoc; + this.xOffset = this.yOffset = 0; + } + dragDocument: Doc; + annotationDocument: Doc; + dropDocument: Doc; + xOffset: number; + yOffset: number; + dropAction: dropActionType; + userDropAction: dropActionType; + } + 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: { [id: string]: any }) => { (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.draggedDocuments.map(d => Doc.MakeCopy(d, true)) : - dragData.draggedDocuments)); + dragData.draggedDocuments + ); + }); + } + + export function StartLinkedDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) { + dragData.moveDocument = moveLinkedDocument; + + runInAction(() => StartDragFunctions.map(func => func())); + StartDrag(eles, dragData, downX, downY, options, + (dropData: { [id: string]: any }) => { + let droppedDocuments: Doc[] = dragData.draggedDocuments.reduce((droppedDocs: Doc[], d) => { + let dvs = DocumentManager.Instance.getDocumentViews(d); + if (dvs.length) { + let inContext = dvs.filter(dv => dv.props.ContainingCollectionView === SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView); + if (inContext.length) { + inContext.forEach(dv => droppedDocs.push(dv.props.Document)); + } else { + droppedDocs.push(Doc.MakeAlias(d)); + } + } else { + droppedDocs.push(Doc.MakeAlias(d)); + } + return droppedDocs; + }, []); + dropData.droppedDocuments = droppedDocuments; + }); + } + + export function StartAnnotationDrag(eles: HTMLElement[], dragData: AnnotationDragData, downX: number, downY: number, options?: DragOptions) { + StartDrag(eles, dragData, downX, downY, options); } export class LinkDragData { @@ -178,27 +278,44 @@ export namespace DragManager { [id: string]: any; } + export class EmbedDragData { + constructor(embeddableSourceDoc: Doc) { + this.embeddableSourceDoc = embeddableSourceDoc; + this.urlField = embeddableSourceDoc.data instanceof URLField ? embeddableSourceDoc.data : undefined; + } + embeddableSourceDoc: Doc; + urlField?: URLField; + [id: string]: any; + } + export function StartLinkDrag(ele: HTMLElement, dragData: LinkDragData, downX: number, downY: number, options?: DragOptions) { StartDrag([ele], dragData, downX, downY, options); } + export function StartEmbedDrag(ele: HTMLElement, dragData: EmbedDragData, downX: number, downY: number, options?: DragOptions) { + StartDrag([ele], dragData, downX, downY, options); + } + export let AbortDrag: () => void = emptyFunction; function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) { + eles = eles.filter(e => e); if (!dragDiv) { dragDiv = document.createElement("div"); dragDiv.className = "dragManager-dragDiv"; dragDiv.style.pointerEvents = "none"; DragManager.Root().appendChild(dragDiv); } - + SelectionManager.SetIsDragging(true); let scaleXs: number[] = []; let scaleYs: number[] = []; let xs: number[] = []; let ys: number[] = []; const docs: Doc[] = - dragData instanceof DocumentDragData ? dragData.draggedDocuments : []; + dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : []; + const datadocs: (Doc | undefined)[] = + dragData instanceof DocumentDragData ? dragData.draggedDataDocs : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : []; let dragElements = eles.map(ele => { const w = ele.offsetWidth, h = ele.offsetHeight; @@ -213,11 +330,13 @@ export namespace DragManager { scaleYs.push(scaleY); let dragElement = ele.cloneNode(true) as HTMLElement; dragElement.style.opacity = "0.7"; + dragElement.style.borderRadius = getComputedStyle(ele).borderRadius; dragElement.style.position = "absolute"; dragElement.style.margin = "0"; dragElement.style.top = "0"; dragElement.style.bottom = ""; dragElement.style.left = "0"; + dragElement.style.transition = "none"; dragElement.style.color = "black"; dragElement.style.transformOrigin = "0 0"; dragElement.style.zIndex = globalCssVariables.contextMenuZindex;// "1000"; @@ -241,6 +360,16 @@ export namespace DragManager { // pdfBox.replaceChild(img, pdfBox.children[0]) // } // } + let set = dragElement.getElementsByTagName('*'); + if (dragElement.hasAttribute("style")) (dragElement as any).style.pointerEvents = "none"; + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < set.length; i++) { + if (set[i].hasAttribute("style")) { + let s = set[i]; + (s as any).style.pointerEvents = "none"; + } + } + dragDiv.appendChild(dragElement); return dragElement; @@ -259,19 +388,18 @@ export namespace DragManager { let lastX = downX; let lastY = downY; const moveHandler = (e: PointerEvent) => { - e.stopPropagation(); - e.preventDefault(); + e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop if (dragData instanceof DocumentDragData) { dragData.userDropAction = e.ctrlKey || e.altKey ? "alias" : undefined; } - if (e.shiftKey && CollectionDockingView.Instance) { + if (((options && !options.withoutShiftDrag) || !options) && e.shiftKey && CollectionDockingView.Instance) { AbortDrag(); - CollectionDockingView.Instance.StartOtherDrag(docs, { + CollectionDockingView.Instance.StartOtherDrag({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 - }); + }, docs, datadocs); } //TODO: Why can't we use e.movementX and e.movementY? let moveX = e.pageX - lastX; @@ -284,6 +412,7 @@ export namespace DragManager { }; let hideDragElements = () => { + SelectionManager.SetIsDragging(false); dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement)); eles.map(ele => (ele.hidden = false)); }; @@ -309,7 +438,7 @@ export namespace DragManager { } function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) { - let removed = dragEles.map(dragEle => { + let removed = dragData.dontHideOnDrop ? [] : dragEles.map(dragEle => { // let parent = dragEle.parentElement; // if (parent) parent.removeChild(dragEle); let ret = [dragEle, dragEle.style.width, dragEle.style.height]; @@ -335,7 +464,7 @@ export namespace DragManager { x: e.x, y: e.y, data: dragData, - mods: e.altKey ? "AltKey" : "" + mods: e.altKey ? "AltKey" : e.ctrlKey ? "CtrlKey" : "" } }) ); diff --git a/src/client/util/History.ts b/src/client/util/History.ts index 545ea8629..cbf5b3fc8 100644 --- a/src/client/util/History.ts +++ b/src/client/util/History.ts @@ -2,6 +2,8 @@ import { Doc, Opt, Field } from "../../new_fields/Doc"; import { DocServer } from "../DocServer"; import { RouteStore } from "../../server/RouteStore"; import { MainView } from "../views/MainView"; +import * as qs from 'query-string'; +import { Utils, OmitKeys } from "../../Utils"; export namespace HistoryUtil { export interface DocInitializerList { @@ -11,9 +13,11 @@ export namespace HistoryUtil { export interface DocUrl { type: "doc"; docId: string; - initializers: { + initializers?: { [docId: string]: DocInitializerList; }; + readonly?: boolean; + nro?: boolean; } export type ParsedUrl = DocUrl; @@ -21,7 +25,7 @@ export namespace HistoryUtil { // const handlers: ((state: ParsedUrl | null) => void)[] = []; function onHistory(e: PopStateEvent) { if (window.location.pathname !== RouteStore.home) { - const url = e.state as ParsedUrl || parseUrl(window.location.pathname); + const url = e.state as ParsedUrl || parseUrl(window.location); if (url) { switch (url.type) { case "doc": @@ -62,42 +66,111 @@ export namespace HistoryUtil { // } // } - export function parseUrl(pathname: string): ParsedUrl | undefined { - let pathnameSplit = pathname.split("/"); - if (pathnameSplit.length !== 2) { - return undefined; + const parsers: { [type: string]: (pathname: string[], opts: qs.ParsedQuery) => ParsedUrl | undefined } = {}; + const stringifiers: { [type: string]: (state: ParsedUrl) => string } = {}; + + type ParserValue = true | "none" | "json" | ((value: string) => any); + + type Parser = { + [key: string]: ParserValue + }; + + function addParser(type: string, requiredFields: Parser, optionalFields: Parser, customParser?: (pathname: string[], opts: qs.ParsedQuery, current: ParsedUrl) => ParsedUrl | null | undefined) { + function parse(parser: ParserValue, value: string | string[] | null | undefined) { + if (value === undefined || value === null) { + return value; + } + if (Array.isArray(value)) { + } else if (parser === true || parser === "json") { + value = JSON.parse(value); + } else if (parser === "none") { + } else { + value = parser(value); + } + return value; } - const type = pathnameSplit[0]; - const data = pathnameSplit[1]; + parsers[type] = (pathname, opts) => { + const current: any = { type }; + for (const required in requiredFields) { + if (!(required in opts)) { + return undefined; + } + const parser = requiredFields[required]; + let value = opts[required]; + value = parse(parser, value); + if (value !== null && value !== undefined) { + current[required] = value; + } + } + for (const opt in optionalFields) { + if (!(opt in opts)) { + continue; + } + const parser = optionalFields[opt]; + let value = opts[opt]; + value = parse(parser, value); + if (value !== undefined) { + current[opt] = value; + } + } + if (customParser) { + const val = customParser(pathname, opts, current); + if (val === null) { + return undefined; + } else if (val === undefined) { + return current; + } else { + return val; + } + } + return current; + }; + } - if (type === "doc") { - const s = data.split("?"); - if (s.length < 1 || s.length > 2) { - return undefined; + function addStringifier(type: string, keys: string[], customStringifier?: (state: ParsedUrl, current: string) => string) { + stringifiers[type] = state => { + let path = DocServer.prepend(`/${type}`); + if (customStringifier) { + path = customStringifier(state, path); } - const docId = s[0]; - const initializers = s.length === 2 ? JSON.parse(decodeURIComponent(s[1])) : {}; - return { - type: "doc", - docId, - initializers - }; + const queryObj = OmitKeys(state, keys).extract; + const query: any = {}; + Object.keys(queryObj).forEach(key => query[key] = queryObj[key] === null ? null : JSON.stringify(queryObj[key])); + const queryString = qs.stringify(query); + return path + (queryString ? `?${queryString}` : ""); + }; + } + + addParser("doc", {}, { readonly: true, initializers: true, nro: true }, (pathname, opts, current) => { + if (pathname.length !== 2) return undefined; + + current.initializers = current.initializers || {}; + const docId = pathname[1]; + current.docId = docId; + }); + addStringifier("doc", ["initializers", "readonly", "nro"], (state, current) => { + return `${current}/${state.docId}`; + }); + + + export function parseUrl(location: Location | URL): ParsedUrl | undefined { + const pathname = location.pathname.substring(1); + const search = location.search; + const opts = qs.parse(search, { sort: false }); + let pathnameSplit = pathname.split("/"); + + const type = pathnameSplit[0]; + + if (type in parsers) { + return parsers[type](pathnameSplit, opts); } return undefined; } export function createUrl(params: ParsedUrl): string { - let baseUrl = DocServer.prepend(`/${params.type}`); - switch (params.type) { - case "doc": - const initializers = encodeURIComponent(JSON.stringify(params.initializers)); - const id = params.docId; - let url = baseUrl + `/${id}`; - if (Object.keys(params.initializers).length) { - url += `?${initializers}`; - } - return url; + if (params.type in stringifiers) { + return stringifiers[params.type](params); } return ""; } @@ -112,7 +185,10 @@ export namespace HistoryUtil { async function onDocUrl(url: DocUrl) { const field = await DocServer.GetRefField(url.docId); - await Promise.all(Object.keys(url.initializers).map(id => initDoc(id, url.initializers[id]))); + const init = url.initializers; + if (init) { + await Promise.all(Object.keys(init).map(id => initDoc(id, init[id]))); + } if (field instanceof Doc) { MainView.Instance.openWorkspace(field, true); } diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx new file mode 100644 index 000000000..c096e9ceb --- /dev/null +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -0,0 +1,376 @@ +import "fs"; +import React = require("react"); +import { Doc, Opt, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { DocServer } from "../../DocServer"; +import { RouteStore } from "../../../server/RouteStore"; +import { action, observable, autorun, runInAction, computed } from "mobx"; +import { FieldViewProps, FieldView } from "../../views/nodes/FieldView"; +import Measure, { ContentRect } from "react-measure"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faTag, faPlus, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'; +import { Docs, DocumentOptions } from "../../documents/Documents"; +import { observer } from "mobx-react"; +import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry"; +import { Utils } from "../../../Utils"; +import { DocumentManager } from "../DocumentManager"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { List } from "../../../new_fields/List"; +import { Cast, BoolCast, NumCast } from "../../../new_fields/Types"; +import { listSpec } from "../../../new_fields/Schema"; + +const unsupported = ["text/html", "text/plain"]; + +@observer +export default class DirectoryImportBox extends React.Component<FieldViewProps> { + private selector = React.createRef<HTMLInputElement>(); + @observable private top = 0; + @observable private left = 0; + private dimensions = 50; + + @observable private entries: ImportMetadataEntry[] = []; + + @observable private quota = 1; + @observable private remaining = 1; + + @observable private uploading = false; + @observable private removeHover = false; + + public static LayoutString() { return FieldView.LayoutString(DirectoryImportBox); } + + constructor(props: FieldViewProps) { + super(props); + library.add(faTag, faPlus); + let doc = this.props.Document; + this.editingMetadata = this.editingMetadata || false; + this.persistent = this.persistent || false; + !Cast(doc.data, listSpec(Doc)) && (doc.data = new List<Doc>()); + } + + @computed + private get editingMetadata() { + return BoolCast(this.props.Document.editingMetadata); + } + + private set editingMetadata(value: boolean) { + this.props.Document.editingMetadata = value; + } + + @computed + private get persistent() { + return BoolCast(this.props.Document.persistent); + } + + private set persistent(value: boolean) { + this.props.Document.persistent = value; + } + + handleSelection = async (e: React.ChangeEvent<HTMLInputElement>) => { + runInAction(() => this.uploading = true); + + let promises: Promise<void>[] = []; + let docs: Doc[] = []; + + let files = e.target.files; + if (!files || files.length === 0) return; + + let directory = (files.item(0) as any).webkitRelativePath.split("/", 1); + + let validated: File[] = []; + for (let i = 0; i < files.length; i++) { + let file = files.item(i); + file && !unsupported.includes(file.type) && validated.push(file); + } + + runInAction(() => this.quota = validated.length); + + let sizes = []; + let modifiedDates = []; + + for (let uploaded_file of validated) { + let formData = new FormData(); + formData.append('file', uploaded_file); + let dropFileName = uploaded_file ? uploaded_file.name : "-empty-"; + let type = uploaded_file.type; + + sizes.push(uploaded_file.size); + modifiedDates.push(uploaded_file.lastModified); + + runInAction(() => this.remaining++); + + let prom = fetch(DocServer.prepend(RouteStore.upload), { + method: 'POST', + body: formData + }).then(async (res: Response) => { + (await res.json()).map(action((file: any) => { + let docPromise = Docs.Get.DocumentFromType(type, DocServer.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName }); + docPromise.then(doc => { + doc && docs.push(doc) && runInAction(() => this.remaining--); + }); + })); + }); + promises.push(prom); + } + + await Promise.all(promises); + + for (let i = 0; i < docs.length; i++) { + let doc = docs[i]; + doc.size = sizes[i]; + doc.modified = modifiedDates[i]; + this.entries.forEach(entry => { + let target = entry.onDataDoc ? Doc.GetProto(doc) : doc; + target[entry.key] = entry.value; + }); + } + + let doc = this.props.Document; + let height: number = NumCast(doc.height) || 0; + let offset: number = this.persistent ? (height === 0 ? 0 : height + 30) : 0; + let options: DocumentOptions = { + title: `Import of ${directory}`, + width: 1105, + height: 500, + x: NumCast(doc.x), + y: NumCast(doc.y) + offset + }; + let parent = this.props.ContainingCollectionView; + if (parent) { + let importContainer = Docs.Create.StackingDocument(docs, options); + importContainer.singleColumn = false; + Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer); + !this.persistent && this.props.removeDocument && this.props.removeDocument(doc); + DocumentManager.Instance.jumpToDocument(importContainer, true); + + } + + runInAction(() => { + this.uploading = false; + this.quota = 1; + this.remaining = 1; + }); + } + + componentDidMount() { + this.selector.current!.setAttribute("directory", ""); + this.selector.current!.setAttribute("webkitdirectory", ""); + } + + @action + preserveCentering = (rect: ContentRect) => { + let bounds = rect.offset!; + if (bounds.width === 0 || bounds.height === 0) { + return; + } + let offset = this.dimensions / 2; + this.left = bounds.width / 2 - offset; + this.top = bounds.height / 2 - offset; + } + + @action + addMetadataEntry = async () => { + let entryDoc = new Doc(); + entryDoc.checked = false; + entryDoc.key = keyPlaceholder; + entryDoc.value = valuePlaceholder; + Doc.AddDocToList(this.props.Document, "data", entryDoc); + } + + @action + remove = async (entry: ImportMetadataEntry) => { + let metadata = await DocListCastAsync(this.props.Document.data); + if (metadata) { + let index = this.entries.indexOf(entry); + if (index !== -1) { + runInAction(() => this.entries.splice(index, 1)); + index = metadata.indexOf(entry.props.Document); + if (index !== -1) { + metadata.splice(index, 1); + } + } + + } + } + + render() { + let dimensions = 50; + let entries = DocListCast(this.props.Document.data); + let isEditing = this.editingMetadata; + let remaining = this.remaining; + let quota = this.quota; + let uploading = this.uploading; + let showRemoveLabel = this.removeHover; + let persistent = this.persistent; + let percent = `${100 - (remaining / quota * 100)}`; + percent = percent.split(".")[0]; + percent = percent.startsWith("100") ? "99" : percent; + let marginOffset = (percent.length === 1 ? 5 : 0) - 1.6; + return ( + <Measure offset onResize={this.preserveCentering}> + {({ measureRef }) => + <div ref={measureRef} style={{ width: "100%", height: "100%", pointerEvents: "all" }} > + <input + id={"selector"} + ref={this.selector} + onChange={this.handleSelection} + type="file" + style={{ + position: "absolute", + display: "none" + }} /> + <label + htmlFor={"selector"} + style={{ + opacity: isEditing ? 0 : 1, + pointerEvents: isEditing ? "none" : "all", + transition: "0.4s ease opacity" + }} + > + <div style={{ + width: dimensions, + height: dimensions, + borderRadius: "50%", + background: "black", + position: "absolute", + left: this.left, + top: this.top + }} /> + <div style={{ + position: "absolute", + left: this.left + 8, + top: this.top + 10, + opacity: uploading ? 0 : 1, + transition: "0.4s opacity ease" + }}> + <FontAwesomeIcon icon={faCloudUploadAlt} color="#FFFFFF" size={"2x"} /> + </div> + <img + style={{ + width: 80, + height: 80, + transition: "0.4s opacity ease", + opacity: uploading ? 0.7 : 0, + position: "absolute", + top: this.top - 15, + left: this.left - 15 + }} + src={"/assets/loading.gif"}></img> + </label> + <input + type={"checkbox"} + onChange={e => runInAction(() => this.persistent = e.target.checked)} + style={{ + margin: 0, + position: "absolute", + left: 10, + bottom: 10, + opacity: isEditing || uploading ? 0 : 1, + transition: "0.4s opacity ease", + pointerEvents: isEditing || uploading ? "none" : "all" + }} + checked={this.persistent} + onPointerEnter={action(() => this.removeHover = true)} + onPointerLeave={action(() => this.removeHover = false)} + /> + <p + style={{ + position: "absolute", + left: 27, + bottom: 8.4, + fontSize: 12, + opacity: showRemoveLabel ? 1 : 0, + transition: "0.4s opacity ease" + }}>Template will be <span style={{ textDecoration: "underline", textDecorationColor: persistent ? "green" : "red", color: persistent ? "green" : "red" }}>{persistent ? "kept" : "removed"}</span> after upload</p> + <div + style={{ + transition: "0.4s opacity ease", + opacity: uploading ? 1 : 0, + pointerEvents: "none", + position: "absolute", + left: 10, + top: this.top + 12.3, + fontSize: 18, + color: "white", + marginLeft: this.left + marginOffset + }}>{percent}%</div> + <div + style={{ + position: "absolute", + top: 10, + right: 10, + borderRadius: "50%", + width: 25, + height: 25, + background: "black", + pointerEvents: uploading ? "none" : "all", + opacity: uploading ? 0 : 1, + transition: "0.4s opacity ease" + }} + title={isEditing ? "Back to Upload" : "Add Metadata"} + onClick={action(() => this.editingMetadata = !this.editingMetadata)} + /> + <FontAwesomeIcon + style={{ + pointerEvents: "none", + position: "absolute", + right: isEditing ? 16.3 : 14.5, + top: isEditing ? 15.4 : 16, + opacity: uploading ? 0 : 1, + transition: "0.4s opacity ease" + }} + icon={isEditing ? faCloudUploadAlt : faTag} + color="#FFFFFF" + size={"1x"} + /> + <div + style={{ + transition: "0.4s ease opacity", + width: "100%", + height: "100%", + pointerEvents: isEditing ? "all" : "none", + opacity: isEditing ? 1 : 0, + overflowY: "scroll" + }} + > + <div + style={{ + borderRadius: "50%", + width: 25, + height: 25, + marginLeft: 10, + position: "absolute", + right: 41, + top: 10 + }} + title={"Add Metadata Entry"} + onClick={this.addMetadataEntry} + > + <FontAwesomeIcon + style={{ + pointerEvents: "none", + marginLeft: 6.4, + marginTop: 5.2 + }} + icon={faPlus} + size={"1x"} + /> + </div> + <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }} >Add metadata to your import...</p> + <hr style={{ margin: "6px 10px 12px 10px" }} /> + {entries.map(doc => + <ImportMetadataEntry + Document={doc} + key={doc[Id]} + remove={this.remove} + ref={(el) => { if (el) this.entries.push(el); }} + next={this.addMetadataEntry} + /> + )} + </div> + </div> + } + </Measure> + ); + } + +}
\ No newline at end of file diff --git a/src/client/util/Import & Export/ImportMetadataEntry.tsx b/src/client/util/Import & Export/ImportMetadataEntry.tsx new file mode 100644 index 000000000..f5198c39b --- /dev/null +++ b/src/client/util/Import & Export/ImportMetadataEntry.tsx @@ -0,0 +1,149 @@ +import React = require("react"); +import { observer } from "mobx-react"; +import { EditableView } from "../../views/EditableView"; +import { observable, action, computed } from "mobx"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { Opt, Doc } from "../../../new_fields/Doc"; +import { StrCast, BoolCast } from "../../../new_fields/Types"; + +interface KeyValueProps { + Document: Doc; + remove: (self: ImportMetadataEntry) => void; + next: () => void; +} + +export const keyPlaceholder = "Key"; +export const valuePlaceholder = "Value"; + +@observer +export default class ImportMetadataEntry extends React.Component<KeyValueProps> { + + private keyRef = React.createRef<EditableView>(); + private valueRef = React.createRef<EditableView>(); + private checkRef = React.createRef<HTMLInputElement>(); + + constructor(props: KeyValueProps) { + super(props); + library.add(faPlus); + } + + @computed + public get valid() { + return (this.key.length > 0 && this.key !== keyPlaceholder) && (this.value.length > 0 && this.value !== valuePlaceholder); + } + + @computed + private get backing() { + return this.props.Document; + } + + @computed + public get onDataDoc() { + return BoolCast(this.backing.checked); + } + + public set onDataDoc(value: boolean) { + this.backing.checked = value; + } + + @computed + public get key() { + return StrCast(this.backing.key); + } + + public set key(value: string) { + this.backing.key = value; + } + + @computed + public get value() { + return StrCast(this.backing.value); + } + + public set value(value: string) { + this.backing.value = value; + } + + @action + updateKey = (newKey: string) => { + this.key = newKey; + this.keyRef.current && this.keyRef.current.setIsFocused(false); + this.valueRef.current && this.valueRef.current.setIsFocused(true); + this.key.length === 0 && (this.key = keyPlaceholder); + return true; + } + + @action + updateValue = (newValue: string, shiftDown: boolean) => { + this.value = newValue; + this.valueRef.current && this.valueRef.current.setIsFocused(false); + this.value.length > 0 && shiftDown && this.props.next(); + this.value.length === 0 && (this.value = valuePlaceholder); + return true; + } + + render() { + let keyValueStyle: React.CSSProperties = { + paddingLeft: 10, + width: "50%", + opacity: this.valid ? 1 : 0.5, + }; + return ( + <div + style={{ + display: "flex", + flexDirection: "row", + paddingBottom: 5, + paddingRight: 5, + justifyContent: "center", + alignItems: "center", + alignContent: "center" + }} + > + <input + onChange={e => this.onDataDoc = e.target.checked} + ref={this.checkRef} + style={{ margin: "0 10px 0 15px" }} + type="checkbox" + title={"Add to Data Document?"} + checked={this.onDataDoc} + /> + <div className={"key_container"} style={keyValueStyle}> + <EditableView + ref={this.keyRef} + contents={this.key} + SetValue={this.updateKey} + GetValue={() => ""} + oneLine={true} + /> + </div> + <div + className={"value_container"} + style={keyValueStyle}> + <EditableView + ref={this.valueRef} + contents={this.value} + SetValue={this.updateValue} + GetValue={() => ""} + oneLine={true} + /> + </div> + <div onClick={() => this.props.remove(this)} title={"Delete Entry"}> + <FontAwesomeIcon + icon={faPlus} + color={"red"} + size={"1x"} + style={{ + marginLeft: 15, + marginRight: 15, + transform: "rotate(45deg)" + }} + /> + </div> + </div> + ); + } + +}
\ No newline at end of file diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts new file mode 100644 index 000000000..a647f22c1 --- /dev/null +++ b/src/client/util/LinkManager.ts @@ -0,0 +1,245 @@ +import { observable, action } from "mobx"; +import { StrCast, Cast, FieldValue } from "../../new_fields/Types"; +import { Doc, DocListCast } from "../../new_fields/Doc"; +import { listSpec } from "../../new_fields/Schema"; +import { List } from "../../new_fields/List"; +import { Id } from "../../new_fields/FieldSymbols"; +import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; +import { Docs } from "../documents/Documents"; + + +/* + * link doc: + * - anchor1: doc + * - anchor1page: number + * - anchor1groups: list of group docs representing the groups anchor1 categorizes this link/anchor2 in + * - anchor2: doc + * - anchor2page: number + * - anchor2groups: list of group docs representing the groups anchor2 categorizes this link/anchor1 in + * + * group doc: + * - type: string representing the group type/name/category + * - metadata: doc representing the metadata kvps + * + * metadata doc: + * - user defined kvps + */ +export class LinkManager { + + private static _instance: LinkManager; + public static get Instance(): LinkManager { + return this._instance || (this._instance = new this()); + } + private constructor() { + } + + // the linkmanagerdoc stores a list of docs representing all linkdocs in 'allLinks' and a list of strings representing all group types in 'allGroupTypes' + // lists of strings representing the metadata keys for each group type is stored under a key that is the same as the group type + public get LinkManagerDoc(): Doc | undefined { + // return FieldValue(Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc)); + + return Docs.Prototypes.MainLinkDocument(); + } + + public getAllLinks(): Doc[] { + return LinkManager.Instance.LinkManagerDoc ? LinkManager.Instance.LinkManagerDoc.allLinks ? DocListCast(LinkManager.Instance.LinkManagerDoc.allLinks) : [] : []; + } + + public addLink(linkDoc: Doc): boolean { + let linkList = LinkManager.Instance.getAllLinks(); + linkList.push(linkDoc); + if (LinkManager.Instance.LinkManagerDoc) { + LinkManager.Instance.LinkManagerDoc.allLinks = new List<Doc>(linkList); + return true; + } + return false; + } + + public deleteLink(linkDoc: Doc): boolean { + let linkList = LinkManager.Instance.getAllLinks(); + let index = LinkManager.Instance.getAllLinks().indexOf(linkDoc); + if (index > -1) { + linkList.splice(index, 1); + if (LinkManager.Instance.LinkManagerDoc) { + LinkManager.Instance.LinkManagerDoc.allLinks = new List<Doc>(linkList); + return true; + } + } + return false; + } + + // finds all links that contain the given anchor + public getAllRelatedLinks(anchor: Doc): Doc[] {//List<Doc> { + let related = LinkManager.Instance.getAllLinks().filter(link => { + let protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, null)); + let protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, null)); + return protomatch1 || protomatch2; + }); + return related; + } + + public deleteAllLinksOnAnchor(anchor: Doc) { + let related = LinkManager.Instance.getAllRelatedLinks(anchor); + related.forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc)); + } + + public addGroupType(groupType: string): boolean { + if (LinkManager.Instance.LinkManagerDoc) { + LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>([]); + let groupTypes = LinkManager.Instance.getAllGroupTypes(); + groupTypes.push(groupType); + LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>(groupTypes); + return true; + } + return false; + } + + // removes all group docs from all links with the given group type + public deleteGroupType(groupType: string): boolean { + if (LinkManager.Instance.LinkManagerDoc) { + if (LinkManager.Instance.LinkManagerDoc[groupType]) { + let groupTypes = LinkManager.Instance.getAllGroupTypes(); + let index = groupTypes.findIndex(type => type.toUpperCase() === groupType.toUpperCase()); + if (index > -1) groupTypes.splice(index, 1); + LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>(groupTypes); + LinkManager.Instance.LinkManagerDoc[groupType] = undefined; + LinkManager.Instance.getAllLinks().forEach(async linkDoc => { + const anchor1 = await Cast(linkDoc.anchor1, Doc); + const anchor2 = await Cast(linkDoc.anchor2, Doc); + anchor1 && LinkManager.Instance.removeGroupFromAnchor(linkDoc, anchor1, groupType); + anchor2 && LinkManager.Instance.removeGroupFromAnchor(linkDoc, anchor2, groupType); + }); + } + return true; + } else return false; + } + + public getAllGroupTypes(): string[] { + if (LinkManager.Instance.LinkManagerDoc) { + if (LinkManager.Instance.LinkManagerDoc.allGroupTypes) { + return Cast(LinkManager.Instance.LinkManagerDoc.allGroupTypes, listSpec("string"), []); + } else { + LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>([]); + return []; + } + } + return []; + } + + // gets the groups associates with an anchor in a link + public getAnchorGroups(linkDoc: Doc, anchor?: Doc): Array<Doc> { + if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) { + return DocListCast(linkDoc.anchor1Groups); + } else { + return DocListCast(linkDoc.anchor2Groups); + } + } + + // sets the groups of the given anchor in the given link + public setAnchorGroups(linkDoc: Doc, anchor: Doc, groups: Doc[]) { + if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) { + linkDoc.anchor1Groups = new List<Doc>(groups); + } else { + linkDoc.anchor2Groups = new List<Doc>(groups); + } + } + + public addGroupToAnchor(linkDoc: Doc, anchor: Doc, groupDoc: Doc, replace: boolean = false) { + let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor); + let index = groups.findIndex(gDoc => { + return StrCast(groupDoc.type).toUpperCase() === StrCast(gDoc.type).toUpperCase(); + }); + if (index > -1 && replace) { + groups[index] = groupDoc; + } + if (index === -1) { + groups.push(groupDoc); + } + LinkManager.Instance.setAnchorGroups(linkDoc, anchor, groups); + } + + // removes group doc of given group type only from given anchor on given link + public removeGroupFromAnchor(linkDoc: Doc, anchor: Doc, groupType: string) { + let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor); + let newGroups = groups.filter(groupDoc => StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase()); + LinkManager.Instance.setAnchorGroups(linkDoc, anchor, newGroups); + } + + // returns map of group type to anchor's links in that group type + public getRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> { + let related = this.getAllRelatedLinks(anchor); + let anchorGroups = new Map<string, Array<Doc>>(); + related.forEach(link => { + let groups = LinkManager.Instance.getAnchorGroups(link, anchor); + + if (groups.length > 0) { + groups.forEach(groupDoc => { + let groupType = StrCast(groupDoc.type); + if (groupType === "") { + let group = anchorGroups.get("*"); + anchorGroups.set("*", group ? [...group, link] : [link]); + } else { + let group = anchorGroups.get(groupType); + anchorGroups.set(groupType, group ? [...group, link] : [link]); + } + }); + } else { + // if link is in no groups then put it in default group + let group = anchorGroups.get("*"); + anchorGroups.set("*", group ? [...group, link] : [link]); + } + + }); + return anchorGroups; + } + + // gets a list of strings representing the keys of the metadata associated with the given group type + public getMetadataKeysInGroup(groupType: string): string[] { + if (LinkManager.Instance.LinkManagerDoc) { + return LinkManager.Instance.LinkManagerDoc[groupType] ? Cast(LinkManager.Instance.LinkManagerDoc[groupType], listSpec("string"), []) : []; + } + return []; + } + + public setMetadataKeysForGroup(groupType: string, keys: string[]): boolean { + if (LinkManager.Instance.LinkManagerDoc) { + LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>(keys); + return true; + } + return false; + } + + // returns a list of all metadata docs associated with the given group type + public getAllMetadataDocsInGroup(groupType: string): Array<Doc> { + let md: Doc[] = []; + let allLinks = LinkManager.Instance.getAllLinks(); + allLinks.forEach(linkDoc => { + let anchor1Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor1, Doc, null)); + let anchor2Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor2, Doc, null)); + anchor1Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) { const meta = Cast(groupDoc.metadata, Doc, null); meta && md.push(meta); } }); + anchor2Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) { const meta = Cast(groupDoc.metadata, Doc, null); meta && md.push(meta); } }); + }); + return md; + } + + // checks if a link with the given anchors exists + public doesLinkExist(anchor1: Doc, anchor2: Doc): boolean { + let allLinks = LinkManager.Instance.getAllLinks(); + let index = allLinks.findIndex(linkDoc => { + return (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor1) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor2)) || + (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor2) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor1)); + }); + return index !== -1; + } + + // finds the opposite anchor of a given anchor in a link + //TODO This should probably return undefined if there isn't an opposite anchor + //TODO This should also await the return value of the anchor so we don't filter out promises + public getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc | undefined { + if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) { + return Cast(linkDoc.anchor2, Doc, null); + } else { + return Cast(linkDoc.anchor1, Doc, null); + } + } +}
\ No newline at end of file diff --git a/src/client/util/ProsemirrorCopy/prompt.js b/src/client/util/ProsemirrorCopy/prompt.js new file mode 100644 index 000000000..b9068195f --- /dev/null +++ b/src/client/util/ProsemirrorCopy/prompt.js @@ -0,0 +1,179 @@ +const prefix = "ProseMirror-prompt" + +export function openPrompt(options) { + let wrapper = document.body.appendChild(document.createElement("div")) + wrapper.className = prefix + wrapper.style.zIndex = 1000; + wrapper.style.width = 250; + wrapper.style.textAlign = "center"; + + let mouseOutside = e => { if (!wrapper.contains(e.target)) close() } + setTimeout(() => window.addEventListener("mousedown", mouseOutside), 50) + let close = () => { + window.removeEventListener("mousedown", mouseOutside) + if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper) + } + + let domFields = [] + for (let name in options.fields) domFields.push(options.fields[name].render()) + + let submitButton = document.createElement("button") + submitButton.type = "submit" + submitButton.className = prefix + "-submit" + submitButton.textContent = "OK" + let cancelButton = document.createElement("button") + cancelButton.type = "button" + cancelButton.className = prefix + "-cancel" + cancelButton.textContent = "Cancel" + cancelButton.addEventListener("click", close) + + let form = wrapper.appendChild(document.createElement("form")) + let title = document.createElement("h5") + title.style.marginBottom = 15 + title.style.marginTop = 10 + if (options.title) form.appendChild(title).textContent = options.title + domFields.forEach(field => { + form.appendChild(document.createElement("div")).appendChild(field) + }) + let b = document.createElement("div"); + b.style.marginTop = 15; + let buttons = form.appendChild(b) + // buttons.className = prefix + "-buttons" + buttons.appendChild(submitButton) + buttons.appendChild(document.createTextNode(" ")) + buttons.appendChild(cancelButton) + + let box = wrapper.getBoundingClientRect() + wrapper.style.top = options.flyout_top + "px" + wrapper.style.left = options.flyout_left + "px" + + let submit = () => { + let params = getValues(options.fields, domFields) + if (params) { + close() + options.callback(params) + } + } + + form.addEventListener("submit", e => { + e.preventDefault() + submit() + }) + + form.addEventListener("keydown", e => { + if (e.keyCode == 27) { + e.preventDefault() + close() + } else if (e.keyCode == 13 && !(e.ctrlKey || e.metaKey || e.shiftKey)) { + e.preventDefault() + submit() + } else if (e.keyCode == 9) { + window.setTimeout(() => { + if (!wrapper.contains(document.activeElement)) close() + }, 500) + } + }) + + let input = form.elements[0] + if (input) input.focus() +} + +function getValues(fields, domFields) { + let result = Object.create(null), i = 0 + for (let name in fields) { + let field = fields[name], dom = domFields[i++] + let value = field.read(dom), bad = field.validate(value) + if (bad) { + reportInvalid(dom, bad) + return null + } + result[name] = field.clean(value) + } + return result +} + +function reportInvalid(dom, message) { + // FIXME this is awful and needs a lot more work + let parent = dom.parentNode + let msg = parent.appendChild(document.createElement("div")) + msg.style.left = (dom.offsetLeft + dom.offsetWidth + 2) + "px" + msg.style.top = (dom.offsetTop - 5) + "px" + msg.className = "ProseMirror-invalid" + msg.textContent = message + setTimeout(() => parent.removeChild(msg), 1500) +} + +// ::- The type of field that `FieldPrompt` expects to be passed to it. +export class Field { + // :: (Object) + // Create a field with the given options. Options support by all + // field types are: + // + // **`value`**`: ?any` + // : The starting value for the field. + // + // **`label`**`: string` + // : The label for the field. + // + // **`required`**`: ?bool` + // : Whether the field is required. + // + // **`validate`**`: ?(any) → ?string` + // : A function to validate the given value. Should return an + // error message if it is not valid. + constructor(options) { this.options = options } + + // render:: (state: EditorState, props: Object) → dom.Node + // Render the field to the DOM. Should be implemented by all subclasses. + + // :: (dom.Node) → any + // Read the field's value from its DOM node. + read(dom) { return dom.value } + + // :: (any) → ?string + // A field-type-specific validation function. + validateType(_value) { } + + validate(value) { + if (!value && this.options.required) + return "Required field" + return this.validateType(value) || (this.options.validate && this.options.validate(value)) + } + + clean(value) { + return this.options.clean ? this.options.clean(value) : value + } +} + +// ::- A field class for single-line text fields. +export class TextField extends Field { + render() { + let input = document.createElement("input") + input.type = "text" + input.placeholder = this.options.label + input.value = this.options.value || "" + input.autocomplete = "off" + input.style.marginBottom = 4 + input.style.border = "1px solid black" + input.style.padding = "4px 4px" + return input + } +} + + +// ::- A field class for dropdown fields based on a plain `<select>` +// tag. Expects an option `options`, which should be an array of +// `{value: string, label: string}` objects, or a function taking a +// `ProseMirror` instance and returning such an array. +export class SelectField extends Field { + render() { + let select = document.createElement("select") + this.options.options.forEach(o => { + let opt = select.appendChild(document.createElement("option")) + opt.value = o.value + opt.selected = o.value == this.options.value + opt.label = o.label + }) + return select + } +} diff --git a/src/client/util/ProsemirrorKeymap.ts b/src/client/util/ProsemirrorExampleTransfer.ts index 00d086b97..fa9e2e5af 100644 --- a/src/client/util/ProsemirrorKeymap.ts +++ b/src/client/util/ProsemirrorExampleTransfer.ts @@ -1,4 +1,4 @@ -import { Schema } from "prosemirror-model"; +import { Schema, NodeType } from "prosemirror-model"; import { wrapIn, setBlockType, chainCommands, toggleMark, exitCode, joinUp, joinDown, lift, selectParentNode @@ -7,6 +7,8 @@ import { wrapInList, splitListItem, liftListItem, sinkListItem } from "prosemirr import { undo, redo } from "prosemirror-history"; import { undoInputRule } from "prosemirror-inputrules"; import { Transaction, EditorState } from "prosemirror-state"; +import { TooltipTextMenu } from "./TooltipTextMenu"; +import { Statement } from "../northstar/model/idea/idea"; const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; @@ -50,7 +52,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: } if (type = schema.nodes.bullet_list) { - bind("Ctrl-b", wrapInList(type)); + bind("Ctrl-.", wrapInList(type)); } if (type = schema.nodes.ordered_list) { bind("Ctrl-n", wrapInList(type)); @@ -96,5 +98,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: }); } + bind("Mod-s", TooltipTextMenu.insertStar); + return keys; -}
\ No newline at end of file +} diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 3e3e98206..269de0f42 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -1,10 +1,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType } from "prosemirror-model"; -import { joinUp, lift, setBlockType, toggleMark, wrapIn } from 'prosemirror-commands'; +import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType, Slice, Mark, Node } from "prosemirror-model"; +import { joinUp, lift, setBlockType, toggleMark, wrapIn, selectNodeForward, deleteSelection } from 'prosemirror-commands'; import { redo, undo } from 'prosemirror-history'; import { orderedList, bulletList, listItem, } from 'prosemirror-schema-list'; -import { EditorState, Transaction, NodeSelection, } from "prosemirror-state"; +import { EditorState, Transaction, NodeSelection, TextSelection, Selection, } from "prosemirror-state"; import { EditorView, } from "prosemirror-view"; +import { View } from '@react-pdf/renderer'; const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"], preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0]; @@ -26,6 +27,14 @@ export const nodes: { [index: string]: NodeSpec } = { toDOM() { return pDOM; } }, + // starmine: { + // inline: true, + // attrs: { oldtext: { default: "" } }, + // group: "inline", + // toDOM() { return ["star", "㊉"]; }, + // parseDOM: [{ tag: "star" }] + // }, + // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks. blockquote: { content: "block+", @@ -77,6 +86,30 @@ export const nodes: { [index: string]: NodeSpec } = { group: "inline" }, + star: { + inline: true, + attrs: { + visibility: { default: false }, + text: { default: undefined }, + textslice: { default: undefined }, + textlen: { default: 0 } + + }, + group: "inline", + toDOM(node) { + const attrs = { style: `width: 40px` }; + return ["span", { ...node.attrs, ...attrs }]; + }, + // parseDOM: [{ + // tag: "star", getAttrs(dom: any) { + // return { + // visibility: dom.getAttribute("visibility"), + // oldtext: dom.getAttribute("oldtext"), + // oldtextlen: dom.getAttribute("oldtextlen"), + // } + // } + // }] + }, // :: NodeSpec An inline image (`<img>`) node. Supports `src`, // `alt`, and `href` attributes. The latter two default to the empty // string. @@ -107,6 +140,32 @@ export const nodes: { [index: string]: NodeSpec } = { } }, + video: { + inline: true, + attrs: { + src: {}, + width: { default: "100px" }, + alt: { default: null }, + title: { default: null } + }, + group: "inline", + draggable: true, + parseDOM: [{ + tag: "video[src]", getAttrs(dom: any) { + return { + src: dom.getAttribute("src"), + title: dom.getAttribute("title"), + alt: dom.getAttribute("alt"), + width: Math.min(100, Number(dom.getAttribute("width"))), + }; + } + }], + toDOM(node) { + const attrs = { style: `width: ${node.attrs.width}` }; + return ["video", { ...node.attrs, ...attrs }]; + } + }, + // :: NodeSpec A hard line break, represented in the DOM as `<br>`. hard_break: { inline: true, @@ -156,12 +215,13 @@ export const marks: { [index: string]: MarkSpec } = { link: { attrs: { href: {}, + location: { default: null }, title: { default: null } }, inclusive: false, parseDOM: [{ tag: "a[href]", getAttrs(dom: any) { - return { href: dom.getAttribute("href"), title: dom.getAttribute("title") }; + return { href: dom.getAttribute("href"), location: dom.getAttribute("location"), title: dom.getAttribute("title") }; } }], toDOM(node: any) { return ["a", node.attrs, 0]; } @@ -222,6 +282,15 @@ export const marks: { [index: string]: MarkSpec } = { toDOM: () => ['sup'] }, + highlight: { + parseDOM: [{ style: 'background: #d9dbdd' }], + toDOM() { + return ['span', { + style: 'color: blue' + }]; + } + }, + // :: MarkSpec Code font mark. Represented as a `<code>` element. code: { @@ -280,7 +349,29 @@ export const marks: { [index: string]: MarkSpec } = { }] }, + pFontColor: { + attrs: { + color: { default: "yellow" } + }, + parseDOM: [{ style: 'background: #d9dbdd' }], + toDOM: (node) => { + return ['span', { + style: `color: ${node.attrs.color}` + }]; + } + }, + /** FONT SIZES */ + pFontSize: { + attrs: { + fontSize: { default: 10 } + }, + inclusive: false, + parseDOM: [{ style: 'font-size: 10px;' }], + toDOM: (node) => ['span', { + style: `font-size: ${node.attrs.fontSize}px;` + }] + }, p10: { parseDOM: [{ style: 'font-size: 10px;' }], @@ -310,6 +401,20 @@ export const marks: { [index: string]: MarkSpec } = { }] }, + p18: { + parseDOM: [{ style: 'font-size: 18px;' }], + toDOM: () => ['span', { + style: 'font-size: 18px;' + }] + }, + + p20: { + parseDOM: [{ style: 'font-size: 20px;' }], + toDOM: () => ['span', { + style: 'font-size: 20px;' + }] + }, + p24: { parseDOM: [{ style: 'font-size: 24px;' }], toDOM: () => ['span', { @@ -407,6 +512,86 @@ export class ImageResizeView { this._handle.style.display = "none"; } } + +export class SummarizedView { + // TODO: highlight text that is summarized. to find end of region, walk along mark + _collapsed: HTMLElement; + _view: any; + constructor(node: any, view: any, getPos: any) { + this._collapsed = document.createElement("span"); + this._collapsed.textContent = node.attrs.visibility ? "㊀" : "㊉"; + this._collapsed.style.opacity = "0.5"; + this._collapsed.style.position = "relative"; + this._collapsed.style.width = "40px"; + this._collapsed.style.height = "20px"; + let self = this; + this._view = view; + const js = node.toJSON; + node.toJSON = function () { + + return js.apply(this, arguments); + }; + this._collapsed.onpointerdown = function (e: any) { + if (node.attrs.visibility) { + // node.attrs.visibility = !node.attrs.visibility; + let y = getPos(); + const attrs = { ...node.attrs }; + attrs.visibility = !attrs.visibility; + let { from, to } = self.updateSummarizedText(y + 1, view.state.schema.marks.highlight); + let length = to - from; + let newSelection = TextSelection.create(view.state.doc, y + 1, y + 1 + length); + // update attrs of node + attrs.text = newSelection.content(); + attrs.textslice = newSelection.content().toJSON(); + view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs)); + view.dispatch(view.state.tr.setSelection(newSelection).deleteSelection(view.state, () => { })); + self._collapsed.textContent = "㊉"; + } else { + // node.attrs.visibility = !node.attrs.visibility; + let y = getPos(); + const attrs = { ...node.attrs }; + attrs.visibility = !attrs.visibility; + view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs)); + let mark = view.state.schema.mark(view.state.schema.marks.highlight); + view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, y + 1, y + 1))); + const from = view.state.selection.from; + let size = node.attrs.text.size; + view.dispatch(view.state.tr.replaceSelection(node.attrs.text).addMark(from, from + size, mark).removeStoredMark(mark)); + self._collapsed.textContent = "㊀"; + } + e.preventDefault(); + e.stopPropagation(); + }; + (this as any).dom = this._collapsed; + + } + selectNode() { + } + + updateSummarizedText(start?: any, mark?: any) { + let $start = this._view.state.doc.resolve(start); + let endPos = start; + + let _mark = this._view.state.schema.mark(this._view.state.schema.marks.highlight); + let visited = new Set(); + for (let i: number = start + 1; i < this._view.state.doc.nodeSize - 1; i++) { + let skip = false; + this._view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => { + if (node.isLeaf && !visited.has(node) && !skip) { + if (node.marks.includes(_mark)) { + visited.add(node); + endPos = i + node.nodeSize - 1; + } + else skip = true; + } + }); + } + return { from: start, to: endPos }; + } + + deselectNode() { + } +} // :: Schema // This schema rougly corresponds to the document schema used by // [CommonMark](http://commonmark.org/), minus the list elements, @@ -415,4 +600,14 @@ export class ImageResizeView { // // To reuse elements from this schema, extend or read from its // `spec.nodes` and `spec.marks` [properties](#model.Schema.spec). -export const schema = new Schema({ nodes, marks });
\ No newline at end of file +export const schema = new Schema({ nodes, marks }); + +const fromJson = schema.nodeFromJSON; + +schema.nodeFromJSON = (json: any) => { + let node = fromJson(json); + if (json.type === "star") { + node.attrs.text = Slice.fromJSON(schema, node.attrs.textslice); + } + return node; +};
\ No newline at end of file diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index beaf5cb03..62c2cfe85 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -7,11 +7,7 @@ let ts = (window as any).ts; // @ts-ignore import * as typescriptlib from '!!raw-loader!./type_decls.d'; -import { Docs } from "../documents/Documents"; import { Doc, Field } from '../../new_fields/Doc'; -import { ImageField, PdfField, VideoField, AudioField } from '../../new_fields/URLField'; -import { List } from '../../new_fields/List'; -import { RichTextField } from '../../new_fields/RichTextField'; export interface ScriptSucccess { success: true; @@ -39,15 +35,45 @@ export interface CompileError { export type CompileResult = CompiledScript | CompileError; +export namespace Scripting { + export function addGlobal(global: { name: string }): void; + export function addGlobal(name: string, global: any): void; + export function addGlobal(nameOrGlobal: any, global?: any) { + let n: string; + let obj: any; + if (global !== undefined && typeof nameOrGlobal === "string") { + n = nameOrGlobal; + obj = global; + } else if (nameOrGlobal && typeof nameOrGlobal.name === "string") { + n = nameOrGlobal.name; + obj = nameOrGlobal; + } else { + throw new Error("Must either register an object with a name, or give a name and an object"); + } + if (scriptingGlobals.hasOwnProperty(n)) { + throw new Error(`Global with name ${n} is already registered, choose another name`); + } + scriptingGlobals[n] = obj; + } +} + +export function scriptingGlobal(constructor: { new(...args: any[]): any }) { + Scripting.addGlobal(constructor); +} + +const scriptingGlobals: { [name: string]: any } = {}; + function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult { const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error); if ((options.typecheck !== false && errors) || !script) { return { compiled: false, errors: diagnostics }; } - let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField]; - let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)]; - let params: any[] = [Docs, ...fieldTypes]; + let paramNames = Object.keys(scriptingGlobals); + let params = paramNames.map(key => scriptingGlobals[key]); + // let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField, ScriptField, ComputedField, CompileScript]; + // let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)]; + // let params: any[] = [Docs, ...fieldTypes]; let compiledFunction = new Function(...paramNames, `return ${script}`); let { capturedVariables = {} } = options; let run = (args: { [name: string]: any } = {}): ScriptResult => { @@ -63,10 +89,20 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an } } let thisParam = args.this || capturedVariables.this; + let batch: { end(): void } | undefined = undefined; try { + if (!options.editable) { + batch = Doc.MakeReadOnly(); + } const result = compiledFunction.apply(thisParam, params).apply(thisParam, argsArray); + if (batch) { + batch.end(); + } return { success: true, result }; } catch (error) { + if (batch) { + batch.end(); + } return { success: false, error }; } }; @@ -132,6 +168,7 @@ export interface ScriptOptions { params?: { [name: string]: string }; capturedVariables?: { [name: string]: Field }; typecheck?: boolean; + editable?: boolean; } export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult { @@ -167,4 +204,6 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics); return Run(outputText, paramNames, diagnostics, script, options); -}
\ No newline at end of file +} + +Scripting.addGlobal(CompileScript);
\ No newline at end of file diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 28ec8ca14..806746496 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -4,23 +4,65 @@ import { Doc } from '../../new_fields/Doc'; import { Id } from '../../new_fields/FieldSymbols'; export namespace SearchUtil { - export function Search(query: string, returnDocs: true): Promise<Doc[]>; - export function Search(query: string, returnDocs: false): Promise<string[]>; - export async function Search(query: string, returnDocs: boolean) { - const ids = JSON.parse(await rp.get(DocServer.prepend("/search"), { - qs: { query } + export interface IdSearchResult { + ids: string[]; + numFound: number; + } + + export interface DocSearchResult { + docs: Doc[]; + numFound: number; + } + + export function Search(query: string, filterQuery: string | undefined, returnDocs: true, start?: number, count?: number): Promise<DocSearchResult>; + export function Search(query: string, filterQuery: string | undefined, returnDocs: false, start?: number, count?: number): Promise<IdSearchResult>; + export async function Search(query: string, filterQuery: string | undefined, returnDocs: boolean, start?: number, rows?: number) { + query = query || "*"; //If we just have a filter query, search for * as the query + const result: IdSearchResult = JSON.parse(await rp.get(DocServer.prepend("/search"), { + qs: { query, filterQuery, start, rows }, })); if (!returnDocs) { - return ids; + return result; } + const { ids, numFound } = result; const docMap = await DocServer.GetRefFields(ids); - return ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc); + const docs = ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc); + return { docs, numFound }; } - export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]> { - const proto = await Doc.GetT(doc, "proto", Doc, true); - const protoId = (proto || doc)[Id]; - return Search(`proto_i:"${protoId}"`, true); + export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]>; + export async function GetAliasesOfDocument(doc: Doc, returnDocs: false): Promise<string[]>; + export async function GetAliasesOfDocument(doc: Doc, returnDocs = true): Promise<Doc[] | string[]> { + const proto = Doc.GetProto(doc); + const protoId = proto[Id]; + if (returnDocs) { + return (await Search("", `proto_i:"${protoId}"`, returnDocs)).docs; + } else { + return (await Search("", `proto_i:"${protoId}"`, returnDocs)).ids; + } // return Search(`{!join from=id to=proto_i}id:${protoId}`, true); } + + export async function GetViewsOfDocument(doc: Doc): Promise<Doc[]> { + const results = await Search("", `proto_i:"${doc[Id]}"`, true); + return results.docs; + } + + export async function GetContextsOfDocument(doc: Doc): Promise<{ contexts: Doc[], aliasContexts: Doc[] }> { + const docContexts = (await Search("", `data_l:"${doc[Id]}"`, true)).docs; + const aliases = await GetAliasesOfDocument(doc, false); + const aliasContexts = (await Promise.all(aliases.map(doc => Search("", `data_l:"${doc}"`, true)))); + const contexts = { contexts: docContexts, aliasContexts: [] as Doc[] }; + aliasContexts.forEach(result => contexts.aliasContexts.push(...result.docs)); + return contexts; + } + + export async function GetContextIdsOfDocument(doc: Doc): Promise<{ contexts: string[], aliasContexts: string[] }> { + const docContexts = (await Search("", `data_l:"${doc[Id]}"`, false)).ids; + const aliases = await GetAliasesOfDocument(doc, false); + const aliasContexts = (await Promise.all(aliases.map(doc => Search("", `data_l:"${doc}"`, false)))); + const contexts = { contexts: docContexts, aliasContexts: [] as string[] }; + aliasContexts.forEach(result => contexts.aliasContexts.push(...result.ids)); + return contexts; + } }
\ No newline at end of file diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 8c92c2023..9efef888d 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,49 +1,65 @@ -import { observable, action } from "mobx"; -import { Doc } from "../../new_fields/Doc"; +import { observable, action, runInAction, IReactionDisposer, reaction, autorun } from "mobx"; +import { Doc, Opt } from "../../new_fields/Doc"; import { DocumentView } from "../views/nodes/DocumentView"; import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; -import { NumCast } from "../../new_fields/Types"; +import { NumCast, StrCast } from "../../new_fields/Types"; +import { InkingControl } from "../views/InkingControl"; export namespace SelectionManager { + class Manager { - @observable - SelectedDocuments: Array<DocumentView> = []; + + @observable IsDragging: boolean = false; + @observable SelectedDocuments: Array<DocumentView> = []; + @action - SelectDoc(doc: DocumentView, ctrlPressed: boolean): void { + SelectDoc(docView: DocumentView, ctrlPressed: boolean): void { // if doc is not in SelectedDocuments, add it - if (!ctrlPressed) { - this.DeselectAll(); - } + if (manager.SelectedDocuments.indexOf(docView) === -1) { + if (!ctrlPressed) { + this.DeselectAll(); + } - if (manager.SelectedDocuments.indexOf(doc) === -1) { - manager.SelectedDocuments.push(doc); - doc.props.whenActiveChanged(true); + manager.SelectedDocuments.push(docView); + // console.log(manager.SelectedDocuments); + docView.props.whenActiveChanged(true); + } + } + @action + DeselectDoc(docView: DocumentView): void { + let ind = manager.SelectedDocuments.indexOf(docView); + if (ind !== -1) { + manager.SelectedDocuments.splice(ind, 1); + docView.props.whenActiveChanged(false); } } - @action DeselectAll(): void { manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false)); manager.SelectedDocuments = []; FormattedTextBox.InputBoxOverlay = undefined; } - @action - ReselectAll() { - let sdocs = manager.SelectedDocuments.map(d => d); - manager.SelectedDocuments = []; - return sdocs; - } - @action - ReselectAll2(sdocs: DocumentView[]) { - sdocs.map(s => SelectionManager.SelectDoc(s, true)); - } } const manager = new Manager(); + reaction(() => manager.SelectedDocuments, sel => { + let targetColor = "#FFFFFF"; + if (sel.length > 0) { + let firstView = sel[0]; + let doc = firstView.props.Document; + let targetDoc = doc.isTemplate ? doc : Doc.GetProto(doc); + let stored = StrCast(targetDoc.backgroundColor); + stored.length > 0 && (targetColor = stored); + } + InkingControl.Instance.updateSelectedColor(targetColor); + }, { fireImmediately: true }); - export function SelectDoc(doc: DocumentView, ctrlPressed: boolean): void { - manager.SelectDoc(doc, ctrlPressed); + export function DeselectDoc(docView: DocumentView): void { + manager.DeselectDoc(docView); + } + export function SelectDoc(docView: DocumentView, ctrlPressed: boolean): void { + manager.SelectDoc(docView, ctrlPressed); } export function IsSelected(doc: DocumentView): boolean { @@ -62,14 +78,13 @@ export namespace SelectionManager { if (found) manager.SelectDoc(found, false); } - export function ReselectAll() { - let sdocs = manager.ReselectAll(); - setTimeout(() => manager.ReselectAll2(sdocs), 0); - } + export function SetIsDragging(dragging: boolean) { runInAction(() => manager.IsDragging = dragging); } + export function GetIsDragging() { return manager.IsDragging; } + export function SelectedDocuments(): Array<DocumentView> { - return manager.SelectedDocuments; + return manager.SelectedDocuments.slice(); } - export function ViewsSortedVertically(): DocumentView[] { + export function ViewsSortedHorizontally(): 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; @@ -77,7 +92,7 @@ export namespace SelectionManager { }); return sorted; } - export function ViewsSortedHorizontally(): DocumentView[] { + export function ViewsSortedVertically(): 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; diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts index 7ded85e43..dca539f3b 100644 --- a/src/client/util/SerializationHelper.ts +++ b/src/client/util/SerializationHelper.ts @@ -1,5 +1,6 @@ import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema, primitive, SKIP } from "serializr"; import { Field } from "../../new_fields/Doc"; +import { ClientUtils } from "./ClientUtils"; export namespace SerializationHelper { let serializing: number = 0; @@ -9,7 +10,7 @@ export namespace SerializationHelper { export function Serialize(obj: Field): any { if (obj === undefined || obj === null) { - return undefined; + return null; } if (typeof obj !== 'object') { @@ -38,20 +39,29 @@ export namespace SerializationHelper { serializing += 1; if (!obj.__type) { - throw Error("No property 'type' found in JSON."); + if (ClientUtils.RELEASE) { + console.warn("No property 'type' found in JSON."); + return undefined; + } else { + throw Error("No property 'type' found in JSON."); + } } if (!(obj.__type in serializationTypes)) { throw Error(`type '${obj.__type}' not registered. Make sure you register it using a @Deserializable decorator`); } - const value = deserialize(serializationTypes[obj.__type], obj); + const type = serializationTypes[obj.__type]; + const value = deserialize(type.ctor, obj); + if (type.afterDeserialize) { + type.afterDeserialize(value); + } serializing -= 1; return value; } } -let serializationTypes: { [name: string]: any } = {}; +let serializationTypes: { [name: string]: { ctor: { new(): any }, afterDeserialize?: (obj: any) => void } } = {}; let reverseMap: { [ctor: string]: string } = {}; export interface DeserializableOpts { @@ -59,9 +69,9 @@ export interface DeserializableOpts { withFields(fields: string[]): Function; } -export function Deserializable(name: string): DeserializableOpts; +export function Deserializable(name: string, afterDeserialize?: (obj: any) => void): DeserializableOpts; export function Deserializable(constructor: { new(...args: any[]): any }): void; -export function Deserializable(constructor: { new(...args: any[]): any } | string): DeserializableOpts | void { +export function Deserializable(constructor: { new(...args: any[]): any } | string, afterDeserialize?: (obj: any) => void): DeserializableOpts | void { function addToMap(name: string, ctor: { new(...args: any[]): any }) { const schema = getDefaultModelSchema(ctor) as any; if (schema.targetClass !== ctor) { @@ -69,7 +79,7 @@ export function Deserializable(constructor: { new(...args: any[]): any } | strin setDefaultModelSchema(ctor, newSchema); } if (!(name in serializationTypes)) { - serializationTypes[name] = ctor; + serializationTypes[name] = { ctor, afterDeserialize }; reverseMap[ctor.name] = name; } else { throw new Error(`Name ${name} has already been registered as deserializable`); diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss index 437da0d63..40ac3abb9 100644 --- a/src/client/util/TooltipTextMenu.scss +++ b/src/client/util/TooltipTextMenu.scss @@ -18,6 +18,7 @@ .ProseMirror-menuitem { margin-right: 3px; display: inline-block; + z-index: 100000; } .ProseMirror-menuseparator { @@ -36,7 +37,7 @@ position: relative; padding-right: 15px; margin: 3px; - background: #333333; + background: white; border-radius: 3px; text-align: center; } @@ -59,7 +60,6 @@ } .ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu { - position: absolute; background: $dark-color; color:white; border: 1px solid rgb(223, 223, 223); @@ -67,8 +67,10 @@ } .ProseMirror-menu-dropdown-menu { - z-index: 15; + z-index: 100000; min-width: 6em; + background: white; + position: absolute; } .linking { @@ -79,10 +81,11 @@ cursor: pointer; padding: 2px 8px 2px 4px; width: auto; + z-index: 100000; } .ProseMirror-menu-dropdown-item:hover { - background: #2e2b2b; + background: white; } .ProseMirror-menu-submenu-wrap { @@ -132,7 +135,7 @@ position: relative; min-height: 1em; color: white; - padding: 1px 6px; + padding: 10px 10px; top: 0; left: 0; right: 0; border-bottom: 1px solid silver; background:$dark-color; @@ -155,7 +158,7 @@ } .ProseMirror-icon svg { - fill: currentColor; + fill:white; height: 1em; } @@ -184,7 +187,7 @@ position: fixed; border-radius: 3px; z-index: 11; - box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2); + box-shadow: -.5px 2px 5px white(255, 255, 255, 0.2); } .ProseMirror-prompt h5 { @@ -196,7 +199,7 @@ .ProseMirror-prompt input[type="text"], .ProseMirror-prompt textarea { - background: #eee; + background: white; border: none; outline: none; } @@ -232,16 +235,20 @@ } .tooltipMenu { - position: absolute; - z-index: 200; - background: $dark-color; + position: relative; + z-index: 2000; + background: #121721; border: 1px solid silver; - border-radius: 4px; - padding: 2px 10px; - margin-bottom: 7px; - -webkit-transform: translateX(-50%); - transform: translateX(-50%); + border-radius: 15px; + //height: 60px; + //padding: 2px 10px; + //margin-top: 100px; + //-webkit-transform: translateX(-50%); + //transform: translateX(-50%); + transform: translateY(-85px); pointer-events: all; + height: 30px; + width:550px; .ProseMirror-example-setup-style hr { padding: 2px 10px; border: none; @@ -257,34 +264,34 @@ } } -.tooltipMenu:before { - content: ""; - height: 0; width: 0; - position: absolute; - left: 50%; - margin-left: -5px; - bottom: -6px; - border: 5px solid transparent; - border-bottom-width: 0; - border-top-color: silver; - } - .tooltipMenu:after { - content: ""; - height: 0; width: 0; - position: absolute; - left: 50%; - margin-left: -5px; - bottom: -4.5px; - border: 5px solid transparent; - border-bottom-width: 0; - border-top-color: $dark-color; - } +// .tooltipMenu:before { +// content: ""; +// height: 0; width: 0; +// position: absolute; +// left: 50%; +// margin-left: -5px; +// bottom: -6px; +// border: 5px solid transparent; +// border-bottom-width: 0; +// border-top-color: silver; +// } +// .tooltipMenu:after { +// content: ""; +// height: 0; width: 0; +// position: absolute; +// left: 50%; +// margin-left: -5px; +// bottom: -4.5px; +// border: 5px solid transparent; +// border-bottom-width: 0; +// border-top-color: $dark-color; +// } .menuicon { display: inline-block; - border-right: 1px solid rgba(0, 0, 0, 0.2); + border-right: 1px solid white(0, 0, 0, 0.2); //color: rgb(19, 18, 18); - color: white; + color: rgb(226, 21, 21); line-height: 1; padding: 0px 2px; margin: 1px; @@ -302,3 +309,10 @@ font-size: 12px; padding-right: 0px; } + .summarize{ + //margin-left: 15px; + color: white; + height: 20px; + // background-color: white; + text-align: center; + }
\ No newline at end of file diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index 34785446b..f4579fc51 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -1,51 +1,38 @@ -import { action, IReactionDisposer, reaction } from "mobx"; -import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css -import { baseKeymap, lift } from "prosemirror-commands"; -import { history, redo, undo } from "prosemirror-history"; -import { keymap } from "prosemirror-keymap"; -import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state"; +import { action } from "mobx"; +import { Dropdown, MenuItem, icons, } from "prosemirror-menu"; //no import css +import { EditorState, NodeSelection, TextSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { schema } from "./RichTextSchema"; -import { Schema, NodeType, MarkType, Mark } from "prosemirror-model"; -import React = require("react"); +import { Schema, NodeType, MarkType, Mark, ResolvedPos } from "prosemirror-model"; +import { Node as ProsNode } from "prosemirror-model"; import "./TooltipTextMenu.scss"; -const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands"); +const { toggleMark, setBlockType } = require("prosemirror-commands"); import { library } from '@fortawesome/fontawesome-svg-core'; -import { wrapInList, bulletList, liftListItem, listItem, } from 'prosemirror-schema-list'; -import { liftTarget, RemoveMarkStep, AddMarkStep } from 'prosemirror-transform'; -import { - faListUl, -} from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { wrapInList, liftListItem, } from 'prosemirror-schema-list'; +import { faListUl } from '@fortawesome/free-solid-svg-icons'; import { FieldViewProps } from "../views/nodes/FieldView"; -import { throwStatement } from "babel-types"; -import { View } from "@react-pdf/renderer"; +const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js"); import { DragManager } from "./DragManager"; import { Doc, Opt, Field } from "../../new_fields/Doc"; -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"; import { Id } from "../../new_fields/FieldSymbols"; - -const SVG = "http://www.w3.org/2000/svg"; +import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox"; //appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc. export class TooltipTextMenu { public tooltip: HTMLElement; - private num_icons = 0; private view: EditorView; private fontStyles: MarkType[]; private fontSizes: MarkType[]; private listTypes: NodeType[]; - private editorProps: FieldViewProps; - private state: EditorState; + private editorProps: FieldViewProps & FormattedTextBoxProps; private fontSizeToNum: Map<MarkType, number>; 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; @@ -54,26 +41,35 @@ export class TooltipTextMenu { private fontStyleDom?: Node; private listTypeBtnDom?: Node; - constructor(view: EditorView, editorProps: FieldViewProps) { + private _activeMarks: Mark[] = []; + + private _collapseBtn?: MenuItem; + + constructor(view: EditorView, editorProps: FieldViewProps & FormattedTextBoxProps) { this.view = view; - this.state = view.state; this.editorProps = editorProps; this.tooltip = document.createElement("div"); this.tooltip.className = "tooltipMenu"; + this.dragElement(this.tooltip); + // this.createCollapse(); + // if (this._collapseBtn) { + // this.tooltip.appendChild(this._collapseBtn.render(this.view).dom); + // } //add the div which is the tooltip - view.dom.parentNode!.parentNode!.appendChild(this.tooltip); + //view.dom.parentNode!.parentNode!.appendChild(this.tooltip); //add additional icons library.add(faListUl); //add the buttons to the tooltip let items = [ - { command: toggleMark(schema.marks.strong), dom: this.icon("B", "strong") }, - { command: toggleMark(schema.marks.em), dom: this.icon("i", "em") }, - { command: toggleMark(schema.marks.underline), dom: this.icon("U", "underline") }, - { command: toggleMark(schema.marks.strikethrough), dom: this.icon("S", "strikethrough") }, - { command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript") }, - { command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript") }, + { command: toggleMark(schema.marks.strong), dom: this.icon("B", "strong", "Bold") }, + { command: toggleMark(schema.marks.em), dom: this.icon("i", "em", "Italic") }, + { command: toggleMark(schema.marks.underline), dom: this.icon("U", "underline", "Underline") }, + { command: toggleMark(schema.marks.strikethrough), dom: this.icon("S", "strikethrough", "Strikethrough") }, + { command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript", "Superscript") }, + { command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript", "Subscript") }, + { command: toggleMark(schema.marks.highlight), dom: this.icon("H", 'blue', 'Blue') } // { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") }, // { command: wrapInList(schema.nodes.ordered_list), dom: this.icon("1)", "bullets") }, // { command: lift, dom: this.icon("<", "lift") }, @@ -86,10 +82,14 @@ export class TooltipTextMenu { dom.addEventListener("pointerdown", e => { e.preventDefault(); view.focus(); - command(view.state, view.dispatch, view); + if (dom.contains(e.target as Node)) { + e.stopPropagation(); + command(view.state, view.dispatch, view); + } }); }); + this.updateLinkMenu(); //list of font styles this.fontStylesToName = new Map(); @@ -108,10 +108,14 @@ export class TooltipTextMenu { 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.p18, 18); + this.fontSizeToNum.set(schema.marks.p20, 20); this.fontSizeToNum.set(schema.marks.p24, 24); this.fontSizeToNum.set(schema.marks.p32, 32); this.fontSizeToNum.set(schema.marks.p48, 48); this.fontSizeToNum.set(schema.marks.p72, 72); + this.fontSizeToNum.set(schema.marks.pFontSize, 10); + this.fontSizeToNum.set(schema.marks.pFontSize, 10); this.fontSizes = Array.from(this.fontSizeToNum.keys()); //list types @@ -120,7 +124,21 @@ export class TooltipTextMenu { this.listTypeToIcon.set(schema.nodes.ordered_list, "1)"); this.listTypes = Array.from(this.listTypeToIcon.keys()); + // this.tooltip.appendChild(this.createLink().render(this.view).dom); + + this.tooltip.appendChild(this.createStar().render(this.view).dom); + + + + this.updateListItemDropdown(":", this.listTypeBtnDom); + this.update(view, undefined); + + //view.dom.parentNode!.parentNode!.insertBefore(this.tooltip, view.dom.parentNode); + + // quick and dirty null check + const outer_div = this.editorProps.outer_div; + outer_div && outer_div(this.tooltip); } //label of dropdown will change to given label @@ -131,15 +149,18 @@ export class TooltipTextMenu { //font SIZES let fontSizeBtns: MenuItem[] = []; this.fontSizeToNum.forEach((number, mark) => { - fontSizeBtns.push(this.dropdownMarkBtn(String(number), "width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes)); + fontSizeBtns.push(this.dropdownMarkBtn(String(number), "color: black; width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes)); }); - if (this.fontSizeDom) { this.tooltip.removeChild(this.fontSizeDom); } - this.fontSizeDom = (new Dropdown(cut(fontSizeBtns), { + let newfontSizeDom = (new Dropdown(cut(fontSizeBtns), { label: label, - css: "color:white; min-width: 60px; padding-left: 5px; margin-right: 0;" + css: "color:black; min-width: 60px; padding-left: 5px; margin-right: 0;" }) as MenuItem).render(this.view).dom; - this.tooltip.appendChild(this.fontSizeDom); + if (this.fontSizeDom) { this.tooltip.replaceChild(newfontSizeDom, this.fontSizeDom); } + else { + this.tooltip.appendChild(newfontSizeDom); + } + this.fontSizeDom = newfontSizeDom; } //label of dropdown will change to given label @@ -150,22 +171,26 @@ export class TooltipTextMenu { //font STYLES let fontBtns: MenuItem[] = []; this.fontStylesToName.forEach((name, mark) => { - fontBtns.push(this.dropdownMarkBtn(name, "font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles)); + fontBtns.push(this.dropdownMarkBtn(name, "color: black; font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles)); }); - if (this.fontStyleDom) { this.tooltip.removeChild(this.fontStyleDom); } - this.fontStyleDom = (new Dropdown(cut(fontBtns), { + let newfontStyleDom = (new Dropdown(cut(fontBtns), { label: label, - css: "color:white; width: 125px; margin-left: -3px; padding-left: 2px;" + css: "color:black; width: 125px; margin-left: -3px; padding-left: 2px;" }) as MenuItem).render(this.view).dom; + if (this.fontStyleDom) { this.tooltip.replaceChild(newfontStyleDom, this.fontStyleDom); } + else { + this.tooltip.appendChild(newfontStyleDom); + } + this.fontStyleDom = newfontStyleDom; - this.tooltip.appendChild(this.fontStyleDom); } updateLinkMenu() { if (!this.linkEditor || !this.linkText) { this.linkEditor = document.createElement("div"); - this.linkEditor.style.color = "white"; + this.linkEditor.className = "ProseMirror-icon menuicon"; + this.linkEditor.style.color = "black"; this.linkText = document.createElement("div"); this.linkText.style.cssFloat = "left"; this.linkText.style.marginRight = "5px"; @@ -178,8 +203,8 @@ export class TooltipTextMenu { this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); }; let linkBtn = document.createElement("div"); linkBtn.textContent = ">>"; - linkBtn.style.width = "20px"; - linkBtn.style.height = "20px"; + linkBtn.style.width = "10px"; + linkBtn.style.height = "10px"; linkBtn.style.color = "white"; linkBtn.style.cssFloat = "left"; linkBtn.onpointerdown = (e: PointerEvent) => { @@ -194,7 +219,7 @@ export class TooltipTextMenu { if (DocumentManager.Instance.getDocumentView(f)) { DocumentManager.Instance.getDocumentView(f)!.props.focus(f, false); } - else if (CollectionDockingView.Instance) CollectionDockingView.Instance.AddRightSplit(f); + else if (CollectionDockingView.Instance) CollectionDockingView.Instance.AddRightSplit(f, undefined); } })); } @@ -205,28 +230,32 @@ export class TooltipTextMenu { }; 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.width = "15px"; + this.linkDrag.style.height = "15px"; + this.linkDrag.title = "Drag to create link"; + this.linkDrag.style.color = "black"; 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; + e.stopPropagation(); + let ctrlKey = e.ctrlKey; DragManager.StartLinkDrag(this.linkDrag!, dragData, e.clientX, e.clientY, { handlers: { dragComplete: action(() => { - let m = dragData.droppedDocuments; - this.makeLink(DocServer.prepend("/doc/" + m[0][Id])); + // let m = dragData.droppedDocuments; + let linkDoc = dragData.linkDocument; + linkDoc instanceof Doc && this.makeLink(DocServer.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab"); }), }, hideSource: false }); }; this.linkEditor.appendChild(this.linkDrag); - this.linkEditor.appendChild(this.linkText); - this.linkEditor.appendChild(linkBtn); + // this.linkEditor.appendChild(this.linkText); + // this.linkEditor.appendChild(linkBtn); this.tooltip.appendChild(this.linkEditor); } @@ -236,39 +265,89 @@ export class TooltipTextMenu { this.linkText.onkeydown = (e: KeyboardEvent) => { if (e.key === "Enter") { - this.makeLink(this.linkText!.textContent!); + // this.makeLink(this.linkText!.textContent!); e.stopPropagation(); e.preventDefault(); } }; - this.tooltip.appendChild(this.linkEditor); + // this.tooltip.appendChild(this.linkEditor); + } + + dragElement(elmnt: HTMLElement) { + var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; + if (elmnt) { + // if present, the header is where you move the DIV from: + elmnt.onpointerdown = dragMouseDown; + } + const self = this; + + function dragMouseDown(e: PointerEvent) { + e = e || window.event; + //e.preventDefault(); + // get the mouse cursor position at startup: + pos3 = e.clientX; + pos4 = e.clientY; + document.onpointerup = closeDragElement; + // call a function whenever the cursor moves: + document.onpointermove = elementDrag; + } + + function elementDrag(e: PointerEvent) { + e = e || window.event; + //e.preventDefault(); + // calculate the new cursor position: + pos1 = pos3 - e.clientX; + pos2 = pos4 - e.clientY; + pos3 = e.clientX; + pos4 = e.clientY; + // set the element's new position: + elmnt.style.top = (elmnt.offsetTop - pos2) + "px"; + elmnt.style.left = (elmnt.offsetLeft - pos1) + "px"; + } + + function closeDragElement() { + // stop moving when mouse button is released: + document.onpointerup = null; + document.onpointermove = null; + //self.highlightSearchTerms(self.state, ["hello"]); + //FormattedTextBox.Instance.unhighlightSearchTerms(); + } } - makeLink = (target: string) => { + makeLink = (target: string, location: string) => { let node = this.view.state.selection.$from.nodeAfter; - let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target }); + let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location }); this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link)); this.view.dispatch(this.view.state.tr.addMark(this.view.state.selection.from, this.view.state.selection.to, link)); node = this.view.state.selection.$from.nodeAfter; link = node && node.marks.find(m => m.type.name === "link"); } + public static insertStar(state: EditorState<any>, dispatch: any) { + let newNode = schema.nodes.star.create({ visibility: false, text: state.selection.content(), textslice: state.selection.content().toJSON(), textlen: state.selection.to - state.selection.from }); + if (dispatch) { + //console.log(newNode.attrs.text.toString()); + dispatch(state.tr.replaceSelectionWith(newNode)); + } + return true; + } + //will display a remove-list-type button if selection is in list, otherwise will show list type dropdown - updateListItemDropdown(label: string, listTypeBtn: Node) { + updateListItemDropdown(label: string, listTypeBtn: any) { //remove old btn if (listTypeBtn) { this.tooltip.removeChild(listTypeBtn); } //Make a dropdown of all list types let toAdd: MenuItem[] = []; this.listTypeToIcon.forEach((icon, type) => { - toAdd.push(this.dropdownNodeBtn(icon, "width: 40px;", type, this.view, this.listTypes, this.changeToNodeType)); + toAdd.push(this.dropdownNodeBtn(icon, "color: black; width: 40px;", type, this.view, this.listTypes, this.changeToNodeType)); }); //option to remove the list formatting - toAdd.push(this.dropdownNodeBtn("X", "width: 40px;", undefined, this.view, this.listTypes, this.changeToNodeType)); + toAdd.push(this.dropdownNodeBtn("X", "color: black; width: 40px;", undefined, this.view, this.listTypes, this.changeToNodeType)); listTypeBtn = (new Dropdown(toAdd, { label: label, - css: "color:white; width: 40px;" + css: "color:black; width: 40px;" }) as MenuItem).render(this.view).dom; //add this new button and return it @@ -277,8 +356,8 @@ export class TooltipTextMenu { } //for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected text - changeToMarkInGroup(markType: MarkType, view: EditorView, fontMarks: MarkType[]) { - let { empty, $cursor, ranges } = view.state.selection as TextSelection; + changeToMarkInGroup = (markType: MarkType, view: EditorView, fontMarks: MarkType[]) => { + let { $cursor, ranges } = view.state.selection as TextSelection; let state = view.state; let dispatch = view.dispatch; @@ -290,25 +369,34 @@ export class TooltipTextMenu { dispatch(state.tr.removeStoredMark(type)); } } else { - let has = false, tr = state.tr; + let has = false; for (let i = 0; !has && i < ranges.length; i++) { let { $from, $to } = ranges[i]; has = state.doc.rangeHasMark($from.pos, $to.pos, type); } for (let i of ranges) { - let { $from, $to } = i; if (has) { toggleMark(type)(view.state, view.dispatch, view); } } } } - }); //actually apply font + }); + // fontsize + if (markType.name[0] === 'p') { + let size = this.fontSizeToNum.get(markType); + if (size) { this.updateFontSizeDropdown(String(size) + " pt"); } + } + else { + let fontName = this.fontStylesToName.get(markType); + if (fontName) { this.updateFontStyleDropdown(fontName); } + } + //actually apply font return toggleMark(markType)(view.state, view.dispatch, view); } //remove all node typeand apply the passed-in one to the selected text - changeToNodeType(nodeType: NodeType | undefined, view: EditorView, allNodes: NodeType[]) { + changeToNodeType(nodeType: NodeType | undefined, view: EditorView) { //remove old liftListItem(schema.nodes.list_item)(view.state, view.dispatch); if (nodeType) { //add new @@ -325,13 +413,114 @@ export class TooltipTextMenu { execEvent: "", class: "menuicon", css: css, - enable(state) { return true; }, + enable() { return true; }, run() { changeToMarkInGroup(markType, view, groupMarks); } }); } + createStar() { + return new MenuItem({ + title: "Summarize", + label: "Summarize", + icon: icons.join, + css: "color:white;", + class: "summarize", + execEvent: "", + run: (state, dispatch) => { + TooltipTextMenu.insertStar(state, dispatch); + } + + }); + } + + createCollapse() { + this._collapseBtn = new MenuItem({ + title: "Collapse", + //label: "Collapse", + icon: icons.join, + execEvent: "", + css: "color:white;", + class: "summarize", + run: () => { + this.collapseToolTip(); + } + }); + } + + collapseToolTip() { + if (this._collapseBtn) { + if (this._collapseBtn.spec.title === "Collapse") { + // const newcollapseBtn = new MenuItem({ + // title: "Expand", + // icon: icons.join, + // execEvent: "", + // css: "color:white;", + // class: "summarize", + // run: (state, dispatch, view) => { + // this.collapseToolTip(); + // } + // }); + // this.tooltip.replaceChild(newcollapseBtn.render(this.view).dom, this._collapseBtn.render(this.view).dom); + // this._collapseBtn = newcollapseBtn; + this.tooltip.style.width = "30px"; + this._collapseBtn.spec.title = "Expand"; + this._collapseBtn.render(this.view); + } + else { + this._collapseBtn.spec.title = "Collapse"; + this.tooltip.style.width = "550px"; + this._collapseBtn.render(this.view); + } + } + } + + createLink() { + let markType = schema.marks.link; + return new MenuItem({ + title: "Add or remove link", + label: "Add or remove link", + execEvent: "", + icon: icons.link, + css: "color:white;", + class: "menuicon", + enable(state) { return !state.selection.empty; }, + run: (state, dispatch, view) => { + // to remove link + let curLink = ""; + if (this.markActive(state, markType)) { + + let { from, $from, to, empty } = state.selection; + let node = state.doc.nodeAt(from); + node && node.marks.map(m => { + m.type === markType && (curLink = m.attrs.href); + }); + //toggleMark(markType)(state, dispatch); + //return true; + } + // to create link + openPrompt({ + title: "Create a link", + fields: { + href: new TextField({ + value: curLink, + label: "Link Target", + required: true + }), + title: new TextField({ label: "Title" }) + }, + callback(attrs: any) { + toggleMark(markType, attrs)(view.state, view.dispatch); + view.focus(); + }, + flyout_top: 0, + flyout_left: 0 + }); + } + }); + } + //makes a button for the drop down FOR NODE TYPES //css is the style you want applied to the button dropdownNodeBtn(label: string, css: string, nodeType: NodeType | undefined, view: EditorView, groupNodes: NodeType[], changeToNodeInGroup: (nodeType: NodeType<any> | undefined, view: EditorView, groupNodes: NodeType[]) => any) { @@ -341,18 +530,24 @@ export class TooltipTextMenu { execEvent: "", class: "menuicon", css: css, - enable(state) { return true; }, + enable() { return true; }, run() { changeToNodeInGroup(nodeType, view, groupNodes); } }); } + markActive = function (state: EditorState<any>, type: MarkType<Schema<string, string>>) { + let { from, $from, to, empty } = state.selection; + if (empty) return type.isInSet(state.storedMarks || $from.marks()); + else return state.doc.rangeHasMark(from, to, type); + }; + // Helper function to create menu icons - icon(text: string, name: string) { + icon(text: string, name: string, title: string = name) { let span = document.createElement("span"); span.className = name + " menuicon"; - span.title = name; + span.title = title; span.textContent = text; span.style.color = "white"; return span; @@ -395,6 +590,20 @@ export class TooltipTextMenu { }; } + getMarksInSelection(state: EditorState<any>, targets: MarkType<any>[]) { + let found: Mark<any>[] = []; + let { from, to } = state.selection as TextSelection; + state.doc.nodesBetween(from, to, (node) => { + let marks = node.marks; + if (marks) { + marks.forEach(m => { + if (targets.includes(m.type)) found.push(m); + }); + } + }); + return found; + } + //updates the tooltip menu when the selection changes update(view: EditorView, lastState: EditorState | undefined) { let state = view.state; @@ -404,76 +613,121 @@ export class TooltipTextMenu { // Hide the tooltip if the selection is empty if (state.selection.empty) { - this.tooltip.style.display = "none"; - return; + //this.tooltip.style.display = "none"; + //return; } - // Otherwise, reposition it and update its content - this.tooltip.style.display = ""; - let { from, to } = state.selection; - let start = view.coordsAtPos(from), end = view.coordsAtPos(to); - // The box in which the tooltip is positioned, to use as base - let box = this.tooltip.offsetParent!.getBoundingClientRect(); - // Find a center-ish x position from the selection endpoints (when - // crossing lines, end may be more to the left) - let left = Math.max((start.left + end.left) / 2, start.left + 3); - this.tooltip.style.left = (left - box.left) * this.editorProps.ScreenToLocalTransform().Scale + "px"; - let width = Math.abs(start.left - end.left) / 2 * this.editorProps.ScreenToLocalTransform().Scale; - let mid = Math.min(start.left, end.left) + width; - - this.tooltip.style.width = 225 + "px"; - this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px"; //UPDATE LIST ITEM DROPDOWN - this.listTypeBtnDom = this.updateListItemDropdown(":", this.listTypeBtnDom!); //UPDATE FONT STYLE DROPDOWN let activeStyles = this.activeMarksOnSelection(this.fontStyles); - if (activeStyles.length === 1) { - // if we want to update something somewhere with active font name - let fontName = this.fontStylesToName.get(activeStyles[0]); - if (fontName) { this.updateFontStyleDropdown(fontName); } - } else if (activeStyles.length === 0) { - //crimson on default - this.updateFontStyleDropdown("Crimson Text"); - } else { - this.updateFontStyleDropdown("Various"); + if (activeStyles !== undefined) { + // activeStyles.forEach((markType) => { + // this._activeMarks.push(this.view.state.schema.mark(markType)); + // }); + if (activeStyles.length === 1) { + // if we want to update something somewhere with active font name + let fontName = this.fontStylesToName.get(activeStyles[0]); + if (fontName) { this.updateFontStyleDropdown(fontName); } + } else if (activeStyles.length === 0) { + //crimson on default + this.updateFontStyleDropdown("Crimson Text"); + } else { + this.updateFontStyleDropdown("Various"); + } } //UPDATE FONT SIZE DROPDOWN let activeSizes = this.activeMarksOnSelection(this.fontSizes); - if (activeSizes.length === 1) { //if there's only one active font size - let size = this.fontSizeToNum.get(activeSizes[0]); - if (size) { this.updateFontSizeDropdown(String(size) + " pt"); } - } else if (activeSizes.length === 0) { - //should be 14 on default - this.updateFontSizeDropdown("14 pt"); - } else { //multiple font sizes selected - this.updateFontSizeDropdown("Various"); + if (activeSizes !== undefined) { + if (activeSizes.length === 1) { //if there's only one active font size + // activeSizes.forEach((markType) => { + // this._activeMarks.push(this.view.state.schema.mark(markType)); + // }); + let size = this.fontSizeToNum.get(activeSizes[0]); + if (size) { this.updateFontSizeDropdown(String(size) + " pt"); } + } else if (activeSizes.length === 0) { + //should be 14 on default + this.updateFontSizeDropdown("14 pt"); + } else { //multiple font sizes selected + this.updateFontSizeDropdown("Various"); + } } - - this.updateLinkMenu(); + this.view.dispatch(this.view.state.tr.setStoredMarks(this._activeMarks)); } //finds all active marks on selection in given group activeMarksOnSelection(markGroup: MarkType[]) { //current selection - let { empty, $cursor, ranges } = this.view.state.selection as TextSelection; + let { empty, ranges } = this.view.state.selection as TextSelection; let state = this.view.state; let dispatch = this.view.dispatch; - - let activeMarks = markGroup.filter(mark => { - if (dispatch) { - let has = false, tr = state.tr; - for (let i = 0; !has && i < ranges.length; i++) { - let { $from, $to } = ranges[i]; - return state.doc.rangeHasMark($from.pos, $to.pos, mark); + let activeMarks: MarkType[]; + if (!empty) { + activeMarks = markGroup.filter(mark => { + if (dispatch) { + let has = false; + for (let i = 0; !has && i < ranges.length; i++) { + let { $from, $to } = ranges[i]; + return state.doc.rangeHasMark($from.pos, $to.pos, mark); + } + } + return false; + }); + } + else { + const pos = this.view.state.selection.$from; + const ref_node: ProsNode = this.reference_node(pos); + if (ref_node !== null && ref_node !== this.view.state.doc) { + if (ref_node.isText) { } + else { + return []; + } + + this._activeMarks = ref_node.marks; + + activeMarks = markGroup.filter(mark_type => { + if (dispatch) { + let mark = state.schema.mark(mark_type); + return ref_node.marks.includes(mark); + } + return false; + }); } - return false; - }); + else { + return []; + } + + } return activeMarks; } - destroy() { this.tooltip.remove(); } + reference_node(pos: ResolvedPos<any>): ProsNode { + let ref_node: ProsNode = this.view.state.doc; + if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) { + ref_node = pos.nodeAfter; + } + else if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) { + ref_node = pos.nodeBefore; + } + else if (pos.pos > 0) { + let skip = false; + for (let i: number = pos.pos - 1; i > 0; i--) { + this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode) => { + if (node.isLeaf && !skip) { + ref_node = node; + skip = true; + } + + }); + } + } + return ref_node; + } + + destroy() { + this.tooltip.remove(); + } } diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index c0ed015bd..156390fd3 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -94,6 +94,7 @@ export namespace UndoManager { } export function PrintBatches(): void { + console.log("Open Undo Batches:"); GetOpenBatches().forEach(batch => console.log(batch.batchName)); } diff --git a/src/client/util/request-image-size.js b/src/client/util/request-image-size.js new file mode 100644 index 000000000..27605d167 --- /dev/null +++ b/src/client/util/request-image-size.js @@ -0,0 +1,75 @@ +/** + * request-image-size: Detect image dimensions via request. + * Licensed under the MIT license. + * + * https://github.com/FdezRomero/request-image-size + * © 2017 Rodrigo Fernández Romero + * + * Based on the work of Johannes J. Schmidt + * https://github.com/jo/http-image-size + */ + +const request = require('request'); +const imageSize = require('image-size'); +const HttpError = require('standard-http-error'); + +module.exports = function requestImageSize(options) { + let opts = { + encoding: null + }; + + if (options && typeof options === 'object') { + opts = Object.assign(options, opts); + } else if (options && typeof options === 'string') { + opts = Object.assign({ + uri: options + }, opts); + } else { + return Promise.reject(new Error('You should provide an URI string or a "request" options object.')); + } + + opts.encoding = null; + + return new Promise((resolve, reject) => { + const req = request(opts); + + req.on('response', res => { + if (res.statusCode >= 400) { + return reject(new HttpError(res.statusCode, res.statusMessage)); + } + + let buffer = new Buffer([]); + let size; + let imageSizeError; + + res.on('data', chunk => { + buffer = Buffer.concat([buffer, chunk]); + + try { + size = imageSize(buffer); + } catch (err) { + imageSizeError = err; + return; + } + + if (size) { + resolve(size); + return req.abort(); + } + }); + + res.on('error', err => reject(err)); + + res.on('end', () => { + if (!size) { + return reject(imageSizeError); + } + + size.downloaded = buffer.length; + return resolve(size); + }); + }); + + req.on('error', err => reject(err)); + }); +};
\ No newline at end of file diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d index 557f6f574..1f95af00c 100644 --- a/src/client/util/type_decls.d +++ b/src/client/util/type_decls.d @@ -141,17 +141,12 @@ declare abstract class RefField { readonly [Id]: FieldId; constructor(); - // protected [HandleUpdate]?(diff: any): void; - - // abstract [ToScriptString](): string; } -declare abstract class ObjectField { - protected [OnUpdate](diff?: any): void; - private [Parent]?: RefField | ObjectField; - // abstract [Copy](): ObjectField; +declare type FieldId = string; - // abstract [ToScriptString](): string; +declare abstract class ObjectField { + abstract [Copy](): ObjectField; } declare abstract class URLField extends ObjectField { @@ -161,32 +156,53 @@ declare abstract class URLField extends ObjectField { constructor(url: URL); } -declare class AudioField extends URLField { } -declare class VideoField extends URLField { } -declare class ImageField extends URLField { } -declare class WebField extends URLField { } -declare class PdfField extends URLField { } +declare class AudioField extends URLField { [Copy](): ObjectField; } +declare class VideoField extends URLField { [Copy](): ObjectField; } +declare class ImageField extends URLField { [Copy](): ObjectField; } +declare class WebField extends URLField { [Copy](): ObjectField; } +declare class PdfField extends URLField { [Copy](): ObjectField; } -declare type FieldId = string; +declare const ComputedField: any; +declare const CompileScript: any; +// @ts-ignore +declare type Extract<T, U> = T extends U ? T : never; declare type Field = number | string | boolean | ObjectField | RefField; +declare type FieldWaiting<T extends RefField = RefField> = T extends undefined ? never : Promise<T | undefined>; +declare type FieldResult<T extends Field = Field> = Opt<T> | FieldWaiting<Extract<T, RefField>>; declare type Opt<T> = T | undefined; declare class Doc extends RefField { constructor(); - [key: string]: Field | undefined; + [key: string]: FieldResult; // [ToScriptString](): string; } declare class ListImpl<T extends Field> extends ObjectField { constructor(fields?: T[]); [index: number]: T | (T extends RefField ? Promise<T> : never); - // [ToScriptString](): string; - // [Copy](): ObjectField; + [Copy](): ObjectField; } // @ts-ignore declare const console: any; -declare const Documents: any; +interface DocumentOptions { } + +declare const Docs: { + ImageDocument(url: string, options?: DocumentOptions): Doc; + VideoDocument(url: string, options?: DocumentOptions): Doc; + // HistogramDocument(url:string, options?:DocumentOptions); + TextDocument(options?: DocumentOptions): Doc; + PdfDocument(url: string, options?: DocumentOptions): Doc; + WebDocument(url: string, options?: DocumentOptions): Doc; + HtmlDocument(html: string, options?: DocumentOptions): Doc; + KVPDocument(document: Doc, options?: DocumentOptions): Doc; + FreeformDocument(documents: Doc[], options?: DocumentOptions): Doc; + SchemaDocument(columns: string[], documents: Doc[], options?: DocumentOptions): Doc; + TreeDocument(documents: Doc[], options?: DocumentOptions): Doc; + StackingDocument(documents: Doc[], options?: DocumentOptions): Doc; +}; + +declare function d(...args:any[]):any; |