aboutsummaryrefslogtreecommitdiff
path: root/src/client/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/util')
-rw-r--r--src/client/util/DictationManager.ts75
-rw-r--r--src/client/util/DocumentManager.ts74
-rw-r--r--src/client/util/DragManager.ts459
-rw-r--r--src/client/util/DropConverter.ts23
-rw-r--r--src/client/util/History.ts9
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx96
-rw-r--r--src/client/util/Import & Export/ImageUtils.ts9
-rw-r--r--src/client/util/Import & Export/ImportMetadataEntry.tsx6
-rw-r--r--src/client/util/InteractionUtils.ts163
-rw-r--r--src/client/util/LinkManager.ts70
-rw-r--r--src/client/util/ParagraphNodeSpec.ts10
-rw-r--r--src/client/util/ProseMirrorEditorView.tsx74
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts60
-rw-r--r--src/client/util/RichTextMenu.scss121
-rw-r--r--src/client/util/RichTextMenu.tsx855
-rw-r--r--src/client/util/RichTextRules.ts228
-rw-r--r--src/client/util/RichTextSchema.tsx483
-rw-r--r--src/client/util/Scripting.ts38
-rw-r--r--src/client/util/SearchUtil.ts37
-rw-r--r--src/client/util/SelectionManager.ts49
-rw-r--r--src/client/util/SerializationHelper.ts9
-rw-r--r--src/client/util/SharingManager.tsx14
-rw-r--r--src/client/util/TooltipLinkingMenu.tsx22
-rw-r--r--src/client/util/TooltipTextMenu.scss571
-rw-r--r--src/client/util/TooltipTextMenu.tsx1538
-rw-r--r--src/client/util/TypedEvent.ts62
-rw-r--r--src/client/util/UndoManager.ts16
27 files changed, 3212 insertions, 1959 deletions
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 6bbd3d0ed..3d8f2d234 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -11,7 +11,6 @@ import { Cast, CastCtor } from "../../new_fields/Types";
import { listSpec } from "../../new_fields/Schema";
import { AudioField, ImageField } from "../../new_fields/URLField";
import { HistogramField } from "../northstar/dash-fields/HistogramField";
-import { MainView } from "../views/MainView";
import { Utils } from "../../Utils";
import { RichTextField } from "../../new_fields/RichTextField";
import { DictationOverlay } from "../views/DictationOverlay";
@@ -48,7 +47,7 @@ export namespace DictationManager {
export const Infringed = "unable to process: dictation manager still involved in previous session";
const browser = (() => {
- let identifier = navigator.userAgent.toLowerCase();
+ const identifier = navigator.userAgent.toLowerCase();
if (identifier.indexOf("safari") >= 0) {
return "Safari";
}
@@ -90,7 +89,7 @@ export namespace DictationManager {
export const listen = async (options?: Partial<ListeningOptions>) => {
let results: string | undefined;
- let overlay = options !== undefined && options.useOverlay;
+ const overlay = options !== undefined && options.useOverlay;
if (overlay) {
DictationOverlay.Instance.dictationOverlayVisible = true;
DictationOverlay.Instance.isListening = { interim: false };
@@ -102,7 +101,7 @@ export namespace DictationManager {
Utils.CopyText(results);
if (overlay) {
DictationOverlay.Instance.isListening = false;
- let execute = options && options.tryExecute;
+ const execute = options && options.tryExecute;
DictationOverlay.Instance.dictatedPhrase = execute ? results.toLowerCase() : results;
DictationOverlay.Instance.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true;
}
@@ -131,12 +130,12 @@ export namespace DictationManager {
}
isListening = true;
- let handler = options ? options.interimHandler : undefined;
- let continuous = options ? options.continuous : undefined;
- let indefinite = continuous && continuous.indefinite;
- let language = options ? options.language : undefined;
- let intra = options && options.delimiters ? options.delimiters.intra : undefined;
- let inter = options && options.delimiters ? options.delimiters.inter : undefined;
+ const handler = options ? options.interimHandler : undefined;
+ const continuous = options ? options.continuous : undefined;
+ const indefinite = continuous && continuous.indefinite;
+ const language = options ? options.language : undefined;
+ const intra = options && options.delimiters ? options.delimiters.intra : undefined;
+ const inter = options && options.delimiters ? options.delimiters.inter : undefined;
recognizer.onstart = () => console.log("initiating speech recognition session...");
recognizer.interimResults = handler !== undefined;
@@ -177,7 +176,7 @@ export namespace DictationManager {
recognizer.start();
};
- let complete = () => {
+ const complete = () => {
if (indefinite) {
current && sessionResults.push(current);
sessionResults.length && resolve(sessionResults.join(inter || interSession));
@@ -213,8 +212,8 @@ export namespace DictationManager {
};
const synthesize = (e: SpeechRecognitionEvent, delimiter?: string) => {
- let results = e.results;
- let transcripts: string[] = [];
+ const results = e.results;
+ const transcripts: string[] = [];
for (let i = 0; i < results.length; i++) {
transcripts.push(results.item(i).item(0).transcript.trim());
}
@@ -238,18 +237,18 @@ export namespace DictationManager {
export const execute = async (phrase: string) => {
return UndoManager.RunInBatch(async () => {
- let targets = SelectionManager.SelectedDocuments();
+ const targets = SelectionManager.SelectedDocuments();
if (!targets || !targets.length) {
return;
}
phrase = phrase.toLowerCase();
- let entry = Independent.get(phrase);
+ const entry = Independent.get(phrase);
if (entry) {
let success = false;
- let restrictTo = entry.restrictTo;
- for (let target of targets) {
+ const restrictTo = entry.restrictTo;
+ for (const target of targets) {
if (!restrictTo || validate(target, restrictTo)) {
await entry.action(target);
success = true;
@@ -258,14 +257,14 @@ export namespace DictationManager {
return success;
}
- for (let entry of Dependent) {
- let regex = entry.expression;
- let matches = regex.exec(phrase);
+ for (const entry of Dependent) {
+ const regex = entry.expression;
+ const matches = regex.exec(phrase);
regex.lastIndex = 0;
if (matches !== null) {
let success = false;
- let restrictTo = entry.restrictTo;
- for (let target of targets) {
+ const restrictTo = entry.restrictTo;
+ for (const target of targets) {
if (!restrictTo || validate(target, restrictTo)) {
await entry.action(target, matches);
success = true;
@@ -289,7 +288,7 @@ export namespace DictationManager {
]);
const tryCast = (view: DocumentView, type: DocumentType) => {
- let ctor = ConstructorMap.get(type);
+ const ctor = ConstructorMap.get(type);
if (!ctor) {
return false;
}
@@ -297,7 +296,7 @@ export namespace DictationManager {
};
const validate = (target: DocumentView, types: DocumentType[]) => {
- for (let type of types) {
+ for (const type of types) {
if (tryCast(target, type)) {
return true;
}
@@ -306,11 +305,11 @@ export namespace DictationManager {
};
const interpretNumber = (number: string) => {
- let initial = parseInt(number);
+ const initial = parseInt(number);
if (!isNaN(initial)) {
return initial;
}
- let converted = interpreter.wordsToNumbers(number, { fuzzy: true });
+ const converted = interpreter.wordsToNumbers(number, { fuzzy: true });
if (converted === null) {
return NaN;
}
@@ -326,20 +325,20 @@ export namespace DictationManager {
["open fields", {
action: (target: DocumentView) => {
- let kvp = Docs.Create.KVPDocument(target.props.Document, { width: 300, height: 300 });
+ const kvp = Docs.Create.KVPDocument(target.props.Document, { width: 300, height: 300 });
target.props.addDocTab(kvp, target.props.DataDoc, "onRight");
}
}],
["new outline", {
action: (target: DocumentView) => {
- let newBox = Docs.Create.TextDocument({ width: 400, height: 200, title: "My Outline" });
+ const newBox = Docs.Create.TextDocument({ width: 400, height: 200, title: "My Outline" });
newBox.autoHeight = true;
- let proto = newBox.proto!;
- let prompt = "Press alt + r to start dictating here...";
- let head = 3;
- let anchor = head + prompt.length;
- let proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`;
+ const proto = newBox.proto!;
+ const prompt = "Press alt + r to start dictating here...";
+ const head = 3;
+ const anchor = head + prompt.length;
+ const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`;
proto.data = new RichTextField(proseMirrorState);
proto.backgroundColor = "#eeffff";
target.props.addDocTab(newBox, proto, "onRight");
@@ -353,10 +352,10 @@ export namespace DictationManager {
{
expression: /create (\w+) documents of type (image|nested collection)/g,
action: (target: DocumentView, matches: RegExpExecArray) => {
- let count = interpretNumber(matches[1]);
- let what = matches[2];
- let dataDoc = Doc.GetProto(target.props.Document);
- let fieldKey = "data";
+ const count = interpretNumber(matches[1]);
+ const what = matches[2];
+ const dataDoc = Doc.GetProto(target.props.Document);
+ const fieldKey = "data";
if (isNaN(count)) {
return;
}
@@ -379,7 +378,7 @@ export namespace DictationManager {
{
expression: /view as (freeform|stacking|masonry|schema|tree)/g,
action: (target: DocumentView, matches: RegExpExecArray) => {
- let mode = CollectionViewType.valueOf(matches[1]);
+ const mode = CollectionViewType.valueOf(matches[1]);
mode && (target.props.Document.viewType = mode);
},
restrictTo: [DocumentType.COL]
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 346e88f40..fb4c2155a 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -33,7 +33,7 @@ export class DocumentManager {
//gets all views
public getDocumentViewsById(id: string) {
- let toReturn: DocumentView[] = [];
+ const toReturn: DocumentView[] = [];
DocumentManager.Instance.DocumentViews.map(view => {
if (view.props.Document[Id] === id) {
toReturn.push(view);
@@ -41,7 +41,7 @@ export class DocumentManager {
});
if (toReturn.length === 0) {
DocumentManager.Instance.DocumentViews.map(view => {
- let doc = view.props.Document.proto;
+ const doc = view.props.Document.proto;
if (doc && doc[Id] && doc[Id] === id) {
toReturn.push(view);
}
@@ -57,9 +57,9 @@ export class DocumentManager {
public getDocumentViewById(id: string, preferredCollection?: CollectionView): DocumentView | undefined {
let toReturn: DocumentView | undefined;
- let passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
+ const passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
- for (let pass of passes) {
+ for (const pass of passes) {
DocumentManager.Instance.DocumentViews.map(view => {
if (view.props.Document[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) {
toReturn = view;
@@ -68,7 +68,7 @@ export class DocumentManager {
});
if (!toReturn) {
DocumentManager.Instance.DocumentViews.map(view => {
- let doc = view.props.Document.proto;
+ const doc = view.props.Document.proto;
if (doc && doc[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) {
toReturn = view;
}
@@ -90,51 +90,57 @@ export class DocumentManager {
return views.length ? views[0] : undefined;
}
public getDocumentViews(toFind: Doc): DocumentView[] {
- let toReturn: DocumentView[] = [];
+ const toReturn: DocumentView[] = [];
DocumentManager.Instance.DocumentViews.map(view =>
- Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view));
+ view.props.Document === toFind && toReturn.push(view));
+ DocumentManager.Instance.DocumentViews.map(view =>
+ view.props.Document !== toFind && Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view));
return toReturn;
}
@computed
public get LinkedDocumentViews() {
- let pairs = DocumentManager.Instance.DocumentViews.filter(dv =>
- (dv.isSelected() || Doc.IsBrushed(dv.props.Document)) // draw links from DocumentViews that are selected or brushed OR
- || DocumentManager.Instance.DocumentViews.some(dv2 => { // Documentviews which
- let rest = DocListCast(dv2.props.Document.links).some(l => Doc.AreProtosEqual(l, dv.props.Document));// are link doc anchors
- let init = (dv2.isSelected() || Doc.IsBrushed(dv2.props.Document)) && dv2.Document.type !== DocumentType.AUDIO; // on a view that is selected or brushed
- return init && rest;
- })
- ).reduce((pairs, dv) => {
- let linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document);
- pairs.push(...linksList.reduce((pairs, link) => {
- let linkToDoc = link && LinkManager.Instance.getOppositeAnchor(link, dv.props.Document);
- linkToDoc && DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
- if (dv.props.Document.type !== DocumentType.LINK || dv.props.layoutKey !== docView1.props.layoutKey) {
- pairs.push({ a: dv, b: docView1, l: link });
- }
- });
+ const pairs = DocumentManager.Instance.DocumentViews
+ //.filter(dv => (dv.isSelected() || Doc.IsBrushed(dv.props.Document))) // draw links from DocumentViews that are selected or brushed OR
+ // || DocumentManager.Instance.DocumentViews.some(dv2 => { // Documentviews which
+ // const rest = DocListCast(dv2.props.Document.links).some(l => Doc.AreProtosEqual(l, dv.props.Document));// are link doc anchors
+ // const init = (dv2.isSelected() || Doc.IsBrushed(dv2.props.Document)) && dv2.Document.type !== DocumentType.AUDIO; // on a view that is selected or brushed
+ // return init && rest;
+ // }
+ // )
+ .reduce((pairs, dv) => {
+ const linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document);
+ pairs.push(...linksList.reduce((pairs, link) => {
+ const linkToDoc = link && LinkManager.Instance.getOppositeAnchor(link, dv.props.Document);
+ linkToDoc && DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
+ if (dv.props.Document.type !== DocumentType.LINK || dv.props.layoutKey !== docView1.props.layoutKey) {
+ pairs.push({ a: dv, b: docView1, l: link });
+ }
+ });
+ return pairs;
+ }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
return pairs;
- }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
- return pairs;
- }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
+ }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
return pairs;
}
public jumpToDocument = async (targetDoc: Doc, willZoom: boolean, dockFunc?: (doc: Doc) => void, docContext?: Doc, linkId?: string, closeContextIfNotFound: boolean = false): Promise<void> => {
- let highlight = () => {
+ const highlight = () => {
const finalDocView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
finalDocView && (finalDocView.Document.scrollToLinkID = linkId);
finalDocView && Doc.linkFollowHighlight(finalDocView.props.Document);
};
const docView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
- const annotatedDoc = await Cast(targetDoc.annotationOn, Doc);
+ let annotatedDoc = await Cast(docView?.props.Document.annotationOn, Doc);
+ if (annotatedDoc) {
+ const first = DocumentManager.Instance.getFirstDocumentView(annotatedDoc);
+ if (first) annotatedDoc = first.props.Document;
+ }
if (docView) { // we have a docView already and aren't forced to create a new one ... just focus on the document. TODO move into view if necessary otherwise just highlight?
- annotatedDoc && docView.props.focus(annotatedDoc, false);
- docView.props.focus(docView.props.Document, willZoom);
+ docView.props.focus(docView.props.Document, false);
highlight();
} else {
const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined;
@@ -176,7 +182,7 @@ export class DocumentManager {
}
public async FollowLink(link: Doc | undefined, doc: Doc, focus: (doc: Doc, maxLocation: string) => void, zoom: boolean = false, reverse: boolean = false, currentContext?: Doc) {
- const linkDocs = link ? [link] : LinkManager.Instance.getAllRelatedLinks(doc);
+ const linkDocs = link ? [link] : DocListCast(doc.links);
SelectionManager.DeselectAll();
const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc));
const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc));
@@ -194,17 +200,19 @@ export class DocumentManager {
const target = linkFollowDocs[reverse ? 1 : 0];
target.currentTimecode !== undefined && (target.currentTimecode = linkFollowTimecodes[reverse ? 1 : 0]);
DocumentManager.Instance.jumpToDocument(linkFollowDocs[reverse ? 1 : 0], zoom, (doc: Doc) => focus(doc, maxLocation), targetContext, linkDoc[Id]);
+ } else if (link) {
+ DocumentManager.Instance.jumpToDocument(link, zoom, (doc: Doc) => focus(doc, "onRight"), undefined, undefined);
}
}
@action
zoomIntoScale = (docDelegate: Doc, scale: number) => {
- let docView = DocumentManager.Instance.getDocumentView(Doc.GetProto(docDelegate));
+ const docView = DocumentManager.Instance.getDocumentView(Doc.GetProto(docDelegate));
docView && docView.props.zoomToScale(scale);
}
getScaleOfDocView = (docDelegate: Doc) => {
- let doc = Doc.GetProto(docDelegate);
+ const doc = Doc.GetProto(docDelegate);
const docView = DocumentManager.Instance.getDocumentView(doc);
if (docView) {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index bbc29585c..df2f5fe3c 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,7 +1,5 @@
-import { action, runInAction } from "mobx";
-import { Doc, Field } from "../../new_fields/Doc";
-import { Cast, StrCast, ScriptCast } from "../../new_fields/Types";
-import { URLField } from "../../new_fields/URLField";
+import { Doc, Field, DocListCast } from "../../new_fields/Doc";
+import { Cast, ScriptCast } from "../../new_fields/Types";
import { emptyFunction } from "../../Utils";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import * as globalCssVariables from "../views/globalCssVariables.scss";
@@ -20,43 +18,46 @@ import { convertDropDataToButtons } from "./DropConverter";
export type dropActionType = "alias" | "copy" | undefined;
export function SetupDrag(
_reference: React.RefObject<HTMLElement>,
- docFunc: () => Doc | Promise<Doc>,
+ docFunc: () => Doc | Promise<Doc> | undefined,
moveFunc?: DragManager.MoveFunction,
dropAction?: dropActionType,
- options?: any,
+ treeViewId?: string,
dontHideOnDrop?: boolean,
dragStarted?: () => void
) {
- let onRowMove = async (e: PointerEvent) => {
+ const onRowMove = async (e: PointerEvent) => {
e.stopPropagation();
e.preventDefault();
document.removeEventListener("pointermove", onRowMove);
document.removeEventListener('pointerup', onRowUp);
- let doc = await docFunc();
- var dragData = new DragManager.DocumentDragData([doc]);
- dragData.dropAction = dropAction;
- dragData.moveDocument = moveFunc;
- dragData.options = options;
- dragData.dontHideOnDrop = dontHideOnDrop;
- DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
- dragStarted && dragStarted();
+ const doc = await docFunc();
+ if (doc) {
+ const dragData = new DragManager.DocumentDragData([doc]);
+ dragData.dropAction = dropAction;
+ dragData.moveDocument = moveFunc;
+ dragData.treeViewId = treeViewId;
+ dragData.dontHideOnDrop = dontHideOnDrop;
+ DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
+ dragStarted && dragStarted();
+ }
};
- let onRowUp = (): void => {
+ const onRowUp = (): void => {
document.removeEventListener("pointermove", onRowMove);
document.removeEventListener('pointerup', onRowUp);
};
- let onItemDown = async (e: React.PointerEvent) => {
+ const onItemDown = async (e: React.PointerEvent) => {
if (e.button === 0) {
e.stopPropagation();
if (e.shiftKey && CollectionDockingView.Instance) {
e.persist();
- CollectionDockingView.Instance.StartOtherDrag({
+ const dragDoc = await docFunc();
+ dragDoc && CollectionDockingView.Instance.StartOtherDrag({
pageX: e.pageX,
pageY: e.pageY,
preventDefault: emptyFunction,
button: 0
- }, [await docFunc()]);
+ }, [dragDoc]);
} else {
document.addEventListener("pointermove", onRowMove);
document.addEventListener("pointerup", onRowUp);
@@ -66,62 +67,9 @@ export function SetupDrag(
return onItemDown;
}
-function moveLinkedDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
- const document = SelectionManager.SelectedDocuments()[0];
- document && 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);
- 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, singleLink?: Doc) {
- let srcTarg = sourceDoc.proto;
- let draggedDocs: Doc[] = [];
-
- if (srcTarg) {
- let linkDocs = singleLink ? [singleLink] : LinkManager.Instance.getAllRelatedLinks(srcTarg);
- if (linkDocs) {
- draggedDocs = linkDocs.map(link => {
- let opp = LinkManager.Instance.getOppositeAnchor(link, sourceDoc);
- if (opp) return opp;
- }) as Doc[];
- }
- }
- if (draggedDocs.length) {
- let moddrag: Doc[] = [];
- for (const draggedDoc of draggedDocs) {
- let doc = await Cast(draggedDoc.annotationOn, Doc);
- if (doc) moddrag.push(doc);
- }
- let dragdocs = moddrag.length ? moddrag : draggedDocs;
- let dragData = new DragManager.DocumentDragData(dragdocs);
- dragData.moveDocument = moveLinkedDocument;
- DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, {
- handlers: {
- dragComplete: action(emptyFunction),
- },
- hideSource: false
- });
- }
-}
-
-
export namespace DragManager {
+ let dragDiv: HTMLDivElement;
+
export function Root() {
const root = document.getElementById("root");
if (!root) {
@@ -129,79 +77,45 @@ export namespace DragManager {
}
return root;
}
+ export let AbortDrag: () => void = emptyFunction;
+ export type MoveFunction = (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean;
- let dragDiv: HTMLDivElement;
-
- export enum DragButtons {
- Left = 1,
- Right = 2,
- Both = Left | Right
- }
-
- interface DragOptions {
- handlers: DragHandlers;
-
- hideSource: boolean | (() => boolean);
-
- dragHasStarted?: () => void;
-
- withoutShiftDrag?: boolean;
-
- finishDrag?: (dropData: { [id: string]: any }) => void;
-
- offsetX?: number;
-
+ export interface DragDropDisposer { (): void; }
+ export interface DragOptions {
+ dragComplete?: (e: DragCompleteEvent) => void; // function to invoke when drag has completed
+ hideSource?: boolean; // hide source document during drag
+ offsetX?: number; // offset of top left of source drag visual from cursor
offsetY?: number;
}
- export interface DragDropDisposer {
- (): void;
- }
-
- export class DragCompleteEvent { }
-
- export interface DragHandlers {
- dragComplete: (e: DragCompleteEvent) => void;
- }
-
- export interface DropOptions {
- handlers: DropHandlers;
- }
+ // event called when the drag operation results in a drop action
export class DropEvent {
constructor(
readonly x: number,
readonly y: number,
- readonly data: { [id: string]: any },
- readonly mods: string
+ readonly complete: DragCompleteEvent,
+ readonly altKey: boolean,
+ readonly metaKey: boolean,
+ readonly ctrlKey: boolean
) { }
}
- export interface DropHandlers {
- drop: (e: Event, de: DropEvent) => void;
- }
-
- export function MakeDropTarget(
- element: HTMLElement,
- options: DropOptions
- ): DragDropDisposer {
- if ("canDrop" in element.dataset) {
- throw new Error(
- "Element is already droppable, can't make it droppable again"
- );
+ // event called when the drag operation has completed (aborted or completed a drop) -- this will be after any drop event has been generated
+ export class DragCompleteEvent {
+ constructor(aborted: boolean, dragData: { [id: string]: any }) {
+ this.aborted = aborted;
+ this.docDragData = dragData instanceof DocumentDragData ? dragData : undefined;
+ this.annoDragData = dragData instanceof PdfAnnoDragData ? dragData : undefined;
+ this.linkDragData = dragData instanceof LinkDragData ? dragData : undefined;
+ this.columnDragData = dragData instanceof ColumnDragData ? dragData : undefined;
}
- element.dataset.canDrop = "true";
- const handler = (e: Event) => {
- const ce = e as CustomEvent<DropEvent>;
- options.handlers.drop(e, ce.detail);
- };
- element.addEventListener("dashOnDrop", handler);
- return () => {
- element.removeEventListener("dashOnDrop", handler);
- delete element.dataset.canDrop;
- };
+ aborted: boolean;
+ docDragData?: DocumentDragData;
+ annoDragData?: PdfAnnoDragData;
+ linkDragData?: LinkDragData;
+ columnDragData?: ColumnDragData;
}
- export type MoveFunction = (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
export class DocumentDragData {
constructor(dragDoc: Doc[]) {
this.draggedDocuments = dragDoc;
@@ -210,6 +124,9 @@ export namespace DragManager {
}
draggedDocuments: Doc[];
droppedDocuments: Doc[];
+ dragDivName?: string;
+ treeViewId?: string;
+ dontHideOnDrop?: boolean;
offset: number[];
dropAction: dropActionType;
userDropAction: dropActionType;
@@ -217,16 +134,32 @@ export namespace DragManager {
moveDocument?: MoveFunction;
isSelectionMove?: boolean; // indicates that an explicitly selected Document is being dragged. this will suppress onDragStart scripts
applyAsTemplate?: boolean;
- [id: string]: any;
}
-
- export class AnnotationDragData {
+ export class LinkDragData {
+ constructor(linkSourceDoc: Doc) {
+ this.linkSourceDocument = linkSourceDoc;
+ }
+ droppedDocuments: Doc[] = [];
+ linkSourceDocument: Doc;
+ dontClearTextBox?: boolean;
+ linkDocument?: Doc;
+ }
+ export class ColumnDragData {
+ constructor(colKey: SchemaHeaderField) {
+ this.colKey = colKey;
+ }
+ colKey: SchemaHeaderField;
+ }
+ // used by PDFs to conditionally (if the drop completes) create a text annotation when dragging from the PDF toolbar when a text region has been selected.
+ // this is pretty clunky and should be rethought out using linkDrag or DocumentDrag
+ export class PdfAnnoDragData {
constructor(dragDoc: Doc, annotationDoc: Doc, dropDoc: Doc) {
this.dragDocument = dragDoc;
this.dropDocument = dropDoc;
this.annotationDocument = annotationDoc;
this.offset = [0, 0];
}
+ linkedToDoc?: boolean;
targetContext: Doc | undefined;
dragDocument: Doc;
annotationDocument: Doc;
@@ -236,98 +169,103 @@ export namespace DragManager {
userDropAction: dropActionType;
}
- export let StartDragFunctions: (() => void)[] = [];
+ export function MakeDropTarget(
+ element: HTMLElement,
+ dropFunc: (e: Event, de: DropEvent) => void
+ ): DragDropDisposer {
+ if ("canDrop" in element.dataset) {
+ throw new Error(
+ "Element is already droppable, can't make it droppable again"
+ );
+ }
+ element.dataset.canDrop = "true";
+ const handler = (e: Event) => dropFunc(e, (e as CustomEvent<DropEvent>).detail);
+ element.addEventListener("dashOnDrop", handler);
+ return () => {
+ element.removeEventListener("dashOnDrop", handler);
+ delete element.dataset.canDrop;
+ };
+ }
+ // drag a document and drop it (or make an alias/copy on drop)
export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
- runInAction(() => StartDragFunctions.map(func => func()));
+ const finishDrag = (e: DragCompleteEvent) => {
+ e.docDragData && (e.docDragData.droppedDocuments =
+ dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? ScriptCast(d.onDragStart).script.run({ this: d }).result :
+ dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ? Doc.MakeAlias(d) :
+ dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? Doc.MakeCopy(d, true) : d)
+ );
+ e.docDragData?.droppedDocuments.forEach((drop: Doc, i: number) =>
+ Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), []).map(prop => drop[prop] = undefined));
+ };
dragData.draggedDocuments.map(d => d.dragFactory); // does this help? trying to make sure the dragFactory Doc is loaded
- StartDrag(eles, dragData, downX, downY, options, options && options.finishDrag ? options.finishDrag :
- (dropData: { [id: string]: any }) => {
- (dropData.droppedDocuments =
- dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? ScriptCast(d.onDragStart).script.run({ this: d }).result :
- dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ? Doc.MakeAlias(d) :
- dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? Doc.MakeCopy(d, true) : d)
- );
- dropData.droppedDocuments.forEach((drop: Doc, i: number) =>
- Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), []).map(prop => drop[prop] = undefined));
- });
+ StartDrag(eles, dragData, downX, downY, options, finishDrag);
}
+ // drag a button template and drop a new button
export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) {
- let dragData = new DragManager.DocumentDragData([]);
- runInAction(() => StartDragFunctions.map(func => func()));
- StartDrag(eles, dragData, downX, downY, options, options && options.finishDrag ? options.finishDrag :
- (dropData: { [id: string]: any }) => {
- let bd = Docs.Create.ButtonDocument({ width: 150, height: 50, title: title });
- bd.onClick = ScriptField.MakeScript(script);
- params.map(p => Object.keys(vars).indexOf(p) !== -1 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc)));
- initialize && initialize(bd);
- bd.buttonParams = new List<string>(params);
- dropData.droppedDocuments = [bd];
- });
+ const finishDrag = (e: DragCompleteEvent) => {
+ const bd = Docs.Create.ButtonDocument({ width: 150, height: 50, title: title });
+ bd.onClick = ScriptField.MakeScript(script);
+ params.map(p => Object.keys(vars).indexOf(p) !== -1 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc)));
+ initialize && initialize(bd);
+ bd.buttonParams = new List<string>(params);
+ e.docDragData && (e.docDragData.droppedDocuments = [bd]);
+ };
+ StartDrag(eles, new DragManager.DocumentDragData([]), downX, downY, options, finishDrag);
}
- export function StartLinkedDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
- dragData.moveDocument = moveLinkedDocument;
+ // drag links and drop link targets (aliasing them if needed)
+ export async function StartLinkTargetsDrag(dragEle: HTMLElement, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) {
+ const draggedDocs = (specificLinks ? specificLinks : DocListCast(sourceDoc.links)).map(link => LinkManager.Instance.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[];
- 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 containingView = SelectionManager.SelectedDocuments()[0] ? SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView : undefined;
- let inContext = dvs.filter(dv => dv.props.ContainingCollectionView === containingView);
- if (inContext.length) {
- inContext.forEach(dv => droppedDocs.push(dv.props.Document));
+ if (draggedDocs.length) {
+ const moddrag: Doc[] = [];
+ for (const draggedDoc of draggedDocs) {
+ const doc = await Cast(draggedDoc.annotationOn, Doc);
+ if (doc) moddrag.push(doc);
+ }
+
+ const dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs);
+ dragData.moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (doc: Doc) => boolean): boolean => {
+ const document = SelectionManager.SelectedDocuments()[0];
+ document && document.props.removeDocument && document.props.removeDocument(doc);
+ addDocument(doc);
+ return true;
+ };
+ const containingView = SelectionManager.SelectedDocuments()[0] ? SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView : undefined;
+ const finishDrag = (e: DragCompleteEvent) =>
+ e.docDragData && (e.docDragData.droppedDocuments =
+ dragData.draggedDocuments.reduce((droppedDocs, d) => {
+ const dvs = DocumentManager.Instance.getDocumentViews(d).filter(dv => dv.props.ContainingCollectionView === containingView);
+ if (dvs.length) {
+ dvs.forEach(dv => droppedDocs.push(dv.props.Document));
} else {
droppedDocs.push(Doc.MakeAlias(d));
}
- } else {
- droppedDocs.push(Doc.MakeAlias(d));
- }
- return droppedDocs;
- }, []);
- dropData.droppedDocuments = droppedDocuments;
- });
- }
+ return droppedDocs;
+ }, [] as Doc[]));
- export function StartAnnotationDrag(eles: HTMLElement[], dragData: AnnotationDragData, downX: number, downY: number, options?: DragOptions) {
- StartDrag(eles, dragData, downX, downY, options);
- }
-
- export class LinkDragData {
- constructor(linkSourceDoc: Doc, blacklist: Doc[] = []) {
- this.linkSourceDocument = linkSourceDoc;
- this.blacklist = blacklist;
+ StartDrag([dragEle], dragData, downX, downY, undefined, finishDrag);
}
- droppedDocuments: Doc[] = [];
- linkSourceDocument: Doc;
- blacklist: Doc[];
- dontClearTextBox?: boolean;
- [id: string]: any;
}
- // for column dragging in schema view
- export class ColumnDragData {
- constructor(colKey: SchemaHeaderField) {
- this.colKey = colKey;
- }
- colKey: SchemaHeaderField;
- [id: string]: any;
+ // drag&drop the pdf annotation anchor which will create a text note on drop via a dropCompleted() DragOption
+ export function StartPdfAnnoDrag(eles: HTMLElement[], dragData: PdfAnnoDragData, downX: number, downY: number, options?: DragOptions) {
+ StartDrag(eles, dragData, downX, downY, options);
}
- export function StartLinkDrag(ele: HTMLElement, dragData: LinkDragData, downX: number, downY: number, options?: DragOptions) {
- StartDrag([ele], dragData, downX, downY, options);
+ // drags a linker button and creates a link on drop
+ export function StartLinkDrag(ele: HTMLElement, sourceDoc: Doc, downX: number, downY: number, options?: DragOptions) {
+ StartDrag([ele], new DragManager.LinkDragData(sourceDoc), downX, downY, options);
}
+ // drags a column from a schema view
export function StartColumnDrag(ele: HTMLElement, dragData: ColumnDragData, 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) {
+ function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) {
eles = eles.filter(e => e);
if (!dragDiv) {
dragDiv = document.createElement("div");
@@ -336,80 +274,64 @@ export namespace DragManager {
DragManager.Root().appendChild(dragDiv);
}
SelectionManager.SetIsDragging(true);
- let scaleXs: number[] = [];
- let scaleYs: number[] = [];
- let xs: number[] = [];
- let ys: number[] = [];
-
- const docs = dragData instanceof DocumentDragData ? dragData.draggedDocuments :
- dragData instanceof AnnotationDragData ? [dragData.dragDocument] : [];
- let dragElements = eles.map(ele => {
- const w = ele.offsetWidth,
- h = ele.offsetHeight;
+ const scaleXs: number[] = [];
+ const scaleYs: number[] = [];
+ const xs: number[] = [];
+ const ys: number[] = [];
+
+ const docs = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof PdfAnnoDragData ? [dragData.dragDocument] : [];
+ const dragElements = eles.map(ele => {
+ if (!ele.parentNode) dragDiv.appendChild(ele);
+ const dragElement = ele.parentNode === dragDiv ? ele : ele.cloneNode(true) as HTMLElement;
const rect = ele.getBoundingClientRect();
- const scaleX = rect.width / w,
- scaleY = rect.height / h;
- let x = rect.left,
- y = rect.top;
- xs.push(x);
- ys.push(y);
+ const scaleX = rect.width / ele.offsetWidth,
+ scaleY = rect.height / ele.offsetHeight;
+ xs.push(rect.left);
+ ys.push(rect.top);
scaleXs.push(scaleX);
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.transition = "none";
dragElement.style.transformOrigin = "0 0";
+ dragElement.style.borderRadius = getComputedStyle(ele).borderRadius;
dragElement.style.zIndex = globalCssVariables.contextMenuZindex;// "1000";
- dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`;
+ dragElement.style.transform = `translate(${rect.left + (options?.offsetX || 0)}px, ${rect.top + (options?.offsetY || 0)}px) scale(${scaleX}, ${scaleY})`;
dragElement.style.width = `${rect.width / scaleX}px`;
dragElement.style.height = `${rect.height / scaleY}px`;
if (docs.length) {
- var pdfBox = dragElement.getElementsByTagName("canvas");
- var pdfBoxSrc = ele.getElementsByTagName("canvas");
+ const pdfBox = dragElement.getElementsByTagName("canvas");
+ const pdfBoxSrc = ele.getElementsByTagName("canvas");
Array.from(pdfBox).map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0));
- var pdfView = dragElement.getElementsByClassName("pdfViewer-viewer");
- var pdfViewSrc = ele.getElementsByClassName("pdfViewer-viewer");
- let tops = Array.from(pdfViewSrc).map(p => p.scrollTop);
- let oldopacity = dragElement.style.opacity;
+ const pdfView = dragElement.getElementsByClassName("pdfViewer-viewer");
+ const pdfViewSrc = ele.getElementsByClassName("pdfViewer-viewer");
+ const tops = Array.from(pdfViewSrc).map(p => p.scrollTop);
+ const oldopacity = dragElement.style.opacity;
dragElement.style.opacity = "0";
setTimeout(() => {
dragElement.style.opacity = oldopacity;
Array.from(pdfView).map((v, i) => v.scrollTo({ top: tops[i] }));
}, 0);
}
- let set = dragElement.getElementsByTagName('*');
if (dragElement.hasAttribute("style")) (dragElement as any).style.pointerEvents = "none";
+ const set = dragElement.getElementsByTagName('*');
// 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";
- }
+ set[i].hasAttribute("style") && ((set[i] as any).style.pointerEvents = "none");
}
-
dragDiv.appendChild(dragElement);
return dragElement;
});
- let hideSource = false;
- if (options) {
- if (typeof options.hideSource === "boolean") {
- hideSource = options.hideSource;
- } else {
- hideSource = options.hideSource();
- }
- }
-
- eles.map(ele => ele.hidden = hideSource);
+ const hideSource = options?.hideSource ? true : false;
+ eles.map(ele => ele.parentElement && ele.parentElement?.className === dragData.dragDivName ? (ele.parentElement.hidden = hideSource) : (ele.hidden = hideSource));
let lastX = downX;
let lastY = downY;
@@ -418,9 +340,9 @@ export namespace DragManager {
if (dragData instanceof DocumentDragData) {
dragData.userDropAction = e.ctrlKey ? "alias" : undefined;
}
- if (((options && !options.withoutShiftDrag) || !options) && e.shiftKey && CollectionDockingView.Instance) {
+ if (e.shiftKey && CollectionDockingView.Instance) {
AbortDrag();
- finishDrag && finishDrag(dragData);
+ finishDrag?.(new DragCompleteEvent(true, dragData));
CollectionDockingView.Instance.StartOtherDrag({
pageX: e.pageX,
pageY: e.pageY,
@@ -429,61 +351,56 @@ export namespace DragManager {
}, dragData.droppedDocuments);
}
//TODO: Why can't we use e.movementX and e.movementY?
- let moveX = e.pageX - lastX;
- let moveY = e.pageY - lastY;
+ const moveX = e.pageX - lastX;
+ const moveY = e.pageY - lastY;
lastX = e.pageX;
lastY = e.pageY;
dragElements.map((dragElement, i) => (dragElement.style.transform =
- `translate(${(xs[i] += moveX) + (options ? (options.offsetX || 0) : 0)}px, ${(ys[i] += moveY) + (options ? (options.offsetY || 0) : 0)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`)
+ `translate(${(xs[i] += moveX) + (options?.offsetX || 0)}px, ${(ys[i] += moveY) + (options?.offsetY || 0)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`)
);
};
- let hideDragShowOriginalElements = () => {
+ const hideDragShowOriginalElements = () => {
dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
- eles.map(ele => ele.hidden = false);
+ eles.map(ele => ele.parentElement && ele.parentElement?.className === dragData.dragDivName ? (ele.parentElement.hidden = false) : (ele.hidden = false));
};
- let endDrag = () => {
+ const endDrag = () => {
document.removeEventListener("pointermove", moveHandler, true);
document.removeEventListener("pointerup", upHandler);
- if (options) {
- options.handlers.dragComplete({});
- }
};
AbortDrag = () => {
hideDragShowOriginalElements();
SelectionManager.SetIsDragging(false);
+ options?.dragComplete?.(new DragCompleteEvent(true, dragData));
endDrag();
};
const upHandler = (e: PointerEvent) => {
hideDragShowOriginalElements();
dispatchDrag(eles, e, dragData, options, finishDrag);
SelectionManager.SetIsDragging(false);
+ options?.dragComplete?.(new DragCompleteEvent(false, dragData));
endDrag();
};
document.addEventListener("pointermove", moveHandler, true);
document.addEventListener("pointerup", upHandler);
}
- function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) {
- 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];
+ function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (e: DragCompleteEvent) => void) {
+ const removed = dragData.dontHideOnDrop ? [] : dragEles.map(dragEle => {
+ const ret = { ele: dragEle, w: dragEle.style.width, h: dragEle.style.height };
dragEle.style.width = "0";
dragEle.style.height = "0";
return ret;
});
const target = document.elementFromPoint(e.x, e.y);
removed.map(r => {
- let dragEle = r[0] as HTMLElement;
- dragEle.style.width = r[1] as string;
- dragEle.style.height = r[2] as string;
- // let parent = r[1];
- // if (parent && dragEle) parent.appendChild(dragEle);
+ r.ele.style.width = r.w;
+ r.ele.style.height = r.h;
});
if (target) {
- finishDrag && finishDrag(dragData);
+ const complete = new DragCompleteEvent(false, dragData);
+ finishDrag?.(complete);
target.dispatchEvent(
new CustomEvent<DropEvent>("dashOnDrop", {
@@ -491,8 +408,10 @@ export namespace DragManager {
detail: {
x: e.x,
y: e.y,
- data: dragData,
- mods: e.altKey ? "AltKey" : e.ctrlKey ? "CtrlKey" : e.metaKey ? "MetaKey" : ""
+ complete: complete,
+ altKey: e.altKey,
+ metaKey: e.metaKey,
+ ctrlKey: e.ctrlKey
}
})
);
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 6b53333d7..dc66bceee 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -9,26 +9,27 @@ import { ScriptField } from "../../new_fields/ScriptField";
function makeTemplate(doc: Doc): boolean {
- let layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
- let layout = StrCast(layoutDoc.layout).match(/fieldKey={"[^"]*"}/)![0];
- let fieldKey = layout.replace('fieldKey={"', "").replace(/"}$/, "");
- let docs = DocListCast(layoutDoc[fieldKey]);
+ const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
+ const layout = StrCast(layoutDoc.layout).match(/fieldKey={'[^']*'}/)![0];
+ const fieldKey = layout.replace("fieldKey={'", "").replace(/'}$/, "");
+ const docs = DocListCast(layoutDoc[fieldKey]);
let any = false;
- docs.map(d => {
+ docs.forEach(d => {
if (!StrCast(d.title).startsWith("-")) {
any = true;
- return Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc));
+ Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc));
+ } else if (d.type === DocumentType.COL) {
+ any = makeTemplate(d) || any;
}
- if (d.type === DocumentType.COL) return makeTemplate(d);
- return false;
});
return any;
}
export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
data && data.draggedDocuments.map((doc, i) => {
let dbox = doc;
- if (!doc.onDragStart && !doc.onClick && doc.viewType !== CollectionViewType.Linear) {
- let layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
+ // bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant
+ if (!doc.onDragStart && !doc.onClick && !doc.isButtonBar) {
+ const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
if (layoutDoc.type === DocumentType.COL) {
layoutDoc.isTemplateDoc = makeTemplate(layoutDoc);
} else {
@@ -38,7 +39,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
dbox.dragFactory = layoutDoc;
dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined;
dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
- } else if (doc.viewType === CollectionViewType.Linear) {
+ } else if (doc.isButtonBar) {
dbox.ignoreClick = true;
}
data.droppedDocuments[i] = dbox;
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index 899abbe40..545e8acb4 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -1,6 +1,5 @@
-import { Doc, Opt, Field } from "../../new_fields/Doc";
+import { Doc } 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";
@@ -26,7 +25,7 @@ export namespace HistoryUtil {
// const handlers: ((state: ParsedUrl | null) => void)[] = [];
function onHistory(e: PopStateEvent) {
- if (window.location.pathname !== RouteStore.home) {
+ if (window.location.pathname !== "/home") {
const url = e.state as ParsedUrl || parseUrl(window.location);
if (url) {
switch (url.type) {
@@ -54,7 +53,7 @@ export namespace HistoryUtil {
}
export function getState(): ParsedUrl {
- let state = copyState(history.state);
+ const state = copyState(history.state);
state.initializers = state.initializers || {};
return state;
}
@@ -161,7 +160,7 @@ export namespace HistoryUtil {
const pathname = location.pathname.substring(1);
const search = location.search;
const opts = search.length ? qs.parse(search, { sort: false }) : {};
- let pathnameSplit = pathname.split("/");
+ const pathnameSplit = pathname.split("/");
const type = pathnameSplit[0];
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 5904088fc..5b5bffd8c 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx
@@ -1,8 +1,7 @@
import "fs";
import React = require("react");
import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
-import { RouteStore } from "../../../server/RouteStore";
-import { action, observable, autorun, runInAction, computed, reaction, IReactionDisposer } from "mobx";
+import { action, observable, runInAction, computed, reaction, IReactionDisposer } from "mobx";
import { FieldViewProps, FieldView } from "../../views/nodes/FieldView";
import Measure, { ContentRect } from "react-measure";
import { library } from '@fortawesome/fontawesome-svg-core';
@@ -20,19 +19,13 @@ import { listSpec } from "../../../new_fields/Schema";
import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import "./DirectoryImportBox.scss";
-import { Identified } from "../../Network";
+import { Networking } from "../../Network";
import { BatchedArray } from "array-batcher";
-import { ExifData } from "exif";
+import * as path from 'path';
+import { AcceptibleMedia } from "../../../server/SharedMediaTypes";
const unsupported = ["text/html", "text/plain"];
-interface ImageUploadResponse {
- name: string;
- path: string;
- type: string;
- exif: any;
-}
-
@observer
export default class DirectoryImportBox extends React.Component<FieldViewProps> {
private selector = React.createRef<HTMLInputElement>();
@@ -55,7 +48,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
constructor(props: FieldViewProps) {
super(props);
library.add(faTag, faPlus);
- let doc = this.props.Document;
+ const doc = this.props.Document;
this.editingMetadata = this.editingMetadata || false;
this.persistent = this.persistent || false;
!Cast(doc.data, listSpec(Doc)) && (doc.data = new List<Doc>());
@@ -85,17 +78,22 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
this.phase = "Initializing download...";
});
- let docs: Doc[] = [];
+ const docs: Doc[] = [];
- let files = e.target.files;
+ const files = e.target.files;
if (!files || files.length === 0) return;
- let directory = (files.item(0) as any).webkitRelativePath.split("/", 1)[0];
+ const directory = (files.item(0) as any).webkitRelativePath.split("/", 1)[0];
- let validated: File[] = [];
+ const validated: File[] = [];
for (let i = 0; i < files.length; i++) {
- let file = files.item(i);
- file && !unsupported.includes(file.type) && validated.push(file);
+ const file = files.item(i);
+ if (file && !unsupported.includes(file.type)) {
+ const ext = path.extname(file.name).toLowerCase();
+ if (AcceptibleMedia.imageFormats.includes(ext)) {
+ validated.push(file);
+ }
+ }
}
runInAction(() => {
@@ -103,13 +101,13 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
this.completed = 0;
});
- let sizes: number[] = [];
- let modifiedDates: number[] = [];
+ const sizes: number[] = [];
+ const modifiedDates: number[] = [];
runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`);
const batched = BatchedArray.from(validated, { batchSize: 15 });
- const uploads = await batched.batchedMapAsync<ImageUploadResponse>(async (batch, collector) => {
+ const uploads = await batched.batchedMapAsync<any>(async (batch, collector) => {
const formData = new FormData();
batch.forEach(file => {
@@ -118,20 +116,14 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
formData.append(Utils.GenerateGuid(), file);
});
- collector.push(...(await Identified.PostFormDataToServer(RouteStore.upload, formData)));
+ collector.push(...(await Networking.PostFormDataToServer("/upload", formData)));
runInAction(() => this.completed += batch.length);
});
- await Promise.all(uploads.map(async upload => {
- const type = upload.type;
- const path = Utils.prepend(upload.path);
- const options = {
- nativeWidth: 300,
- width: 300,
- title: upload.name
- };
- const document = await Docs.Get.DocumentFromType(type, path, options);
- const { data, error } = upload.exif;
+ await Promise.all(uploads.map(async ({ name, type, clientAccessPath, exifData }) => {
+ const path = Utils.prepend(clientAccessPath);
+ const document = await Docs.Get.DocumentFromType(type, path, { width: 300, title: name });
+ const { data, error } = exifData;
if (document) {
Doc.GetProto(document).exif = error || Docs.Get.DocumentHierarchyFromJson(data);
docs.push(document);
@@ -139,26 +131,26 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
}));
for (let i = 0; i < docs.length; i++) {
- let doc = docs[i];
+ const doc = docs[i];
doc.size = sizes[i];
doc.modified = modifiedDates[i];
this.entries.forEach(entry => {
- let target = entry.onDataDoc ? Doc.GetProto(doc) : doc;
+ const 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 = {
+ const doc = this.props.Document;
+ const height: number = NumCast(doc.height) || 0;
+ const offset: number = this.persistent ? (height === 0 ? 0 : height + 30) : 0;
+ const options: DocumentOptions = {
title: `Import of ${directory}`,
width: 1105,
height: 500,
x: NumCast(doc.x),
y: NumCast(doc.y) + offset
};
- let parent = this.props.ContainingCollectionView;
+ const parent = this.props.ContainingCollectionView;
if (parent) {
let importContainer: Doc;
if (docs.length < 50) {
@@ -197,18 +189,18 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
@action
preserveCentering = (rect: ContentRect) => {
- let bounds = rect.offset!;
+ const bounds = rect.offset!;
if (bounds.width === 0 || bounds.height === 0) {
return;
}
- let offset = this.dimensions / 2;
+ const offset = this.dimensions / 2;
this.left = bounds.width / 2 - offset;
this.top = bounds.height / 2 - offset;
}
@action
addMetadataEntry = async () => {
- let entryDoc = new Doc();
+ const entryDoc = new Doc();
entryDoc.checked = false;
entryDoc.key = keyPlaceholder;
entryDoc.value = valuePlaceholder;
@@ -217,7 +209,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
@action
remove = async (entry: ImportMetadataEntry) => {
- let metadata = await DocListCastAsync(this.props.Document.data);
+ const metadata = await DocListCastAsync(this.props.Document.data);
if (metadata) {
let index = this.entries.indexOf(entry);
if (index !== -1) {
@@ -231,18 +223,18 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
}
render() {
- let dimensions = 50;
- let entries = DocListCast(this.props.Document.data);
- let isEditing = this.editingMetadata;
- let completed = this.completed;
- let quota = this.quota;
- let uploading = this.uploading;
- let showRemoveLabel = this.removeHover;
- let persistent = this.persistent;
+ const dimensions = 50;
+ const entries = DocListCast(this.props.Document.data);
+ const isEditing = this.editingMetadata;
+ const completed = this.completed;
+ const quota = this.quota;
+ const uploading = this.uploading;
+ const showRemoveLabel = this.removeHover;
+ const persistent = this.persistent;
let percent = `${completed / quota * 100}`;
percent = percent.split(".")[0];
percent = percent.startsWith("100") ? "99" : percent;
- let marginOffset = (percent.length === 1 ? 5 : 0) - 1.6;
+ const marginOffset = (percent.length === 1 ? 5 : 0) - 1.6;
const message = <span className={"phase"}>{this.phase}</span>;
const centerPiece = this.phase.includes("Google Photos") ?
<img src={"/assets/google_photos.png"} style={{
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index c9abf38fa..6a9486f83 100644
--- a/src/client/util/Import & Export/ImageUtils.ts
+++ b/src/client/util/Import & Export/ImageUtils.ts
@@ -1,9 +1,8 @@
-import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
+import { Doc } from "../../../new_fields/Doc";
import { ImageField } from "../../../new_fields/URLField";
import { Cast, StrCast } from "../../../new_fields/Types";
-import { RouteStore } from "../../../server/RouteStore";
import { Docs } from "../../documents/Documents";
-import { Identified } from "../../Network";
+import { Networking } from "../../Network";
import { Id } from "../../../new_fields/FieldSymbols";
import { Utils } from "../../../Utils";
@@ -15,7 +14,7 @@ export namespace ImageUtils {
return false;
}
const source = field.url.href;
- const response = await Identified.PostToServer(RouteStore.inspectImage, { source });
+ const response = await Networking.PostToServer("/inspectImage", { source });
const { error, data } = response.exifData;
document.exif = error || Docs.Get.DocumentHierarchyFromJson(data);
return data !== undefined;
@@ -23,7 +22,7 @@ export namespace ImageUtils {
export const ExportHierarchyToFileSystem = async (collection: Doc): Promise<void> => {
const a = document.createElement("a");
- a.href = Utils.prepend(`${RouteStore.imageHierarchyExport}/${collection[Id]}`);
+ a.href = Utils.prepend(`/imageHierarchyExport/${collection[Id]}`);
a.download = `Dash Export [${StrCast(collection.title)}].zip`;
a.click();
};
diff --git a/src/client/util/Import & Export/ImportMetadataEntry.tsx b/src/client/util/Import & Export/ImportMetadataEntry.tsx
index f5198c39b..8e1c50bea 100644
--- a/src/client/util/Import & Export/ImportMetadataEntry.tsx
+++ b/src/client/util/Import & Export/ImportMetadataEntry.tsx
@@ -1,11 +1,11 @@
import React = require("react");
import { observer } from "mobx-react";
import { EditableView } from "../../views/EditableView";
-import { observable, action, computed } from "mobx";
+import { 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 { Doc } from "../../../new_fields/Doc";
import { StrCast, BoolCast } from "../../../new_fields/Types";
interface KeyValueProps {
@@ -85,7 +85,7 @@ export default class ImportMetadataEntry extends React.Component<KeyValueProps>
}
render() {
- let keyValueStyle: React.CSSProperties = {
+ const keyValueStyle: React.CSSProperties = {
paddingLeft: 10,
width: "50%",
opacity: this.valid ? 1 : 0.5,
diff --git a/src/client/util/InteractionUtils.ts b/src/client/util/InteractionUtils.ts
new file mode 100644
index 000000000..2e4e8c7ca
--- /dev/null
+++ b/src/client/util/InteractionUtils.ts
@@ -0,0 +1,163 @@
+export namespace InteractionUtils {
+ export const MOUSETYPE = "mouse";
+ export const TOUCHTYPE = "touch";
+ export const PENTYPE = "pen";
+ export const ERASERTYPE = "eraser";
+
+ const POINTER_PEN_BUTTON = -1;
+ const REACT_POINTER_PEN_BUTTON = 0;
+ const ERASER_BUTTON = 5;
+
+ export function GetMyTargetTouches(e: TouchEvent | React.TouchEvent, prevPoints: Map<number, React.Touch>): React.Touch[] {
+ const myTouches = new Array<React.Touch>();
+ for (let i = 0; i < e.targetTouches.length; i++) {
+ const pt = e.targetTouches.item(i);
+ if (pt && prevPoints.has(pt.identifier)) {
+ myTouches.push(pt);
+ }
+ }
+ return myTouches;
+ }
+
+ export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean {
+ switch (type) {
+ // pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2
+ case PENTYPE:
+ return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? POINTER_PEN_BUTTON : REACT_POINTER_PEN_BUTTON);
+ case ERASERTYPE:
+ return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON);
+ default:
+ return e.pointerType === type;
+ }
+ }
+
+ export function TwoPointEuclidist(pt1: React.Touch, pt2: React.Touch): number {
+ return Math.sqrt(Math.pow(pt1.clientX - pt2.clientX, 2) + Math.pow(pt1.clientY - pt2.clientY, 2));
+ }
+
+ /**
+ * Returns the centroid of an n-arbitrary long list of points (takes the average the x and y components of each point)
+ * @param pts - n-arbitrary long list of points
+ */
+ export function CenterPoint(pts: React.Touch[]): { X: number, Y: number } {
+ const centerX = pts.map(pt => pt.clientX).reduce((a, b) => a + b, 0) / pts.length;
+ const centerY = pts.map(pt => pt.clientY).reduce((a, b) => a + b, 0) / pts.length;
+ return { X: centerX, Y: centerY };
+ }
+
+ /**
+ * Returns -1 if pinching out, 0 if not pinching, and 1 if pinching in
+ * @param pt1 - new point that corresponds to oldPoint1
+ * @param pt2 - new point that corresponds to oldPoint2
+ * @param oldPoint1 - previous point 1
+ * @param oldPoint2 - previous point 2
+ */
+ export function Pinching(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number {
+ const threshold = 4;
+ const oldDist = TwoPointEuclidist(oldPoint1, oldPoint2);
+ const newDist = TwoPointEuclidist(pt1, pt2);
+
+ /** if they have the same sign, then we are either pinching in or out.
+ * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch)
+ * so that it can still pan without freaking out
+ */
+ if (Math.sign(oldDist) === Math.sign(newDist) && Math.abs(oldDist - newDist) > threshold) {
+ return Math.sign(oldDist - newDist);
+ }
+ return 0;
+ }
+
+ /**
+ * Returns -1 if pinning and pinching out, 0 if not pinning, and 1 if pinching in
+ * @param pt1 - new point that corresponds to oldPoint1
+ * @param pt2 - new point that corresponds to oldPoint2
+ * @param oldPoint1 - previous point 1
+ * @param oldPoint2 - previous point 2
+ */
+ export function Pinning(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number {
+ const threshold = 4;
+
+ const pt1Dist = TwoPointEuclidist(oldPoint1, pt1);
+ const pt2Dist = TwoPointEuclidist(oldPoint2, pt2);
+
+ const pinching = Pinching(pt1, pt2, oldPoint1, oldPoint2);
+
+ if (pinching !== 0) {
+ if ((pt1Dist < threshold && pt2Dist > threshold) || (pt1Dist > threshold && pt2Dist < threshold)) {
+ return pinching;
+ }
+ }
+ return 0;
+ }
+
+ export function IsDragging(oldTouches: Map<number, React.Touch>, newTouches: React.Touch[], leniency: number): boolean {
+ for (const touch of newTouches) {
+ if (touch) {
+ const oldTouch = oldTouches.get(touch.identifier);
+ if (oldTouch) {
+ if (TwoPointEuclidist(touch, oldTouch) >= leniency) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ // These might not be very useful anymore, but I'll leave them here for now -syip2
+ {
+
+
+ /**
+ * Returns the type of Touch Interaction from a list of points.
+ * Also returns any data that is associated with a Touch Interaction
+ * @param pts - List of points
+ */
+ // export function InterpretPointers(pts: React.Touch[]): { type: Opt<TouchInteraction>, data?: any } {
+ // const leniency = 200;
+ // switch (pts.length) {
+ // case 1:
+ // return { type: OneFinger };
+ // case 2:
+ // return { type: TwoSeperateFingers };
+ // case 3:
+ // let pt1 = pts[0];
+ // let pt2 = pts[1];
+ // let pt3 = pts[2];
+ // if (pt1 && pt2 && pt3) {
+ // let dist12 = TwoPointEuclidist(pt1, pt2);
+ // let dist23 = TwoPointEuclidist(pt2, pt3);
+ // let dist13 = TwoPointEuclidist(pt1, pt3);
+ // console.log(`distances: ${dist12}, ${dist23}, ${dist13}`);
+ // let dist12close = dist12 < leniency;
+ // let dist23close = dist23 < leniency;
+ // let dist13close = dist13 < leniency;
+ // let xor2313 = dist23close ? !dist13close : dist13close;
+ // let xor = dist12close ? !xor2313 : xor2313;
+ // // three input xor because javascript doesn't have logical xor's
+ // if (xor) {
+ // let points: number[] = [];
+ // let min = Math.min(dist12, dist23, dist13);
+ // switch (min) {
+ // case dist12:
+ // points = [0, 1, 2];
+ // break;
+ // case dist23:
+ // points = [1, 2, 0];
+ // break;
+ // case dist13:
+ // points = [0, 2, 1];
+ // break;
+ // }
+ // return { type: TwoToOneFingers, data: points };
+ // }
+ // else {
+ // return { type: ThreeSeperateFingers, data: null };
+ // }
+ // }
+ // default:
+ // return { type: undefined };
+ // }
+ // }
+ }
+} \ No newline at end of file
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index ee2f2dadc..5f3667acc 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -34,22 +34,20 @@ export class LinkManager {
// 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[] {
- let ldoc = LinkManager.Instance.LinkManagerDoc;
+ const ldoc = LinkManager.Instance.LinkManagerDoc;
if (ldoc) {
- let docs = DocListCast(ldoc.allLinks);
+ const docs = DocListCast(ldoc.allLinks);
return docs;
}
return [];
}
public addLink(linkDoc: Doc): boolean {
- let linkList = LinkManager.Instance.getAllLinks();
+ const linkList = LinkManager.Instance.getAllLinks();
linkList.push(linkDoc);
if (LinkManager.Instance.LinkManagerDoc) {
LinkManager.Instance.LinkManagerDoc.allLinks = new List<Doc>(linkList);
@@ -59,8 +57,8 @@ export class LinkManager {
}
public deleteLink(linkDoc: Doc): boolean {
- let linkList = LinkManager.Instance.getAllLinks();
- let index = LinkManager.Instance.getAllLinks().indexOf(linkDoc);
+ const linkList = LinkManager.Instance.getAllLinks();
+ const index = LinkManager.Instance.getAllLinks().indexOf(linkDoc);
if (index > -1) {
linkList.splice(index, 1);
if (LinkManager.Instance.LinkManagerDoc) {
@@ -72,24 +70,24 @@ export class LinkManager {
}
// 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));
+ public getAllRelatedLinks(anchor: Doc): Doc[] {
+ const related = LinkManager.Instance.getAllLinks().filter(link => {
+ const protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, null));
+ const protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, null));
return protomatch1 || protomatch2 || Doc.AreProtosEqual(link, anchor);
});
return related;
}
public deleteAllLinksOnAnchor(anchor: Doc) {
- let related = LinkManager.Instance.getAllRelatedLinks(anchor);
+ const 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();
+ const groupTypes = LinkManager.Instance.getAllGroupTypes();
groupTypes.push(groupType);
LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>(groupTypes);
return true;
@@ -101,8 +99,8 @@ export class LinkManager {
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());
+ const groupTypes = LinkManager.Instance.getAllGroupTypes();
+ const 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;
@@ -148,8 +146,8 @@ export class LinkManager {
}
public addGroupToAnchor(linkDoc: Doc, anchor: Doc, groupDoc: Doc, replace: boolean = false) {
- let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
- let index = groups.findIndex(gDoc => {
+ const groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
+ const index = groups.findIndex(gDoc => {
return StrCast(groupDoc.type).toUpperCase() === StrCast(gDoc.type).toUpperCase();
});
if (index > -1 && replace) {
@@ -163,32 +161,32 @@ export class LinkManager {
// 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());
+ const groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
+ const 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>>();
+ const related = this.getAllRelatedLinks(anchor);
+ const anchorGroups = new Map<string, Array<Doc>>();
related.forEach(link => {
- let groups = LinkManager.Instance.getAnchorGroups(link, anchor);
+ const groups = LinkManager.Instance.getAnchorGroups(link, anchor);
if (groups.length > 0) {
groups.forEach(groupDoc => {
- let groupType = StrCast(groupDoc.type);
+ const groupType = StrCast(groupDoc.type);
if (groupType === "") {
- let group = anchorGroups.get("*");
+ const group = anchorGroups.get("*");
anchorGroups.set("*", group ? [...group, link] : [link]);
} else {
- let group = anchorGroups.get(groupType);
+ const 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("*");
+ const group = anchorGroups.get("*");
anchorGroups.set("*", group ? [...group, link] : [link]);
}
@@ -214,11 +212,11 @@ export class LinkManager {
// 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();
+ const md: Doc[] = [];
+ const 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));
+ const anchor1Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor1, Doc, null));
+ const 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); } });
});
@@ -227,8 +225,8 @@ export class LinkManager {
// 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 => {
+ const allLinks = LinkManager.Instance.getAllLinks();
+ const 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));
});
@@ -239,14 +237,12 @@ export class LinkManager {
//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 {
- let a1 = Cast(linkDoc.anchor1, Doc, null);
- let a2 = Cast(linkDoc.anchor2, Doc, null);
+ const a1 = Cast(linkDoc.anchor1, Doc, null);
+ const a2 = Cast(linkDoc.anchor2, Doc, null);
if (Doc.AreProtosEqual(anchor, a1)) return a2;
if (Doc.AreProtosEqual(anchor, a2)) return a1;
if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc;
}
}
-Scripting.addGlobal(function links(doc: any) {
- return new List(LinkManager.Instance.getAllRelatedLinks(doc));
-});
+Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); }); \ No newline at end of file
diff --git a/src/client/util/ParagraphNodeSpec.ts b/src/client/util/ParagraphNodeSpec.ts
index 3a993e1ff..0a3b68217 100644
--- a/src/client/util/ParagraphNodeSpec.ts
+++ b/src/client/util/ParagraphNodeSpec.ts
@@ -34,6 +34,7 @@ const ParagraphNodeSpec: NodeSpec = {
color: { default: null },
id: { default: null },
indent: { default: null },
+ inset: { default: null },
lineSpacing: { default: null },
// TODO: Add UI to let user edit / clear padding.
paddingBottom: { default: null },
@@ -76,6 +77,7 @@ function toDOM(node: Node): DOMOutputSpec {
const {
align,
indent,
+ inset,
lineSpacing,
paddingTop,
paddingBottom,
@@ -105,6 +107,14 @@ function toDOM(node: Node): DOMOutputSpec {
style += `padding-bottom: ${paddingBottom};`;
}
+ if (indent) {
+ style += `text-indent: ${indent}; padding-left: ${indent < 0 ? -indent : undefined};`;
+ }
+
+ if (inset) {
+ style += `margin-left: ${inset}; margin-right: ${inset};`;
+ }
+
style && (attrs.style = style);
if (indent) {
diff --git a/src/client/util/ProseMirrorEditorView.tsx b/src/client/util/ProseMirrorEditorView.tsx
new file mode 100644
index 000000000..b42adfbb4
--- /dev/null
+++ b/src/client/util/ProseMirrorEditorView.tsx
@@ -0,0 +1,74 @@
+import React from "react";
+import { EditorView } from "prosemirror-view";
+import { EditorState } from "prosemirror-state";
+
+export interface ProseMirrorEditorViewProps {
+ /* EditorState instance to use. */
+ editorState: EditorState;
+ /* Called when EditorView produces new EditorState. */
+ onEditorState: (editorState: EditorState) => any;
+}
+
+/**
+ * This wraps ProseMirror's EditorView into React component.
+ * This code was found on https://discuss.prosemirror.net/t/using-with-react/904
+ */
+export class ProseMirrorEditorView extends React.Component<ProseMirrorEditorViewProps> {
+
+ private _editorView?: EditorView;
+
+ _createEditorView = (element: HTMLDivElement | null) => {
+ if (element !== null) {
+ this._editorView = new EditorView(element, {
+ state: this.props.editorState,
+ dispatchTransaction: this.dispatchTransaction,
+ });
+ }
+ }
+
+ dispatchTransaction = (tx: any) => {
+ // In case EditorView makes any modification to a state we funnel those
+ // modifications up to the parent and apply to the EditorView itself.
+ const editorState = this.props.editorState.apply(tx);
+ if (this._editorView) {
+ this._editorView.updateState(editorState);
+ }
+ this.props.onEditorState(editorState);
+ }
+
+ focus() {
+ if (this._editorView) {
+ this._editorView.focus();
+ }
+ }
+
+ componentWillReceiveProps(nextProps: { editorState: EditorState<any>; }) {
+ // In case we receive new EditorState through props — we apply it to the
+ // EditorView instance.
+ if (this._editorView) {
+ if (nextProps.editorState !== this.props.editorState) {
+ this._editorView.updateState(nextProps.editorState);
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ if (this._editorView) {
+ this._editorView.destroy();
+ }
+ }
+
+ shouldComponentUpdate() {
+ // Note that EditorView manages its DOM itself so we'd ratrher don't mess
+ // with it.
+ return false;
+ }
+
+ render() {
+ // Render just an empty div which is then used as a container for an
+ // EditorView instance.
+ return (
+ <div ref={this._createEditorView} />
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index 003ff6272..c028dbf8b 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -4,8 +4,10 @@ import { undoInputRule } from "prosemirror-inputrules";
import { Schema } from "prosemirror-model";
import { liftListItem, sinkListItem } from "./prosemirrorPatches.js";
import { splitListItem, wrapInList, } from "prosemirror-schema-list";
-import { EditorState, Transaction, TextSelection, NodeSelection } from "prosemirror-state";
+import { EditorState, Transaction, TextSelection } from "prosemirror-state";
import { TooltipTextMenu } from "./TooltipTextMenu";
+import { SelectionManager } from "./SelectionManager";
+import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
@@ -15,22 +17,22 @@ export let updateBullets = (tx2: Transaction, schema: Schema, mapStyle?: string)
let fontSize: number | undefined = undefined;
tx2.doc.descendants((node: any, offset: any, index: any) => {
if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
- let path = (tx2.doc.resolve(offset) as any).path;
+ const path = (tx2.doc.resolve(offset) as any).path;
let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
if (node.type === schema.nodes.ordered_list) depth++;
fontSize = depth === 1 && node.attrs.setFontSize ? Number(node.attrs.setFontSize) : fontSize;
- let fsize = fontSize && node.type === schema.nodes.ordered_list ? Math.max(6, fontSize - (depth - 1) * 4) : undefined;
+ const fsize = fontSize && node.type === schema.nodes.ordered_list ? Math.max(6, fontSize - (depth - 1) * 4) : undefined;
tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: mapStyle ? mapStyle : node.attrs.mapStyle, bulletStyle: depth, inheritedFontSize: fsize }, node.marks);
}
});
return tx2;
};
export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: KeyMap): KeyMap {
- let keys: { [key: string]: any } = {}, type;
+ const keys: { [key: string]: any } = {};
function bind(key: string, cmd: any) {
if (mapKeys) {
- let mapped = mapKeys[key];
+ const mapped = mapKeys[key];
if (mapped === false) return;
if (mapped) key = mapped;
}
@@ -46,7 +48,11 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
bind("Alt-ArrowUp", joinUp);
bind("Alt-ArrowDown", joinDown);
bind("Mod-BracketLeft", lift);
- bind("Escape", selectParentNode);
+ bind("Escape", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
+ (document.activeElement as any).blur?.();
+ SelectionManager.DeselectAll();
+ });
bind("Mod-b", toggleMark(schema.marks.strong));
bind("Mod-B", toggleMark(schema.marks.strong));
@@ -79,7 +85,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
// });
- let cmd = chainCommands(exitCode, (state, dispatch) => {
+ const cmd = chainCommands(exitCode, (state, dispatch) => {
if (dispatch) {
dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView());
return true;
@@ -99,27 +105,25 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
bind("Shift-Ctrl-" + i, setBlockType(schema.nodes.heading, { level: i }));
}
- let hr = schema.nodes.horizontal_rule;
+ const hr = schema.nodes.horizontal_rule;
bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView());
return true;
});
- bind("Mod-s", TooltipTextMenu.insertStar);
-
bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
- var ref = state.selection;
- var range = ref.$from.blockRange(ref.$to);
- var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ const ref = state.selection;
+ const range = ref.$from.blockRange(ref.$to);
+ const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
if (!sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
- let tx3 = updateBullets(tx2, schema);
+ const tx3 = updateBullets(tx2, schema);
marks && tx3.ensureMarks([...marks]);
marks && tx3.setStoredMarks([...marks]);
dispatch(tx3);
})) { // couldn't sink into an existing list, so wrap in a new one
- let newstate = state.applyTransaction(state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end)));
+ const newstate = state.applyTransaction(state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end)));
if (!wrapInList(schema.nodes.ordered_list)(newstate.state, (tx2: Transaction) => {
- let tx3 = updateBullets(tx2, schema);
+ const tx3 = updateBullets(tx2, schema);
// when promoting to a list, assume list will format things so don't copy the stored marks.
marks && tx3.ensureMarks([...marks]);
marks && tx3.setStoredMarks([...marks]);
@@ -131,10 +135,10 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
});
bind("Shift-Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
- var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
if (!liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => {
- let tx3 = updateBullets(tx2, schema);
+ const tx3 = updateBullets(tx2, schema);
marks && tx3.ensureMarks([...marks]);
marks && tx3.setStoredMarks([...marks]);
dispatch(tx3);
@@ -143,14 +147,14 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
}
});
- let splitMetadata = (marks: any, tx: Transaction) => {
+ const splitMetadata = (marks: any, tx: Transaction) => {
marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
return tx;
};
- bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
- var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
- if (!splitListItem(schema.nodes.list_item)(state, (tx3: Transaction) => dispatch(tx3))) {
+ bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => {
+ const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ if (!splitListItem(schema.nodes.list_item)(state, dispatch)) {
if (!splitBlockKeepMarks(state, (tx3: Transaction) => {
splitMetadata(marks, tx3);
if (!liftListItem(schema.nodes.list_item)(tx3, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) {
@@ -163,18 +167,18 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
return true;
});
bind("Space", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
- var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
dispatch(splitMetadata(marks, state.tr));
return false;
});
bind(":", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
- let range = state.selection.$from.blockRange(state.selection.$to, (node: any) => {
+ const range = state.selection.$from.blockRange(state.selection.$to, (node: any) => {
return !node.marks || !node.marks.find((m: any) => m.type === schema.marks.metadata);
});
- let path = (state.doc.resolve(state.selection.from - 1) as any).path;
- let spaceSeparator = path[path.length - 3].childCount > 1 ? 0 : -1;
- let textsel = TextSelection.create(state.doc, range!.end - path[path.length - 3].lastChild.nodeSize + spaceSeparator, range!.end);
- let text = range ? state.doc.textBetween(textsel.from, textsel.to) : "";
+ const path = (state.doc.resolve(state.selection.from - 1) as any).path;
+ const spaceSeparator = path[path.length - 3].childCount > 1 ? 0 : -1;
+ const textsel = TextSelection.create(state.doc, range!.end - path[path.length - 3].lastChild.nodeSize + spaceSeparator, range!.end);
+ const text = range ? state.doc.textBetween(textsel.from, textsel.to) : "";
let whitespace = text.length - 1;
for (; whitespace >= 0 && text[whitespace] !== " "; whitespace--) { }
if (text.endsWith(":")) {
diff --git a/src/client/util/RichTextMenu.scss b/src/client/util/RichTextMenu.scss
new file mode 100644
index 000000000..43cc23ecd
--- /dev/null
+++ b/src/client/util/RichTextMenu.scss
@@ -0,0 +1,121 @@
+@import "../views/globalCssVariables";
+
+.button-dropdown-wrapper {
+ position: relative;
+
+ .dropdown-button {
+ width: 15px;
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+
+ .dropdown-button-combined {
+ width: 50px;
+ display: flex;
+ justify-content: space-between;
+
+ svg:nth-child(2) {
+ margin-top: 2px;
+ }
+ }
+
+ .dropdown {
+ position: absolute;
+ top: 35px;
+ left: 0;
+ background-color: #323232;
+ color: $light-color-secondary;
+ border: 1px solid #4d4d4d;
+ border-radius: 0 6px 6px 6px;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+ min-width: 150px;
+ padding: 5px;
+ font-size: 12px;
+ z-index: 10001;
+
+ button {
+ background-color: #323232;
+ border: 1px solid black;
+ border-radius: 1px;
+ padding: 6px;
+ margin: 5px 0;
+ font-size: 10px;
+
+ &:hover {
+ background-color: black;
+ }
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ input {
+ color: black;
+ }
+}
+
+.link-menu {
+ .divider {
+ background-color: white;
+ height: 1px;
+ width: 100%;
+ }
+}
+
+.color-preview-button {
+ .color-preview {
+ width: 100%;
+ height: 3px;
+ margin-top: 3px;
+ }
+}
+
+.color-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+
+ button.color-button {
+ width: 20px;
+ height: 20px;
+ border-radius: 15px !important;
+ margin: 3px;
+ border: 2px solid transparent !important;
+ padding: 3px;
+
+ &.active {
+ border: 2px solid white !important;
+ }
+ }
+}
+
+select {
+ background-color: #323232;
+ color: white;
+ border: 1px solid black;
+ // border-top: none;
+ // border-bottom: none;
+ font-size: 12px;
+ height: 100%;
+ margin-right: 3px;
+
+ &:focus,
+ &:hover {
+ background-color: black;
+ }
+
+ &::-ms-expand {
+ color: white;
+ }
+}
+
+.row-2 {
+ display: flex;
+ justify-content: space-between;
+
+ >div {
+ display: flex;
+ }
+} \ No newline at end of file
diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
new file mode 100644
index 000000000..419d7caf9
--- /dev/null
+++ b/src/client/util/RichTextMenu.tsx
@@ -0,0 +1,855 @@
+import React = require("react");
+import AntimodeMenu from "../views/AntimodeMenu";
+import { observable, action, } from "mobx";
+import { observer } from "mobx-react";
+import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model";
+import { schema } from "./RichTextSchema";
+import { EditorView } from "prosemirror-view";
+import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
+import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller, faSleigh } from "@fortawesome/free-solid-svg-icons";
+import { MenuItem, Dropdown } from "prosemirror-menu";
+import { updateBullets } from "./ProsemirrorExampleTransfer";
+import { FieldViewProps } from "../views/nodes/FieldView";
+import { NumCast, Cast, StrCast } from "../../new_fields/Types";
+import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
+import { unimplementedFunction, Utils } from "../../Utils";
+import { wrapInList } from "prosemirror-schema-list";
+import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../new_fields/SchemaHeaderField';
+import "./RichTextMenu.scss";
+import { DocServer } from "../DocServer";
+import { Doc } from "../../new_fields/Doc";
+import { SelectionManager } from "./SelectionManager";
+import { LinkManager } from "./LinkManager";
+const { toggleMark, setBlockType } = require("prosemirror-commands");
+
+library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller);
+
+@observer
+export default class RichTextMenu extends AntimodeMenu {
+ static Instance: RichTextMenu;
+ public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable
+
+ private view?: EditorView;
+ private editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
+
+ private fontSizeOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean, style?: {} }[];
+ private fontFamilyOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean, style?: {} }[];
+ private listTypeOptions: { node: NodeType | any | null, title: string, label: string, command: any, style?: {} }[];
+ private fontColors: (string | undefined)[];
+ private highlightColors: (string | undefined)[];
+
+ @observable private boldActive: boolean = false;
+ @observable private italicsActive: boolean = false;
+ @observable private underlineActive: boolean = false;
+ @observable private strikethroughActive: boolean = false;
+ @observable private subscriptActive: boolean = false;
+ @observable private superscriptActive: boolean = false;
+
+ @observable private activeFontSize: string = "";
+ @observable private activeFontFamily: string = "";
+ @observable private activeListType: string = "";
+
+ @observable private brushIsEmpty: boolean = true;
+ @observable private brushMarks: Set<Mark> = new Set();
+ @observable private showBrushDropdown: boolean = false;
+
+ @observable private activeFontColor: string = "black";
+ @observable private showColorDropdown: boolean = false;
+
+ @observable private activeHighlightColor: string = "transparent";
+ @observable private showHighlightDropdown: boolean = false;
+
+ @observable private currentLink: string | undefined = "";
+ @observable private showLinkDropdown: boolean = false;
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+ RichTextMenu.Instance = this;
+ this._canFade = false;
+
+ this.fontSizeOptions = [
+ { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 8 }), title: "Set font size", label: "8pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 9 }), title: "Set font size", label: "8pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 10 }), title: "Set font size", label: "10pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 12 }), title: "Set font size", label: "12pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 14 }), title: "Set font size", label: "14pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 16 }), title: "Set font size", label: "16pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 18 }), title: "Set font size", label: "18pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 20 }), title: "Set font size", label: "20pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 24 }), title: "Set font size", label: "24pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 32 }), title: "Set font size", label: "32pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 48 }), title: "Set font size", label: "48pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 72 }), title: "Set font size", label: "72pt", command: this.changeFontSize },
+ { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
+ { mark: null, title: "", label: "13pt", command: unimplementedFunction, hidden: true }, // this is here because the default size is 13, but there is no actual 13pt option
+ ];
+
+ this.fontFamilyOptions = [
+ { mark: schema.marks.pFontFamily.create({ family: "Times New Roman" }), title: "Set font family", label: "Times New Roman", command: this.changeFontFamily, style: { fontFamily: "Times New Roman" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Arial" }), title: "Set font family", label: "Arial", command: this.changeFontFamily, style: { fontFamily: "Arial" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Georgia" }), title: "Set font family", label: "Georgia", command: this.changeFontFamily, style: { fontFamily: "Georgia" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Comic Sans MS" }), title: "Set font family", label: "Comic Sans MS", command: this.changeFontFamily, style: { fontFamily: "Comic Sans MS" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Tahoma" }), title: "Set font family", label: "Tahoma", command: this.changeFontFamily, style: { fontFamily: "Tahoma" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Impact" }), title: "Set font family", label: "Impact", command: this.changeFontFamily, style: { fontFamily: "Impact" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Crimson Text" }), title: "Set font family", label: "Crimson Text", command: this.changeFontFamily, style: { fontFamily: "Crimson Text" } },
+ { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
+ // { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true },
+ ];
+
+ this.listTypeOptions = [
+ { node: schema.nodes.ordered_list.create({ mapStyle: "bullet" }), title: "Set list type", label: ":", command: this.changeListType },
+ { node: schema.nodes.ordered_list.create({ mapStyle: "decimal" }), title: "Set list type", label: "1.1", command: this.changeListType },
+ { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "1.A", command: this.changeListType },
+ { node: undefined, title: "Set list type", label: "Remove", command: this.changeListType },
+ ];
+
+ this.fontColors = [
+ DarkPastelSchemaPalette.get("pink2"),
+ DarkPastelSchemaPalette.get("purple4"),
+ DarkPastelSchemaPalette.get("bluegreen1"),
+ DarkPastelSchemaPalette.get("yellow4"),
+ DarkPastelSchemaPalette.get("red2"),
+ DarkPastelSchemaPalette.get("bluegreen7"),
+ DarkPastelSchemaPalette.get("bluegreen5"),
+ DarkPastelSchemaPalette.get("orange1"),
+ "#757472",
+ "#000"
+ ];
+
+ this.highlightColors = [
+ PastelSchemaPalette.get("pink2"),
+ PastelSchemaPalette.get("purple4"),
+ PastelSchemaPalette.get("bluegreen1"),
+ PastelSchemaPalette.get("yellow4"),
+ PastelSchemaPalette.get("red2"),
+ PastelSchemaPalette.get("bluegreen7"),
+ PastelSchemaPalette.get("bluegreen5"),
+ PastelSchemaPalette.get("orange1"),
+ "white",
+ "transparent"
+ ];
+ }
+
+ @action
+ changeView(view: EditorView) {
+ this.view = view;
+ }
+
+ update(view: EditorView, lastState: EditorState | undefined) {
+ this.updateFromDash(view, lastState, this.editorProps);
+ }
+
+ @action
+ public async updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) {
+ if (!view) {
+ console.log("no editor? why?");
+ return;
+ }
+ this.view = view;
+ const state = view.state;
+ props && (this.editorProps = props);
+
+ // Don't do anything if the document/selection didn't change
+ if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) return;
+
+ // update active marks
+ const activeMarks = this.getActiveMarksOnSelection();
+ this.setActiveMarkButtons(activeMarks);
+
+ // update active font family and size
+ const active = this.getActiveFontStylesOnSelection();
+ const activeFamilies = active && active.get("families");
+ const activeSizes = active && active.get("sizes");
+
+ this.activeFontFamily = !activeFamilies || activeFamilies.length === 0 ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
+ this.activeFontSize = !activeSizes || activeSizes.length === 0 ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) + "pt" : "various";
+
+ // update link in current selection
+ const targetTitle = await this.getTextLinkTargetTitle();
+ this.setCurrentLink(targetTitle);
+ }
+
+ setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => {
+ if (mark) {
+ const node = (state.selection as NodeSelection).node;
+ if (node?.type === schema.nodes.ordered_list) {
+ let attrs = node.attrs;
+ if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family };
+ if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize };
+ if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, setFontColor: mark.attrs.color };
+ const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema);
+ dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from))));
+ } else {
+ toggleMark(mark.type, mark.attrs)(state, (tx: any) => {
+ const { from, $from, to, empty } = tx.selection;
+ if (!tx.doc.rangeHasMark(from, to, mark.type)) {
+ toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch);
+ } else dispatch(tx);
+ });
+ }
+ }
+ }
+
+ // finds font sizes and families in selection
+ getActiveFontStylesOnSelection() {
+ if (!this.view) return;
+
+ const activeFamilies: string[] = [];
+ const activeSizes: string[] = [];
+ const state = this.view.state;
+ const pos = this.view.state.selection.$from;
+ const ref_node = this.reference_node(pos);
+ if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
+ ref_node.marks.forEach(m => {
+ m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family);
+ m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt");
+ });
+ }
+
+ const styles = new Map<String, String[]>();
+ styles.set("families", activeFamilies);
+ styles.set("sizes", activeSizes);
+ return styles;
+ }
+
+ getMarksInSelection(state: EditorState<any>) {
+ const found = new Set<Mark>();
+ const { from, to } = state.selection as TextSelection;
+ state.doc.nodesBetween(from, to, (node) => node.marks.forEach(m => found.add(m)));
+ return found;
+ }
+
+ //finds all active marks on selection in given group
+ getActiveMarksOnSelection() {
+ if (!this.view) return;
+
+ const markGroup = [schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript];
+ if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type);
+ //current selection
+ const { empty, ranges, $to } = this.view.state.selection as TextSelection;
+ const state = this.view.state;
+ let activeMarks: MarkType[] = [];
+ if (!empty) {
+ activeMarks = markGroup.filter(mark => {
+ const has = false;
+ for (let i = 0; !has && i < ranges.length; i++) {
+ return state.doc.rangeHasMark(ranges[i].$from.pos, ranges[i].$to.pos, mark);
+ }
+ return false;
+ });
+ }
+ else {
+ const pos = this.view.state.selection.$from;
+ const ref_node: ProsNode | null = this.reference_node(pos);
+ if (ref_node !== null && ref_node !== this.view.state.doc) {
+ if (ref_node.isText) {
+ }
+ else {
+ return [];
+ }
+ activeMarks = markGroup.filter(mark_type => {
+ if (mark_type === state.schema.marks.pFontSize) {
+ return ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name);
+ }
+ const mark = state.schema.mark(mark_type);
+ return ref_node.marks.includes(mark);
+ });
+ }
+ }
+ return activeMarks;
+ }
+
+ destroy() {
+ }
+
+ @action
+ setActiveMarkButtons(activeMarks: MarkType[] | undefined) {
+ if (!activeMarks) return;
+
+ this.boldActive = false;
+ this.italicsActive = false;
+ this.underlineActive = false;
+ this.strikethroughActive = false;
+ this.subscriptActive = false;
+ this.superscriptActive = false;
+
+ activeMarks.forEach(mark => {
+ switch (mark.name) {
+ case "strong": this.boldActive = true; break;
+ case "em": this.italicsActive = true; break;
+ case "underline": this.underlineActive = true; break;
+ case "strikethrough": this.strikethroughActive = true; break;
+ case "subscript": this.subscriptActive = true; break;
+ case "superscript": this.superscriptActive = true; break;
+ }
+ });
+ }
+
+ createButton(faIcon: string, title: string, isActive: boolean = false, command?: any, onclick?: any) {
+ const self = this;
+ function onClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && command && command(self.view.state, self.view.dispatch, self.view);
+ self.view && onclick && onclick(self.view.state, self.view.dispatch, self.view);
+ self.setActiveMarkButtons(self.getActiveMarksOnSelection());
+ }
+
+ return (
+ <button className={"antimodeMenu-button" + (isActive ? " active" : "")} title={title} onPointerDown={onClick}>
+ <FontAwesomeIcon icon={faIcon as IconProp} size="lg" />
+ </button>
+ );
+ }
+
+ createMarksDropdown(activeOption: string, options: { mark: Mark | null, title: string, label: string, command: (mark: Mark, view: EditorView) => void, hidden?: boolean, style?: {} }[]): JSX.Element {
+ const items = options.map(({ title, label, hidden, style }) => {
+ if (hidden) {
+ return label === activeOption ?
+ <option value={label} title={title} style={style ? style : {}} selected hidden>{label}</option> :
+ <option value={label} title={title} style={style ? style : {}} hidden>{label}</option>;
+ }
+ return label === activeOption ?
+ <option value={label} title={title} style={style ? style : {}} selected>{label}</option> :
+ <option value={label} title={title} style={style ? style : {}}>{label}</option>;
+ });
+
+ const self = this;
+ function onChange(e: React.ChangeEvent<HTMLSelectElement>) {
+ e.stopPropagation();
+ e.preventDefault();
+ options.forEach(({ label, mark, command }) => {
+ if (e.target.value === label) {
+ self.view && mark && command(mark, self.view);
+ }
+ });
+ }
+ return <select onChange={onChange}>{items}</select>;
+ }
+
+ createNodesDropdown(activeOption: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean, style?: {} }[]): JSX.Element {
+ const items = options.map(({ title, label, hidden, style }) => {
+ if (hidden) {
+ return label === activeOption ?
+ <option value={label} title={title} style={style ? style : {}} selected hidden>{label}</option> :
+ <option value={label} title={title} style={style ? style : {}} hidden>{label}</option>;
+ }
+ return label === activeOption ?
+ <option value={label} title={title} style={style ? style : {}} selected>{label}</option> :
+ <option value={label} title={title} style={style ? style : {}}>{label}</option>;
+ });
+
+ const self = this;
+ function onChange(val: string) {
+ options.forEach(({ label, node, command }) => {
+ if (val === label) {
+ self.view && node && command(node);
+ }
+ });
+ }
+ return <select onChange={e => onChange(e.target.value)}>{items}</select>;
+ }
+
+ changeFontSize = (mark: Mark, view: EditorView) => {
+ const size = mark.attrs.fontSize;
+ if (this.editorProps) {
+ const ruleProvider = this.editorProps.ruleProvider;
+ const heading = NumCast(this.editorProps.Document.heading);
+ if (ruleProvider && heading) {
+ ruleProvider["ruleSize_" + heading] = size;
+ }
+ }
+ this.setMark(view.state.schema.marks.pFontSize.create({ fontSize: size }), view.state, view.dispatch);
+ }
+
+ changeFontFamily = (mark: Mark, view: EditorView) => {
+ const fontName = mark.attrs.family;
+ if (this.editorProps) {
+ const ruleProvider = this.editorProps.ruleProvider;
+ const heading = NumCast(this.editorProps.Document.heading);
+ if (ruleProvider && heading) {
+ ruleProvider["ruleFont_" + heading] = fontName;
+ }
+ }
+ this.setMark(view.state.schema.marks.pFontFamily.create({ family: fontName }), view.state, view.dispatch);
+ }
+
+ // TODO: remove doesn't work
+ //remove all node type and apply the passed-in one to the selected text
+ changeListType = (nodeType: NodeType | undefined) => {
+ if (!this.view) return;
+
+ if (nodeType === schema.nodes.bullet_list) {
+ wrapInList(nodeType)(this.view.state, this.view.dispatch);
+ } else {
+ const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks());
+ if (!wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => {
+ const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+
+ this.view!.dispatch(tx2);
+ })) {
+ const tx2 = this.view.state.tr;
+ const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+
+ this.view.dispatch(tx3);
+ }
+ }
+ }
+
+ insertSummarizer(state: EditorState<any>, dispatch: any) {
+ if (state.selection.empty) return false;
+ const mark = state.schema.marks.summarize.create();
+ const tr = state.tr;
+ tr.addMark(state.selection.from, state.selection.to, mark);
+ const content = tr.selection.content();
+ const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() });
+ dispatch && dispatch(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
+ return true;
+ }
+
+ @action toggleBrushDropdown() { this.showBrushDropdown = !this.showBrushDropdown; }
+
+ createBrushButton() {
+ const self = this;
+ function onBrushClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && self.fillBrush(self.view.state, self.view.dispatch);
+ }
+
+ let label = "Stored marks: ";
+ if (this.brushMarks && this.brushMarks.size > 0) {
+ this.brushMarks.forEach((mark: Mark) => {
+ const markType = mark.type;
+ label += markType.name;
+ label += ", ";
+ });
+ label = label.substring(0, label.length - 2);
+ } else {
+ label = "No marks are currently stored";
+ }
+
+ const button =
+ <button className="antimodeMenu-button" title="" onPointerDown={onBrushClick} style={this.brushMarks && this.brushMarks.size > 0 ? { backgroundColor: "121212" } : {}}>
+ <FontAwesomeIcon icon="paint-roller" size="lg" style={{ transition: "transform 0.1s", transform: this.brushMarks && this.brushMarks.size > 0 ? "rotate(45deg)" : "" }} />
+ </button>;
+
+ const dropdownContent =
+ <div className="dropdown">
+ <p>{label}</p>
+ <button onPointerDown={this.clearBrush}>Clear brush</button>
+ {/* <input placeholder="Enter URL"></input> */}
+ </div>;
+
+ return (
+ <ButtonDropdown view={this.view} button={button} dropdownContent={dropdownContent} />
+ );
+ }
+
+ @action
+ clearBrush() {
+ RichTextMenu.Instance.brushIsEmpty = true;
+ RichTextMenu.Instance.brushMarks = new Set();
+ }
+
+ @action
+ fillBrush(state: EditorState<any>, dispatch: any) {
+ if (!this.view) return;
+
+ if (this.brushIsEmpty) {
+ const selected_marks = this.getMarksInSelection(this.view.state);
+ if (selected_marks.size >= 0) {
+ this.brushMarks = selected_marks;
+ this.brushIsEmpty = !this.brushIsEmpty;
+ }
+ }
+ else {
+ const { from, to, $from } = this.view.state.selection;
+ if (!this.view.state.selection.empty && $from && $from.nodeAfter) {
+ if (this.brushMarks && to - from > 0) {
+ this.view.dispatch(this.view.state.tr.removeMark(from, to));
+ Array.from(this.brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => {
+ this.setMark(mark, this.view!.state, this.view!.dispatch);
+ });
+ }
+ }
+ else {
+ this.brushIsEmpty = !this.brushIsEmpty;
+ }
+ }
+ }
+
+ @action toggleColorDropdown() { this.showColorDropdown = !this.showColorDropdown; }
+ @action setActiveColor(color: string) { this.activeFontColor = color; }
+
+ createColorButton() {
+ const self = this;
+ function onColorClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch);
+ }
+ function changeColor(e: React.PointerEvent, color: string) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.setActiveColor(color);
+ self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch);
+ }
+
+ const button =
+ <button className="antimodeMenu-button color-preview-button" title="" onPointerDown={onColorClick}>
+ <FontAwesomeIcon icon="palette" size="lg" />
+ <div className="color-preview" style={{ backgroundColor: this.activeFontColor }}></div>
+ </button>;
+
+ const dropdownContent =
+ <div className="dropdown">
+ <p>Change font color:</p>
+ <div className="color-wrapper">
+ {this.fontColors.map(color => {
+ if (color) {
+ return this.activeFontColor === color ?
+ <button className="color-button active" style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button> :
+ <button className="color-button" style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button>;
+ }
+ })}
+ </div>
+ </div>;
+
+ return (
+ <ButtonDropdown view={this.view} button={button} dropdownContent={dropdownContent} />
+ );
+ }
+
+ public insertColor(color: String, state: EditorState<any>, dispatch: any) {
+ const colorMark = state.schema.mark(state.schema.marks.pFontColor, { color: color });
+ if (state.selection.empty) {
+ dispatch(state.tr.addStoredMark(colorMark));
+ return false;
+ }
+ this.setMark(colorMark, state, dispatch);
+ }
+
+ @action toggleHighlightDropdown() { this.showHighlightDropdown = !this.showHighlightDropdown; }
+ @action setActiveHighlight(color: string) { this.activeHighlightColor = color; }
+
+ createHighlighterButton() {
+ const self = this;
+ function onHighlightClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch);
+ }
+ function changeHighlight(e: React.PointerEvent, color: string) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.setActiveHighlight(color);
+ self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch);
+ }
+
+ const button =
+ <button className="antimodeMenu-button color-preview-button" title="" onPointerDown={onHighlightClick}>
+ <FontAwesomeIcon icon="highlighter" size="lg" />
+ <div className="color-preview" style={{ backgroundColor: this.activeHighlightColor }}></div>
+ </button>;
+
+ const dropdownContent =
+ <div className="dropdown">
+ <p>Change highlight color:</p>
+ <div className="color-wrapper">
+ {this.highlightColors.map(color => {
+ if (color) {
+ return this.activeHighlightColor === color ?
+ <button className="color-button active" style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}>{color === "transparent" ? "X" : ""}</button> :
+ <button className="color-button" style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}>{color === "transparent" ? "X" : ""}</button>;
+ }
+ })}
+ </div>
+ </div>;
+
+ return (
+ <ButtonDropdown view={this.view} button={button} dropdownContent={dropdownContent} />
+ );
+ }
+
+ insertHighlight(color: String, state: EditorState<any>, dispatch: any) {
+ if (state.selection.empty) return false;
+ toggleMark(state.schema.marks.marker, { highlight: color })(state, dispatch);
+ }
+
+ @action toggleLinkDropdown() { this.showLinkDropdown = !this.showLinkDropdown; }
+ @action setCurrentLink(link: string) { this.currentLink = link; }
+
+ createLinkButton() {
+ const self = this;
+
+ function onLinkChange(e: React.ChangeEvent<HTMLInputElement>) {
+ self.setCurrentLink(e.target.value);
+ }
+
+ const link = this.currentLink ? this.currentLink : "";
+
+ const button = <FontAwesomeIcon icon="link" size="lg" />;
+
+ const dropdownContent =
+ <div className="dropdown link-menu">
+ <p>Linked to:</p>
+ <input value={link} placeholder="Enter URL" onChange={onLinkChange} />
+ <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, "onRight")}>Apply hyperlink</button>
+ <div className="divider"></div>
+ <button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button>
+ </div>;
+
+ return (
+ <ButtonDropdown view={this.view} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} />
+ );
+ }
+
+ async getTextLinkTargetTitle() {
+ if (!this.view) return;
+
+ const node = this.view.state.selection.$from.nodeAfter;
+ const link = node && node.marks.find(m => m.type.name === "link");
+ if (link) {
+ const href = link.attrs.href;
+ if (href) {
+ if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+ const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ if (linkclicked) {
+ const linkDoc = await DocServer.GetRefField(linkclicked);
+ if (linkDoc instanceof Doc) {
+ const anchor1 = await Cast(linkDoc.anchor1, Doc);
+ const anchor2 = await Cast(linkDoc.anchor2, Doc);
+ const currentDoc = SelectionManager.SelectedDocuments().length && SelectionManager.SelectedDocuments()[0].props.Document;
+ if (currentDoc && anchor1 && anchor2) {
+ if (Doc.AreProtosEqual(currentDoc, anchor1)) {
+ return StrCast(anchor2.title);
+ }
+ if (Doc.AreProtosEqual(currentDoc, anchor2)) {
+ return StrCast(anchor1.title);
+ }
+ }
+ }
+ }
+ } else {
+ return href;
+ }
+ } else {
+ return link.attrs.title;
+ }
+ }
+ }
+
+ // TODO: should check for valid URL
+ makeLinkToURL = (target: String, lcoation: string) => {
+ if (!this.view) return;
+
+ let node = this.view.state.selection.$from.nodeAfter;
+ 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");
+ }
+
+ deleteLink = () => {
+ if (!this.view) return;
+
+ const node = this.view.state.selection.$from.nodeAfter;
+ const link = node && node.marks.find(m => m.type === this.view!.state.schema.marks.link);
+ const href = link!.attrs.href;
+ if (href) {
+ if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+ const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ if (linkclicked) {
+ DocServer.GetRefField(linkclicked).then(async linkDoc => {
+ if (linkDoc instanceof Doc) {
+ LinkManager.Instance.deleteLink(linkDoc);
+ this.view!.dispatch(this.view!.state.tr.removeMark(this.view!.state.selection.from, this.view!.state.selection.to, this.view!.state.schema.marks.link));
+ }
+ });
+ }
+ } else {
+ if (node) {
+ const { tr, schema, selection } = this.view.state;
+ const extension = this.linkExtend(selection.$anchor, href);
+ this.view.dispatch(tr.removeMark(extension.from, extension.to, schema.marks.link));
+ }
+ }
+ }
+ }
+
+ linkExtend($start: ResolvedPos, href: string) {
+ const mark = this.view!.state.schema.marks.link;
+
+ let startIndex = $start.index();
+ let endIndex = $start.indexAfter();
+
+ while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.href === href).length) startIndex--;
+ while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.href === href).length) endIndex++;
+
+ let startPos = $start.start();
+ let endPos = startPos;
+ for (let i = 0; i < endIndex; i++) {
+ const size = $start.parent.child(i).nodeSize;
+ if (i < startIndex) startPos += size;
+ endPos += size;
+ }
+ return { from: startPos, to: endPos };
+ }
+
+ reference_node(pos: ResolvedPos<any>): ProsNode | null {
+ if (!this.view) return null;
+
+ let ref_node: ProsNode = this.view.state.doc;
+ if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) {
+ ref_node = pos.nodeBefore;
+ }
+ else if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) {
+ ref_node = pos.nodeAfter;
+ }
+ 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;
+ }
+
+ });
+ }
+ }
+ if (!ref_node.isLeaf && ref_node.childCount > 0) {
+ ref_node = ref_node.child(0);
+ }
+ return ref_node;
+ }
+
+ @action onPointerEnter(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = true; }
+ @action onPointerLeave(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = false; }
+
+ @action
+ toggleMenuPin = (e: React.MouseEvent) => {
+ this.Pinned = !this.Pinned;
+ if (!this.Pinned) {
+ this.fadeOut(true);
+ }
+ }
+
+ render() {
+
+ const row1 = <div className="antimodeMenu-row">{[
+ this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)),
+ this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)),
+ this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)),
+ this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)),
+ this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)),
+ this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)),
+ this.createColorButton(),
+ this.createHighlighterButton(),
+ this.createLinkButton(),
+ this.createBrushButton(),
+ this.createButton("indent", "Summarize", undefined, this.insertSummarizer),
+ ]}</div>;
+
+ const row2 = <div className="antimodeMenu-row row-2">
+ <div>
+ {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions),
+ this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions),
+ this.createNodesDropdown(this.activeListType, this.listTypeOptions)]}
+ </div>
+ <div>
+ <button className="antimodeMenu-button" title="Pin menu" onClick={this.toggleMenuPin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}>
+ <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} />
+ </button>
+ {this.getDragger()}
+ </div>
+ </div>;
+
+ return (
+ <div className="richTextMenu" onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+ {this.getElementWithRows([row1, row2], 2, false)}
+ </div>
+ );
+ }
+}
+
+interface ButtonDropdownProps {
+ view?: EditorView;
+ button: JSX.Element;
+ dropdownContent: JSX.Element;
+ openDropdownOnButton?: boolean;
+}
+
+@observer
+class ButtonDropdown extends React.Component<ButtonDropdownProps> {
+
+ @observable private showDropdown: boolean = false;
+ private ref: HTMLDivElement | null = null;
+
+ componentDidMount() {
+ document.addEventListener("pointerdown", this.onBlur);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("pointerdown", this.onBlur);
+ }
+
+ @action
+ setShowDropdown(show: boolean) {
+ this.showDropdown = show;
+ }
+ @action
+ toggleDropdown() {
+ this.showDropdown = !this.showDropdown;
+ }
+
+ onDropdownClick = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.props.view && this.props.view.focus();
+ this.toggleDropdown();
+ }
+
+ onBlur = (e: PointerEvent) => {
+ setTimeout(() => {
+ if (this.ref !== null && !this.ref.contains(e.target as Node)) {
+ this.setShowDropdown(false);
+ }
+ }, 0);
+ }
+
+ render() {
+ return (
+ <div className="button-dropdown-wrapper" ref={node => this.ref = node}>
+ {this.props.openDropdownOnButton ?
+ <button className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.onDropdownClick}>
+ {this.props.button}
+ <FontAwesomeIcon icon="caret-down" size="sm" />
+ </button> :
+ <>
+ {this.props.button}
+ <button className="dropdown-button antimodeMenu-button" onPointerDown={this.onDropdownClick}>
+ <FontAwesomeIcon icon="caret-down" size="sm" />
+ </button>
+ </>}
+
+ {this.showDropdown ? this.props.dropdownContent : <></>}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index ebb9bda8a..29b378299 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -5,8 +5,11 @@ import { NodeSelection, TextSelection } from "prosemirror-state";
import { NumCast, Cast } from "../../new_fields/Types";
import { Doc } from "../../new_fields/Doc";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
-import { Docs } from "../documents/Documents";
+import { TooltipTextMenuManager } from "../util/TooltipTextMenu";
+import { Docs, DocUtils } from "../documents/Documents";
import { Id } from "../../new_fields/FieldSymbols";
+import { DocServer } from "../DocServer";
+import { returnFalse, Utils } from "../../Utils";
export const inpRules = {
rules: [
@@ -59,137 +62,222 @@ export const inpRules = {
}
),
+ // set the font size using #<font-size>
new InputRule(
- new RegExp(/^#([0-9]+)\s$/),
+ new RegExp(/^%([0-9]+)\s$/),
(state, match, start, end) => {
- let size = Number(match[1]);
- let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
- let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
+ const size = Number(match[1]);
+ const ruleProvider = FormattedTextBox.FocusedBox!.props.ruleProvider;
+ const heading = NumCast(FormattedTextBox.FocusedBox!.props.Document.heading);
if (ruleProvider && heading) {
- (Cast(FormattedTextBox.InputBoxOverlay!.props.Document, Doc) as Doc).heading = Number(match[1]);
+ (Cast(FormattedTextBox.FocusedBox!.props.Document, Doc) as Doc).heading = size;
return state.tr.deleteRange(start, end);
}
- return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: Number(match[1]) }));
+ return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size }));
}),
+
+ // make current selection a hyperlink portal (assumes % was used to initiate an EnteringStyle mode)
+ new InputRule(
+ new RegExp(/@$/),
+ (state, match, start, end) => {
+ if (state.selection.to === state.selection.from || !(schema as any).EnteringStyle) return null;
+
+ const value = state.doc.textBetween(start, end);
+ if (value) {
+ DocServer.GetRefField(value).then(docx => {
+ const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500, }, value);
+ DocUtils.Publish(target, value, returnFalse, returnFalse);
+ DocUtils.MakeLink({ doc: (schema as any).Document }, { doc: target }, "portal link", "");
+ });
+ const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + value), location: "onRight", title: value, targetId: value });
+ return state.tr.addMark(start, end, link);
+ }
+ return state.tr;
+ }),
+ // stop using active style
new InputRule(
- new RegExp(/t/),
+ new RegExp(/%%$/),
(state, match, start, end) => {
- if (state.selection.to === state.selection.from) return null;
- let node = (state.doc.resolve(start) as any).nodeAfter;
- return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "todo", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ const tr = state.tr.deleteRange(start, end);
+ const marks = state.tr.selection.$anchor.nodeBefore?.marks;
+ return marks ? Array.from(marks).filter(m => m !== state.schema.marks.user_mark).reduce((tr, m) => tr.removeStoredMark(m), tr) : tr;
}),
+
+ // set the Todo user-tag on the current selection (assumes % was used to initiate an EnteringStyle mode)
+ new InputRule(
+ new RegExp(/[ti!x]$/),
+ (state, match, start, end) => {
+ if (state.selection.to === state.selection.from || !(schema as any).EnteringStyle) return null;
+ const tag = match[0] === "t" ? "todo" : match[0] === "i" ? "ignore" : match[0] === "x" ? "disagree" : match[0] === "!" ? "important" : "??";
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag);
+ return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ }),
+
+ // set the First-line indent node type for the selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
new InputRule(
- new RegExp(/i/),
+ new RegExp(/(%d|d)$/),
(state, match, start, end) => {
- if (state.selection.to === state.selection.from) return null;
- let node = (state.doc.resolve(start) as any).nodeAfter;
- return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "ignore", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ if (!match[0].startsWith("%") && !(schema as any).EnteringStyle) return null;
+ const pos = (state.doc.resolve(start) as any);
+ for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
+ const node = pos.node(depth);
+ if (node.type === schema.nodes.paragraph) {
+ const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === 25 ? undefined : 25 });
+ const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
+ return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
+ }
+ }
+ return null;
}),
+
+ // set the Hanging indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
new InputRule(
- new RegExp(/\!/),
+ new RegExp(/(%h|h)$/),
(state, match, start, end) => {
- if (state.selection.to === state.selection.from) return null;
- let node = (state.doc.resolve(start) as any).nodeAfter;
- return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "important", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ if (!match[0].startsWith("%") && !(schema as any).EnteringStyle) return null;
+ const pos = (state.doc.resolve(start) as any);
+ for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
+ const node = pos.node(depth);
+ if (node.type === schema.nodes.paragraph) {
+ const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === -25 ? undefined : -25 });
+ const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
+ return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
+ }
+ }
+ return null;
}),
+ // set the Quoted indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
new InputRule(
- new RegExp(/\x/),
+ new RegExp(/(%q|q)$/),
(state, match, start, end) => {
- if (state.selection.to === state.selection.from) return null;
- let node = (state.doc.resolve(start) as any).nodeAfter;
- return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "disagree", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ if (!match[0].startsWith("%") && !(schema as any).EnteringStyle) return null;
+ const pos = (state.doc.resolve(start) as any);
+ if (state.selection instanceof NodeSelection && state.selection.node.type === schema.nodes.ordered_list) {
+ const node = state.selection.node;
+ return state.tr.setNodeMarkup(pos.pos, node.type, { ...node.attrs, indent: node.attrs.indent === 30 ? undefined : 30 });
+ }
+ for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
+ const node = pos.node(depth);
+ if (node.type === schema.nodes.paragraph) {
+ const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, inset: node.attrs.inset === 30 ? undefined : 30 });
+ const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
+ return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
+ }
+ }
+ return null;
}),
+
+
+ // center justify text
new InputRule(
- new RegExp(/^\^\^\s$/),
+ new RegExp(/%\^$/),
(state, match, start, end) => {
- let node = (state.doc.resolve(start) as any).nodeAfter;
- let sm = state.storedMarks || undefined;
- let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
- let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || undefined;
+ const ruleProvider = FormattedTextBox.FocusedBox!.props.ruleProvider;
+ const heading = NumCast(FormattedTextBox.FocusedBox!.props.Document.heading);
if (ruleProvider && heading) {
ruleProvider["ruleAlign_" + heading] = "center";
return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
}
- let replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
state.tr;
return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
}),
+ // left justify text
new InputRule(
- new RegExp(/^\[\[\s$/),
+ new RegExp(/%\[$/),
(state, match, start, end) => {
- let node = (state.doc.resolve(start) as any).nodeAfter;
- let sm = state.storedMarks || undefined;
- let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
- let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || undefined;
+ const ruleProvider = FormattedTextBox.FocusedBox!.props.ruleProvider;
+ const heading = NumCast(FormattedTextBox.FocusedBox!.props.Document.heading);
if (ruleProvider && heading) {
ruleProvider["ruleAlign_" + heading] = "left";
return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
}
- let replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
state.tr;
return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
}),
+ // right justify text
new InputRule(
- new RegExp(/^\]\]\s$/),
+ new RegExp(/%\]$/),
(state, match, start, end) => {
- let node = (state.doc.resolve(start) as any).nodeAfter;
- let sm = state.storedMarks || undefined;
- let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
- let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || undefined;
+ const ruleProvider = FormattedTextBox.FocusedBox!.props.ruleProvider;
+ const heading = NumCast(FormattedTextBox.FocusedBox!.props.Document.heading);
if (ruleProvider && heading) {
ruleProvider["ruleAlign_" + heading] = "right";
return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
}
- let replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
state.tr;
return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
}),
new InputRule(
- new RegExp(/##\s$/),
+ new RegExp(/%#$/),
(state, match, start, end) => {
- let node = (state.doc.resolve(start) as any).nodeAfter;
- let sm = state.storedMarks || undefined;
- let target = Docs.Create.TextDocument({ width: 75, height: 35, autoHeight: true, fontSize: 9, title: "inline comment" });
- let replaced = node ? state.tr.insertText("←", start).replaceRangeWith(start + 1, end + 1, schema.nodes.dashDoc.create({
- width: 75, height: 35,
- title: "dashDoc", docid: target[Id],
- float: "right"
- })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ const target = Docs.Create.TextDocument({ width: 75, height: 35, backgroundColor: "yellow", annotationOn: FormattedTextBox.FocusedBox!.dataDoc, autoHeight: true, fontSize: 9, title: "inline comment" });
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const newNode = schema.nodes.dashComment.create({ docid: target[Id] });
+ const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: target[Id], float: "right" });
+ const sm = state.storedMarks || undefined;
+ const replaced = node ? state.tr.insert(start, newNode).replaceRangeWith(start + 1, end + 1, dashDoc).insertText(" ", start + 2).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
state.tr;
- return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 1)));
+ return replaced;//.setSelection(new NodeSelection(replaced.doc.resolve(end)));
}),
new InputRule(
- new RegExp(/\(\(/),
+ new RegExp(/%\(/),
(state, match, start, end) => {
- let node = (state.doc.resolve(start) as any).nodeAfter;
- let sm = state.storedMarks || undefined;
- let mark = state.schema.marks.highlight.create();
- let selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark);
- let content = selected.selection.content();
- let replaced = node ? selected.replaceRangeWith(start, start,
- schema.nodes.star.create({ visibility: true, text: content, textslice: content.toJSON() })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || [];
+ const mark = state.schema.marks.summarizeInclusive.create();
+ sm.push(mark);
+ const selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark);
+ const content = selected.selection.content();
+ const replaced = node ? selected.replaceRangeWith(start, end,
+ schema.nodes.summary.create({ visibility: true, text: content, textslice: content.toJSON() })) :
state.tr;
- return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1)));
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]);
}),
new InputRule(
- new RegExp(/\)\)/),
+ new RegExp(/%\)/),
(state, match, start, end) => {
- let mark = state.schema.marks.highlight.create();
- return state.tr.removeStoredMark(mark);
+ return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create());
}),
new InputRule(
- new RegExp(/\^f\s$/),
+ new RegExp(/%f$/),
(state, match, start, end) => {
- let newNode = schema.nodes.footnote.create({});
- let tr = state.tr;
+ const newNode = schema.nodes.footnote.create({});
+ const tr = state.tr;
tr.deleteRange(start, end).replaceSelectionWith(newNode); // replace insertion with a footnote.
return tr.setSelection(new NodeSelection( // select the footnote node to open its display
tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize)));
}),
- // let newNode = schema.nodes.footnote.create({});
- // if (dispatch && state.selection.from === state.selection.to) {
- // return true;
- // }
+
+ // activate a style by name using prefix '%'
+ new InputRule(
+ new RegExp(/%[a-z]+$/),
+ (state, match, start, end) => {
+ const color = match[0].substring(1, match[0].length);
+ const marks = TooltipTextMenuManager.Instance._brushMap.get(color);
+ if (marks) {
+ const tr = state.tr.deleteRange(start, end);
+ return marks ? Array.from(marks).reduce((tr, m) => tr.addStoredMark(m), tr) : tr;
+ }
+ const isValidColor = (strColor: string) => {
+ const s = new Option().style;
+ s.color = strColor;
+ return s.color === strColor.toLowerCase(); // 'false' if color wasn't assigned
+ };
+ if (isValidColor(color)) {
+ return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontColor.create({ color: color }));
+ }
+ return null;
+ }),
]
};
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index cc3548e1a..ef90a7294 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,4 +1,4 @@
-import { action, observable, runInAction, reaction, IReactionDisposer } from "mobx";
+import { reaction, IReactionDisposer } from "mobx";
import { baseKeymap, toggleMark } from "prosemirror-commands";
import { redo, undo } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
@@ -16,11 +16,10 @@ import { DocumentManager } from "./DocumentManager";
import ParagraphNodeSpec from "./ParagraphNodeSpec";
import { Transform } from "./Transform";
import React = require("react");
-import { BoolCast, NumCast } from "../../new_fields/Types";
+import { BoolCast, NumCast, Cast } from "../../new_fields/Types";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
-import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
-const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
+const blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
// :: Object
@@ -31,7 +30,6 @@ export const nodes: { [index: string]: NodeSpec } = {
content: "block+"
},
-
footnote: {
group: "inline",
content: "inline*",
@@ -46,15 +44,6 @@ export const nodes: { [index: string]: NodeSpec } = {
parseDOM: [{ tag: "footnote" }]
},
- // // :: NodeSpec A plain paragraph textblock. Represented in the DOM
- // // as a `<p>` element.
- // paragraph: {
- // content: "inline*",
- // group: "block",
- // parseDOM: [{ tag: "p" }],
- // toDOM() { return pDOM; }
- // },
-
paragraph: ParagraphNodeSpec,
// :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
@@ -108,7 +97,19 @@ export const nodes: { [index: string]: NodeSpec } = {
group: "inline"
},
- star: {
+ dashComment: {
+ attrs: {
+ docid: { default: "" },
+ },
+ inline: true,
+ group: "inline",
+ toDOM(node) {
+ const attrs = { style: `width: 40px` };
+ return ["span", { ...node.attrs, ...attrs }, "←"];
+ },
+ },
+
+ summary: {
inline: true,
attrs: {
visibility: { default: false },
@@ -120,16 +121,8 @@ export const nodes: { [index: string]: NodeSpec } = {
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.
@@ -171,21 +164,11 @@ export const nodes: { [index: string]: NodeSpec } = {
title: { default: null },
float: { default: "right" },
location: { default: "onRight" },
- docid: { default: "" }
+ hidden: { default: false },
+ docid: { default: "" },
},
group: "inline",
- draggable: true,
- // parseDOM: [{
- // tag: "img[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"))),
- // };
- // }
- // }],
- // TODO if we don't define toDom, dragging the image crashes. Why?
+ draggable: false,
toDOM(node) {
const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
return ["div", { ...node.attrs, ...attrs }];
@@ -235,19 +218,21 @@ export const nodes: { [index: string]: NodeSpec } = {
bulletStyle: { default: 0 },
mapStyle: { default: "decimal" },
setFontSize: { default: undefined },
- setFontFamily: { default: undefined },
+ setFontFamily: { default: "inherit" },
+ setFontColor: { default: "inherit" },
inheritedFontSize: { default: undefined },
- visibility: { default: true }
+ visibility: { default: true },
+ indent: { default: undefined }
},
toDOM(node: Node<any>) {
- const bs = node.attrs.bulletStyle;
- const decMap = bs ? "decimal" + bs : "";
- const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
- let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
- let fsize = node.attrs.setFontSize ? node.attrs.setFontSize : node.attrs.inheritedFontSize;
- let ffam = node.attrs.setFontFamily;
- return node.attrs.visibility ? ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}` }, 0] :
- ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}` }];
+ if (node.attrs.mapStyle === "bullet") return ['ul', 0];
+ const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : "";
+ const fsize = node.attrs.setFontSize ? node.attrs.setFontSize : node.attrs.inheritedFontSize;
+ const ffam = node.attrs.setFontFamily;
+ const color = node.attrs.setFontColor;
+ return node.attrs.visibility ?
+ ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}; color:${color}; margin-left: ${node.attrs.indent}` }, 0] :
+ ['ol', { class: `${map}-ol`, style: `list-style: none;` }];
}
},
@@ -270,10 +255,7 @@ export const nodes: { [index: string]: NodeSpec } = {
...listItem,
content: 'paragraph block*',
toDOM(node: any) {
- const bs = node.attrs.bulletStyle;
- const decMap = bs ? "decimal" + bs : "";
- const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
- let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
+ const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : "";
return node.attrs.visibility ? ["li", { class: `${map}` }, 0] : ["li", { class: `${map}` }, "..."];
//return ["li", { class: `${map}` }, 0];
}
@@ -292,6 +274,8 @@ export const marks: { [index: string]: MarkSpec } = {
link: {
attrs: {
href: {},
+ targetId: { default: "" },
+ showPreview: { default: true },
location: { default: null },
title: { default: null },
docref: { default: false } // flags whether the linked text comes from a document within Dash. If so, an attribution label is appended after the text
@@ -299,13 +283,45 @@ export const marks: { [index: string]: MarkSpec } = {
inclusive: false,
parseDOM: [{
tag: "a[href]", getAttrs(dom: any) {
- return { href: dom.getAttribute("href"), location: dom.getAttribute("location"), title: dom.getAttribute("title") };
+ return { href: dom.getAttribute("href"), location: dom.getAttribute("location"), title: dom.getAttribute("title"), targetId: dom.getAttribute("id") };
}
}],
toDOM(node: any) {
return node.attrs.docref && node.attrs.title ?
- ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, class: "prosemirror-attribution" }, node.attrs.title], ["br"]] :
- ["a", { ...node.attrs }, 0];
+ ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, class: "prosemirror-attribution", title: `${node.attrs.title}` }, node.attrs.title], ["br"]] :
+ ["a", { ...node.attrs, id: node.attrs.targetId, title: `${node.attrs.title}` }, 0];
+ }
+ },
+
+
+ // :: MarkSpec Coloring on text. Has `color` attribute that defined the color of the marked text.
+ pFontColor: {
+ attrs: {
+ color: { default: "#000" }
+ },
+ inclusive: true,
+ parseDOM: [{
+ tag: "span", getAttrs(dom: any) {
+ return { color: dom.getAttribute("color") };
+ }
+ }],
+ toDOM(node: any) {
+ return node.attrs.color ? ['span', { style: 'color:' + node.attrs.color }] : ['span', { style: 'color: black' }];
+ }
+ },
+
+ marker: {
+ attrs: {
+ highlight: { default: "transparent" }
+ },
+ inclusive: true,
+ parseDOM: [{
+ tag: "span", getAttrs(dom: any) {
+ return { highlight: dom.getAttribute("backgroundColor") };
+ }
+ }],
+ toDOM(node: any) {
+ return node.attrs.highlight ? ['span', { style: 'background-color:' + node.attrs.highlight }] : ['span', { style: 'background-color: transparent' }];
}
},
@@ -381,16 +397,16 @@ export const marks: { [index: string]: MarkSpec } = {
}
},
- highlight: {
+ summarizeInclusive: {
parseDOM: [
{
tag: "span",
getAttrs: (p: any) => {
if (typeof (p) !== "string") {
- let style = getComputedStyle(p);
+ const style = getComputedStyle(p);
if (style.textDecoration === "underline") return null;
if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 &&
- p.parentElement.outerHTML.indexOf("text-decoration-style: dotted") !== -1) {
+ p.parentElement.outerHTML.indexOf("text-decoration-style: solid") !== -1) {
return null;
}
}
@@ -401,6 +417,31 @@ export const marks: { [index: string]: MarkSpec } = {
inclusive: true,
toDOM() {
return ['span', {
+ style: 'text-decoration: underline; text-decoration-style: solid; text-decoration-color: rgba(204, 206, 210, 0.92)'
+ }];
+ }
+ },
+
+ summarize: {
+ inclusive: false,
+ parseDOM: [
+ {
+ tag: "span",
+ getAttrs: (p: any) => {
+ if (typeof (p) !== "string") {
+ const style = getComputedStyle(p);
+ if (style.textDecoration === "underline") return null;
+ if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 &&
+ p.parentElement.outerHTML.indexOf("text-decoration-style: dotted") !== -1) {
+ return null;
+ }
+ }
+ return false;
+ }
+ },
+ ],
+ toDOM() {
+ return ['span', {
style: 'text-decoration: underline; text-decoration-style: dotted; text-decoration-color: rgba(204, 206, 210, 0.92)'
}];
}
@@ -412,7 +453,7 @@ export const marks: { [index: string]: MarkSpec } = {
tag: "span",
getAttrs: (p: any) => {
if (typeof (p) !== "string") {
- let style = getComputedStyle(p);
+ const style = getComputedStyle(p);
if (style.textDecoration === "underline" || p.parentElement.outerHTML.indexOf("text-decoration-style:line") !== -1) {
return null;
}
@@ -443,35 +484,30 @@ export const marks: { [index: string]: MarkSpec } = {
user_mark: {
attrs: {
userid: { default: "" },
- opened: { default: true },
modified: { default: "when?" }, // 5 second intervals since 1970
},
group: "inline",
toDOM(node: any) {
- let uid = node.attrs.userid.replace(".", "").replace("@", "");
- let min = Math.round(node.attrs.modified / 12);
- let hr = Math.round(min / 60);
- let day = Math.round(hr / 60 / 24);
- let remote = node.attrs.userid !== Doc.CurrentUserEmail ? " userMark-remote" : "";
- return node.attrs.opened ?
- ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, 0] :
- ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, ['span', 0]];
+ const uid = node.attrs.userid.replace(".", "").replace("@", "");
+ const min = Math.round(node.attrs.modified / 12);
+ const hr = Math.round(min / 60);
+ const day = Math.round(hr / 60 / 24);
+ const remote = node.attrs.userid !== Doc.CurrentUserEmail ? " userMark-remote" : "";
+ return ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, 0];
}
},
// the id of the user who entered the text
user_tag: {
attrs: {
userid: { default: "" },
- opened: { default: true },
modified: { default: "when?" }, // 5 second intervals since 1970
tag: { default: "" }
},
group: "inline",
+ inclusive: false,
toDOM(node: any) {
- let uid = node.attrs.userid.replace(".", "").replace("@", "");
- return node.attrs.opened ?
- ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, 0] :
- ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, ['span', 0]];
+ const uid = node.attrs.userid.replace(".", "").replace("@", "");
+ return ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, 0];
}
},
@@ -482,85 +518,27 @@ export const marks: { [index: string]: MarkSpec } = {
toDOM() { return codeDOM; }
},
- // pFontFamily: {
- // attrs: {
- // style: { default: 'font-family: "Times New Roman", Times, serif;' },
- // },
- // parseDOM: [{
- // tag: "span", getAttrs(dom: any) {
- // if (getComputedStyle(dom).font === "Times New Roman") return { style: `font-family: "Times New Roman", Times, serif;` };
- // if (getComputedStyle(dom).font === "Arial, Helvetica") return { style: `font-family: Arial, Helvetica, sans-serif;` };
- // if (getComputedStyle(dom).font === "Georgia") return { style: `font-family: Georgia, serif;` };
- // if (getComputedStyle(dom).font === "Comic Sans") return { style: `font-family: "Comic Sans MS", cursive, sans-serif;` };
- // if (getComputedStyle(dom).font === "Tahoma, Geneva") return { style: `font-family: Tahoma, Geneva, sans-serif;` };
- // }
- // }],
- // toDOM: (node: any) => ['span', {
- // style: node.attrs.style
- // }]
- // },
-
-
/* FONTS */
- timesNewRoman: {
- parseDOM: [{ style: 'font-family: "Times New Roman", Times, serif;' }],
- toDOM: () => ['span', {
- style: 'font-family: "Times New Roman", Times, serif;'
- }]
- },
-
- arial: {
- parseDOM: [{ style: 'font-family: Arial, Helvetica, sans-serif;' }],
- toDOM: () => ['span', {
- style: 'font-family: Arial, Helvetica, sans-serif;'
- }]
- },
-
- georgia: {
- parseDOM: [{ style: 'font-family: Georgia, serif;' }],
- toDOM: () => ['span', {
- style: 'font-family: Georgia, serif;'
- }]
- },
-
- comicSans: {
- parseDOM: [{ style: 'font-family: "Comic Sans MS", cursive, sans-serif;' }],
- toDOM: () => ['span', {
- style: 'font-family: "Comic Sans MS", cursive, sans-serif;'
- }]
- },
-
- tahoma: {
- parseDOM: [{ style: 'font-family: Tahoma, Geneva, sans-serif;' }],
- toDOM: () => ['span', {
- style: 'font-family: Tahoma, Geneva, sans-serif;'
- }]
- },
-
- impact: {
- parseDOM: [{ style: 'font-family: Impact, Charcoal, sans-serif;' }],
- toDOM: () => ['span', {
- style: 'font-family: Impact, Charcoal, sans-serif;'
- }]
- },
-
- crimson: {
- parseDOM: [{ style: 'font-family: "Crimson Text", sans-serif;' }],
- toDOM: () => ['span', {
- style: 'font-family: "Crimson Text", sans-serif;'
- }]
- },
-
- pFontColor: {
+ pFontFamily: {
attrs: {
- color: { default: "yellow" }
+ family: { default: "Crimson Text" },
},
- parseDOM: [{ style: 'background: #d9dbdd' }],
- toDOM: (node) => {
- return ['span', {
- style: `color: ${node.attrs.color}`
- }];
- }
+ parseDOM: [{
+ tag: "span", getAttrs(dom: any) {
+ const cstyle = getComputedStyle(dom);
+ if (cstyle.font) {
+ if (cstyle.font.indexOf("Times New Roman") !== -1) return { family: "Times New Roman" };
+ if (cstyle.font.indexOf("Arial") !== -1) return { family: "Arial" };
+ if (cstyle.font.indexOf("Georgia") !== -1) return { family: "Georgia" };
+ if (cstyle.font.indexOf("Comic Sans") !== -1) return { family: "Comic Sans MS" };
+ if (cstyle.font.indexOf("Tahoma") !== -1) return { family: "Tahoma" };
+ if (cstyle.font.indexOf("Crimson") !== -1) return { family: "Crimson Text" };
+ }
+ }
+ }],
+ toDOM: (node) => ['span', {
+ style: `font-family: "${node.attrs.family}";`
+ }]
},
/** FONT SIZES */
@@ -573,76 +551,6 @@ export const marks: { [index: string]: MarkSpec } = {
style: `font-size: ${node.attrs.fontSize}px;`
}]
},
-
- p10: {
- parseDOM: [{ style: 'font-size: 10px;' }],
- toDOM: () => ['span', {
- style: 'font-size: 10px;'
- }]
- },
-
- p12: {
- parseDOM: [{ style: 'font-size: 12px;' }],
- toDOM: () => ['span', {
- style: 'font-size: 12px;'
- }]
- },
-
- p14: {
- parseDOM: [{ style: 'font-size: 14px;' }],
- toDOM: () => ['span', {
- style: 'font-size: 14px;'
- }]
- },
-
- p16: {
- parseDOM: [{ style: 'font-size: 16px;' }],
- toDOM: () => ['span', {
- style: 'font-size: 16px;'
- }]
- },
-
- 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', {
- style: 'font-size: 24px;'
- }]
- },
-
- p32: {
- parseDOM: [{ style: 'font-size: 32px;' }],
- toDOM: () => ['span', {
- style: 'font-size: 32px;'
- }]
- },
-
- p48: {
- parseDOM: [{ style: 'font-size: 48px;' }],
- toDOM: () => ['span', {
- style: 'font-size: 48px;'
- }]
- },
-
- p72: {
- parseDOM: [{ style: 'font-size: 72px;' }],
- toDOM: () => ['span', {
- style: 'font-size: 72px;'
- }]
- },
};
export class ImageResizeView {
@@ -670,7 +578,7 @@ export class ImageResizeView {
this._handle.style.display = "none";
this._handle.style.bottom = "-10px";
this._handle.style.right = "-10px";
- let self = this;
+ const self = this;
this._img.onclick = function (e: any) {
e.stopPropagation();
e.preventDefault();
@@ -691,8 +599,8 @@ export class ImageResizeView {
this._handle.onpointerdown = function (e: any) {
e.preventDefault();
e.stopPropagation();
- let wid = Number(getComputedStyle(self._img).width!.replace(/px/, ""));
- let hgt = Number(getComputedStyle(self._img).height!.replace(/px/, ""));
+ const wid = Number(getComputedStyle(self._img).width.replace(/px/, ""));
+ const hgt = Number(getComputedStyle(self._img).height.replace(/px/, ""));
const startX = e.pageX;
const startWidth = parseFloat(node.attrs.width);
const onpointermove = (e: any) => {
@@ -705,7 +613,7 @@ export class ImageResizeView {
const onpointerup = () => {
document.removeEventListener("pointermove", onpointermove);
document.removeEventListener("pointerup", onpointerup);
- let pos = view.state.selection.from;
+ const pos = view.state.selection.from;
view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: self._outer.style.width, height: self._outer.style.height }));
view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(pos))));
};
@@ -732,6 +640,58 @@ export class ImageResizeView {
}
}
+
+export class DashDocCommentView {
+ _collapsed: HTMLElement;
+ _view: any;
+ constructor(node: any, view: any, getPos: any) {
+ this._collapsed = document.createElement("span");
+ this._collapsed.className = "formattedTextBox-inlineComment";
+ this._collapsed.id = "DashDocCommentView-" + node.attrs.docid;
+ this._view = view;
+ const targetNode = () => { // search forward in the prosemirror doc for the attached dashDocNode that is the target of the comment anchor
+ for (let i = getPos() + 1; i < view.state.doc.content.size; i++) {
+ const m = view.state.doc.nodeAt(i);
+ if (m && m.type === view.state.schema.nodes.dashDoc && m.attrs.docid === node.attrs.docid) {
+ return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any, pos: number, hidden: boolean };
+ }
+ }
+ const dashDoc = view.state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: node.attrs.docid, float: "right" });
+ view.dispatch(view.state.tr.insert(getPos() + 1, dashDoc));
+ setTimeout(() => { try { view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.tr.doc, getPos() + 2))); } catch (e) { } }, 0);
+ return undefined;
+ };
+ this._collapsed.onpointerdown = (e: any) => {
+ e.stopPropagation();
+ };
+ this._collapsed.onpointerup = (e: any) => {
+ const target = targetNode();
+ if (target) {
+ const expand = target.hidden;
+ const tr = view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true });
+ view.dispatch(tr.setSelection(TextSelection.create(tr.doc, getPos() + (expand ? 2 : 1)))); // update the attrs
+ setTimeout(() => {
+ expand && DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
+ try { view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.tr.doc, getPos() + (expand ? 2 : 1)))); } catch (e) { }
+ }, 0);
+ }
+ e.stopPropagation();
+ };
+ this._collapsed.onpointerenter = (e: any) => {
+ DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
+ e.preventDefault();
+ e.stopPropagation();
+ };
+ this._collapsed.onpointerleave = (e: any) => {
+ DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
+ e.preventDefault();
+ e.stopPropagation();
+ };
+ (this as any).dom = this._collapsed;
+ }
+ selectNode() { }
+}
+
export class DashDocView {
_dashSpan: HTMLDivElement;
_outer: HTMLElement;
@@ -740,36 +700,55 @@ export class DashDocView {
_textBox: FormattedTextBox;
getDocTransform = () => {
- let { scale, translateX, translateY } = Utils.GetScreenTransform(this._outer);
+ const { scale, translateX, translateY } = Utils.GetScreenTransform(this._outer);
return new Transform(-translateX, -translateY, 1).scale(1 / this.contentScaling() / scale);
}
contentScaling = () => NumCast(this._dashDoc!.nativeWidth) > 0 && !this._dashDoc!.ignoreAspect ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!.nativeWidth) : 1;
+ outerFocus = (target: Doc) => this._textBox.props.focus(this._textBox.props.Document); // ideally, this would scroll to show the focus target
constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
this._textBox = tbox;
this._dashSpan = document.createElement("div");
this._outer = document.createElement("span");
this._outer.style.position = "relative";
+ this._outer.style.textIndent = "0";
this._outer.style.width = node.attrs.width;
this._outer.style.height = node.attrs.height;
- this._outer.style.display = "inline-block";
- this._outer.style.overflow = "hidden";
+ this._outer.style.display = node.attrs.hidden ? "none" : "inline-block";
+ // this._outer.style.overflow = "hidden"; // bcz: not sure if this is needed. if it's used, then the doc doesn't highlight when you hover over a docComment
(this._outer.style as any).float = node.attrs.float;
this._dashSpan.style.width = node.attrs.width;
this._dashSpan.style.height = node.attrs.height;
this._dashSpan.style.position = "absolute";
this._dashSpan.style.display = "inline-block";
- let removeDoc = () => {
- let pos = getPos();
- let ns = new NodeSelection(view.state.doc.resolve(pos));
+ const removeDoc = () => {
+ const pos = getPos();
+ const ns = new NodeSelection(view.state.doc.resolve(pos));
view.dispatch(view.state.tr.setSelection(ns).deleteSelection());
return true;
};
+ this._dashSpan.onpointerleave = () => {
+ const ele = document.getElementById("DashDocCommentView-" + node.attrs.docid);
+ if (ele) {
+ (ele as HTMLDivElement).style.backgroundColor = "";
+ }
+ };
+ this._dashSpan.onpointerenter = () => {
+ const ele = document.getElementById("DashDocCommentView-" + node.attrs.docid);
+ if (ele) {
+ (ele as HTMLDivElement).style.backgroundColor = "orange";
+ }
+ };
DocServer.GetRefField(node.attrs.docid).then(async dashDoc => {
if (dashDoc instanceof Doc) {
self._dashDoc = dashDoc;
+ dashDoc.hideSidebar = true;
if (node.attrs.width !== dashDoc.width + "px" || node.attrs.height !== dashDoc.height + "px") {
- view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc.width + "px", height: dashDoc.height + "px" }));
+ try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made
+ view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc.width + "px", height: dashDoc.height + "px" }));
+ } catch (e) {
+ console.log(e);
+ }
}
this._reactionDisposer && this._reactionDisposer();
this._reactionDisposer = reaction(() => dashDoc[HeightSym]() + dashDoc[WidthSym](), () => {
@@ -777,8 +756,9 @@ export class DashDocView {
this._dashSpan.style.width = this._outer.style.width = dashDoc[WidthSym]() + "px";
});
ReactDOM.render(<DocumentView
- fitToBox={BoolCast(dashDoc.fitToBox)}
Document={dashDoc}
+ LibraryPath={tbox.props.LibraryPath}
+ fitToBox={BoolCast(dashDoc.fitToBox)}
addDocument={returnFalse}
removeDocument={removeDoc}
ruleProvider={undefined}
@@ -788,21 +768,27 @@ export class DashDocView {
renderDepth={1}
PanelWidth={self._dashDoc[WidthSym]}
PanelHeight={self._dashDoc[HeightSym]}
- focus={emptyFunction}
+ focus={self.outerFocus}
backgroundColor={returnEmptyString}
parentActive={returnFalse}
whenActiveChanged={returnFalse}
bringToFront={emptyFunction}
zoomToScale={emptyFunction}
getScale={returnOne}
+ dontRegisterView={false}
ContainingCollectionView={undefined}
ContainingCollectionDoc={undefined}
ContentScaling={this.contentScaling}
/>, this._dashSpan);
}
});
- let self = this;
- this._dashSpan.onkeydown = function (e: any) { e.stopPropagation(); };
+ const self = this;
+ this._dashSpan.onkeydown = function (e: any) {
+ e.stopPropagation();
+ if (e.key === "Tab" || e.key === "Enter") {
+ e.preventDefault();
+ }
+ };
this._dashSpan.onkeypress = function (e: any) { e.stopPropagation(); };
this._dashSpan.onwheel = function (e: any) { e.preventDefault(); };
this._dashSpan.onkeyup = function (e: any) { e.stopPropagation(); };
@@ -854,7 +840,7 @@ export class FootnoteView {
}
open() {
// Append a tooltip to the outer node
- let tooltip = this.dom.appendChild(document.createElement("div"));
+ const tooltip = this.dom.appendChild(document.createElement("div"));
tooltip.className = "footnote-tooltip";
// And put a sub-ProseMirror into that
this.innerView = new EditorView(tooltip, {
@@ -909,14 +895,14 @@ export class FootnoteView {
this.dom.textContent = "";
}
dispatchInner(tr: any) {
- let { state, transactions } = this.innerView.state.applyTransaction(tr);
+ const { state, transactions } = this.innerView.state.applyTransaction(tr);
this.innerView.updateState(state);
if (!tr.getMeta("fromOutside")) {
- let outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1);
- for (let transaction of transactions) {
- let steps = transaction.steps;
- for (let step of steps) {
+ const outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1);
+ for (const transaction of transactions) {
+ const steps = transaction.steps;
+ for (const step of steps) {
outerTr.step(step.map(offsetMap));
}
}
@@ -927,11 +913,11 @@ export class FootnoteView {
if (!node.sameMarkup(this.node)) return false;
this.node = node;
if (this.innerView) {
- let state = this.innerView.state;
- let start = node.content.findDiffStart(state.doc.content);
+ const state = this.innerView.state;
+ const start = node.content.findDiffStart(state.doc.content);
if (start !== null) {
let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content);
- let overlap = start - Math.min(endA, endB);
+ const overlap = start - Math.min(endA, endB);
if (overlap > 0) { endA += overlap; endB += overlap; }
this.innerView.dispatch(
state.tr
@@ -953,7 +939,7 @@ export class FootnoteView {
ignoreMutation() { return true; }
}
-export class SummarizedView {
+export class SummaryView {
_collapsed: HTMLElement;
_view: any;
constructor(node: any, view: any, getPos: any) {
@@ -991,15 +977,16 @@ export class SummarizedView {
className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed");
updateSummarizedText(start?: any) {
- let mark = this._view.state.schema.marks.highlight.create();
+ const mtype = this._view.state.schema.marks.summarize;
+ const mtypeInc = this._view.state.schema.marks.summarizeInclusive;
let endPos = start;
- let visited = new Set();
+ const 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.find((m: any) => m.type === mark.type)) {
+ if (node.marks.find((m: any) => m.type === mtype || m.type === mtypeInc)) {
visited.add(node);
endPos = i + node.nodeSize - 1;
}
@@ -1023,8 +1010,8 @@ export const schema = new Schema({ nodes, marks });
const fromJson = schema.nodeFromJSON;
schema.nodeFromJSON = (json: any) => {
- let node = fromJson(json);
- if (json.type === "star") {
+ const node = fromJson(json);
+ if (json.type === schema.marks.summarize.name) {
node.attrs.text = Slice.fromJSON(schema, node.attrs.textslice);
}
return node;
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index ff4451824..0fa96963e 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -94,16 +94,16 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
return { compiled: false, errors: diagnostics };
}
- let paramNames = Object.keys(scriptingGlobals);
- let params = paramNames.map(key => scriptingGlobals[key]);
+ const paramNames = Object.keys(scriptingGlobals);
+ const 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 } = {}, onError?: (e: any) => void, errorVal?: any): ScriptResult => {
- let argsArray: any[] = [];
- for (let name of customParams) {
+ const compiledFunction = new Function(...paramNames, `return ${script}`);
+ const { capturedVariables = {} } = options;
+ const run = (args: { [name: string]: any } = {}, onError?: (e: any) => void, errorVal?: any): ScriptResult => {
+ const argsArray: any[] = [];
+ for (const name of customParams) {
if (name === "this") {
continue;
}
@@ -113,7 +113,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
argsArray.push(capturedVariables[name]);
}
}
- let thisParam = args.this || capturedVariables.this;
+ const thisParam = args.this || capturedVariables.this;
let batch: { end(): void } | undefined = undefined;
try {
if (!options.editable) {
@@ -146,7 +146,7 @@ class ScriptingCompilerHost {
// getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: ((message: string) => void) | undefined, shouldCreateNewSourceFile?: boolean | undefined): ts.SourceFile | undefined {
getSourceFile(fileName: string, languageVersion: any, onError?: ((message: string) => void) | undefined, shouldCreateNewSourceFile?: boolean | undefined): any | undefined {
- let contents = this.readFile(fileName);
+ const contents = this.readFile(fileName);
if (contents !== undefined) {
return ts.createSourceFile(fileName, contents, languageVersion, true);
}
@@ -180,7 +180,7 @@ class ScriptingCompilerHost {
return this.files.some(file => file.fileName === fileName);
}
readFile(fileName: string): string | undefined {
- let file = this.files.find(file => file.fileName === fileName);
+ const file = this.files.find(file => file.fileName === fileName);
if (file) {
return file.content;
}
@@ -218,7 +218,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
if (options.globals) {
Scripting.setScriptingGlobals(options.globals);
}
- let host = new ScriptingCompilerHost;
+ const host = new ScriptingCompilerHost;
if (options.traverser) {
const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true);
const onEnter = typeof options.traverser === "object" ? options.traverser.onEnter : options.traverser;
@@ -240,7 +240,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
script = printer.printFile(transformed[0]);
result.dispose();
}
- let paramNames: string[] = [];
+ const paramNames: string[] = [];
if ("this" in params || "this" in capturedVariables) {
paramNames.push("this");
}
@@ -248,7 +248,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
if (key === "this") continue;
paramNames.push(key);
}
- let paramList = paramNames.map(key => {
+ const paramList = paramNames.map(key => {
const val = params[key];
return `${key}: ${val}`;
});
@@ -258,18 +258,18 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
paramNames.push(key);
paramList.push(`${key}: ${typeof val === "object" ? Object.getPrototypeOf(val).constructor.name : typeof val}`);
}
- let paramString = paramList.join(", ");
- let funcScript = `(function(${paramString})${requiredType ? `: ${requiredType}` : ''} {
+ const paramString = paramList.join(", ");
+ const funcScript = `(function(${paramString})${requiredType ? `: ${requiredType}` : ''} {
${addReturn ? `return ${script};` : script}
})`;
host.writeFile("file.ts", funcScript);
if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
- let program = ts.createProgram(["file.ts"], {}, host);
- let testResult = program.emit();
- let outputText = host.readFile("file.js");
+ const program = ts.createProgram(["file.ts"], {}, host);
+ const testResult = program.emit();
+ const outputText = host.readFile("file.js");
- let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics);
+ const diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics);
const result = Run(outputText, paramNames, diagnostics, script, options);
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index 6706dcb89..8ff54d052 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -28,42 +28,43 @@ export namespace SearchUtil {
start?: number;
rows?: number;
fq?: string;
+ allowAliases?: boolean;
}
export function Search(query: string, returnDocs: true, options?: SearchParams): Promise<DocSearchResult>;
export function Search(query: string, returnDocs: false, options?: SearchParams): Promise<IdSearchResult>;
export async function Search(query: string, returnDocs: boolean, options: SearchParams = {}) {
query = query || "*"; //If we just have a filter query, search for * as the query
- let result: IdSearchResult = JSON.parse(await rp.get(Utils.prepend("/search"), {
- qs: { ...options, q: query },
- }));
+ const rpquery = Utils.prepend("/search");
+ const gotten = await rp.get(rpquery, { qs: { ...options, q: query } });
+ const result: IdSearchResult = gotten.startsWith("<") ? { ids: [], docs: [], numFound: 0, lines: [] } : JSON.parse(gotten);
if (!returnDocs) {
return result;
}
- let { ids, numFound, highlighting } = result;
+ const { ids, highlighting } = result;
- let txtresult = query !== "*" && JSON.parse(await rp.get(Utils.prepend("/textsearch"), {
+ const txtresult = query !== "*" && JSON.parse(await rp.get(Utils.prepend("/textsearch"), {
qs: { ...options, q: query },
}));
- let fileids = txtresult ? txtresult.ids : [];
- let newIds: string[] = [];
- let newLines: string[][] = [];
+ const fileids = txtresult ? txtresult.ids : [];
+ const newIds: string[] = [];
+ const newLines: string[][] = [];
await Promise.all(fileids.map(async (tr: string, i: number) => {
- let docQuery = "fileUpload_t:" + tr.substr(0, 7); //If we just have a filter query, search for * as the query
- let docResult = JSON.parse(await rp.get(Utils.prepend("/search"), { qs: { ...options, q: docQuery } }));
+ const docQuery = "fileUpload_t:" + tr.substr(0, 7); //If we just have a filter query, search for * as the query
+ const docResult = JSON.parse(await rp.get(Utils.prepend("/search"), { qs: { ...options, q: docQuery } }));
newIds.push(...docResult.ids);
newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i]));
}));
- let theDocs: Doc[] = [];
- let theLines: string[][] = [];
+ const theDocs: Doc[] = [];
+ const theLines: string[][] = [];
const textDocMap = await DocServer.GetRefFields(newIds);
const textDocs = newIds.map((id: string) => textDocMap[id]).map(doc => doc as Doc);
for (let i = 0; i < textDocs.length; i++) {
- let testDoc = textDocs[i];
- if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) {
+ const testDoc = textDocs[i];
+ if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && testDoc.type !== DocumentType.EXTENSION && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) {
theDocs.push(Doc.GetProto(testDoc));
theLines.push(newLines[i].map(line => line.replace(query, query.toUpperCase())));
}
@@ -72,8 +73,8 @@ export namespace SearchUtil {
const docMap = await DocServer.GetRefFields(ids);
const docs = ids.map((id: string) => docMap[id]).map(doc => doc as Doc);
for (let i = 0; i < ids.length; i++) {
- let testDoc = docs[i];
- if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) {
+ const testDoc = docs[i];
+ if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && testDoc.type !== DocumentType.EXTENSION && (options.allowAliases || theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1)) {
theDocs.push(testDoc);
theLines.push([]);
}
@@ -88,9 +89,9 @@ export namespace SearchUtil {
const proto = Doc.GetProto(doc);
const protoId = proto[Id];
if (returnDocs) {
- return (await Search("", returnDocs, { fq: `proto_i:"${protoId}"` })).docs;
+ return (await Search("", returnDocs, { fq: `proto_i:"${protoId}"`, allowAliases: true })).docs;
} else {
- return (await Search("", returnDocs, { fq: `proto_i:"${protoId}"` })).ids;
+ return (await Search("", returnDocs, { fq: `proto_i:"${protoId}"`, allowAliases: true })).ids;
}
// return Search(`{!join from=id to=proto_i}id:${protoId}`, true);
}
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 2d717ca57..86a7a620e 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,46 +1,50 @@
-import { observable, action, runInAction, IReactionDisposer, reaction, autorun } from "mobx";
-import { Doc, Opt } from "../../new_fields/Doc";
+import { observable, action, runInAction, ObservableMap } from "mobx";
+import { Doc } from "../../new_fields/Doc";
import { DocumentView } from "../views/nodes/DocumentView";
-import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
-import { NumCast, StrCast } from "../../new_fields/Types";
-import { InkingControl } from "../views/InkingControl";
+import { computedFn } from "mobx-utils";
+import { List } from "../../new_fields/List";
+import { DocumentDecorations } from "../views/DocumentDecorations";
+import RichTextMenu from "./RichTextMenu";
export namespace SelectionManager {
class Manager {
@observable IsDragging: boolean = false;
- @observable SelectedDocuments: Array<DocumentView> = [];
+ SelectedDocuments: ObservableMap<DocumentView, boolean> = new ObservableMap();
@action
SelectDoc(docView: DocumentView, ctrlPressed: boolean): void {
// if doc is not in SelectedDocuments, add it
- if (manager.SelectedDocuments.indexOf(docView) === -1) {
+ if (!manager.SelectedDocuments.get(docView)) {
if (!ctrlPressed) {
this.DeselectAll();
}
- manager.SelectedDocuments.push(docView);
+ manager.SelectedDocuments.set(docView, true);
// console.log(manager.SelectedDocuments);
docView.props.whenActiveChanged(true);
- } else if (!ctrlPressed && manager.SelectedDocuments.length > 1) {
- manager.SelectedDocuments.map(dv => dv !== docView && dv.props.whenActiveChanged(false));
- manager.SelectedDocuments = [docView];
+ } else if (!ctrlPressed && Array.from(manager.SelectedDocuments.entries()).length > 1) {
+ Array.from(manager.SelectedDocuments.keys()).map(dv => dv !== docView && dv.props.whenActiveChanged(false));
+ manager.SelectedDocuments.clear();
+ manager.SelectedDocuments.set(docView, true);
}
+ Doc.UserDoc().SelectedDocs = new List(SelectionManager.SelectedDocuments().map(dv => dv.props.Document));
}
@action
DeselectDoc(docView: DocumentView): void {
- let ind = manager.SelectedDocuments.indexOf(docView);
- if (ind !== -1) {
- manager.SelectedDocuments.splice(ind, 1);
+ if (manager.SelectedDocuments.get(docView)) {
+ manager.SelectedDocuments.delete(docView);
docView.props.whenActiveChanged(false);
+ Doc.UserDoc().SelectedDocs = new List(SelectionManager.SelectedDocuments().map(dv => dv.props.Document));
}
}
@action
DeselectAll(): void {
- manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false));
- manager.SelectedDocuments = [];
+ Array.from(manager.SelectedDocuments.keys()).map(dv => dv.props.whenActiveChanged(false));
+ manager.SelectedDocuments.clear();
+ Doc.UserDoc().SelectedDocs = new List<Doc>([]);
}
}
@@ -53,14 +57,18 @@ export namespace SelectionManager {
manager.SelectDoc(docView, ctrlPressed);
}
- export function IsSelected(doc: DocumentView): boolean {
- return manager.SelectedDocuments.indexOf(doc) !== -1;
+ export function IsSelected(doc: DocumentView, outsideReaction?: boolean): boolean {
+ return outsideReaction ?
+ manager.SelectedDocuments.get(doc) ? true : false :
+ computedFn(function isSelected(doc: DocumentView) {
+ return manager.SelectedDocuments.get(doc) ? true : false;
+ })(doc);
}
export function DeselectAll(except?: Doc): void {
let found: DocumentView | undefined = undefined;
if (except) {
- for (const view of manager.SelectedDocuments) {
+ for (const view of Array.from(manager.SelectedDocuments.keys())) {
if (view.props.Document === except) found = view;
}
}
@@ -73,6 +81,7 @@ export namespace SelectionManager {
export function GetIsDragging() { return manager.IsDragging; }
export function SelectedDocuments(): Array<DocumentView> {
- return manager.SelectedDocuments.slice();
+ return Array.from(manager.SelectedDocuments.keys());
}
}
+
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
index ff048f647..1f6b939d3 100644
--- a/src/client/util/SerializationHelper.ts
+++ b/src/client/util/SerializationHelper.ts
@@ -1,7 +1,6 @@
-import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema, primitive, SKIP } from "serializr";
-import { Field, Doc } from "../../new_fields/Doc";
+import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema } from "serializr";
+import { Field } from "../../new_fields/Doc";
import { ClientUtils } from "./ClientUtils";
-import { emptyFunction } from "../../Utils";
let serializing = 0;
export function afterDocDeserialize(cb: (err: any, val: any) => void, err: any, newValue: any) {
@@ -65,8 +64,8 @@ export namespace SerializationHelper {
}
}
-let serializationTypes: { [name: string]: { ctor: { new(): any }, afterDeserialize?: (obj: any) => void | Promise<any> } } = {};
-let reverseMap: { [ctor: string]: string } = {};
+const serializationTypes: { [name: string]: { ctor: { new(): any }, afterDeserialize?: (obj: any) => void | Promise<any> } } = {};
+const reverseMap: { [ctor: string]: string } = {};
export interface DeserializableOpts {
(constructor: { new(...args: any[]): any }): void;
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 2082d6324..7496ac73c 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -4,13 +4,11 @@ import MainViewModal from "../views/MainViewModal";
import { Doc, Opt, DocCastAsync } from "../../new_fields/Doc";
import { DocServer } from "../DocServer";
import { Cast, StrCast } from "../../new_fields/Types";
-import { RouteStore } from "../../server/RouteStore";
import * as RequestPromise from "request-promise";
import { Utils } from "../../Utils";
import "./SharingManager.scss";
import { Id } from "../../new_fields/FieldSymbols";
import { observer } from "mobx-react";
-import { MainView } from "../views/MainView";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { library } from '@fortawesome/fontawesome-svg-core';
import * as fa from '@fortawesome/free-solid-svg-icons';
@@ -104,10 +102,10 @@ export default class SharingManager extends React.Component<{}> {
}
populateUsers = async () => {
- let userList = await RequestPromise.get(Utils.prepend(RouteStore.getUsers));
+ const userList = await RequestPromise.get(Utils.prepend("/getUsers"));
const raw = JSON.parse(userList) as User[];
const evaluating = raw.map(async user => {
- let isCandidate = user.email !== Doc.CurrentUserEmail;
+ const isCandidate = user.email !== Doc.CurrentUserEmail;
if (isCandidate) {
const userDocument = await DocServer.GetRefField(user.userDocumentId);
if (userDocument instanceof Doc) {
@@ -131,7 +129,7 @@ export default class SharingManager extends React.Component<{}> {
if (state === SharingPermissions.None) {
const metadata = (await DocCastAsync(manager[key]));
if (metadata) {
- let sharedAlias = (await DocCastAsync(metadata.sharedAlias))!;
+ const sharedAlias = (await DocCastAsync(metadata.sharedAlias))!;
Doc.RemoveDocFromList(notificationDoc, storage, sharedAlias);
manager[key] = undefined;
}
@@ -146,7 +144,7 @@ export default class SharingManager extends React.Component<{}> {
}
private setExternalSharing = (state: string) => {
- let sharingDoc = this.sharingDoc;
+ const sharingDoc = this.sharingDoc;
if (!sharingDoc) {
return;
}
@@ -157,7 +155,7 @@ export default class SharingManager extends React.Component<{}> {
if (!this.targetDoc) {
return undefined;
}
- let baseUrl = Utils.prepend("/doc/" + this.targetDoc[Id]);
+ const baseUrl = Utils.prepend("/doc/" + this.targetDoc[Id]);
return `${baseUrl}?sharing=true`;
}
@@ -179,7 +177,7 @@ export default class SharingManager extends React.Component<{}> {
}
private focusOn = (contents: string) => {
- let title = this.targetDoc ? StrCast(this.targetDoc.title) : "";
+ const title = this.targetDoc ? StrCast(this.targetDoc.title) : "";
return (
<span
className={"focus-span"}
diff --git a/src/client/util/TooltipLinkingMenu.tsx b/src/client/util/TooltipLinkingMenu.tsx
index e6d6c471f..b46675a04 100644
--- a/src/client/util/TooltipLinkingMenu.tsx
+++ b/src/client/util/TooltipLinkingMenu.tsx
@@ -2,10 +2,6 @@ import { EditorState } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { FieldViewProps } from "../views/nodes/FieldView";
import "./TooltipTextMenu.scss";
-import React = require("react");
-const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands");
-
-const SVG = "http://www.w3.org/2000/svg";
//appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc.
export class TooltipLinkingMenu {
@@ -23,9 +19,9 @@ export class TooltipLinkingMenu {
//add the div which is the tooltip
view.dom.parentNode!.parentNode!.appendChild(this.tooltip);
- let target = "https://www.google.com";
+ const target = "https://www.google.com";
- let link = document.createElement("a");
+ const link = document.createElement("a");
link.href = target;
link.textContent = target;
link.target = "_blank";
@@ -37,7 +33,7 @@ export class TooltipLinkingMenu {
//updates the tooltip menu when the selection changes
update(view: EditorView, lastState: EditorState | undefined) {
- let state = view.state;
+ const state = view.state;
// Don't do anything if the document/selection didn't change
if (lastState && lastState.doc.eq(state.doc) &&
lastState.selection.eq(state.selection)) return;
@@ -53,16 +49,16 @@ export class TooltipLinkingMenu {
// 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);
+ const { from, to } = state.selection;
+ const 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();
+ const 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);
+ const 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;
+ const width = Math.abs(start.left - end.left) / 2 * this.editorProps.ScreenToLocalTransform().Scale;
+ const mid = Math.min(start.left, end.left) + width;
this.tooltip.style.width = "auto";
this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px";
diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss
index ebf833dbe..2a38fe118 100644
--- a/src/client/util/TooltipTextMenu.scss
+++ b/src/client/util/TooltipTextMenu.scss
@@ -1,100 +1,62 @@
@import "../views/globalCssVariables";
-
-.ProseMirror-textblock-dropdown {
- min-width: 3em;
- }
-
- .ProseMirror-menu {
- margin: 0 -4px;
- line-height: 1;
- }
-
- .ProseMirror-tooltip .ProseMirror-menu {
- width: -webkit-fit-content;
- width: fit-content;
- white-space: pre;
- }
-
- .ProseMirror-menuitem {
- margin-right: 3px;
+.ProseMirror-menu-dropdown-wrap {
display: inline-block;
- z-index: 50000;
position: relative;
- }
-
- .ProseMirror-menuseparator {
- // border-right: 1px solid #ddd;
- margin-right: 3px;
- }
-
- .ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu {
- font-size: 90%;
- white-space: nowrap;
- }
+}
- .ProseMirror-menu-dropdown {
+.ProseMirror-menu-dropdown {
vertical-align: 1px;
cursor: pointer;
position: relative;
- padding-right: 15px;
- margin: 3px;
+ padding: 0 15px 0 4px;
background: white;
- border-radius: 3px;
- text-align: center;
- }
-
- .ProseMirror-menu-dropdown-wrap {
- padding: 1px 0 1px 4px;
- display: inline-block;
+ border-radius: 2px;
+ text-align: left;
+ font-size: 12px;
+ white-space: nowrap;
+ margin-right: 4px;
+
+ &:after {
+ content: "";
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 4px solid currentColor;
+ opacity: .6;
+ position: absolute;
+ right: 4px;
+ top: calc(50% - 2px);
+ }
+}
+
+.ProseMirror-menu-submenu-wrap {
position: relative;
- }
-
- .ProseMirror-menu-dropdown:after {
- content: "";
- border-left: 4px solid transparent;
- border-right: 4px solid transparent;
- border-top: 4px solid currentColor;
- opacity: .6;
- position: absolute;
- right: 4px;
- top: calc(50% - 2px);
- }
-
- .ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu {
- background: $dark-color;
- color:white;
+ margin-right: -4px;
+}
+
+.ProseMirror-menu-dropdown-menu,
+.ProseMirror-menu-submenu {
+ font-size: 12px;
+ background: white;
border: 1px solid rgb(223, 223, 223);
- padding: 2px;
- }
-
- .ProseMirror-menu-dropdown-menu {
+ min-width: 40px;
z-index: 50000;
- min-width: 6em;
- background: white;
position: absolute;
- }
-
- .linking {
- text-align: center;
- }
+ box-sizing: content-box;
- .ProseMirror-menu-dropdown-item {
- cursor: pointer;
- padding: 2px 8px 2px 4px;
- width: auto;
- z-index: 100000;
- }
-
- .ProseMirror-menu-dropdown-item:hover {
- background: white;
- }
-
- .ProseMirror-menu-submenu-wrap {
- position: relative;
- margin-right: -4px;
- }
-
- .ProseMirror-menu-submenu-label:after {
+ .ProseMirror-menu-dropdown-item {
+ cursor: pointer;
+ z-index: 100000;
+ text-align: left;
+ padding: 3px;
+
+ &:hover {
+ background-color: $light-color-secondary;
+ }
+ }
+}
+
+
+.ProseMirror-menu-submenu-label:after {
content: "";
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
@@ -103,153 +65,51 @@
position: absolute;
right: 4px;
top: calc(50% - 4px);
- }
-
- .ProseMirror-menu-submenu {
- display: none;
- min-width: 4em;
- left: 100%;
- top: -3px;
- }
-
- .ProseMirror-menu-active {
- background: #eee;
- border-radius: 4px;
- }
-
- .ProseMirror-menu-active {
- background: #eee;
- border-radius: 4px;
- }
-
- .ProseMirror-menu-disabled {
- opacity: .3;
- }
-
- .ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu {
- display: block;
- }
-
- .ProseMirror-menubar {
- border-top-left-radius: inherit;
- border-top-right-radius: inherit;
- position: relative;
- min-height: 1em;
- color: white;
- padding: 10px 10px;
- top: 0; left: 0; right: 0;
- border-bottom: 1px solid silver;
- background:$dark-color;
- z-index: 10;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- overflow: visible;
- }
+}
.ProseMirror-icon {
display: inline-block;
- line-height: .8;
- vertical-align: -2px; /* Compensate for padding */
- padding: 2px 8px;
+ // line-height: .8;
+ // vertical-align: -2px; /* Compensate for padding */
+ // padding: 2px 8px;
cursor: pointer;
- }
-
- .ProseMirror-menu-disabled.ProseMirror-icon {
- cursor: default;
- }
-
- .ProseMirror-icon svg {
- fill:white;
- height: 1em;
- }
-
- .ProseMirror-icon span {
- vertical-align: text-top;
- }
-
- .ProseMirror ul, .ProseMirror ol {
- padding-left: 30px;
- }
-
- .ProseMirror blockquote {
- padding-left: 1em;
- border-left: 3px solid #eee;
- margin-left: 0; margin-right: 0;
- }
-
- .ProseMirror-example-setup-style img {
- cursor: default;
- }
-
- .ProseMirror-prompt {
- background: white;
- padding: 5px 10px 5px 15px;
- border: 1px solid silver;
- position: fixed;
- border-radius: 3px;
- z-index: 11;
- box-shadow: -.5px 2px 5px white(255, 255, 255, 0.2);
- }
-
- .ProseMirror-prompt h5 {
- margin: 0;
- font-weight: normal;
- font-size: 100%;
- color: #444;
- }
-
- .ProseMirror-prompt input[type="text"],
- .ProseMirror-prompt textarea {
- background: white;
- border: none;
- outline: none;
- }
-
- .ProseMirror-prompt input[type="text"] {
- padding: 0 4px;
- }
-
- .ProseMirror-prompt-close {
- position: absolute;
- left: 2px; top: 1px;
- color: #666;
- border: none; background: transparent; padding: 0;
- }
-
- .ProseMirror-prompt-close:after {
- content: "✕";
- font-size: 12px;
- }
-
- .ProseMirror-invalid {
- background: #ffc;
- border: 1px solid #cc7;
- border-radius: 4px;
- padding: 5px 10px;
- position: absolute;
- min-width: 10em;
- }
-
- .ProseMirror-prompt-buttons {
- margin-top: 5px;
- display: none;
+
+ &.ProseMirror-menu-disabled {
+ cursor: default;
+ }
+
+ svg {
+ fill:white;
+ height: 1em;
+ }
+
+ span {
+ vertical-align: text-top;
+ }
}
-.tooltipMenu {
+.wrapper {
position: absolute;
- z-index: 20000;
- background: #121721;
- border: 1px solid silver;
- border-radius: 15px;
- //height: 60px;
- //padding: 2px 10px;
- //margin-top: 100px;
- //-webkit-transform: translateX(-50%);
- //transform: translateX(-50%);
+ pointer-events: all;
+ display: flex;
+ align-items: center;
transform: translateY(-85px);
+
+ height: 35px;
+ background: #323232;
+ border-radius: 6px;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+
+}
+
+.tooltipMenu, .basic-tools {
+ z-index: 20000;
pointer-events: all;
- height: fit-content;
- width:550px;
+ padding: 3px;
+ padding-bottom: 5px;
+ display: flex;
+ align-items: center;
+
.ProseMirror-example-setup-style hr {
padding: 2px 10px;
border: none;
@@ -265,60 +125,89 @@
}
}
-.tooltipExtras {
- position: absolute;
- z-index: 20000;
- background: #121721;
- border: 1px solid silver;
- border-radius: 15px;
- //height: 60px;
- //padding: 2px 10px;
- //margin-top: 100px;
- //-webkit-transform: translateX(-50%);
- //transform: translateX(-50%);
- transform: translateY(-115px);
- pointer-events: all;
+.menuicon {
+ width: 25px;
height: 25px;
- width:fit-content;
- .ProseMirror-example-setup-style hr {
- padding: 2px 10px;
- border: none;
- margin: 1em 0;
+ cursor: pointer;
+ text-align: center;
+ line-height: 25px;
+ margin: 0 2px;
+ border-radius: 3px;
+
+ &:hover {
+ background-color: black;
+
+ #link-drag {
+ background-color: black;
+ }
}
-
- .ProseMirror-example-setup-style hr:after {
- content: "";
- display: block;
- height: 1px;
- background-color: silver;
- line-height: 2px;
+
+ &> * {
+ margin-top: 50%;
+ margin-left: 50%;
+ transform: translate(-50%, -50%);
}
-}
-.wrapper {
- position: absolute;
- pointer-events: all;
+ svg {
+ fill: inherit;
+ width: 18px;
+ height: 18px;
+ }
}
- .menuicon {
- display: inline-block;
- border-right: 1px solid white(0, 0, 0, 0.2);
- //color: rgb(19, 18, 18);
- color: rgb(226, 21, 21);
- line-height: 1;
- padding: 0px 2px;
- margin: 1px;
+.menuicon-active {
+ width: 25px;
+ height: 25px;
cursor: pointer;
text-align: center;
- min-width: 10px;
-
- }
- .strong, .heading { font-weight: bold; }
- .em { font-style: italic; }
- .underline {text-decoration: underline}
- .superscript {vertical-align:super}
- .subscript { vertical-align:sub }
- .strikethrough {text-decoration-line:line-through}
+ line-height: 25px;
+ margin: 0 2px;
+ border-radius: 3px;
+
+ &:hover {
+ background-color: black;
+ }
+
+ &> * {
+ margin-top: 50%;
+ margin-left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ svg {
+ fill: greenyellow;
+ width: 18px;
+ height: 18px;
+ }
+}
+
+.colorPicker {
+ position: relative;
+
+ svg {
+ width: 18px;
+ height: 18px;
+ // margin-top: 11px;
+ }
+
+ .buttonColor {
+ position: absolute;
+ top: 24px;
+ left: 1px;
+ width: 24px;
+ height: 4px;
+ margin-top: 0;
+ }
+}
+
+#link-drag {
+ background-color: #323232;
+}
+
+.underline svg {
+ margin-top: 13px;
+}
+
.font-size-indicator {
font-size: 12px;
padding-right: 0px;
@@ -328,8 +217,9 @@
height: 20px;
text-align: center;
}
+
- .brush{
+.brush{
display: inline-block;
width: 1em;
height: 1em;
@@ -337,19 +227,146 @@
stroke: currentColor;
fill: currentColor;
margin-right: 15px;
- }
+}
- .brush-active{
+.brush-active{
display: inline-block;
width: 1em;
height: 1em;
stroke-width: 3;
- stroke: greenyellow;
fill: greenyellow;
margin-right: 15px;
- }
+}
+
+.dragger-wrapper {
+ color: #eee;
+ height: 22px;
+ padding: 0 5px;
+ box-sizing: content-box;
+ cursor: grab;
- .dragger{
- color: #eee;
- margin-left: 5px;
- } \ No newline at end of file
+ .dragger {
+ width: 18px;
+ height: 100%;
+ display: flex;
+ justify-content: space-evenly;
+ }
+
+ .dragger-line {
+ width: 2px;
+ height: 100%;
+ background-color: black;
+ }
+}
+
+.button-dropdown-wrapper {
+ display: flex;
+ align-content: center;
+
+ &:hover {
+ background-color: black;
+ }
+}
+
+.buttonSettings-dropdown {
+
+ &.ProseMirror-menu-dropdown {
+ width: 10px;
+ height: 25px;
+ margin: 0;
+ padding: 0 2px;
+ background-color: #323232;
+ text-align: center;
+
+ &:after {
+ border-top: 4px solid white;
+ right: 2px;
+ }
+
+ &:hover {
+ background-color: black;
+ }
+ }
+
+ &.ProseMirror-menu-dropdown-menu {
+ min-width: 150px;
+ left: -27px;
+ top: 31px;
+ background-color: #323232;
+ border: 1px solid #4d4d4d;
+ color: $light-color-secondary;
+ // border: none;
+ // border: 1px solid $light-color-secondary;
+ border-radius: 0 6px 6px 6px;
+ padding: 3px;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+
+ .ProseMirror-menu-dropdown-item{
+ cursor: default;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &:hover {
+ background-color: #323232;
+ }
+
+ .button-setting, .button-setting-disabled {
+ padding: 2px;
+ border-radius: 2px;
+ }
+
+ .button-setting:hover {
+ cursor: pointer;
+ background-color: black;
+ }
+
+ .separated-button {
+ border-top: 1px solid $light-color-secondary;
+ padding-top: 6px;
+ }
+
+ input {
+ color: black;
+ border: none;
+ border-radius: 1px;
+ padding: 3px;
+ }
+
+ button {
+ padding: 6px;
+ background-color: #323232;
+ border: 1px solid black;
+ border-radius: 1px;
+
+ &:hover {
+ background-color: black;
+ }
+ }
+ }
+
+
+ }
+}
+
+.colorPicker-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ margin-top: 3px;
+ margin-left: -3px;
+ width: calc(100% + 6px);
+}
+
+button.colorPicker {
+ width: 20px;
+ height: 20px;
+ border-radius: 15px !important;
+ margin: 3px;
+ border: none !important;
+
+ &.active {
+ border: 2px solid white !important;
+ }
+} \ No newline at end of file
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 31d98887f..1c15dca7f 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -1,354 +1,230 @@
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faListUl } from '@fortawesome/free-solid-svg-icons';
-import { action, observable } from "mobx";
import { Dropdown, icons, MenuItem } from "prosemirror-menu"; //no import css
import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model";
import { wrapInList } from 'prosemirror-schema-list';
-import { EditorState, NodeSelection, TextSelection, Transaction } from "prosemirror-state";
+import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { Doc, Field, Opt } from "../../new_fields/Doc";
-import { Id } from "../../new_fields/FieldSymbols";
import { Utils } from "../../Utils";
import { DocServer } from "../DocServer";
import { FieldViewProps } from "../views/nodes/FieldView";
import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
-import { DocumentManager } from "./DocumentManager";
-import { DragManager } from "./DragManager";
import { LinkManager } from "./LinkManager";
import { schema } from "./RichTextSchema";
import "./TooltipTextMenu.scss";
-import { Cast, NumCast } from '../../new_fields/Types';
+import { Cast, NumCast, StrCast } from '../../new_fields/Types';
import { updateBullets } from './ProsemirrorExampleTransfer';
import { DocumentDecorations } from '../views/DocumentDecorations';
-const { toggleMark, setBlockType } = require("prosemirror-commands");
-const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js");
+import { SelectionManager } from './SelectionManager';
+import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../new_fields/SchemaHeaderField';
+const { toggleMark } = require("prosemirror-commands");
//appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc.
export class TooltipTextMenu {
+ public static Toolbar: HTMLDivElement | undefined;
- public tooltip: HTMLElement;
+ // editor state properties
private view: EditorView;
private editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
- private fontStyles: MarkType[];
- private fontSizes: MarkType[];
- private listTypes: (NodeType | any)[];
- private fontSizeToNum: Map<MarkType, number>;
- private fontStylesToName: Map<MarkType, string>;
- private listTypeToIcon: Map<NodeType | any, string>;
- //private link: HTMLAnchorElement;
- private wrapper: HTMLDivElement;
- private extras: HTMLDivElement;
-
- private linkEditor?: HTMLDivElement;
- private linkText?: HTMLDivElement;
- private linkDrag?: HTMLImageElement;
- //dropdown doms
+
+ private fontStyles: Mark[] = [];
+ private fontSizes: Mark[] = [];
+ private _marksToDoms: Map<MarkType, HTMLSpanElement> = new Map();
+ private _collapsed: boolean = false;
+
+ // editor doms
+ public tooltip: HTMLElement = document.createElement("div");
+ private wrapper: HTMLDivElement = document.createElement("div");
+
+ // editor button doms
+ private colorDom?: Node;
+ private colorDropdownDom?: Node;
+ private linkDom?: Node;
+ private highighterDom?: Node;
+ private highlighterDropdownDom?: Node;
+ private linkDropdownDom?: Node;
+ private _brushdom?: Node;
+ private _brushDropdownDom?: Node;
private fontSizeDom?: Node;
private fontStyleDom?: Node;
- private listTypeBtnDom?: Node;
+ private basicTools?: HTMLElement;
- private _activeMarks: Mark[] = [];
+ static createDiv(className: string) { const div = document.createElement("div"); div.className = className; return div; }
+ static createSpan(className: string) { const div = document.createElement("span"); div.className = className; return div; }
+ constructor(view: EditorView) {
+ this.view = view;
- private _collapseBtn?: MenuItem;
+ // initialize the tooltip -- sets this.tooltip
+ this.initTooltip(view);
- private _brushMarks?: Set<Mark>;
- private _brushIsEmpty: boolean = true;
- private _brushdom?: Node;
+ // initialize the wrapper
+ this.wrapper = TooltipTextMenu.createDiv("wrapper");
+ this.wrapper.appendChild(this.tooltip);
- private _marksToDoms: Map<Mark, HTMLSpanElement> = new Map();
+ TooltipTextMenu.Toolbar = this.wrapper;
+ }
- private _collapsed: boolean = false;
+ private async initTooltip(view: EditorView) {
+ const self = this;
+ this.tooltip = TooltipTextMenu.createDiv("tooltipMenu");
+ this.basicTools = TooltipTextMenu.createDiv("basic-tools");
- constructor(view: EditorView) {
- this.view = view;
+ const svgIcon = (name: string, title: string = name, dpath: string) => {
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ svg.setAttribute("viewBox", "-100 -100 650 650");
+ const path = document.createElementNS('http://www.w3.org/2000/svg', "path");
+ path.setAttributeNS(null, "d", dpath);
+ svg.appendChild(path);
- this.wrapper = document.createElement("div");
- this.tooltip = document.createElement("div");
- this.extras = document.createElement("div");
+ const span = TooltipTextMenu.createSpan(name + " menuicon");
+ span.title = title;
+ span.appendChild(svg);
- this.wrapper.appendChild(this.extras);
- this.wrapper.appendChild(this.tooltip);
+ return span;
+ };
- this.tooltip.className = "tooltipMenu";
- this.extras.className = "tooltipExtras";
- this.wrapper.className = "wrapper";
-
- const dragger = document.createElement("span");
- dragger.className = "dragger";
- dragger.textContent = ">>>";
- this.extras.appendChild(dragger);
-
- this.dragElement(dragger);
-
- // 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);
-
- //add additional icons
- library.add(faListUl);
- //add the buttons to the tooltip
- let items = [
- { 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') }
+ const basicItems = [ // init basicItems in minimized toolbar -- paths to svgs are obtained from fontawesome
+ { mark: schema.marks.strong, dom: svgIcon("strong", "Bold", "M333.49 238a122 122 0 0 0 27-65.21C367.87 96.49 308 32 233.42 32H34a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h31.87v288H34a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h209.32c70.8 0 134.14-51.75 141-122.4 4.74-48.45-16.39-92.06-50.83-119.6zM145.66 112h87.76a48 48 0 0 1 0 96h-87.76zm87.76 288h-87.76V288h87.76a56 56 0 0 1 0 112z") },
+ { mark: schema.marks.em, dom: svgIcon("em", "Italic", "M320 48v32a16 16 0 0 1-16 16h-62.76l-80 320H208a16 16 0 0 1 16 16v32a16 16 0 0 1-16 16H16a16 16 0 0 1-16-16v-32a16 16 0 0 1 16-16h62.76l80-320H112a16 16 0 0 1-16-16V48a16 16 0 0 1 16-16h192a16 16 0 0 1 16 16z") },
+ { mark: schema.marks.underline, dom: svgIcon("underline", "Underline", "M32 64h32v160c0 88.22 71.78 160 160 160s160-71.78 160-160V64h32a16 16 0 0 0 16-16V16a16 16 0 0 0-16-16H272a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h32v160a80 80 0 0 1-160 0V64h32a16 16 0 0 0 16-16V16a16 16 0 0 0-16-16H32a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16zm400 384H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z") },
+ ];
+ const items = [ // init items in full size toolbar
+ { mark: schema.marks.strikethrough, dom: svgIcon("strikethrough", "Strikethrough", "M496 224H293.9l-87.17-26.83A43.55 43.55 0 0 1 219.55 112h66.79A49.89 49.89 0 0 1 331 139.58a16 16 0 0 0 21.46 7.15l42.94-21.47a16 16 0 0 0 7.16-21.46l-.53-1A128 128 0 0 0 287.51 32h-68a123.68 123.68 0 0 0-123 135.64c2 20.89 10.1 39.83 21.78 56.36H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h480a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm-180.24 96A43 43 0 0 1 336 356.45 43.59 43.59 0 0 1 292.45 400h-66.79A49.89 49.89 0 0 1 181 372.42a16 16 0 0 0-21.46-7.15l-42.94 21.47a16 16 0 0 0-7.16 21.46l.53 1A128 128 0 0 0 224.49 480h68a123.68 123.68 0 0 0 123-135.64 114.25 114.25 0 0 0-5.34-24.36z") },
+ { mark: schema.marks.superscript, dom: svgIcon("superscript", "Superscript", "M496 160h-16V16a16 16 0 0 0-16-16h-48a16 16 0 0 0-14.29 8.83l-16 32A16 16 0 0 0 400 64h16v96h-16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h96a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM336 64h-67a16 16 0 0 0-13.14 6.87l-79.9 115-79.9-115A16 16 0 0 0 83 64H16A16 16 0 0 0 0 80v48a16 16 0 0 0 16 16h33.48l77.81 112-77.81 112H16a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h67a16 16 0 0 0 13.14-6.87l79.9-115 79.9 115A16 16 0 0 0 269 448h67a16 16 0 0 0 16-16v-48a16 16 0 0 0-16-16h-33.48l-77.81-112 77.81-112H336a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16z") },
+ { mark: schema.marks.subscript, dom: svgIcon("subscript", "Subscript", "M496 448h-16V304a16 16 0 0 0-16-16h-48a16 16 0 0 0-14.29 8.83l-16 32A16 16 0 0 0 400 352h16v96h-16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h96a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM336 64h-67a16 16 0 0 0-13.14 6.87l-79.9 115-79.9-115A16 16 0 0 0 83 64H16A16 16 0 0 0 0 80v48a16 16 0 0 0 16 16h33.48l77.81 112-77.81 112H16a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h67a16 16 0 0 0 13.14-6.87l79.9-115 79.9 115A16 16 0 0 0 269 448h67a16 16 0 0 0 16-16v-48a16 16 0 0 0-16-16h-33.48l-77.81-112 77.81-112H336a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16z") },
];
- this._marksToDoms = new Map();
- //add menu items
- items.forEach(({ dom, command }) => {
+ basicItems.map(({ dom, mark }) => this.basicTools ?.appendChild(dom.cloneNode(true)));
+ basicItems.concat(items).forEach(({ dom, mark }) => {
this.tooltip.appendChild(dom);
- switch (dom.title) {
- case "Bold":
- this._marksToDoms.set(schema.mark(schema.marks.strong), dom);
- break;
- case "Italic":
- this._marksToDoms.set(schema.mark(schema.marks.em), dom);
- break;
- case "Underline":
- this._marksToDoms.set(schema.mark(schema.marks.underline), dom);
- break;
- }
+ this._marksToDoms.set(mark, dom);
//pointer down handler to activate button effects
dom.addEventListener("pointerdown", e => {
- e.preventDefault();
this.view.focus();
if (dom.contains(e.target as Node)) {
+ e.preventDefault();
e.stopPropagation();
- command(this.view.state, this.view.dispatch, this.view);
- // if (this.view.state.selection.empty) {
- // if (dom.style.color === "white") { dom.style.color = "greenyellow"; }
- // else { dom.style.color = "white"; }
- // }
+ toggleMark(mark)(this.view.state, this.view.dispatch, this.view);
+ this.updateHighlightStateOfButtons();
}
});
-
- });
- this.updateLinkMenu();
-
-
- //list of font styles
- this.fontStylesToName = new Map();
- this.fontStylesToName.set(schema.marks.timesNewRoman, "Times New Roman");
- this.fontStylesToName.set(schema.marks.arial, "Arial");
- this.fontStylesToName.set(schema.marks.georgia, "Georgia");
- this.fontStylesToName.set(schema.marks.comicSans, "Comic Sans MS");
- this.fontStylesToName.set(schema.marks.tahoma, "Tahoma");
- this.fontStylesToName.set(schema.marks.impact, "Impact");
- this.fontStylesToName.set(schema.marks.crimson, "Crimson Text");
- this.fontStyles = Array.from(this.fontStylesToName.keys());
-
- //font sizes
- this.fontSizeToNum = new Map();
- this.fontSizeToNum.set(schema.marks.p10, 10);
- this.fontSizeToNum.set(schema.marks.p12, 12);
- this.fontSizeToNum.set(schema.marks.p14, 14);
- this.fontSizeToNum.set(schema.marks.p16, 16);
- this.fontSizeToNum.set(schema.marks.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, 12);
- // this.fontSizeToNum.set(schema.marks.pFontSize, 14);
- // this.fontSizeToNum.set(schema.marks.pFontSize, 16);
- // this.fontSizeToNum.set(schema.marks.pFontSize, 18);
- // this.fontSizeToNum.set(schema.marks.pFontSize, 20);
- // this.fontSizeToNum.set(schema.marks.pFontSize, 24);
- // this.fontSizeToNum.set(schema.marks.pFontSize, 32);
- // this.fontSizeToNum.set(schema.marks.pFontSize, 48);
- // this.fontSizeToNum.set(schema.marks.pFontSize, 72);
- this.fontSizes = Array.from(this.fontSizeToNum.keys());
-
- //list types
- this.listTypeToIcon = new Map();
- this.listTypeToIcon.set(schema.nodes.bullet_list, ":");
- this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "decimal" }), "1.1");
- this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "multi" }), "1.A");
- // this.listTypeToIcon.set(schema.nodes.bullet_list, "⬜");
- this.listTypes = Array.from(this.listTypeToIcon.keys());
-
- //custom tools
- // this.tooltip.appendChild(this.createLink().render(this.view).dom);
-
- this._brushdom = this.createBrush().render(this.view).dom;
- this.tooltip.appendChild(this._brushdom);
- this.tooltip.appendChild(this.createLink().render(this.view).dom);
- this.tooltip.appendChild(this.createStar().render(this.view).dom);
-
- this.updateListItemDropdown(":", this.listTypeBtnDom);
-
- this.updateFromDash(view, undefined, undefined);
- TooltipTextMenu.Toolbar = this.wrapper;
- }
- public static Toolbar: HTMLDivElement | undefined;
-
- //label of dropdown will change to given label
- updateFontSizeDropdown(label: string) {
- //filtering function - might be unecessary
- let cut = (arr: MenuItem[]) => arr.filter(x => x);
-
- //font SIZES
- let fontSizeBtns: MenuItem[] = [];
- this.fontSizeToNum.forEach((number, mark) => {
- fontSizeBtns.push(this.dropdownMarkBtn(String(number), "color: black; width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes));
});
- let newfontSizeDom = (new Dropdown(cut(fontSizeBtns), {
- label: label,
- css: "color:black; min-width: 60px; padding-left: 5px; margin-right: 0;"
- }) as MenuItem).render(this.view).dom;
- if (this.fontSizeDom) { this.tooltip.replaceChild(newfontSizeDom, this.fontSizeDom); }
- else {
- this.tooltip.appendChild(newfontSizeDom);
- }
- this.fontSizeDom = newfontSizeDom;
- }
-
- // Make the DIV element draggable
-
- //label of dropdown will change to given label
- updateFontStyleDropdown(label: string) {
- //filtering function - might be unecessary
- let cut = (arr: MenuItem[]) => arr.filter(x => x);
+ // summarize menu
+ this.highighterDom = this.createHighlightTool().render(this.view).dom;
+ this.highlighterDropdownDom = this.createHighlightDropdown().render(this.view).dom;
+ this.tooltip.appendChild(this.highighterDom);
+ this.tooltip.appendChild(this.highlighterDropdownDom);
+
+ // color menu
+ this.colorDom = this.createColorTool().render(this.view).dom;
+ this.colorDropdownDom = this.createColorDropdown().render(this.view).dom;
+ this.tooltip.appendChild(this.colorDom);
+ this.tooltip.appendChild(this.colorDropdownDom);
+
+ // link menu
+ this.linkDom = this.createLinkTool().render(this.view).dom;
+ this.linkDropdownDom = this.createLinkDropdown("").render(this.view).dom;
+ this.tooltip.appendChild(this.linkDom);
+ this.tooltip.appendChild(this.linkDropdownDom);
+
+ // list of font styles
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 7 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 8 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 9 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 10 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 12 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 14 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 16 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 18 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 20 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 24 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 32 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 48 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 72 }));
+
+ // font sizes
+ this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Times New Roman" }));
+ this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Arial" }));
+ this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Georgia" }));
+ this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Comic Sans MS" }));
+ this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Tahoma" }));
+ this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Impact" }));
+ this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Crimson Text" }));
+
+
+ // init brush tool
+ this._brushdom = this.createBrushTool().render(this.view).dom;
+ this.tooltip.appendChild(this._brushdom);
+ this._brushDropdownDom = this.createBrushDropdown().render(this.view).dom;
+ this.tooltip.appendChild(this._brushDropdownDom);
- //font STYLES
- let fontBtns: MenuItem[] = [];
- this.fontStylesToName.forEach((name, mark) => {
- fontBtns.push(this.dropdownMarkBtn(name, "color: black; font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles));
+ // summarizer tool
+ const summarizer = new MenuItem({
+ title: "Summarize",
+ label: "Summarize",
+ icon: icons.join,
+ css: "fill:white;",
+ class: "menuicon",
+ execEvent: "",
+ run: (state, dispatch) => TooltipTextMenu.insertSummarizer(state, dispatch)
});
-
- let newfontStyleDom = (new Dropdown(cut(fontBtns), {
- label: label,
- 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;
-
- }
-
- updateLinkMenu() {
- if (!this.linkEditor || !this.linkText) {
- this.linkEditor = document.createElement("div");
- 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";
- this.linkText.style.marginLeft = "5px";
- this.linkText.setAttribute("contenteditable", "true");
- this.linkText.style.whiteSpace = "nowrap";
- this.linkText.style.width = "150px";
- this.linkText.style.overflow = "hidden";
- this.linkText.style.color = "white";
- this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); };
- let linkBtn = document.createElement("div");
- linkBtn.textContent = ">>";
- linkBtn.style.width = "10px";
- linkBtn.style.height = "10px";
- linkBtn.style.color = "white";
- linkBtn.style.cssFloat = "left";
- linkBtn.onpointerdown = (e: PointerEvent) => {
- let node = this.view.state.selection.$from.nodeAfter;
- let link = node && node.marks.find(m => m.type.name === "link");
- if (link) {
- let href: string = link.attrs.href;
- if (href.indexOf(Utils.prepend("/doc/")) === 0) {
- let docid = href.replace(Utils.prepend("/doc/"), "");
- DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
- if (f instanceof Doc) {
- if (DocumentManager.Instance.getDocumentView(f)) {
- DocumentManager.Instance.getDocumentView(f)!.props.focus(f, false);
- }
- else this.editorProps && this.editorProps.addDocTab(f, undefined, "onRight");
- }
- }));
+ this.tooltip.appendChild(summarizer.render(this.view).dom);
+
+ // list types dropdown
+ const listDropdownTypes = [{ mapStyle: "bullet", label: ":" }, { mapStyle: "decimal", label: "1.1" }, { mapStyle: "multi", label: "A.1" }, { label: "X" }];
+ const listTypes = new Dropdown(listDropdownTypes.map(({ mapStyle, label }) =>
+ new MenuItem({
+ title: "Set Bullet Style",
+ label: label,
+ execEvent: "",
+ class: "dropdown-item",
+ css: "color: black; width: 40px;",
+ enable() { return true; },
+ run() {
+ const marks = self.view.state.storedMarks || (view.state.selection.$to.parentOffset && view.state.selection.$from.marks());
+ if (!wrapInList(schema.nodes.ordered_list)(view.state, (tx2: any) => {
+ const tx3 = updateBullets(tx2, schema, mapStyle);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+
+ view.dispatch(tx2);
+ })) {
+ const tx2 = view.state.tr;
+ const tx3 = updateBullets(tx2, schema, mapStyle);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+
+ view.dispatch(tx3);
}
- // TODO This should have an else to handle external links
- e.stopPropagation();
- e.preventDefault();
}
- };
- this.linkDrag = document.createElement("img");
- this.linkDrag.src = "https://seogurusnyc.com/wp-content/uploads/2016/12/link-1.png";
- this.linkDrag.style.width = "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) => {
- if (!this.editorProps) return;
- let dragData = new DragManager.LinkDragData(this.editorProps.Document);
- dragData.dontClearTextBox = true;
- // hack to get source context -sy
- let docView = DocumentManager.Instance.getDocumentView(this.editorProps.Document);
- e.stopPropagation();
- let ctrlKey = e.ctrlKey;
- DragManager.StartLinkDrag(this.linkDrag!, dragData, e.clientX, e.clientY,
- {
- handlers: {
- dragComplete: action(() => {
- if (dragData.linkDocument) {
- let linkDoc = dragData.linkDocument;
- let proto = Doc.GetProto(linkDoc);
- if (proto && docView) {
- proto.sourceContext = docView.props.ContainingCollectionDoc;
- }
- let text = this.makeLink(linkDoc, ctrlKey ? "onRight" : "inTab");
- if (linkDoc instanceof Doc && linkDoc.anchor2 instanceof Doc) {
- proto.title = text === "" ? proto.title : text + " to " + linkDoc.anchor2.title; // TODODO open to more descriptive descriptions of following in text link
- }
- }
- }),
- },
- hideSource: false
- });
- e.stopPropagation();
- e.preventDefault();
- };
- this.linkEditor.appendChild(this.linkDrag);
- this.tooltip.appendChild(this.linkEditor);
- }
-
- let node = this.view.state.selection.$from.nodeAfter;
- let link = node && node.marks.find(m => m.type.name === "link");
- this.linkText.textContent = link ? link.attrs.href : "-empty-";
-
- this.linkText.onkeydown = (e: KeyboardEvent) => {
- if (e.key === "Enter") {
- // this.makeLink(this.linkText!.textContent!);
- e.stopPropagation();
- e.preventDefault();
- }
- };
- // this.tooltip.appendChild(this.linkEditor);
+ })), { label: ":", css: "color:black; width: 40px;" });
+ this.tooltip.appendChild(listTypes.render(this.view).dom);
+
+ await this.updateFromDash(view, undefined, undefined);
+
+ const draggerWrapper = TooltipTextMenu.createDiv("dragger-wrapper");
+ const dragger = TooltipTextMenu.createDiv("dragger");
+ dragger.appendChild(TooltipTextMenu.createSpan("dragger-line"));
+ dragger.appendChild(TooltipTextMenu.createSpan("dragger-line"));
+ dragger.appendChild(TooltipTextMenu.createSpan("dragger-line"));
+ draggerWrapper.appendChild(dragger);
+ this.wrapper.appendChild(draggerWrapper);
+ this.setupDragElementInteractions(draggerWrapper);
}
- dragElement(elmnt: HTMLElement) {
+ setupDragElementInteractions(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;
+ elmnt.onpointerdown = dragPointerDown;
elmnt.ondblclick = onClick;
}
const self = this;
- function dragMouseDown(e: PointerEvent) {
+ function dragPointerDown(e: PointerEvent) {
e = e || window.event;
- //e.preventDefault();
+ e.preventDefault();
// get the mouse cursor position at startup:
pos3 = e.clientX;
pos4 = e.clientY;
@@ -360,11 +236,13 @@ export class TooltipTextMenu {
function onClick(e: MouseEvent) {
self._collapsed = !self._collapsed;
const children = self.wrapper.childNodes;
- if (self._collapsed && children.length > 1) {
+ if (self._collapsed && children.length > 0) {
self.wrapper.removeChild(self.tooltip);
+ self.basicTools && self.wrapper.prepend(self.basicTools);
}
else {
- self.wrapper.appendChild(self.tooltip);
+ self.wrapper.prepend(self.tooltip);
+ self.basicTools && self.wrapper.removeChild(self.basicTools);
}
}
@@ -388,583 +266,697 @@ export class TooltipTextMenu {
// stop moving when mouse button is released:
document.onpointerup = null;
document.onpointermove = null;
- //self.highlightSearchTerms(self.state, ["hello"]);
- //FormattedTextBox.Instance.unhighlightSearchTerms();
}
}
- // makeLinkWithState = (state: EditorState, target: string, location: string) => {
- // let link = state.schema.mark(state.schema.marks.link, { href: target, location: location });
- // }
-
- makeLink = (targetDoc: Doc, location: string): string => {
- let target = Utils.prepend("/doc/" + targetDoc[Id]);
- let node = this.view.state.selection.$from.nodeAfter;
- let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location, guid: targetDoc[Id] });
- 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");
- if (node) {
- if (node.text) {
- return node.text;
- }
+ //label of dropdown will change to given label
+ updateFontSizeDropdown(label: string) {
+ //font SIZES
+ const fontSizeBtns: MenuItem[] = [];
+ const self = this;
+ this.fontSizes.forEach(mark =>
+ fontSizeBtns.push(new MenuItem({
+ title: "Set Font Size",
+ label: String(mark.attrs.fontSize),
+ execEvent: "",
+ class: "dropdown-item",
+ css: "color: black; width: 50px;",
+ enable() { return true; },
+ run() {
+ const size = mark.attrs.fontSize;
+ if (size) { self.updateFontSizeDropdown(String(size) + " pt"); }
+ if (self.editorProps) {
+ const ruleProvider = self.editorProps.ruleProvider;
+ const heading = NumCast(self.editorProps.Document.heading);
+ if (ruleProvider && heading) {
+ ruleProvider["ruleSize_" + heading] = size;
+ }
+ }
+ TooltipTextMenu.setMark(self.view.state.schema.marks.pFontSize.create({ fontSize: size }), self.view.state, self.view.dispatch);
+ }
+ })));
+
+ const newfontSizeDom = (new Dropdown(fontSizeBtns, { label: label, css: "color:black; min-width: 60px;" }) as MenuItem).render(this.view).dom;
+ if (this.fontSizeDom) {
+ this.tooltip.replaceChild(newfontSizeDom, this.fontSizeDom);
}
- return "";
+ else {
+ this.tooltip.appendChild(newfontSizeDom);
+ }
+ this.fontSizeDom = newfontSizeDom;
}
- deleteLink = () => {
- let node = this.view.state.selection.$from.nodeAfter;
- let link = node && node.marks.find(m => m.type.name === "link");
- let href = link!.attrs.href;
- if (href) {
- if (href.indexOf(Utils.prepend("/doc/")) === 0) {
- const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
- if (linkclicked) {
- DocServer.GetRefField(linkclicked).then(async linkDoc => {
+ //label of dropdown will change to given label
+ updateFontStyleDropdown(label: string) {
+ //font STYLES
+ const fontBtns: MenuItem[] = [];
+ const self = this;
+ this.fontStyles.forEach(mark =>
+ fontBtns.push(new MenuItem({
+ title: "Set Font Family",
+ label: mark.attrs.family,
+ execEvent: "",
+ class: "dropdown-item",
+ css: "color: black; font-family: " + mark.attrs.family + ", sans-serif; width: 125px;",
+ enable() { return true; },
+ run() {
+ const fontName = mark.attrs.family;
+ if (fontName) { self.updateFontStyleDropdown(fontName); }
+ if (self.editorProps) {
+ const ruleProvider = self.editorProps.ruleProvider;
+ const heading = NumCast(self.editorProps.Document.heading);
+ if (ruleProvider && heading) {
+ ruleProvider["ruleFont_" + heading] = fontName;
+ }
+ }
+ TooltipTextMenu.setMark(self.view.state.schema.marks.pFontFamily.create({ family: fontName }), self.view.state, self.view.dispatch);
+ }
+ })));
+
+ const newfontStyleDom = (new Dropdown(fontBtns, { label: label, css: "color:black; width: 125px;" }) as MenuItem).render(this.view).dom;
+ if (this.fontStyleDom) {
+ this.tooltip.replaceChild(newfontStyleDom, this.fontStyleDom);
+ }
+ else {
+ this.tooltip.appendChild(newfontStyleDom);
+ }
+ this.fontStyleDom = newfontStyleDom;
+ }
+ async getTextLinkTargetTitle() {
+ const node = this.view.state.selection.$from.nodeAfter;
+ const link = node && node.marks.find(m => m.type.name === "link");
+ if (link) {
+ const href = link.attrs.href;
+ if (href) {
+ if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+ const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ if (linkclicked) {
+ const linkDoc = await DocServer.GetRefField(linkclicked);
if (linkDoc instanceof Doc) {
- LinkManager.Instance.deleteLink(linkDoc);
- this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link));
+ const anchor1 = await Cast(linkDoc.anchor1, Doc);
+ const anchor2 = await Cast(linkDoc.anchor2, Doc);
+ const currentDoc = SelectionManager.SelectedDocuments().length && SelectionManager.SelectedDocuments()[0].props.Document;
+ if (currentDoc && anchor1 && anchor2) {
+ if (Doc.AreProtosEqual(currentDoc, anchor1)) {
+ return StrCast(anchor2.title);
+ }
+ if (Doc.AreProtosEqual(currentDoc, anchor2)) {
+ return StrCast(anchor1.title);
+ }
+ }
}
- });
+ }
+ } else {
+ return href;
}
+ } else {
+ return link.attrs.title;
}
}
-
-
}
- public static insertStar(state: EditorState<any>, dispatch: any) {
- if (state.selection.empty) return false;
- let mark = state.schema.marks.highlight.create();
- let tr = state.tr;
- tr.addMark(state.selection.from, state.selection.to, mark);
- let content = tr.selection.content();
- let newNode = state.schema.nodes.star.create({ visibility: false, text: content, textslice: content.toJSON() });
- dispatch && dispatch(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
- return true;
+ // LINK TOOL
+ createLinkTool(active: boolean = false) {
+ return new MenuItem({
+ title: "Link tool",
+ label: "Link tool",
+ icon: icons.link,
+ css: "fill:white;",
+ class: active ? "menuicon-active" : "menuicon",
+ execEvent: "",
+ run: async (state, dispatch) => { },
+ active: (state) => true
+ });
}
- //will display a remove-list-type button if selection is in list, otherwise will show list type dropdown
- updateListItemDropdown(label: string, listTypeBtn: any) {
- //remove old btn
- if (listTypeBtn) { this.tooltip.removeChild(listTypeBtn); }
+ createLinkDropdown(targetTitle: string) {
+ const input = document.createElement("input");
- //Make a dropdown of all list types
- let toAdd: MenuItem[] = [];
- this.listTypeToIcon.forEach((icon, type) => {
- toAdd.push(this.dropdownNodeBtn(icon, "color: black; width: 40px;", type, this.view, this.listTypes, this.changeToNodeType));
+ // menu item for input for hyperlink url
+ // TODO: integrate search to allow users to search for a doc to link to
+ const linkInfo = new MenuItem({
+ title: "",
+ execEvent: "",
+ class: "button-setting-disabled",
+ css: "",
+ render() {
+ const p = document.createElement("p");
+ p.textContent = "Linked to:";
+
+ input.type = "text";
+ input.placeholder = "Enter URL";
+ if (targetTitle) input.value = targetTitle;
+ input.onclick = (e: MouseEvent) => {
+ input.select();
+ input.focus();
+ };
+
+ const div = document.createElement("div");
+ div.appendChild(p);
+ div.appendChild(input);
+ return div;
+ },
+ enable() { return false; },
+ run(p1, p2, p3, event) { event.stopPropagation(); }
});
- //option to remove the list formatting
- toAdd.push(this.dropdownNodeBtn("X", "color: black; width: 40px;", undefined, this.view, this.listTypes, this.changeToNodeType));
-
- listTypeBtn = (new Dropdown(toAdd, {
- label: label,
- css: "color:black; width: 40px;"
- }) as MenuItem).render(this.view).dom;
- //add this new button and return it
- this.tooltip.appendChild(listTypeBtn);
- return listTypeBtn;
- }
+ // menu item to update/apply the hyperlink to the selected text
+ const linkApply = new MenuItem({
+ title: "",
+ execEvent: "",
+ class: "",
+ css: "",
+ render() {
+ const button = document.createElement("button");
+ button.className = "link-url-button";
+ button.textContent = "Apply hyperlink";
+ return button;
+ },
+ enable() { return false; },
+ run: async (state, dispatch, view, event) => {
+ event.stopPropagation();
+ let node = this.view.state.selection.$from.nodeAfter;
+ let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: input.value, location: "onRight" });
+ 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");
+
+ // update link menu
+ const linkDom = self.createLinkTool(true).render(self.view).dom;
+ const linkDropdownDom = self.createLinkDropdown(await self.getTextLinkTargetTitle()).render(self.view).dom;
+ self.linkDom && self.tooltip.replaceChild(linkDom, self.linkDom);
+ self.linkDropdownDom && self.tooltip.replaceChild(linkDropdownDom, self.linkDropdownDom);
+ self.linkDom = linkDom;
+ self.linkDropdownDom = linkDropdownDom;
+ }
+ });
- //for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected text
- changeToMarkInGroup = (markType: MarkType | undefined, view: EditorView, fontMarks: MarkType[]) => {
- let { $cursor, ranges } = view.state.selection as TextSelection;
- let state = view.state;
- let dispatch = view.dispatch;
-
- //remove all other active font marks
- fontMarks.forEach((type) => {
- if (dispatch) {
- if ($cursor) {
- if (type.isInSet(state.storedMarks || $cursor.marks())) {
- dispatch(state.tr.removeStoredMark(type));
- }
- } else {
- 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) {
- if (has) {
- toggleMark(type)(view.state, view.dispatch, view);
+ // menu item to remove the link
+ // TODO: allow this to be undoable
+ const self = this;
+ const deleteLink = new MenuItem({
+ title: "Delete link",
+ execEvent: "",
+ class: "separated-button",
+ css: "",
+ render() {
+ const button = document.createElement("button");
+ button.textContent = "Remove link";
+
+ const wrapper = document.createElement("div");
+ wrapper.appendChild(button);
+ return wrapper;
+ },
+ enable() { return true; },
+ async run() {
+ // delete the link
+ const node = self.view.state.selection.$from.nodeAfter;
+ const link = node && node.marks.find(m => m.type === self.view.state.schema.marks.link);
+ const href = link!.attrs.href;
+ if (href ?.indexOf(Utils.prepend("/doc/")) === 0) {
+ const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ linkclicked && DocServer.GetRefField(linkclicked).then(async linkDoc => {
+ if (linkDoc instanceof Doc) {
+ LinkManager.Instance.deleteLink(linkDoc);
+ self.view.dispatch(self.view.state.tr.removeMark(self.view.state.selection.from, self.view.state.selection.to, self.view.state.schema.marks.link));
}
- }
+ });
}
+ // update link menu
+ const linkDom = self.createLinkTool(false).render(self.view).dom;
+ const linkDropdownDom = self.createLinkDropdown("").render(self.view).dom;
+ self.linkDom && self.tooltip.replaceChild(linkDom, self.linkDom);
+ self.linkDropdownDom && self.tooltip.replaceChild(linkDropdownDom, self.linkDropdownDom);
+ self.linkDom = linkDom;
+ self.linkDropdownDom = linkDropdownDom;
}
});
- if (markType) {
- // fontsize
- if (markType.name[0] === 'p') {
- let size = this.fontSizeToNum.get(markType);
- if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
- if (this.editorProps) {
- let ruleProvider = this.editorProps.ruleProvider;
- let heading = NumCast(this.editorProps.Document.heading);
- if (ruleProvider && heading) {
- ruleProvider["ruleSize_" + heading] = size;
- }
- }
- }
- else {
- let fontName = this.fontStylesToName.get(markType);
- if (fontName) { this.updateFontStyleDropdown(fontName); }
- if (this.editorProps) {
- let ruleProvider = this.editorProps.ruleProvider;
- let heading = NumCast(this.editorProps.Document.heading);
- if (ruleProvider && heading) {
- ruleProvider["ruleFont_" + heading] = fontName;
- }
- }
- }
- //actually apply font
- if ((view.state.selection as any).node && (view.state.selection as any).node.type === view.state.schema.nodes.ordered_list) {
- let status = updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
- { ...(view.state.selection as NodeSelection).node.attrs, setFontFamily: markType.name, setFontSize: Number(markType.name.replace(/p/, "")) }), view.state.schema);
- view.dispatch(status.setSelection(new NodeSelection(status.doc.resolve(view.state.selection.from))));
- }
- else toggleMark(markType)(view.state, view.dispatch, view);
- }
+ return new Dropdown(targetTitle ? [linkInfo, linkApply, deleteLink] : [linkInfo, linkApply], { class: "buttonSettings-dropdown" }) as MenuItem;
}
- //remove all node typeand apply the passed-in one to the selected text
- changeToNodeType = (nodeType: NodeType | undefined, view: EditorView) => {
- //remove oldif (nodeType) { //add new
- if (nodeType === schema.nodes.bullet_list) {
- wrapInList(nodeType)(view.state, view.dispatch);
- } else {
- var marks = view.state.storedMarks || (view.state.selection.$to.parentOffset && view.state.selection.$from.marks());
- if (!wrapInList(schema.nodes.ordered_list)(view.state, (tx2: any) => {
- let tx3 = updateBullets(tx2, schema, (nodeType as any).attrs.mapStyle);
- marks && tx3.ensureMarks([...marks]);
- marks && tx3.setStoredMarks([...marks]);
-
- view.dispatch(tx2);
- })) {
- let tx2 = view.state.tr;
- let tx3 = nodeType ? updateBullets(tx2, schema, (nodeType as any).attrs.mapStyle) : tx2;
- marks && tx3.ensureMarks([...marks]);
- marks && tx3.setStoredMarks([...marks]);
-
- view.dispatch(tx3);
- }
+ public MakeLinkToSelection = (linkDocId: string, title: string, location: string, targetDocId: string): string => {
+ const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, targetId: targetDocId });
+ this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link).
+ addMark(this.view.state.selection.from, this.view.state.selection.to, link));
+ return this.view.state.selection.$from.nodeAfter ?.text || "";
+ }
+
+ // SUMMARIZER TOOL
+ static insertSummarizer(state: EditorState<any>, dispatch: any) {
+ if (!state.selection.empty) {
+ const mark = state.schema.marks.summarize.create();
+ const tr = state.tr.addMark(state.selection.from, state.selection.to, mark);
+ const content = tr.selection.content();
+ const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() });
+ dispatch ?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
}
}
- //makes a button for the drop down FOR MARKS
- //css is the style you want applied to the button
- dropdownMarkBtn(label: string, css: string, markType: MarkType, view: EditorView, changeToMarkInGroup: (markType: MarkType<any>, view: EditorView, groupMarks: MarkType[]) => any, groupMarks: MarkType[]) {
+ // HIGHLIGHTER TOOL
+ createHighlightTool() {
return new MenuItem({
+ title: "Highlight",
+ css: "fill:white;",
+ class: "menuicon",
+ execEvent: "",
+ render() {
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ svg.setAttribute("viewBox", "-100 -100 650 650");
+ const path = document.createElementNS('http://www.w3.org/2000/svg', "path");
+ path.setAttributeNS(null, "d", "M0 479.98L99.92 512l35.45-35.45-67.04-67.04L0 479.98zm124.61-240.01a36.592 36.592 0 0 0-10.79 38.1l13.05 42.83-50.93 50.94 96.23 96.23 50.86-50.86 42.74 13.08c13.73 4.2 28.65-.01 38.15-10.78l35.55-41.64-173.34-173.34-41.52 35.44zm403.31-160.7l-63.2-63.2c-20.49-20.49-53.38-21.52-75.12-2.35L190.55 183.68l169.77 169.78L530.27 154.4c19.18-21.74 18.15-54.63-2.35-75.13z");
+ svg.appendChild(path);
+
+ const color = TooltipTextMenu.createDiv("buttonColor");
+ color.style.backgroundColor = TooltipTextMenuManager.Instance.highlighter.toString();
+
+ const wrapper = TooltipTextMenu.createDiv("colorPicker");
+ wrapper.appendChild(svg);
+ wrapper.appendChild(color);
+ return wrapper;
+ },
+ run: (state, dispatch) => TooltipTextMenu.insertHighlight(TooltipTextMenuManager.Instance.highlighter, state, dispatch)
+ });
+ }
+
+ static insertHighlight(color: String, state: EditorState<any>, dispatch: any) {
+ if (!state.selection.empty) {
+ toggleMark(state.schema.marks.marker, { highlight: color })(state, dispatch);
+ }
+ }
+
+ createHighlightDropdown() {
+ // menu item for color picker
+ const self = this;
+ const colors = new MenuItem({
title: "",
- label: label,
execEvent: "",
- class: "menuicon",
- css: css,
- enable() { return true; },
- run() {
- changeToMarkInGroup(markType, view, groupMarks);
+ class: "button-setting-disabled",
+ css: "",
+ render() {
+ const p = document.createElement("p");
+ p.textContent = "Change highlight:";
+
+ const colorsWrapper = TooltipTextMenu.createDiv("colorPicker-wrapper");
+
+ const colors = [
+ PastelSchemaPalette.get("pink2"),
+ PastelSchemaPalette.get("purple4"),
+ PastelSchemaPalette.get("bluegreen1"),
+ PastelSchemaPalette.get("yellow4"),
+ PastelSchemaPalette.get("red2"),
+ PastelSchemaPalette.get("bluegreen7"),
+ PastelSchemaPalette.get("bluegreen5"),
+ PastelSchemaPalette.get("orange1"),
+ "white",
+ "transparent"
+ ];
+
+ colors.forEach(color => {
+ const button = document.createElement("button");
+ button.className = color === TooltipTextMenuManager.Instance.highlighter ? "colorPicker active" : "colorPicker";
+ if (color) {
+ button.style.backgroundColor = color;
+ button.textContent = color === "transparent" ? "X" : "";
+ button.onclick = e => {
+ TooltipTextMenuManager.Instance.highlighter = color;
+
+ TooltipTextMenu.insertHighlight(TooltipTextMenuManager.Instance.highlighter, self.view.state, self.view.dispatch);
+
+ // update color menu
+ const highlightDom = self.createHighlightTool().render(self.view).dom;
+ const highlightDropdownDom = self.createHighlightDropdown().render(self.view).dom;
+ self.highighterDom && self.tooltip.replaceChild(highlightDom, self.highighterDom);
+ self.highlighterDropdownDom && self.tooltip.replaceChild(highlightDropdownDom, self.highlighterDropdownDom);
+ self.highighterDom = highlightDom;
+ self.highlighterDropdownDom = highlightDropdownDom;
+ };
+ }
+ colorsWrapper.appendChild(button);
+ });
+
+ const div = document.createElement("div");
+ div.appendChild(p);
+ div.appendChild(colorsWrapper);
+ return div;
+ },
+ enable() { return false; },
+ run(p1, p2, p3, event) {
+ event.stopPropagation();
}
});
+
+ return new Dropdown([colors], { class: "buttonSettings-dropdown" }) as MenuItem;
}
- createStar() {
+ // COLOR TOOL
+ createColorTool() {
return new MenuItem({
- title: "Summarize",
- label: "Summarize",
- icon: icons.join,
- css: "color:white;",
- class: "summarize",
+ title: "Color",
+ css: "fill:white;",
+ class: "menuicon",
execEvent: "",
- run: (state, dispatch) => {
- TooltipTextMenu.insertStar(this.view.state, this.view.dispatch);
- }
-
+ render() {
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ svg.setAttribute("viewBox", "-100 -100 650 650");
+ const path = document.createElementNS('http://www.w3.org/2000/svg', "path");
+ path.setAttributeNS(null, "d", "M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368.1-26.9 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z");
+ svg.appendChild(path);
+
+ const color = TooltipTextMenu.createDiv("buttonColor");
+ color.style.backgroundColor = TooltipTextMenuManager.Instance.color.toString();
+
+ const wrapper = TooltipTextMenu.createDiv("colorPicker");
+ wrapper.appendChild(svg);
+ wrapper.appendChild(color);
+ return wrapper;
+ },
+ run: (state, dispatch) => TooltipTextMenu.insertColor(TooltipTextMenuManager.Instance.color, state, dispatch)
});
}
- deleteLinkItem() {
- const icon = {
- height: 16, width: 16,
- path: "M15.898,4.045c-0.271-0.272-0.713-0.272-0.986,0l-4.71,4.711L5.493,4.045c-0.272-0.272-0.714-0.272-0.986,0s-0.272,0.714,0,0.986l4.709,4.711l-4.71,4.711c-0.272,0.271-0.272,0.713,0,0.986c0.136,0.136,0.314,0.203,0.492,0.203c0.179,0,0.357-0.067,0.493-0.203l4.711-4.711l4.71,4.711c0.137,0.136,0.314,0.203,0.494,0.203c0.178,0,0.355-0.067,0.492-0.203c0.273-0.273,0.273-0.715,0-0.986l-4.711-4.711l4.711-4.711C16.172,4.759,16.172,4.317,15.898,4.045z"
- };
- return new MenuItem({
- title: "Delete Link",
- label: "X",
- icon: icon,
- css: "color: red",
- class: "summarize",
+ static insertColor(color: String, state: EditorState<any>, dispatch: any) {
+ const colorMark = state.schema.mark(state.schema.marks.pFontColor, { color: color });
+ if (state.selection.empty) {
+ dispatch(state.tr.addStoredMark(colorMark));
+ } else {
+ this.setMark(colorMark, state, dispatch);
+ }
+ }
+
+ createColorDropdown() {
+ // menu item for color picker
+ const self = this;
+ const colors = new MenuItem({
+ title: "",
execEvent: "",
- run: (state, dispatch) => {
- this.deleteLink();
- }
+ class: "button-setting-disabled",
+ css: "",
+ render() {
+ const p = document.createElement("p");
+ p.textContent = "Change color:";
+
+ const colorsWrapper = TooltipTextMenu.createDiv("colorPicker-wrapper");
+
+ const colors = [
+ DarkPastelSchemaPalette.get("pink2"),
+ DarkPastelSchemaPalette.get("purple4"),
+ DarkPastelSchemaPalette.get("bluegreen1"),
+ DarkPastelSchemaPalette.get("yellow4"),
+ DarkPastelSchemaPalette.get("red2"),
+ DarkPastelSchemaPalette.get("bluegreen7"),
+ DarkPastelSchemaPalette.get("bluegreen5"),
+ DarkPastelSchemaPalette.get("orange1"),
+ "#757472",
+ "#000"
+ ];
+
+ colors.forEach(color => {
+ const button = document.createElement("button");
+ button.className = color === TooltipTextMenuManager.Instance.color ? "colorPicker active" : "colorPicker";
+ if (color) {
+ button.style.backgroundColor = color;
+ button.onclick = e => {
+ TooltipTextMenuManager.Instance.color = color;
+
+ TooltipTextMenu.insertColor(TooltipTextMenuManager.Instance.color, self.view.state, self.view.dispatch);
+
+ // update color menu
+ const colorDom = self.createColorTool().render(self.view).dom;
+ const colorDropdownDom = self.createColorDropdown().render(self.view).dom;
+ self.colorDom && self.tooltip.replaceChild(colorDom, self.colorDom);
+ self.colorDropdownDom && self.tooltip.replaceChild(colorDropdownDom, self.colorDropdownDom);
+ self.colorDom = colorDom;
+ self.colorDropdownDom = colorDropdownDom;
+ };
+ }
+ colorsWrapper.appendChild(button);
+ });
+
+ const div = document.createElement("div");
+ div.appendChild(p);
+ div.appendChild(colorsWrapper);
+ return div;
+ },
+ enable() { return false; },
+ run(p1, p2, p3, event) { event.stopPropagation(); }
});
+
+ return new Dropdown([colors], { class: "buttonSettings-dropdown" }) as MenuItem;
}
- createBrush(active: boolean = false) {
+ // BRUSH TOOL
+ createBrushTool(active: boolean = false) {
const icon = {
height: 32, width: 32,
path: "M30.828 1.172c-1.562-1.562-4.095-1.562-5.657 0l-5.379 5.379-3.793-3.793-4.243 4.243 3.326 3.326-14.754 14.754c-0.252 0.252-0.358 0.592-0.322 0.921h-0.008v5c0 0.552 0.448 1 1 1h5c0 0 0.083 0 0.125 0 0.288 0 0.576-0.11 0.795-0.329l14.754-14.754 3.326 3.326 4.243-4.243-3.793-3.793 5.379-5.379c1.562-1.562 1.562-4.095 0-5.657zM5.409 30h-3.409v-3.409l14.674-14.674 3.409 3.409-14.674 14.674z"
};
+ const self = this;
return new MenuItem({
title: "Brush tool",
label: "Brush tool",
icon: icon,
- css: "color:white;",
- class: active ? "brush-active" : "brush",
+ css: "fill:white;",
+ class: active ? "menuicon-active" : "menuicon",
execEvent: "",
run: (state, dispatch) => {
this.brush_function(state, dispatch);
+
+ // update dropdown with marks
+ const newBrushDropdowndom = self.createBrushDropdown().render(self.view).dom;
+ self._brushDropdownDom && self.tooltip.replaceChild(newBrushDropdowndom, self._brushDropdownDom);
+ self._brushDropdownDom = newBrushDropdowndom;
},
- active: (state) => {
- return true;
- }
+ active: (state) => true
});
}
- // selectionchanged event handler
-
brush_function(state: EditorState<any>, dispatch: any) {
- if (this._brushIsEmpty) {
- const selected_marks = this.getMarksInSelection(this.view.state);
- if (this._brushdom) {
- if (selected_marks.size >= 0) {
- this._brushMarks = selected_marks;
- const newbrush = this.createBrush(true).render(this.view).dom;
- this.tooltip.replaceChild(newbrush, this._brushdom);
- this._brushdom = newbrush;
- this._brushIsEmpty = !this._brushIsEmpty;
- }
+ if (TooltipTextMenuManager.Instance._brushIsEmpty) {
+ // get marks in the selection
+ const selected_marks = new Set<Mark>();
+ const { from, to } = state.selection as TextSelection;
+ state.doc.nodesBetween(from, to, (node) => node.marks ?.forEach(m => selected_marks.add(m)));
+
+ if (this._brushdom && selected_marks.size >= 0) {
+ TooltipTextMenuManager.Instance._brushMarks = selected_marks;
+ const newbrush = this.createBrushTool(true).render(this.view).dom;
+ this.tooltip.replaceChild(newbrush, this._brushdom);
+ this._brushdom = newbrush;
+ TooltipTextMenuManager.Instance._brushIsEmpty = !TooltipTextMenuManager.Instance._brushIsEmpty;
}
}
else {
- let { from, to, $from } = this.view.state.selection;
+ const { from, to, $from } = this.view.state.selection;
if (this._brushdom) {
if (!this.view.state.selection.empty && $from && $from.nodeAfter) {
- if (this._brushMarks && to - from > 0) {
+ if (TooltipTextMenuManager.Instance._brushMarks && to - from > 0) {
this.view.dispatch(this.view.state.tr.removeMark(from, to));
- Array.from(this._brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => {
- const markType = mark.type;
- this.changeToMarkInGroup(markType, this.view, []);
+ Array.from(TooltipTextMenuManager.Instance._brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => {
+ TooltipTextMenu.setMark(mark, this.view.state, this.view.dispatch);
});
}
}
else {
- const newbrush = this.createBrush(false).render(this.view).dom;
+ const newbrush = this.createBrushTool(false).render(this.view).dom;
this.tooltip.replaceChild(newbrush, this._brushdom);
this._brushdom = newbrush;
- this._brushIsEmpty = !this._brushIsEmpty;
+ TooltipTextMenuManager.Instance._brushIsEmpty = !TooltipTextMenuManager.Instance._brushIsEmpty;
}
}
}
-
-
- }
-
- 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);
- }
+ createBrushDropdown(active: boolean = false) {
+ let label = "Stored marks: ";
+ if (TooltipTextMenuManager.Instance._brushMarks && TooltipTextMenuManager.Instance._brushMarks.size > 0) {
+ TooltipTextMenuManager.Instance._brushMarks.forEach((mark: Mark) => label += mark.type.name + ", ");
+ label = label.substring(0, label.length - 2);
+ } else {
+ label = "No marks are currently stored";
}
- }
- createLink() {
- let markType = schema.marks.link;
- return new MenuItem({
- title: "Add or remove link",
- label: "Add or remove link",
+ const brushInfo = new MenuItem({
+ title: "",
+ label: label,
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
- });
- }
+ class: "button-setting-disabled",
+ css: "",
+ enable() { return false; },
+ run(p1, p2, p3, event) { event.stopPropagation(); }
});
- }
- //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) {
- return new MenuItem({
- title: "",
- label: label,
+ const self = this;
+ const input = document.createElement("input");
+ const clearBrush = new MenuItem({
+ title: "Clear brush",
execEvent: "",
- class: "menuicon",
- css: css,
+ class: "separated-button",
+ css: "",
+ render() {
+ const button = document.createElement("button");
+ button.textContent = "Clear brush";
+
+ input.textContent = "editme";
+ input.style.width = "75px";
+ input.style.height = "30px";
+ input.style.background = "white";
+ input.setAttribute("contenteditable", "true");
+ input.style.whiteSpace = "nowrap";
+ input.type = "text";
+ input.placeholder = "Enter URL";
+ input.onpointerdown = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ };
+ input.onclick = (e: MouseEvent) => {
+ input.select();
+ input.focus();
+ };
+ input.onkeypress = (e: KeyboardEvent) => {
+ if (e.key === "Enter") {
+ TooltipTextMenuManager.Instance._brushMarks && TooltipTextMenuManager.Instance._brushMap.set(input.value, TooltipTextMenuManager.Instance._brushMarks);
+ input.style.background = "lightGray";
+ }
+ };
+
+ const wrapper = document.createElement("div");
+ wrapper.appendChild(input);
+ wrapper.appendChild(button);
+ return wrapper;
+ },
enable() { return true; },
run() {
- changeToNodeInGroup(nodeType, view, groupNodes);
+ TooltipTextMenuManager.Instance._brushIsEmpty = true;
+ TooltipTextMenuManager.Instance._brushMarks = new Set();
+
+ // update brush tool
+ // TODO: this probably isn't very clean
+ const newBrushdom = self.createBrushTool().render(self.view).dom;
+ self._brushdom && self.tooltip.replaceChild(newBrushdom, self._brushdom);
+ self._brushdom = newBrushdom;
+ const newBrushDropdowndom = self.createBrushDropdown().render(self.view).dom;
+ self._brushDropdownDom && self.tooltip.replaceChild(newBrushDropdowndom, self._brushDropdownDom);
+ self._brushDropdownDom = newBrushDropdowndom;
}
});
- }
- 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, title: string = name) {
- let span = document.createElement("span");
- span.className = name + " menuicon";
- span.title = title;
- span.textContent = text;
- span.style.color = "white";
- return span;
+ const hasMarks = TooltipTextMenuManager.Instance._brushMarks && TooltipTextMenuManager.Instance._brushMarks.size > 0;
+ return new Dropdown(hasMarks ? [brushInfo, clearBrush] : [brushInfo], { class: "buttonSettings-dropdown" }) as MenuItem;
}
- //method for checking whether node can be inserted
- canInsert(state: EditorState, nodeType: NodeType<Schema<string, string>>) {
- let $from = state.selection.$from;
- for (let d = $from.depth; d >= 0; d--) {
- let index = $from.index(d);
- if ($from.node(d).canReplaceWith(index, index, nodeType)) return true;
- }
- return false;
- }
-
-
- //adapted this method - use it to check if block has a tag (ie bulleting)
- blockActive(type: NodeType<Schema<string, string>>, state: EditorState) {
- let attrs = {};
-
- if (state.selection instanceof NodeSelection) {
- const sel: NodeSelection = state.selection;
- let $from = sel.$from;
- let to = sel.to;
- let node = sel.node;
-
- if (node) {
- return node.hasMarkup(type, attrs);
- }
-
- return to <= $from.end() && $from.parent.hasMarkup(type, attrs);
- }
- }
-
- // Create an icon for a heading at the given level
- heading(level: number) {
- return {
- command: setBlockType(schema.nodes.heading, { level }),
- dom: this.icon("H" + level, "heading")
- };
- }
-
- getMarksInSelection(state: EditorState<any>) {
- let found = new Set<Mark>();
- let { from, to } = state.selection as TextSelection;
- state.doc.nodesBetween(from, to, (node) => {
- let marks = node.marks;
- if (marks) {
- marks.forEach(m => {
- found.add(m);
+ static setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => {
+ if (mark) {
+ const node = (state.selection as NodeSelection).node;
+ if (node ?.type === schema.nodes.ordered_list) {
+ let attrs = node.attrs;
+ if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family };
+ if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize };
+ if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, setFontColor: mark.attrs.color };
+ const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema);
+ dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from))));
+ } else {
+ toggleMark(mark.type, mark.attrs)(state, (tx: any) => {
+ const { from, $from, to, empty } = tx.selection;
+ if (!tx.doc.rangeHasMark(from, to, mark.type)) {
+ toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch);
+ } else dispatch(tx);
});
}
- });
- return found;
- }
-
- reset_mark_doms() {
- let iterator = this._marksToDoms.values();
- let next = iterator.next();
- while (!next.done) {
- next.value.style.color = "white";
- next = iterator.next();
}
}
+ // called by Prosemirror
update(view: EditorView, lastState: EditorState | undefined) { this.updateFromDash(view, lastState, this.editorProps); }
//updates the tooltip menu when the selection changes
- public updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) {
+ public async updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) {
if (!view) {
console.log("no editor? why?");
return;
}
this.view = view;
- let state = view.state;
DocumentDecorations.Instance.showTextBar();
props && (this.editorProps = props);
- // Don't do anything if the document/selection didn't change
- if (lastState && lastState.doc.eq(state.doc) &&
- lastState.selection.eq(state.selection)) return;
-
- this.reset_mark_doms();
- // Hide the tooltip if the selection is empty
- if (state.selection.empty) {
- //this.tooltip.style.display = "none";
- //return;
- }
- //UPDATE LIST ITEM DROPDOWN
-
- //UPDATE FONT STYLE DROPDOWN
- let activeStyles = this.activeMarksOnSelection(this.fontStyles);
- 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");
- }
+ // Don't do anything if the document/selection didn't change
+ if (!lastState || !lastState.doc.eq(view.state.doc) || !lastState.selection.eq(view.state.selection)) {
+
+ // UPDATE LINK DROPDOWN
+ const linkTarget = await this.getTextLinkTargetTitle();
+ const linkDom = this.createLinkTool(linkTarget ? true : false).render(this.view).dom;
+ const linkDropdownDom = this.createLinkDropdown(linkTarget).render(this.view).dom;
+ this.linkDom && this.tooltip.replaceChild(linkDom, this.linkDom);
+ this.linkDropdownDom && this.tooltip.replaceChild(linkDropdownDom, this.linkDropdownDom);
+ this.linkDom = linkDom;
+ this.linkDropdownDom = linkDropdownDom;
+
+ //UPDATE FONT STYLE DROPDOWN
+ const activeStyles = this.activeFontFamilyOnSelection();
+ this.updateFontStyleDropdown(activeStyles.length === 1 ? activeStyles[0] : activeStyles.length ? "various" : "default");
+
+ //UPDATE FONT SIZE DROPDOWN
+ const activeSizes = this.activeFontSizeOnSelection();
+ this.updateFontSizeDropdown(activeSizes.length === 1 ? String(activeSizes[0]) + " pt" : activeSizes.length ? "various" : "default");
+
+ //UPDATE ALL OTHER BUTTONS
+ this.updateHighlightStateOfButtons();
}
+ }
- //UPDATE FONT SIZE DROPDOWN
- let activeSizes = this.activeMarksOnSelection(this.fontSizes);
- 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");
- }
- }
+ updateHighlightStateOfButtons() {
+ Array.from(this._marksToDoms.values()).forEach(val => val.style.fill = "white");
+ this.activeMarksOnSelection().filter(mark => this._marksToDoms.has(mark)).forEach(mark =>
+ this._marksToDoms.get(mark)!.style.fill = "greenyellow");
- this.update_mark_doms();
+ // keeps brush tool highlighted if active when switching between textboxes
+ if (!TooltipTextMenuManager.Instance._brushIsEmpty && this._brushdom) {
+ const newbrush = this.createBrushTool(true).render(this.view).dom;
+ this.tooltip.replaceChild(newbrush, this._brushdom);
+ this._brushdom = newbrush;
+ }
}
- public mark_key_pressed(marks: Mark<any>[]) {
- if (this.view.state.selection.empty) {
- if (marks) this._activeMarks = marks;
- this.update_mark_doms();
+ //finds fontSize at start of selection
+ activeFontSizeOnSelection() {
+ //current selection
+ const state = this.view.state;
+ const activeSizes: number[] = [];
+ const pos = this.view.state.selection.$from;
+ const ref_node: ProsNode = this.reference_node(pos);
+ if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
+ ref_node.marks.forEach(m => m.type === state.schema.marks.pFontSize && activeSizes.push(m.attrs.fontSize));
}
+ return activeSizes;
}
-
- update_mark_doms() {
- this.reset_mark_doms();
- let foundlink = false;
- let children = this.extras.childNodes;
- this._activeMarks.forEach((mark) => {
- if (this._marksToDoms.has(mark)) {
- let dom = this._marksToDoms.get(mark);
- if (dom) dom.style.color = "greenyellow";
- }
- if (children.length > 1) {
- foundlink = true;
- }
- if (mark.type.name === "link" && children.length === 1) {
- // let del = document.createElement("button");
- // del.textContent = "X";
- // del.style.color = "red";
- // del.style.height = "10px";
- // del.style.width = "10px";
- // del.style.marginLeft = "5px";
- // del.onclick = this.deleteLink;
- // this.extras.appendChild(del);
- let del = this.deleteLinkItem().render(this.view).dom;
- this.extras.appendChild(del);
- foundlink = true;
- }
- });
- if (!foundlink) {
- if (children.length > 1) {
- this.extras.removeChild(children[1]);
- }
+ //finds fontSize at start of selection
+ activeFontFamilyOnSelection() {
+ //current selection
+ const state = this.view.state;
+ const activeFamilies: string[] = [];
+ const pos = this.view.state.selection.$from;
+ const ref_node: ProsNode = this.reference_node(pos);
+ if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
+ ref_node.marks.forEach(m => m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family));
}
-
+ return activeFamilies;
}
-
//finds all active marks on selection in given group
- activeMarksOnSelection(markGroup: MarkType[]) {
+ activeMarksOnSelection() {
+ const markGroup = Array.from(this._marksToDoms.keys());
+ if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type);
//current selection
- let { empty, ranges, $to } = this.view.state.selection as TextSelection;
- let state = this.view.state;
- let dispatch = this.view.dispatch;
- let activeMarks: MarkType[];
+ const { empty, ranges, $to } = this.view.state.selection as TextSelection;
+ const state = this.view.state;
+ 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);
- }
+ const has = false;
+ for (let i = 0; !has && i < ranges.length; i++) {
+ return state.doc.rangeHasMark(ranges[i].$from.pos, ranges[i].$to.pos, mark);
}
return false;
});
-
- const refnode = this.reference_node($to);
- this._activeMarks = refnode.marks;
}
else {
const pos = this.view.state.selection.$from;
@@ -975,19 +967,14 @@ export class TooltipTextMenu {
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);
+ if (mark_type === state.schema.marks.pFontSize) {
+ return ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name);
}
- return false;
+ const mark = state.schema.mark(mark_type);
+ return ref_node.marks.includes(mark);
});
}
- else {
- return [];
- }
-
}
return activeMarks;
}
@@ -1019,6 +1006,37 @@ export class TooltipTextMenu {
}
destroy() {
- this.wrapper.remove();
+ // this.wrapper.remove();
+ }
+}
+
+
+export class TooltipTextMenuManager {
+ private static _instance: TooltipTextMenuManager;
+ private _isPinned: boolean = false;
+
+ public pinnedX: number = 0;
+ public pinnedY: number = 0;
+ public unpinnedX: number = 0;
+ public unpinnedY: number = 0;
+
+ public _brushMarks: Set<Mark> | undefined;
+ public _brushMap: Map<string, Set<Mark>> = new Map();
+ public _brushIsEmpty: boolean = true;
+
+ public color: String = "#000";
+ public highlighter: String = "transparent";
+
+ public activeMenu: TooltipTextMenu | undefined;
+
+ static get Instance() {
+ if (!TooltipTextMenuManager._instance) {
+ TooltipTextMenuManager._instance = new TooltipTextMenuManager();
+ }
+ return TooltipTextMenuManager._instance;
}
+
+ public get isPinned() { return this._isPinned; }
+
+ public toggleIsPinned() { this._isPinned = !this._isPinned; }
}
diff --git a/src/client/util/TypedEvent.ts b/src/client/util/TypedEvent.ts
index 532ba78eb..90fd299c1 100644
--- a/src/client/util/TypedEvent.ts
+++ b/src/client/util/TypedEvent.ts
@@ -1,40 +1,40 @@
export interface Listener<T> {
- (event: T): any;
+ (event: T): any;
}
export interface Disposable {
- dispose(): void;
+ dispose(): void;
}
/** passes through events as they happen. You will not get events from before you start listening */
export class TypedEvent<T> {
- private listeners: Listener<T>[] = [];
- private listenersOncer: Listener<T>[] = [];
-
- on = (listener: Listener<T>): Disposable => {
- this.listeners.push(listener);
- return {
- dispose: () => this.off(listener)
- };
- }
-
- once = (listener: Listener<T>): void => {
- this.listenersOncer.push(listener);
- }
-
- off = (listener: Listener<T>) => {
- var callbackIndex = this.listeners.indexOf(listener);
- if (callbackIndex > -1) this.listeners.splice(callbackIndex, 1);
- }
-
- emit = (event: T) => {
- /** Update any general listeners */
- this.listeners.forEach((listener) => listener(event));
-
- /** Clear the `once` queue */
- this.listenersOncer.forEach((listener) => listener(event));
- this.listenersOncer = [];
- }
-
- pipe = (te: TypedEvent<T>): Disposable => this.on((e) => te.emit(e));
+ private listeners: Listener<T>[] = [];
+ private listenersOncer: Listener<T>[] = [];
+
+ on = (listener: Listener<T>): Disposable => {
+ this.listeners.push(listener);
+ return {
+ dispose: () => this.off(listener)
+ };
+ }
+
+ once = (listener: Listener<T>): void => {
+ this.listenersOncer.push(listener);
+ }
+
+ off = (listener: Listener<T>) => {
+ const callbackIndex = this.listeners.indexOf(listener);
+ if (callbackIndex > -1) this.listeners.splice(callbackIndex, 1);
+ }
+
+ emit = (event: T) => {
+ /** Update any general listeners */
+ this.listeners.forEach((listener) => listener(event));
+
+ /** Clear the `once` queue */
+ this.listenersOncer.forEach((listener) => listener(event));
+ this.listenersOncer = [];
+ }
+
+ pipe = (te: TypedEvent<T>): Disposable => this.on((e) => te.emit(e));
} \ No newline at end of file
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 472afac1d..314b52bf3 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -3,7 +3,7 @@ import 'source-map-support/register';
import { Without } from "../../Utils";
function getBatchName(target: any, key: string | symbol): string {
- let keyName = key.toString();
+ const keyName = key.toString();
if (target && target.constructor && target.constructor.name) {
return `${target.constructor.name}.${keyName}`;
}
@@ -23,7 +23,7 @@ function propertyDecorator(target: any, key: string | symbol) {
writable: true,
configurable: true,
value: function (...args: any[]) {
- let batch = UndoManager.StartBatch(getBatchName(target, key));
+ const batch = UndoManager.StartBatch(getBatchName(target, key));
try {
return value.apply(this, args);
} finally {
@@ -40,7 +40,7 @@ export function undoBatch(fn: (...args: any[]) => any): (...args: any[]) => any;
export function undoBatch(target: any, key?: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any {
if (!key) {
return function () {
- let batch = UndoManager.StartBatch("");
+ const batch = UndoManager.StartBatch("");
try {
return target.apply(undefined, arguments);
} finally {
@@ -55,7 +55,7 @@ export function undoBatch(target: any, key?: string | symbol, descriptor?: Typed
const oldFunction = descriptor.value;
descriptor.value = function (...args: any[]) {
- let batch = UndoManager.StartBatch(getBatchName(target, key));
+ const batch = UndoManager.StartBatch(getBatchName(target, key));
try {
return oldFunction.apply(this, args);
} finally {
@@ -98,7 +98,7 @@ export namespace UndoManager {
GetOpenBatches().forEach(batch => console.log(batch.batchName));
}
- let openBatches: Batch[] = [];
+ const openBatches: Batch[] = [];
export function GetOpenBatches(): Without<Batch, 'end'>[] {
return openBatches;
}
@@ -146,7 +146,7 @@ export namespace UndoManager {
//TODO Make this return the return value
export function RunInBatch<T>(fn: () => T, batchName: string) {
- let batch = StartBatch(batchName);
+ const batch = StartBatch(batchName);
try {
return runInAction(fn);
} finally {
@@ -159,7 +159,7 @@ export namespace UndoManager {
return;
}
- let commands = undoStack.pop();
+ const commands = undoStack.pop();
if (!commands) {
return;
}
@@ -178,7 +178,7 @@ export namespace UndoManager {
return;
}
- let commands = redoStack.pop();
+ const commands = redoStack.pop();
if (!commands) {
return;
}