diff options
| author | Eleanor Eng <eleanor_eng@brown.edu> | 2019-08-06 10:54:33 -0400 |
|---|---|---|
| committer | Eleanor Eng <eleanor_eng@brown.edu> | 2019-08-06 10:54:33 -0400 |
| commit | b7194d88ba9733413063c7f371dedefd3a97e35f (patch) | |
| tree | 418203fe1ee1f4aa0771c555ab672541cc775473 /src/client/util | |
| parent | 2c4440be2807b3b1da691ea04b061c35e50ecb72 (diff) | |
| parent | 298d1c9b29d6ce2171fd9ac8274b64583b73f6f5 (diff) | |
merge with master
Diffstat (limited to 'src/client/util')
| -rw-r--r-- | src/client/util/DictationManager.ts | 39 | ||||
| -rw-r--r-- | src/client/util/DocumentManager.ts | 16 | ||||
| -rw-r--r-- | src/client/util/DragManager.ts | 36 | ||||
| -rw-r--r-- | src/client/util/History.ts | 2 | ||||
| -rw-r--r-- | src/client/util/Import & Export/DirectoryImportBox.tsx | 4 | ||||
| -rw-r--r-- | src/client/util/LinkManager.ts | 13 | ||||
| -rw-r--r-- | src/client/util/ProsemirrorExampleTransfer.ts | 4 | ||||
| -rw-r--r-- | src/client/util/RichTextSchema.tsx | 11 | ||||
| -rw-r--r-- | src/client/util/Scripting.ts | 85 | ||||
| -rw-r--r-- | src/client/util/SearchUtil.ts | 3 | ||||
| -rw-r--r-- | src/client/util/SelectionManager.ts | 1 | ||||
| -rw-r--r-- | src/client/util/SerializationHelper.ts | 34 | ||||
| -rw-r--r-- | src/client/util/TooltipTextMenu.scss | 95 | ||||
| -rw-r--r-- | src/client/util/TooltipTextMenu.tsx | 319 | ||||
| -rw-r--r-- | src/client/util/type_decls.d | 2 |
15 files changed, 552 insertions, 112 deletions
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts new file mode 100644 index 000000000..b58bdb6c7 --- /dev/null +++ b/src/client/util/DictationManager.ts @@ -0,0 +1,39 @@ +namespace CORE { + export interface IWindow extends Window { + webkitSpeechRecognition: any; + } +} + +const { webkitSpeechRecognition }: CORE.IWindow = window as CORE.IWindow; + +export default class DictationManager { + public static Instance = new DictationManager(); + private isListening = false; + private recognizer: any; + + constructor() { + this.recognizer = new webkitSpeechRecognition(); + this.recognizer.interimResults = false; + this.recognizer.continuous = true; + } + + finish = (handler: any, data: any) => { + handler(data); + this.isListening = false; + this.recognizer.stop(); + } + + listen = () => { + if (this.isListening) { + return undefined; + } + this.isListening = true; + this.recognizer.start(); + return new Promise<string>((resolve, reject) => { + this.recognizer.onresult = (e: any) => this.finish(resolve, e.results[0][0].transcript); + this.recognizer.onerror = (e: any) => this.finish(reject, e); + }); + + } + +}
\ No newline at end of file diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 262194a40..32f728c71 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,16 +1,14 @@ -import { computed, observable, action } from 'mobx'; -import { DocumentView } from '../views/nodes/DocumentView'; -import { Doc, DocListCast, Opt } from '../../new_fields/Doc'; -import { FieldValue, Cast, NumCast, BoolCast, StrCast } from '../../new_fields/Types'; -import { listSpec } from '../../new_fields/Schema'; -import { undoBatch, UndoManager } from './UndoManager'; +import { action, computed, observable } from 'mobx'; +import { Doc } from '../../new_fields/Doc'; +import { Id } from '../../new_fields/FieldSymbols'; +import { BoolCast, Cast, NumCast } from '../../new_fields/Types'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; -import { CollectionView } from '../views/collections/CollectionView'; import { CollectionPDFView } from '../views/collections/CollectionPDFView'; import { CollectionVideoView } from '../views/collections/CollectionVideoView'; -import { Id } from '../../new_fields/FieldSymbols'; +import { CollectionView } from '../views/collections/CollectionView'; +import { DocumentView } from '../views/nodes/DocumentView'; import { LinkManager } from './LinkManager'; -import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; +import { undoBatch, UndoManager } from './UndoManager'; export class DocumentManager { diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 323908302..a7aaaed7c 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,6 +1,6 @@ import { action, runInAction } from "mobx"; import { Doc } from "../../new_fields/Doc"; -import { Cast } from "../../new_fields/Types"; +import { Cast, StrCast } from "../../new_fields/Types"; import { URLField } from "../../new_fields/URLField"; import { emptyFunction } from "../../Utils"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; @@ -8,6 +8,9 @@ import * as globalCssVariables from "../views/globalCssVariables.scss"; import { DocumentManager } from "./DocumentManager"; import { LinkManager } from "./LinkManager"; import { SelectionManager } from "./SelectionManager"; +import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField"; +import { DocumentDecorations } from "../views/DocumentDecorations"; +import { NumberLiteralType } from "typescript"; export type dropActionType = "alias" | "copy" | undefined; export function SetupDrag( @@ -138,6 +141,10 @@ export namespace DragManager { dragHasStarted?: () => void; withoutShiftDrag?: boolean; + + offsetX?: number; + + offsetY?: number; } export interface DragDropDisposer { @@ -214,6 +221,7 @@ export namespace DragManager { this.annotationDocument = annotationDoc; this.xOffset = this.yOffset = 0; } + targetContext: Doc | undefined; dragDocument: Doc; annotationDocument: Doc; dropDocument: Doc; @@ -288,6 +296,15 @@ export namespace DragManager { [id: string]: any; } + // for column dragging in schema view + export class ColumnDragData { + constructor(colKey: SchemaHeaderField) { + this.colKey = colKey; + } + colKey: SchemaHeaderField; + [id: string]: any; + } + export function StartLinkDrag(ele: HTMLElement, dragData: LinkDragData, downX: number, downY: number, options?: DragOptions) { StartDrag([ele], dragData, downX, downY, options); } @@ -296,6 +313,10 @@ export namespace DragManager { StartDrag([ele], dragData, downX, downY, options); } + 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) { @@ -383,7 +404,8 @@ export namespace DragManager { hideSource = options.hideSource(); } } - eles.map(ele => (ele.hidden = hideSource)); + eles.map(ele => (ele.hidden = hideSource) && + (ele.parentElement && ele.parentElement.className.indexOf("collectionFreeFormDocumentView") !== -1 && (ele.parentElement.hidden = hideSource))); let lastX = downX; let lastY = downY; @@ -407,14 +429,16 @@ export namespace DragManager { lastX = e.pageX; lastY = e.pageY; dragElements.map((dragElement, i) => (dragElement.style.transform = - `translate(${(xs[i] += moveX)}px, ${(ys[i] += moveY)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`) + `translate(${(xs[i] += moveX) + (options ? (options.offsetX || 0) : 0)}px, ${(ys[i] += moveY) + (options ? (options.offsetY || 0) : 0)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`) ); }; let hideDragElements = () => { - SelectionManager.SetIsDragging(false); dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement)); - eles.map(ele => (ele.hidden = false)); + eles.map(ele => { + ele.hidden = false; + (ele.parentElement && ele.parentElement.className.indexOf("collectionFreeFormDocumentView") !== -1 && (ele.parentElement.hidden = false)); + }); }; let endDrag = () => { document.removeEventListener("pointermove", moveHandler, true); @@ -426,11 +450,13 @@ export namespace DragManager { AbortDrag = () => { hideDragElements(); + SelectionManager.SetIsDragging(false); endDrag(); }; const upHandler = (e: PointerEvent) => { hideDragElements(); dispatchDrag(eles, e, dragData, options, finishDrag); + SelectionManager.SetIsDragging(false); endDrag(); }; document.addEventListener("pointermove", moveHandler, true); diff --git a/src/client/util/History.ts b/src/client/util/History.ts index cbf5b3fc8..e9ff21b22 100644 --- a/src/client/util/History.ts +++ b/src/client/util/History.ts @@ -129,7 +129,7 @@ export namespace HistoryUtil { function addStringifier(type: string, keys: string[], customStringifier?: (state: ParsedUrl, current: string) => string) { stringifiers[type] = state => { - let path = DocServer.prepend(`/${type}`); + let path = Utils.prepend(`/${type}`); if (customStringifier) { path = customStringifier(state, path); } diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index c096e9ceb..75b0b52a7 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -98,12 +98,12 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps> runInAction(() => this.remaining++); - let prom = fetch(DocServer.prepend(RouteStore.upload), { + let prom = fetch(Utils.prepend(RouteStore.upload), { method: 'POST', body: formData }).then(async (res: Response) => { (await res.json()).map(action((file: any) => { - let docPromise = Docs.Get.DocumentFromType(type, DocServer.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName }); + let docPromise = Docs.Get.DocumentFromType(type, Utils.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName }); docPromise.then(doc => { doc && docs.push(doc) && runInAction(() => this.remaining--); }); diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index a647f22c1..448a8e9cf 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -6,6 +6,7 @@ import { List } from "../../new_fields/List"; import { Id } from "../../new_fields/FieldSymbols"; import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; import { Docs } from "../documents/Documents"; +import { Scripting } from "./Scripting"; /* @@ -42,7 +43,12 @@ export class LinkManager { } public getAllLinks(): Doc[] { - return LinkManager.Instance.LinkManagerDoc ? LinkManager.Instance.LinkManagerDoc.allLinks ? DocListCast(LinkManager.Instance.LinkManagerDoc.allLinks) : [] : []; + let ldoc = LinkManager.Instance.LinkManagerDoc; + if (ldoc) { + let docs = DocListCast(ldoc.allLinks); + return docs; + } + return []; } public addLink(linkDoc: Doc): boolean { @@ -242,4 +248,7 @@ export class LinkManager { return Cast(linkDoc.anchor1, Doc, null); } } -}
\ No newline at end of file +} +Scripting.addGlobal(function links(doc: any) { + return new List(LinkManager.Instance.getAllRelatedLinks(doc)); +}); diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts index fa9e2e5af..c38f84551 100644 --- a/src/client/util/ProsemirrorExampleTransfer.ts +++ b/src/client/util/ProsemirrorExampleTransfer.ts @@ -47,6 +47,10 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: bind("Mod-i", toggleMark(type)); bind("Mod-I", toggleMark(type)); } + if (type = schema.marks.underline) { + bind("Mod-u", toggleMark(type)); + bind("Mod-U", toggleMark(type)); + } if (type = schema.marks.code) { bind("Mod-`", toggleMark(type)); } diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 269de0f42..ce9e29b26 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -283,7 +283,7 @@ export const marks: { [index: string]: MarkSpec } = { }, highlight: { - parseDOM: [{ style: 'background: #d9dbdd' }], + parseDOM: [{ style: 'color: blue' }], toDOM() { return ['span', { style: 'color: blue' @@ -291,6 +291,15 @@ export const marks: { [index: string]: MarkSpec } = { } }, + search_highlight: { + parseDOM: [{ style: 'background: yellow' }], + toDOM() { + return ['span', { + style: 'background: yellow' + }]; + } + }, + // :: MarkSpec Code font mark. Represented as a `<code>` element. code: { diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 62c2cfe85..1d0916ac0 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -1,5 +1,7 @@ -// import * as ts from "typescript" -let ts = (window as any).ts; +import * as ts from "typescript"; +export { ts }; +// export const ts = (window as any).ts; + // // @ts-ignore // import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts' // // @ts-ignore @@ -50,10 +52,31 @@ export namespace Scripting { } else { throw new Error("Must either register an object with a name, or give a name and an object"); } - if (scriptingGlobals.hasOwnProperty(n)) { + if (_scriptingGlobals.hasOwnProperty(n)) { throw new Error(`Global with name ${n} is already registered, choose another name`); } - scriptingGlobals[n] = obj; + _scriptingGlobals[n] = obj; + } + + export function makeMutableGlobalsCopy(globals?: { [name: string]: any }) { + return { ..._scriptingGlobals, ...(globals || {}) }; + } + + export function setScriptingGlobals(globals: { [key: string]: any }) { + scriptingGlobals = globals; + } + + export function resetScriptingGlobals() { + scriptingGlobals = _scriptingGlobals; + } + + // const types = Object.keys(ts.SyntaxKind).map(kind => ts.SyntaxKind[kind]); + export function printNodeType(node: any, indentation = "") { + console.log(indentation + ts.SyntaxKind[node.kind]); + } + + export function getGlobals() { + return Object.keys(scriptingGlobals); } } @@ -61,7 +84,8 @@ export function scriptingGlobal(constructor: { new(...args: any[]): any }) { Scripting.addGlobal(constructor); } -const scriptingGlobals: { [name: string]: any } = {}; +const _scriptingGlobals: { [name: string]: any } = {}; +let scriptingGlobals: { [name: string]: any } = _scriptingGlobals; function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult { const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error); @@ -162,6 +186,12 @@ class ScriptingCompilerHost { } } +export type Traverser = (node: ts.Node, indentation: string) => boolean | void; +export type TraverserParam = Traverser | { onEnter: Traverser, onLeave: Traverser }; +export type Transformer = { + transformer: ts.TransformerFactory<ts.SourceFile>, + getVars?: () => { capturedVariables: { [name: string]: Field } } +}; export interface ScriptOptions { requiredType?: string; addReturn?: boolean; @@ -169,11 +199,45 @@ export interface ScriptOptions { capturedVariables?: { [name: string]: Field }; typecheck?: boolean; editable?: boolean; + traverser?: TraverserParam; + transformer?: Transformer; + globals?: { [name: string]: any }; +} + +// function forEachNode(node:ts.Node, fn:(node:any) => void); +function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, indentation = "") { + return onEnter(node, indentation) || ts.forEachChild(node, (n: any) => { + forEachNode(n, onEnter, onExit, indentation + " "); + }) || (onExit && onExit(node, indentation)); } export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult { const { requiredType = "", addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options; + if (options.globals) { + Scripting.setScriptingGlobals(options.globals); + } let 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; + const onLeave = typeof options.traverser === "object" ? options.traverser.onLeave : undefined; + forEachNode(sourceFile, onEnter, onLeave); + } + if (options.transformer) { + const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true); + const result = ts.transform(sourceFile, [options.transformer.transformer]); + if (options.transformer.getVars) { + const newCaptures = options.transformer.getVars(); + // tslint:disable-next-line: prefer-object-spread + options.capturedVariables = Object.assign(capturedVariables, newCaptures.capturedVariables) as any; + } + const transformed = result.transformed; + const printer = ts.createPrinter({ + newLine: ts.NewLineKind.LineFeed + }); + script = printer.printFile(transformed[0]); + result.dispose(); + } let paramNames: string[] = []; if ("this" in params || "this" in capturedVariables) { paramNames.push("this"); @@ -188,14 +252,16 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp }); for (const key in capturedVariables) { if (key === "this") continue; + const val = capturedVariables[key]; paramNames.push(key); - paramList.push(`${key}: ${capturedVariables[key].constructor.name}`); + paramList.push(`${key}: ${typeof val === "object" ? Object.getPrototypeOf(val).constructor.name : typeof val}`); } let paramString = paramList.join(", "); let 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(); @@ -203,7 +269,12 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics); - return Run(outputText, paramNames, diagnostics, script, options); + const result = Run(outputText, paramNames, diagnostics, script, options); + + if (options.globals) { + Scripting.resetScriptingGlobals(); + } + return result; } Scripting.addGlobal(CompileScript);
\ No newline at end of file diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index abf1a7c32..ee5a83710 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -2,6 +2,7 @@ import * as rp from 'request-promise'; import { DocServer } from '../DocServer'; import { Doc } from '../../new_fields/Doc'; import { Id } from '../../new_fields/FieldSymbols'; +import { Utils } from '../../Utils'; export namespace SearchUtil { export type HighlightingResult = { [id: string]: { [key: string]: string[] } }; @@ -29,7 +30,7 @@ export namespace SearchUtil { 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 - const result: IdSearchResult = JSON.parse(await rp.get(DocServer.prepend("/search"), { + const result: IdSearchResult = JSON.parse(await rp.get(Utils.prepend("/search"), { qs: { ...options, q: query }, })); if (!returnDocs) { diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 9efef888d..ee623d082 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -4,6 +4,7 @@ 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 { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; export namespace SelectionManager { diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts index dca539f3b..ff048f647 100644 --- a/src/client/util/SerializationHelper.ts +++ b/src/client/util/SerializationHelper.ts @@ -1,9 +1,15 @@ import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema, primitive, SKIP } from "serializr"; -import { Field } from "../../new_fields/Doc"; +import { Field, Doc } 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) { + serializing++; + cb(err, newValue); + serializing--; +} export namespace SerializationHelper { - let serializing: number = 0; export function IsSerializing() { return serializing > 0; } @@ -17,18 +23,18 @@ export namespace SerializationHelper { return obj; } - serializing += 1; + serializing++; if (!(obj.constructor.name in reverseMap)) { throw Error(`type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`); } const json = serialize(obj); json.__type = reverseMap[obj.constructor.name]; - serializing -= 1; + serializing--; return json; } - export function Deserialize(obj: any): any { + export async function Deserialize(obj: any): Promise<any> { if (obj === undefined || obj === null) { return undefined; } @@ -37,7 +43,6 @@ export namespace SerializationHelper { return obj; } - serializing += 1; if (!obj.__type) { if (ClientUtils.RELEASE) { console.warn("No property 'type' found in JSON."); @@ -52,16 +57,15 @@ export namespace SerializationHelper { } const type = serializationTypes[obj.__type]; - const value = deserialize(type.ctor, obj); + const value = await new Promise(res => deserialize(type.ctor, obj, (err, result) => res(result))); if (type.afterDeserialize) { - type.afterDeserialize(value); + await type.afterDeserialize(value); } - serializing -= 1; return value; } } -let serializationTypes: { [name: string]: { ctor: { new(): any }, afterDeserialize?: (obj: any) => void } } = {}; +let serializationTypes: { [name: string]: { ctor: { new(): any }, afterDeserialize?: (obj: any) => void | Promise<any> } } = {}; let reverseMap: { [ctor: string]: string } = {}; export interface DeserializableOpts { @@ -69,7 +73,7 @@ export interface DeserializableOpts { withFields(fields: string[]): Function; } -export function Deserializable(name: string, afterDeserialize?: (obj: any) => void): DeserializableOpts; +export function Deserializable(name: string, afterDeserialize?: (obj: any) => void | Promise<any>): DeserializableOpts; export function Deserializable(constructor: { new(...args: any[]): any }): void; export function Deserializable(constructor: { new(...args: any[]): any } | string, afterDeserialize?: (obj: any) => void): DeserializableOpts | void { function addToMap(name: string, ctor: { new(...args: any[]): any }) { @@ -88,15 +92,15 @@ export function Deserializable(constructor: { new(...args: any[]): any } | strin if (typeof constructor === "string") { return Object.assign((ctor: { new(...args: any[]): any }) => { addToMap(constructor, ctor); - }, { withFields: Deserializable.withFields }); + }, { withFields: (fields: string[]) => Deserializable.withFields(fields, name, afterDeserialize) }); } addToMap(constructor.name, constructor); } export namespace Deserializable { - export function withFields(fields: string[]) { + export function withFields(fields: string[], name?: string, afterDeserialize?: (obj: any) => void | Promise<any>) { return function (constructor: { new(...fields: any[]): any }) { - Deserializable(constructor); + Deserializable(name || constructor.name, afterDeserialize)(constructor); let schema = getDefaultModelSchema(constructor); if (schema) { schema.factory = context => { @@ -135,6 +139,6 @@ export namespace Deserializable { export function autoObject(): PropSchema { return custom( (s) => SerializationHelper.Serialize(s), - (s) => SerializationHelper.Deserialize(s) + (json: any, context: any, oldValue: any, cb: (err: any, result: any) => void) => SerializationHelper.Deserialize(json).then(res => cb(null, res)) ); }
\ No newline at end of file diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss index 40ac3abb9..ebf833dbe 100644 --- a/src/client/util/TooltipTextMenu.scss +++ b/src/client/util/TooltipTextMenu.scss @@ -18,7 +18,8 @@ .ProseMirror-menuitem { margin-right: 3px; display: inline-block; - z-index: 100000; + z-index: 50000; + position: relative; } .ProseMirror-menuseparator { @@ -67,7 +68,7 @@ } .ProseMirror-menu-dropdown-menu { - z-index: 100000; + z-index: 50000; min-width: 6em; background: white; position: absolute; @@ -235,8 +236,8 @@ } .tooltipMenu { - position: relative; - z-index: 2000; + position: absolute; + z-index: 20000; background: #121721; border: 1px solid silver; border-radius: 15px; @@ -247,7 +248,7 @@ //transform: translateX(-50%); transform: translateY(-85px); pointer-events: all; - height: 30px; + height: fit-content; width:550px; .ProseMirror-example-setup-style hr { padding: 2px 10px; @@ -264,28 +265,40 @@ } } -// .tooltipMenu:before { -// content: ""; -// height: 0; width: 0; -// position: absolute; -// left: 50%; -// margin-left: -5px; -// bottom: -6px; -// border: 5px solid transparent; -// border-bottom-width: 0; -// border-top-color: silver; -// } -// .tooltipMenu:after { -// content: ""; -// height: 0; width: 0; -// position: absolute; -// left: 50%; -// margin-left: -5px; -// bottom: -4.5px; -// border: 5px solid transparent; -// border-bottom-width: 0; -// border-top-color: $dark-color; -// } +.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; + height: 25px; + width:fit-content; + .ProseMirror-example-setup-style hr { + padding: 2px 10px; + border: none; + margin: 1em 0; + } + + .ProseMirror-example-setup-style hr:after { + content: ""; + display: block; + height: 1px; + background-color: silver; + line-height: 2px; + } +} + +.wrapper { + position: absolute; + pointer-events: all; +} .menuicon { display: inline-block; @@ -298,6 +311,7 @@ cursor: pointer; text-align: center; min-width: 10px; + } .strong, .heading { font-weight: bold; } .em { font-style: italic; } @@ -310,9 +324,32 @@ padding-right: 0px; } .summarize{ - //margin-left: 15px; color: white; height: 20px; - // background-color: white; text-align: center; + } + + .brush{ + display: inline-block; + width: 1em; + height: 1em; + stroke-width: 0; + stroke: currentColor; + fill: currentColor; + margin-right: 15px; + } + + .brush-active{ + display: inline-block; + width: 1em; + height: 1em; + stroke-width: 3; + stroke: greenyellow; + fill: greenyellow; + margin-right: 15px; + } + + .dragger{ + color: #eee; + margin-left: 5px; }
\ No newline at end of file diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index 309d19016..d33a52d7f 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -1,6 +1,8 @@ -import { action } from "mobx"; +import { action, observable, observe } from "mobx"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faTag, faPlus, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'; import { Dropdown, MenuItem, icons, } from "prosemirror-menu"; //no import css -import { EditorState, NodeSelection, TextSelection } from "prosemirror-state"; +import { EditorState, NodeSelection, TextSelection, Transaction } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { schema } from "./RichTextSchema"; import { Schema, NodeType, MarkType, Mark, ResolvedPos } from "prosemirror-model"; @@ -18,8 +20,13 @@ import { DocServer } from "../DocServer"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { DocumentManager } from "./DocumentManager"; import { Id } from "../../new_fields/FieldSymbols"; -import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox"; -import { SelectionManager } from "./SelectionManager"; +import { FormattedTextBoxProps, FormattedTextBox } from "../views/nodes/FormattedTextBox"; +import { typeAlias } from "babel-types"; +import React, { Children } from "react"; +import ReactDOM from "react-dom"; +import { Utils } from "../../Utils"; +import { LinkManager } from "./LinkManager"; +import { bool } from "prop-types"; //appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc. export class TooltipTextMenu { @@ -33,6 +40,9 @@ export class TooltipTextMenu { private fontSizeToNum: Map<MarkType, number>; private fontStylesToName: Map<MarkType, string>; private listTypeToIcon: Map<NodeType, string>; + //private link: HTMLAnchorElement; + private wrapper: HTMLDivElement; + private extras: HTMLDivElement; private linkEditor?: HTMLDivElement; private linkText?: HTMLDivElement; @@ -46,13 +56,42 @@ export class TooltipTextMenu { private _collapseBtn?: MenuItem; + private _brushMarks?: Set<Mark>; + private _brushIsEmpty: boolean = true; + private _brushdom?: Node; + + private _marksToDoms: Map<Mark, HTMLSpanElement> = new Map(); + + private _collapsed: boolean = false; + + @observable + private _storedMarks: Mark<any>[] | null | undefined; + + constructor(view: EditorView, editorProps: FieldViewProps & FormattedTextBoxProps) { this.view = view; this.editorProps = editorProps; + + this.wrapper = document.createElement("div"); this.tooltip = document.createElement("div"); + this.extras = document.createElement("div"); + + this.wrapper.appendChild(this.extras); + this.wrapper.appendChild(this.tooltip); + 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._storedMarks = this.view.state.storedMarks; - this.dragElement(this.tooltip); // this.createCollapse(); // if (this._collapseBtn) { // this.tooltip.appendChild(this._collapseBtn.render(this.view).dom); @@ -71,13 +110,23 @@ export class TooltipTextMenu { { command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript", "Superscript") }, { command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript", "Subscript") }, { command: toggleMark(schema.marks.highlight), dom: this.icon("H", 'blue', 'Blue') } - // { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") }, - // { command: wrapInList(schema.nodes.ordered_list), dom: this.icon("1)", "bullets") }, - // { command: lift, dom: this.icon("<", "lift") }, ]; + + this._marksToDoms = new Map(); //add menu items items.forEach(({ dom, command }) => { 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; + } //pointer down handler to activate button effects dom.addEventListener("pointerdown", e => { @@ -86,12 +135,17 @@ export class TooltipTextMenu { if (dom.contains(e.target as Node)) { e.stopPropagation(); command(view.state, view.dispatch, view); + // if (this.view.state.selection.empty) { + // if (dom.style.color === "white") { dom.style.color = "greenyellow"; } + // else { dom.style.color = "white"; } + // } } }); }); this.updateLinkMenu(); + //list of font styles this.fontStylesToName = new Map(); this.fontStylesToName.set(schema.marks.timesNewRoman, "Times New Roman"); @@ -125,21 +179,23 @@ export class TooltipTextMenu { this.listTypeToIcon.set(schema.nodes.ordered_list, "1)"); 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.update(view, undefined); //view.dom.parentNode!.parentNode!.insertBefore(this.tooltip, view.dom.parentNode); - // quick and dirty null check + // add tooltip to outerdiv to circumvent scaling problem const outer_div = this.editorProps.outer_div; - outer_div && outer_div(this.tooltip); + outer_div && outer_div(this.wrapper); } //label of dropdown will change to given label @@ -164,6 +220,8 @@ export class TooltipTextMenu { this.fontSizeDom = newfontSizeDom; } + // Make the DIV element draggable + //label of dropdown will change to given label updateFontStyleDropdown(label: string) { //filtering function - might be unecessary @@ -213,8 +271,8 @@ export class TooltipTextMenu { let link = node && node.marks.find(m => m.type.name === "link"); if (link) { let href: string = link.attrs.href; - if (href.indexOf(DocServer.prepend("/doc/")) === 0) { - let docid = href.replace(DocServer.prepend("/doc/"), ""); + 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)) { @@ -254,11 +312,13 @@ export class TooltipTextMenu { if (docView && docView.props.ContainingCollectionView) { proto.sourceContext = docView.props.ContainingCollectionView.props.Document; } - linkDoc instanceof Doc && this.makeLink(DocServer.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab"); + linkDoc instanceof Doc && this.makeLink(Utils.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab"); }), }, hideSource: false }); + e.stopPropagation(); + e.preventDefault(); }; this.linkEditor.appendChild(this.linkDrag); // this.linkEditor.appendChild(this.linkText); @@ -285,6 +345,7 @@ export class TooltipTextMenu { if (elmnt) { // if present, the header is where you move the DIV from: elmnt.onpointerdown = dragMouseDown; + elmnt.ondblclick = onClick; } const self = this; @@ -299,6 +360,17 @@ export class TooltipTextMenu { document.onpointermove = elementDrag; } + function onClick(e: MouseEvent) { + self._collapsed = !self._collapsed; + const children = self.wrapper.childNodes; + if (self._collapsed && children.length > 1) { + self.wrapper.removeChild(self.tooltip); + } + else { + self.wrapper.appendChild(self.tooltip); + } + } + function elementDrag(e: PointerEvent) { e = e || window.event; //e.preventDefault(); @@ -308,8 +380,11 @@ export class TooltipTextMenu { pos3 = e.clientX; pos4 = e.clientY; // set the element's new position: - elmnt.style.top = (elmnt.offsetTop - pos2) + "px"; - elmnt.style.left = (elmnt.offsetLeft - pos1) + "px"; + // elmnt.style.top = (elmnt.offsetTop - pos2) + "px"; + // elmnt.style.left = (elmnt.offsetLeft - pos1) + "px"; + + self.wrapper.style.top = (self.wrapper.offsetTop - pos2) + "px"; + self.wrapper.style.left = (self.wrapper.offsetLeft - pos1) + "px"; } function closeDragElement() { @@ -321,6 +396,10 @@ export class TooltipTextMenu { } } + makeLinkWithState = (state: EditorState, target: string, location: string) => { + let link = state.schema.mark(state.schema.marks.link, { href: target, location: location }); + } + makeLink = (target: string, location: string) => { let node = this.view.state.selection.$from.nodeAfter; let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location }); @@ -330,6 +409,27 @@ export class TooltipTextMenu { link = node && node.marks.find(m => m.type.name === "link"); } + 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 => { + 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)); + } + }); + } + } + } + + + } + public static insertStar(state: EditorState<any>, dispatch: any) { let newNode = schema.nodes.star.create({ visibility: false, text: state.selection.content(), textslice: state.selection.content().toJSON(), textlen: state.selection.to - state.selection.from }); if (dispatch) { @@ -363,7 +463,7 @@ export class TooltipTextMenu { } //for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected text - changeToMarkInGroup = (markType: MarkType, view: EditorView, fontMarks: MarkType[]) => { + changeToMarkInGroup = (markType: MarkType | undefined, view: EditorView, fontMarks: MarkType[]) => { let { $cursor, ranges } = view.state.selection as TextSelection; let state = view.state; let dispatch = view.dispatch; @@ -389,17 +489,23 @@ export class TooltipTextMenu { } } }); - // fontsize - if (markType.name[0] === 'p') { - let size = this.fontSizeToNum.get(markType); - if (size) { this.updateFontSizeDropdown(String(size) + " pt"); } + + if (markType) { + // fontsize + if (markType.name[0] === 'p') { + let size = this.fontSizeToNum.get(markType); + if (size) { this.updateFontSizeDropdown(String(size) + " pt"); } + } + else { + let fontName = this.fontStylesToName.get(markType); + if (fontName) { this.updateFontStyleDropdown(fontName); } + } + //actually apply font + return toggleMark(markType)(view.state, view.dispatch, view); } else { - let fontName = this.fontStylesToName.get(markType); - if (fontName) { this.updateFontStyleDropdown(fontName); } + return; } - //actually apply font - return toggleMark(markType)(view.state, view.dispatch, view); } //remove all node typeand apply the passed-in one to the selected text @@ -442,6 +548,85 @@ export class TooltipTextMenu { }); } + 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", + execEvent: "", + run: (state, dispatch) => { + this.deleteLink(); + } + }); + } + + createBrush(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" + }; + return new MenuItem({ + title: "Brush tool", + label: "Brush tool", + icon: icon, + css: "color:white;", + class: active ? "brush-active" : "brush", + execEvent: "", + run: (state, dispatch) => { + this.brush_function(state, dispatch); + }, + active: (state) => { + return 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; + } + } + } + else { + let { 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) { + this.view.dispatch(this.view.state.tr.removeMark(from, to)); + this._brushMarks.forEach((mark: Mark) => { + const markType = mark.type; + this.changeToMarkInGroup(markType, this.view, []); + + }); + } + } + else { + const newbrush = this.createBrush(false).render(this.view).dom; + this.tooltip.replaceChild(newbrush, this._brushdom); + this._brushdom = newbrush; + this._brushIsEmpty = !this._brushIsEmpty; + } + } + } + + + } + createCollapse() { this._collapseBtn = new MenuItem({ title: "Collapse", @@ -597,20 +782,29 @@ export class TooltipTextMenu { }; } - getMarksInSelection(state: EditorState<any>, targets: MarkType<any>[]) { - let found: Mark<any>[] = []; + 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 => { - if (targets.includes(m.type)) found.push(m); + found.add(m); }); } }); 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(); + } + } + //updates the tooltip menu when the selection changes update(view: EditorView, lastState: EditorState | undefined) { let state = view.state; @@ -618,13 +812,13 @@ export class TooltipTextMenu { 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 @@ -662,12 +856,55 @@ export class TooltipTextMenu { } } this.view.dispatch(this.view.state.tr.setStoredMarks(this._activeMarks)); + + this.update_mark_doms(); + } + + public mark_key_pressed(marks: Mark<any>[]) { + if (this.view.state.selection.empty) { + if (marks) this._activeMarks = marks; + this.update_mark_doms(); + } + } + + 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 all active marks on selection in given group activeMarksOnSelection(markGroup: MarkType[]) { //current selection - let { empty, ranges } = this.view.state.selection as TextSelection; + let { empty, ranges, $to } = this.view.state.selection as TextSelection; let state = this.view.state; let dispatch = this.view.dispatch; let activeMarks: MarkType[]; @@ -682,6 +919,9 @@ export class TooltipTextMenu { } return false; }); + + const refnode = this.reference_node($to); + this._activeMarks = refnode.marks; } else { const pos = this.view.state.selection.$from; @@ -692,9 +932,7 @@ export class TooltipTextMenu { else { return []; } - this._activeMarks = ref_node.marks; - activeMarks = markGroup.filter(mark_type => { if (dispatch) { let mark = state.schema.mark(mark_type); @@ -713,12 +951,12 @@ export class TooltipTextMenu { reference_node(pos: ResolvedPos<any>): ProsNode { let ref_node: ProsNode = this.view.state.doc; - if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) { - ref_node = pos.nodeAfter; - } - else if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) { + 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--) { @@ -731,10 +969,13 @@ export class TooltipTextMenu { }); } } + if (!ref_node.isLeaf) { + ref_node = ref_node.child(0); + } return ref_node; } destroy() { - this.tooltip.remove(); + this.wrapper.remove(); } } diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d index 1f95af00c..79a4e50d5 100644 --- a/src/client/util/type_decls.d +++ b/src/client/util/type_decls.d @@ -179,7 +179,7 @@ declare class Doc extends RefField { // [ToScriptString](): string; } -declare class ListImpl<T extends Field> extends ObjectField { +declare class List<T extends Field> extends ObjectField { constructor(fields?: T[]); [index: number]: T | (T extends RefField ? Promise<T> : never); [Copy](): ObjectField; |
