aboutsummaryrefslogtreecommitdiff
path: root/src/client/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/util')
-rw-r--r--src/client/util/ClientUtils.ts.temp3
-rw-r--r--src/client/util/DocumentManager.ts110
-rw-r--r--src/client/util/DragManager.ts181
-rw-r--r--src/client/util/History.ts136
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx376
-rw-r--r--src/client/util/Import & Export/ImportMetadataEntry.tsx149
-rw-r--r--src/client/util/LinkManager.ts245
-rw-r--r--src/client/util/ProsemirrorCopy/prompt.js179
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts (renamed from src/client/util/ProsemirrorKeymap.ts)10
-rw-r--r--src/client/util/RichTextSchema.tsx205
-rw-r--r--src/client/util/Scripting.ts55
-rw-r--r--src/client/util/SearchUtil.ts64
-rw-r--r--src/client/util/SelectionManager.ts79
-rw-r--r--src/client/util/SerializationHelper.ts24
-rw-r--r--src/client/util/TooltipTextMenu.scss94
-rw-r--r--src/client/util/TooltipTextMenu.tsx498
-rw-r--r--src/client/util/UndoManager.ts1
-rw-r--r--src/client/util/request-image-size.js75
-rw-r--r--src/client/util/type_decls.d52
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;