diff options
| author | Tyler Schicke <tyler_schicke@brown.edu> | 2019-06-24 13:56:30 -0400 |
|---|---|---|
| committer | Tyler Schicke <tyler_schicke@brown.edu> | 2019-06-24 13:56:30 -0400 |
| commit | d01039b10f0ebd328224c0b1a190b0f884a7c727 (patch) | |
| tree | a97d85738f3173896aeffa04a22368b70b9dcdc3 /src/client/util | |
| parent | 6abf829099e4f61f2f557078f645fb9f2aa2414c (diff) | |
| parent | c9f77d5aab98e6e7865cdcad957d5c937631775d (diff) | |
Merge branch 'master' of github-tsch-brown:browngraphicslab/Dash-Web into schema_fixes
Diffstat (limited to 'src/client/util')
| -rw-r--r-- | src/client/util/ClientUtils.ts.temp | 3 | ||||
| -rw-r--r-- | src/client/util/DocumentManager.ts | 35 | ||||
| -rw-r--r-- | src/client/util/DragManager.ts | 63 | ||||
| -rw-r--r-- | src/client/util/ProsemirrorCopy/prompt.js | 179 | ||||
| -rw-r--r-- | src/client/util/ProsemirrorExampleTransfer.ts (renamed from src/client/util/ProsemirrorKeymap.ts) | 6 | ||||
| -rw-r--r-- | src/client/util/RichTextSchema.tsx | 129 | ||||
| -rw-r--r-- | src/client/util/Scripting.ts | 12 | ||||
| -rw-r--r-- | src/client/util/SelectionManager.ts | 40 | ||||
| -rw-r--r-- | src/client/util/TooltipTextMenu.scss | 76 | ||||
| -rw-r--r-- | src/client/util/TooltipTextMenu.tsx | 166 | ||||
| -rw-r--r-- | src/client/util/request-image-size.js | 73 |
11 files changed, 682 insertions, 100 deletions
diff --git a/src/client/util/ClientUtils.ts.temp b/src/client/util/ClientUtils.ts.temp new file mode 100644 index 000000000..f9fad5ed9 --- /dev/null +++ b/src/client/util/ClientUtils.ts.temp @@ -0,0 +1,3 @@ +export namespace ClientUtils { + export const RELEASE = "mode"; +}
\ No newline at end of file diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 65c4b9e4b..862395d74 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -35,9 +35,9 @@ export class DocumentManager { let toReturn: DocumentView | null = null; let passes = preferredCollection ? [preferredCollection, undefined] : [undefined]; - for (let i = 0; i < passes.length; i++) { + for (let pass of passes) { DocumentManager.Instance.DocumentViews.map(view => { - if (view.props.Document[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) { + if (view.props.Document[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) { toReturn = view; return; } @@ -45,7 +45,7 @@ export class DocumentManager { if (!toReturn) { DocumentManager.Instance.DocumentViews.map(view => { let doc = view.props.Document.proto; - if (doc && doc[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) { + if (doc && doc[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) { toReturn = view; } }); @@ -115,7 +115,7 @@ export class DocumentManager { } @undoBatch - public jumpToDocument = async (docDelegate: Doc, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number): Promise<void> => { + public jumpToDocument = async (docDelegate: Doc, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number, docContext?: Doc): Promise<void> => { let doc = Doc.GetProto(docDelegate); const contextDoc = await Cast(doc.annotationOn, Doc); if (contextDoc) { @@ -123,6 +123,7 @@ export class DocumentManager { const curPage = NumCast(contextDoc.curPage, page); if (page !== curPage) contextDoc.curPage = page; } + let docView: DocumentView | null; // using forceDockFunc as a flag for splitting linked to doc to the right...can change later if needed if (!forceDockFunc && (docView = DocumentManager.Instance.getDocumentView(doc))) { @@ -131,18 +132,34 @@ export class DocumentManager { docView.props.focus(docView.props.Document); } else { if (!contextDoc) { - const actualDoc = Doc.MakeAlias(docDelegate); - actualDoc.libraryBrush = true; - if (linkPage !== undefined) actualDoc.curPage = linkPage; - (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc); + if (docContext) { + let targetContextView: DocumentView | null; + if (!forceDockFunc && docContext && (targetContextView = DocumentManager.Instance.getDocumentView(docContext))) { + docContext.panTransformType = "Ease"; + targetContextView.props.focus(docDelegate); + } else { + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext); + setTimeout(() => { + this.jumpToDocument(docDelegate, forceDockFunc, dockFunc, linkPage); + }, 10); + } + } else { + const actualDoc = Doc.MakeAlias(docDelegate); + actualDoc.libraryBrush = true; + if (linkPage !== undefined) actualDoc.curPage = linkPage; + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc); + } } else { let contextView: DocumentView | null; docDelegate.libraryBrush = true; if (!forceDockFunc && (contextView = DocumentManager.Instance.getDocumentView(contextDoc))) { contextDoc.panTransformType = "Ease"; - contextView.props.focus(contextDoc); + contextView.props.focus(docDelegate); } else { (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc); + setTimeout(() => { + this.jumpToDocument(docDelegate, forceDockFunc, dockFunc, linkPage); + }, 10); } } } diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 1e84a0db0..c3c92daa5 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,12 +1,14 @@ -import { action, runInAction } from "mobx"; +import { action, runInAction, observable } from "mobx"; import { Doc, DocListCastAsync } from "../../new_fields/Doc"; import { Cast } from "../../new_fields/Types"; import { emptyFunction } from "../../Utils"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import * as globalCssVariables from "../views/globalCssVariables.scss"; +import { URLField } from "../../new_fields/URLField"; +import { SelectionManager } from "./SelectionManager"; export type dropActionType = "alias" | "copy" | undefined; -export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc>, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType) { +export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc>, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType, options?: any, dontHideOnDrop?: boolean) { let onRowMove = async (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); @@ -16,6 +18,8 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () var dragData = new DragManager.DocumentDragData([await docFunc()]); dragData.dropAction = dropAction; dragData.moveDocument = moveFunc; + dragData.options = options; + dragData.dontHideOnDrop = dontHideOnDrop; DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y); }; let onRowUp = (): void => { @@ -153,6 +157,22 @@ export namespace DragManager { [id: string]: any; } + export class AnnotationDragData { + constructor(dragDoc: Doc, annotationDoc: Doc, dropDoc: Doc) { + this.dragDocument = dragDoc; + this.dropDocument = dropDoc; + this.annotationDocument = annotationDoc; + this.xOffset = this.yOffset = 0; + } + dragDocument: Doc; + annotationDocument: Doc; + dropDocument: Doc; + xOffset: number; + yOffset: number; + dropAction: dropActionType; + userDropAction: dropActionType; + } + export let StartDragFunctions: (() => void)[] = []; export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) { @@ -166,6 +186,10 @@ export namespace DragManager { dragData.draggedDocuments)); } + 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; @@ -178,27 +202,42 @@ export namespace DragManager { [id: string]: any; } + export class EmbedDragData { + constructor(embeddableSourceDoc: Doc) { + this.embeddableSourceDoc = embeddableSourceDoc; + this.urlField = embeddableSourceDoc.data instanceof URLField ? embeddableSourceDoc.data : undefined; + } + embeddableSourceDoc: Doc; + urlField?: URLField; + [id: string]: any; + } + export function StartLinkDrag(ele: HTMLElement, dragData: LinkDragData, downX: number, downY: number, options?: DragOptions) { StartDrag([ele], dragData, downX, downY, options); } + export function StartEmbedDrag(ele: HTMLElement, dragData: EmbedDragData, downX: number, downY: number, options?: DragOptions) { + StartDrag([ele], dragData, downX, downY, options); + } + export let AbortDrag: () => void = emptyFunction; function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) { + eles = eles.filter(e => e); if (!dragDiv) { dragDiv = document.createElement("div"); dragDiv.className = "dragManager-dragDiv"; dragDiv.style.pointerEvents = "none"; DragManager.Root().appendChild(dragDiv); } - + SelectionManager.SetIsDragging(true); let scaleXs: number[] = []; let scaleYs: number[] = []; let xs: number[] = []; let ys: number[] = []; const docs: Doc[] = - dragData instanceof DocumentDragData ? dragData.draggedDocuments : []; + dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : []; let dragElements = eles.map(ele => { const w = ele.offsetWidth, h = ele.offsetHeight; @@ -241,6 +280,16 @@ export namespace DragManager { // pdfBox.replaceChild(img, pdfBox.children[0]) // } // } + let set = dragElement.getElementsByTagName('*'); + if (dragElement.hasAttribute("style")) (dragElement as any).style.pointerEvents = "none"; + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < set.length; i++) { + if (set[i].hasAttribute("style")) { + let s = set[i]; + (s as any).style.pointerEvents = "none"; + } + } + dragDiv.appendChild(dragElement); return dragElement; @@ -259,8 +308,7 @@ export namespace DragManager { let lastX = downX; let lastY = downY; const moveHandler = (e: PointerEvent) => { - e.stopPropagation(); - e.preventDefault(); + e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop if (dragData instanceof DocumentDragData) { dragData.userDropAction = e.ctrlKey || e.altKey ? "alias" : undefined; } @@ -284,6 +332,7 @@ export namespace DragManager { }; let hideDragElements = () => { + SelectionManager.SetIsDragging(false); dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement)); eles.map(ele => (ele.hidden = false)); }; @@ -309,7 +358,7 @@ export namespace DragManager { } function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) { - let removed = dragEles.map(dragEle => { + let removed = dragData.dontHideOnDrop ? [] : dragEles.map(dragEle => { // let parent = dragEle.parentElement; // if (parent) parent.removeChild(dragEle); let ret = [dragEle, dragEle.style.width, dragEle.style.height]; diff --git a/src/client/util/ProsemirrorCopy/prompt.js b/src/client/util/ProsemirrorCopy/prompt.js new file mode 100644 index 000000000..b9068195f --- /dev/null +++ b/src/client/util/ProsemirrorCopy/prompt.js @@ -0,0 +1,179 @@ +const prefix = "ProseMirror-prompt" + +export function openPrompt(options) { + let wrapper = document.body.appendChild(document.createElement("div")) + wrapper.className = prefix + wrapper.style.zIndex = 1000; + wrapper.style.width = 250; + wrapper.style.textAlign = "center"; + + let mouseOutside = e => { if (!wrapper.contains(e.target)) close() } + setTimeout(() => window.addEventListener("mousedown", mouseOutside), 50) + let close = () => { + window.removeEventListener("mousedown", mouseOutside) + if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper) + } + + let domFields = [] + for (let name in options.fields) domFields.push(options.fields[name].render()) + + let submitButton = document.createElement("button") + submitButton.type = "submit" + submitButton.className = prefix + "-submit" + submitButton.textContent = "OK" + let cancelButton = document.createElement("button") + cancelButton.type = "button" + cancelButton.className = prefix + "-cancel" + cancelButton.textContent = "Cancel" + cancelButton.addEventListener("click", close) + + let form = wrapper.appendChild(document.createElement("form")) + let title = document.createElement("h5") + title.style.marginBottom = 15 + title.style.marginTop = 10 + if (options.title) form.appendChild(title).textContent = options.title + domFields.forEach(field => { + form.appendChild(document.createElement("div")).appendChild(field) + }) + let b = document.createElement("div"); + b.style.marginTop = 15; + let buttons = form.appendChild(b) + // buttons.className = prefix + "-buttons" + buttons.appendChild(submitButton) + buttons.appendChild(document.createTextNode(" ")) + buttons.appendChild(cancelButton) + + let box = wrapper.getBoundingClientRect() + wrapper.style.top = options.flyout_top + "px" + wrapper.style.left = options.flyout_left + "px" + + let submit = () => { + let params = getValues(options.fields, domFields) + if (params) { + close() + options.callback(params) + } + } + + form.addEventListener("submit", e => { + e.preventDefault() + submit() + }) + + form.addEventListener("keydown", e => { + if (e.keyCode == 27) { + e.preventDefault() + close() + } else if (e.keyCode == 13 && !(e.ctrlKey || e.metaKey || e.shiftKey)) { + e.preventDefault() + submit() + } else if (e.keyCode == 9) { + window.setTimeout(() => { + if (!wrapper.contains(document.activeElement)) close() + }, 500) + } + }) + + let input = form.elements[0] + if (input) input.focus() +} + +function getValues(fields, domFields) { + let result = Object.create(null), i = 0 + for (let name in fields) { + let field = fields[name], dom = domFields[i++] + let value = field.read(dom), bad = field.validate(value) + if (bad) { + reportInvalid(dom, bad) + return null + } + result[name] = field.clean(value) + } + return result +} + +function reportInvalid(dom, message) { + // FIXME this is awful and needs a lot more work + let parent = dom.parentNode + let msg = parent.appendChild(document.createElement("div")) + msg.style.left = (dom.offsetLeft + dom.offsetWidth + 2) + "px" + msg.style.top = (dom.offsetTop - 5) + "px" + msg.className = "ProseMirror-invalid" + msg.textContent = message + setTimeout(() => parent.removeChild(msg), 1500) +} + +// ::- The type of field that `FieldPrompt` expects to be passed to it. +export class Field { + // :: (Object) + // Create a field with the given options. Options support by all + // field types are: + // + // **`value`**`: ?any` + // : The starting value for the field. + // + // **`label`**`: string` + // : The label for the field. + // + // **`required`**`: ?bool` + // : Whether the field is required. + // + // **`validate`**`: ?(any) → ?string` + // : A function to validate the given value. Should return an + // error message if it is not valid. + constructor(options) { this.options = options } + + // render:: (state: EditorState, props: Object) → dom.Node + // Render the field to the DOM. Should be implemented by all subclasses. + + // :: (dom.Node) → any + // Read the field's value from its DOM node. + read(dom) { return dom.value } + + // :: (any) → ?string + // A field-type-specific validation function. + validateType(_value) { } + + validate(value) { + if (!value && this.options.required) + return "Required field" + return this.validateType(value) || (this.options.validate && this.options.validate(value)) + } + + clean(value) { + return this.options.clean ? this.options.clean(value) : value + } +} + +// ::- A field class for single-line text fields. +export class TextField extends Field { + render() { + let input = document.createElement("input") + input.type = "text" + input.placeholder = this.options.label + input.value = this.options.value || "" + input.autocomplete = "off" + input.style.marginBottom = 4 + input.style.border = "1px solid black" + input.style.padding = "4px 4px" + return input + } +} + + +// ::- A field class for dropdown fields based on a plain `<select>` +// tag. Expects an option `options`, which should be an array of +// `{value: string, label: string}` objects, or a function taking a +// `ProseMirror` instance and returning such an array. +export class SelectField extends Field { + render() { + let select = document.createElement("select") + this.options.options.forEach(o => { + let opt = select.appendChild(document.createElement("option")) + opt.value = o.value + opt.selected = o.value == this.options.value + opt.label = o.label + }) + return select + } +} diff --git a/src/client/util/ProsemirrorKeymap.ts b/src/client/util/ProsemirrorExampleTransfer.ts index 00d086b97..091926d0a 100644 --- a/src/client/util/ProsemirrorKeymap.ts +++ b/src/client/util/ProsemirrorExampleTransfer.ts @@ -1,4 +1,4 @@ -import { Schema } from "prosemirror-model"; +import { Schema, NodeType } from "prosemirror-model"; import { wrapIn, setBlockType, chainCommands, toggleMark, exitCode, joinUp, joinDown, lift, selectParentNode @@ -50,7 +50,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: } if (type = schema.nodes.bullet_list) { - bind("Ctrl-b", wrapInList(type)); + bind("Ctrl-.", wrapInList(type)); } if (type = schema.nodes.ordered_list) { bind("Ctrl-n", wrapInList(type)); @@ -97,4 +97,4 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: } return keys; -}
\ No newline at end of file +} diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 3e3e98206..943cdb4d1 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -1,9 +1,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType } from "prosemirror-model"; -import { joinUp, lift, setBlockType, toggleMark, wrapIn } from 'prosemirror-commands'; +import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType, Slice } from "prosemirror-model"; +import { joinUp, lift, setBlockType, toggleMark, wrapIn, selectNodeForward, deleteSelection } from 'prosemirror-commands'; import { redo, undo } from 'prosemirror-history'; import { orderedList, bulletList, listItem, } from 'prosemirror-schema-list'; -import { EditorState, Transaction, NodeSelection, } from "prosemirror-state"; +import { EditorState, Transaction, NodeSelection, TextSelection, Selection, } from "prosemirror-state"; import { EditorView, } from "prosemirror-view"; const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"], @@ -26,6 +26,14 @@ export const nodes: { [index: string]: NodeSpec } = { toDOM() { return pDOM; } }, + // starmine: { + // inline: true, + // attrs: { oldtext: { default: "" } }, + // group: "inline", + // toDOM() { return ["star", "㊉"]; }, + // parseDOM: [{ tag: "star" }] + // }, + // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks. blockquote: { content: "block+", @@ -77,6 +85,30 @@ export const nodes: { [index: string]: NodeSpec } = { group: "inline" }, + star: { + inline: true, + attrs: { + visibility: { default: false }, + oldtext: { default: undefined }, + oldtextslice: { default: undefined }, + oldtextlen: { default: 0 } + + }, + group: "inline", + toDOM(node) { + const attrs = { style: `width: 40px` }; + return ["span", { ...node.attrs, ...attrs }]; + }, + // parseDOM: [{ + // tag: "star", getAttrs(dom: any) { + // return { + // visibility: dom.getAttribute("visibility"), + // oldtext: dom.getAttribute("oldtext"), + // oldtextlen: dom.getAttribute("oldtextlen"), + // } + // } + // }] + }, // :: NodeSpec An inline image (`<img>`) node. Supports `src`, // `alt`, and `href` attributes. The latter two default to the empty // string. @@ -107,6 +139,32 @@ export const nodes: { [index: string]: NodeSpec } = { } }, + video: { + inline: true, + attrs: { + src: {}, + width: { default: "100px" }, + alt: { default: null }, + title: { default: null } + }, + group: "inline", + draggable: true, + parseDOM: [{ + tag: "video[src]", getAttrs(dom: any) { + return { + src: dom.getAttribute("src"), + title: dom.getAttribute("title"), + alt: dom.getAttribute("alt"), + width: Math.min(100, Number(dom.getAttribute("width"))), + }; + } + }], + toDOM(node) { + const attrs = { style: `width: ${node.attrs.width}` }; + return ["video", { ...node.attrs, ...attrs }]; + } + }, + // :: NodeSpec A hard line break, represented in the DOM as `<br>`. hard_break: { inline: true, @@ -222,6 +280,15 @@ export const marks: { [index: string]: MarkSpec } = { toDOM: () => ['sup'] }, + collapse: { + parseDOM: [{ style: 'color: blue' }], + toDOM() { + return ['span', { + style: 'color: blue' + }]; + } + }, + // :: MarkSpec Code font mark. Represented as a `<code>` element. code: { @@ -280,6 +347,7 @@ export const marks: { [index: string]: MarkSpec } = { }] }, + /** FONT SIZES */ p10: { @@ -407,6 +475,49 @@ export class ImageResizeView { this._handle.style.display = "none"; } } + +export class SummarizedView { + // TODO: highlight text that is summarized. to find end of region, walk along mark + _collapsed: HTMLElement; + constructor(node: any, view: any, getPos: any) { + this._collapsed = document.createElement("span"); + this._collapsed.textContent = "㊉"; + this._collapsed.style.opacity = "0.5"; + // this._collapsed.style.background = "yellow"; + this._collapsed.style.position = "relative"; + this._collapsed.style.width = "40px"; + this._collapsed.style.height = "20px"; + this._collapsed.onpointerdown = function (e: any) { + console.log("star pressed!"); + if (node.attrs.visibility) { + node.attrs.visibility = !node.attrs.visibility; + console.log("content is visible"); + let y = getPos(); + view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, y + 1, y + 1 + node.attrs.oldtextlen))); + view.dispatch(view.state.tr.deleteSelection(view.state, () => { })); + //this._collapsed.textContent = "㊀"; + } else { + node.attrs.visibility = !node.attrs.visibility; + console.log("content is invisible"); + let y = getPos(); + let mark = view.state.schema.mark(view.state.schema.marks.underline); + console.log("PASTING " + node.attrs.oldtext.toString()); + view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, y + 1, y + 1))); + view.dispatch(view.state.tr.replaceSelection(node.attrs.oldtext).addMark(view.state.selection.from, view.state.selection.from + node.attrs.oldtextlen, mark)); + //this._collapsed.textContent = "㊉"; + } + e.preventDefault(); + e.stopPropagation(); + }; + (this as any).dom = this._collapsed; + + } + selectNode() { + } + + deselectNode() { + } +} // :: Schema // This schema rougly corresponds to the document schema used by // [CommonMark](http://commonmark.org/), minus the list elements, @@ -415,4 +526,14 @@ export class ImageResizeView { // // To reuse elements from this schema, extend or read from its // `spec.nodes` and `spec.marks` [properties](#model.Schema.spec). -export const schema = new Schema({ nodes, marks });
\ No newline at end of file +export const schema = new Schema({ nodes, marks }); + +const fromJson = schema.nodeFromJSON; + +schema.nodeFromJSON = (json: any) => { + let node = fromJson(json); + if (json.type === "star") { + node.attrs.oldtext = Slice.fromJSON(schema, node.attrs.oldtextslice); + } + return node; +};
\ No newline at end of file diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 688716d5f..30a05154a 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -39,7 +39,6 @@ export interface CompileError { } export type CompileResult = CompiledScript | CompileError; - function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult { const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error); if ((options.typecheck !== false && errors) || !script) { @@ -64,10 +63,20 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an } } let thisParam = args.this || capturedVariables.this; + let batch: { end(): void } | undefined = undefined; try { + if (!options.editable) { + batch = Doc.MakeReadOnly(); + } const result = compiledFunction.apply(thisParam, params).apply(thisParam, argsArray); + if (batch) { + batch.end(); + } return { success: true, result }; } catch (error) { + if (batch) { + batch.end(); + } return { success: false, error }; } }; @@ -133,6 +142,7 @@ export interface ScriptOptions { params?: { [name: string]: string }; capturedVariables?: { [name: string]: Field }; typecheck?: boolean; + editable?: boolean; } export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult { diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 0e22d576c..7dbb81e76 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,4 +1,4 @@ -import { observable, action } from "mobx"; +import { observable, action, runInAction } from "mobx"; import { Doc } from "../../new_fields/Doc"; import { DocumentView } from "../views/nodes/DocumentView"; import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; @@ -6,19 +6,27 @@ import { NumCast } from "../../new_fields/Types"; export namespace SelectionManager { class Manager { - @observable - SelectedDocuments: Array<DocumentView> = []; + @observable IsDragging: boolean = false; + @observable SelectedDocuments: Array<DocumentView> = []; @action - SelectDoc(doc: DocumentView, ctrlPressed: boolean): void { + SelectDoc(docView: DocumentView, ctrlPressed: boolean): void { // if doc is not in SelectedDocuments, add it - if (!ctrlPressed) { - this.DeselectAll(); - } + if (manager.SelectedDocuments.indexOf(docView) === -1) { + if (!ctrlPressed) { + this.DeselectAll(); + } - if (manager.SelectedDocuments.indexOf(doc) === -1) { - manager.SelectedDocuments.push(doc); - doc.props.whenActiveChanged(true); + manager.SelectedDocuments.push(docView); + docView.props.whenActiveChanged(true); + } + } + @action + DeselectDoc(docView: DocumentView): void { + let ind = manager.SelectedDocuments.indexOf(docView); + if (ind !== -1) { + manager.SelectedDocuments.splice(ind, 1); + docView.props.whenActiveChanged(false); } } @action @@ -31,8 +39,11 @@ export namespace SelectionManager { const manager = new Manager(); - export function SelectDoc(doc: DocumentView, ctrlPressed: boolean): void { - manager.SelectDoc(doc, ctrlPressed); + export function DeselectDoc(docView: DocumentView): void { + manager.DeselectDoc(docView); + } + export function SelectDoc(docView: DocumentView, ctrlPressed: boolean): void { + manager.SelectDoc(docView, ctrlPressed); } export function IsSelected(doc: DocumentView): boolean { @@ -51,8 +62,11 @@ export namespace SelectionManager { if (found) manager.SelectDoc(found, false); } + export function SetIsDragging(dragging: boolean) { runInAction(() => manager.IsDragging = dragging); } + export function GetIsDragging() { return manager.IsDragging; } + export function SelectedDocuments(): Array<DocumentView> { - return manager.SelectedDocuments; + return manager.SelectedDocuments.slice(); } export function ViewsSortedHorizontally(): DocumentView[] { let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => { diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss index 437da0d63..4d4eb386d 100644 --- a/src/client/util/TooltipTextMenu.scss +++ b/src/client/util/TooltipTextMenu.scss @@ -36,7 +36,7 @@ position: relative; padding-right: 15px; margin: 3px; - background: #333333; + background: white; border-radius: 3px; text-align: center; } @@ -69,6 +69,7 @@ .ProseMirror-menu-dropdown-menu { z-index: 15; min-width: 6em; + background: white; } .linking { @@ -82,7 +83,7 @@ } .ProseMirror-menu-dropdown-item:hover { - background: #2e2b2b; + background: white; } .ProseMirror-menu-submenu-wrap { @@ -132,7 +133,7 @@ position: relative; min-height: 1em; color: white; - padding: 1px 6px; + padding: 10px 10px; top: 0; left: 0; right: 0; border-bottom: 1px solid silver; background:$dark-color; @@ -155,7 +156,7 @@ } .ProseMirror-icon svg { - fill: currentColor; + fill:white; height: 1em; } @@ -184,7 +185,7 @@ position: fixed; border-radius: 3px; z-index: 11; - box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2); + box-shadow: -.5px 2px 5px white(255, 255, 255, 0.2); } .ProseMirror-prompt h5 { @@ -196,7 +197,7 @@ .ProseMirror-prompt input[type="text"], .ProseMirror-prompt textarea { - background: #eee; + background: white; border: none; outline: none; } @@ -233,15 +234,18 @@ .tooltipMenu { position: absolute; - z-index: 200; - background: $dark-color; + z-index: 50; + background: whitesmoke; border: 1px solid silver; - border-radius: 4px; + border-radius: 15px; padding: 2px 10px; - margin-bottom: 7px; - -webkit-transform: translateX(-50%); - transform: translateX(-50%); + //margin-bottom: 100px; + //-webkit-transform: translateX(-50%); + //transform: translateX(-50%); + transform: translateY(-50%); pointer-events: all; + height: auto; + width:inherit; .ProseMirror-example-setup-style hr { padding: 2px 10px; border: none; @@ -257,34 +261,34 @@ } } -.tooltipMenu:before { - content: ""; - height: 0; width: 0; - position: absolute; - left: 50%; - margin-left: -5px; - bottom: -6px; - border: 5px solid transparent; - border-bottom-width: 0; - border-top-color: silver; - } - .tooltipMenu:after { - content: ""; - height: 0; width: 0; - position: absolute; - left: 50%; - margin-left: -5px; - bottom: -4.5px; - border: 5px solid transparent; - border-bottom-width: 0; - border-top-color: $dark-color; - } +// .tooltipMenu:before { +// content: ""; +// height: 0; width: 0; +// position: absolute; +// left: 50%; +// margin-left: -5px; +// bottom: -6px; +// border: 5px solid transparent; +// border-bottom-width: 0; +// border-top-color: silver; +// } +// .tooltipMenu:after { +// content: ""; +// height: 0; width: 0; +// position: absolute; +// left: 50%; +// margin-left: -5px; +// bottom: -4.5px; +// border: 5px solid transparent; +// border-bottom-width: 0; +// border-top-color: $dark-color; +// } .menuicon { display: inline-block; - border-right: 1px solid rgba(0, 0, 0, 0.2); + border-right: 1px solid white(0, 0, 0, 0.2); //color: rgb(19, 18, 18); - color: white; + color: rgb(226, 21, 21); line-height: 1; padding: 0px 2px; margin: 1px; diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index f517f757a..f2559db74 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -1,6 +1,6 @@ import { action, IReactionDisposer, reaction } from "mobx"; import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css -import { baseKeymap, lift } from "prosemirror-commands"; +import { baseKeymap, lift, deleteSelection } from "prosemirror-commands"; import { history, redo, undo } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state"; @@ -14,20 +14,22 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { wrapInList, bulletList, liftListItem, listItem, } from 'prosemirror-schema-list'; import { liftTarget, RemoveMarkStep, AddMarkStep } from 'prosemirror-transform'; import { - faListUl, + faListUl, faGrinTongueSquint, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FieldViewProps } from "../views/nodes/FieldView"; import { throwStatement } from "babel-types"; +const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js"); import { View } from "@react-pdf/renderer"; import { DragManager } from "./DragManager"; import { Doc, Opt, Field } from "../../new_fields/Doc"; -import { Utils } from "../northstar/utils/Utils"; import { DocServer } from "../DocServer"; import { CollectionFreeFormDocumentView } from "../views/nodes/CollectionFreeFormDocumentView"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { DocumentManager } from "./DocumentManager"; import { Id } from "../../new_fields/FieldSymbols"; +import { Utils } from "../../Utils"; +// import { wrap } from "module"; const SVG = "http://www.w3.org/2000/svg"; @@ -46,6 +48,8 @@ export class TooltipTextMenu { private fontStylesToName: Map<MarkType, string>; private listTypeToIcon: Map<NodeType, string>; private fontSizeIndicator: HTMLSpanElement = document.createElement("span"); + private link: HTMLAnchorElement; + private linkEditor?: HTMLDivElement; private linkText?: HTMLDivElement; private linkDrag?: HTMLImageElement; @@ -68,12 +72,13 @@ export class TooltipTextMenu { library.add(faListUl); //add the buttons to the tooltip let items = [ - { command: toggleMark(schema.marks.strong), dom: this.icon("B", "strong") }, - { command: toggleMark(schema.marks.em), dom: this.icon("i", "em") }, - { command: toggleMark(schema.marks.underline), dom: this.icon("U", "underline") }, - { command: toggleMark(schema.marks.strikethrough), dom: this.icon("S", "strikethrough") }, - { command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript") }, - { command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript") }, + { command: toggleMark(schema.marks.strong), dom: this.icon("B", "strong", "Bold") }, + { command: toggleMark(schema.marks.em), dom: this.icon("i", "em", "Italic") }, + { command: toggleMark(schema.marks.underline), dom: this.icon("U", "underline", "Underline") }, + { command: toggleMark(schema.marks.strikethrough), dom: this.icon("S", "strikethrough", "Strikethrough") }, + { command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript", "Superscript") }, + { command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript", "Subscript") }, + { command: deleteSelection, dom: this.icon("C", 'collapse', 'Collapse') } // { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") }, // { command: wrapInList(schema.nodes.ordered_list), dom: this.icon("1)", "bullets") }, // { command: lift, dom: this.icon("<", "lift") }, @@ -86,7 +91,10 @@ export class TooltipTextMenu { dom.addEventListener("pointerdown", e => { e.preventDefault(); view.focus(); - command(view.state, view.dispatch, view); + if (dom.contains(e.target as Node)) { + e.stopPropagation(); + command(view.state, view.dispatch, view); + } }); }); @@ -120,6 +128,13 @@ export class TooltipTextMenu { this.listTypeToIcon.set(schema.nodes.ordered_list, "1)"); this.listTypes = Array.from(this.listTypeToIcon.keys()); + this.link = document.createElement("a"); + this.link.target = "_blank"; + this.link.style.color = "white"; + this.tooltip.appendChild(this.link); + + this.tooltip.appendChild(this.createLink().render(this.view).dom); + this.update(view, undefined); } @@ -131,13 +146,13 @@ export class TooltipTextMenu { //font SIZES let fontSizeBtns: MenuItem[] = []; this.fontSizeToNum.forEach((number, mark) => { - fontSizeBtns.push(this.dropdownMarkBtn(String(number), "width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes)); + fontSizeBtns.push(this.dropdownMarkBtn(String(number), "color: black; width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes)); }); if (this.fontSizeDom) { this.tooltip.removeChild(this.fontSizeDom); } this.fontSizeDom = (new Dropdown(cut(fontSizeBtns), { label: label, - css: "color:white; min-width: 60px; padding-left: 5px; margin-right: 0;" + css: "color:black; min-width: 60px; padding-left: 5px; margin-right: 0;" }) as MenuItem).render(this.view).dom; this.tooltip.appendChild(this.fontSizeDom); } @@ -150,13 +165,13 @@ export class TooltipTextMenu { //font STYLES let fontBtns: MenuItem[] = []; this.fontStylesToName.forEach((name, mark) => { - fontBtns.push(this.dropdownMarkBtn(name, "font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles)); + fontBtns.push(this.dropdownMarkBtn(name, "color: black; font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles)); }); if (this.fontStyleDom) { this.tooltip.removeChild(this.fontStyleDom); } this.fontStyleDom = (new Dropdown(cut(fontBtns), { label: label, - css: "color:white; width: 125px; margin-left: -3px; padding-left: 2px;" + css: "color:black; width: 125px; margin-left: -3px; padding-left: 2px;" }) as MenuItem).render(this.view).dom; this.tooltip.appendChild(this.fontStyleDom); @@ -165,7 +180,7 @@ export class TooltipTextMenu { updateLinkMenu() { if (!this.linkEditor || !this.linkText) { this.linkEditor = document.createElement("div"); - this.linkEditor.style.color = "white"; + this.linkEditor.style.color = "black"; this.linkText = document.createElement("div"); this.linkText.style.cssFloat = "left"; this.linkText.style.marginRight = "5px"; @@ -174,13 +189,13 @@ export class TooltipTextMenu { this.linkText.style.whiteSpace = "nowrap"; this.linkText.style.width = "150px"; this.linkText.style.overflow = "hidden"; - this.linkText.style.color = "white"; + this.linkText.style.color = "black"; this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); }; let linkBtn = document.createElement("div"); linkBtn.textContent = ">>"; - linkBtn.style.width = "20px"; - linkBtn.style.height = "20px"; - linkBtn.style.color = "white"; + linkBtn.style.width = "10px"; + linkBtn.style.height = "10px"; + linkBtn.style.color = "black"; linkBtn.style.cssFloat = "left"; linkBtn.onpointerdown = (e: PointerEvent) => { let node = this.view.state.selection.$from.nodeAfter; @@ -207,7 +222,7 @@ export class TooltipTextMenu { this.linkDrag.src = "https://seogurusnyc.com/wp-content/uploads/2016/12/link-1.png"; this.linkDrag.style.width = "20px"; this.linkDrag.style.height = "20px"; - this.linkDrag.style.color = "white"; + this.linkDrag.style.color = "black"; this.linkDrag.style.background = "black"; this.linkDrag.style.cssFloat = "left"; this.linkDrag.onpointerdown = (e: PointerEvent) => { @@ -228,6 +243,22 @@ export class TooltipTextMenu { this.linkEditor.appendChild(this.linkText); this.linkEditor.appendChild(linkBtn); this.tooltip.appendChild(this.linkEditor); + + let starButton = document.createElement("span"); + // starButton.style.width = '10px'; + // starButton.style.height = '10px'; + starButton.style.marginLeft = '10px'; + starButton.textContent = "Summarize"; + starButton.style.color = 'black'; + starButton.style.height = '20px'; + starButton.style.backgroundColor = 'white'; + starButton.style.textAlign = 'center'; + starButton.onclick = () => { + let state = this.view.state; + this.insertStar(state, this.view.dispatch); + }; + + this.tooltip.appendChild(starButton); } let node = this.view.state.selection.$from.nodeAfter; @@ -253,6 +284,16 @@ export class TooltipTextMenu { link = node && node.marks.find(m => m.type.name === "link"); } + insertStar(state: EditorState<any>, dispatch: any) { + console.log("creating star..."); + let newNode = schema.nodes.star.create({ visibility: false, oldtext: state.selection.content(), oldtextslice: state.selection.content().toJSON(), oldtextlen: state.selection.to - state.selection.from }); + if (dispatch) { + console.log(newNode.attrs.oldtext.toString()); + dispatch(state.tr.replaceSelectionWith(newNode)); + } + return true; + } + //will display a remove-list-type button if selection is in list, otherwise will show list type dropdown updateListItemDropdown(label: string, listTypeBtn: Node) { //remove old btn @@ -261,14 +302,14 @@ export class TooltipTextMenu { //Make a dropdown of all list types let toAdd: MenuItem[] = []; this.listTypeToIcon.forEach((icon, type) => { - toAdd.push(this.dropdownNodeBtn(icon, "width: 40px;", type, this.view, this.listTypes, this.changeToNodeType)); + toAdd.push(this.dropdownNodeBtn(icon, "color: black; width: 40px;", type, this.view, this.listTypes, this.changeToNodeType)); }); //option to remove the list formatting - toAdd.push(this.dropdownNodeBtn("X", "width: 40px;", undefined, this.view, this.listTypes, this.changeToNodeType)); + toAdd.push(this.dropdownNodeBtn("X", "color: black; width: 40px;", undefined, this.view, this.listTypes, this.changeToNodeType)); listTypeBtn = (new Dropdown(toAdd, { label: label, - css: "color:white; width: 40px;" + css: "color:black; width: 40px;" }) as MenuItem).render(this.view).dom; //add this new button and return it @@ -332,6 +373,43 @@ export class TooltipTextMenu { }); } + createLink() { + let markType = schema.marks.link; + return new MenuItem({ + title: "Add or remove link", + label: "Add or remove link", + execEvent: "", + icon: icons.link, + css: "color:white;", + class: "menuicon", + enable(state) { return !state.selection.empty; }, + run: (state, dispatch, view) => { + // to remove link + if (this.markActive(state, markType)) { + toggleMark(markType)(state, dispatch); + return true; + } + // to create link + openPrompt({ + title: "Create a link", + fields: { + href: new TextField({ + label: "Link target", + required: true + }), + title: new TextField({ label: "Title" }) + }, + callback(attrs: any) { + toggleMark(markType, attrs)(view.state, view.dispatch); + view.focus(); + }, + flyout_top: 0, + flyout_left: 0 + }); + } + }); + } + //makes a button for the drop down FOR NODE TYPES //css is the style you want applied to the button dropdownNodeBtn(label: string, css: string, nodeType: NodeType | undefined, view: EditorView, groupNodes: NodeType[], changeToNodeInGroup: (nodeType: NodeType<any> | undefined, view: EditorView, groupNodes: NodeType[]) => any) { @@ -348,13 +426,19 @@ export class TooltipTextMenu { }); } + markActive = function (state: EditorState<any>, type: MarkType<Schema<string, string>>) { + let { from, $from, to, empty } = state.selection; + if (empty) return type.isInSet(state.storedMarks || $from.marks()); + else return state.doc.rangeHasMark(from, to, type); + }; + // Helper function to create menu icons - icon(text: string, name: string) { + icon(text: string, name: string, title: string = name) { let span = document.createElement("span"); span.className = name + " menuicon"; - span.title = name; + span.title = title; span.textContent = text; - span.style.color = "white"; + span.style.color = "black"; return span; } @@ -395,6 +479,20 @@ export class TooltipTextMenu { }; } + getMarksInSelection(state: EditorState<any>, targets: MarkType<any>[]) { + let found: Mark<any>[] = []; + let { from, to } = state.selection as TextSelection; + state.doc.nodesBetween(from, to, (node) => { + let marks = node.marks; + if (marks) { + marks.forEach(m => { + if (targets.includes(m.type)) found.push(m); + }); + } + }); + return found; + } + //updates the tooltip menu when the selection changes update(view: EditorView, lastState: EditorState | undefined) { let state = view.state; @@ -408,6 +506,14 @@ export class TooltipTextMenu { return; } + let linksInSelection = this.activeMarksOnSelection([schema.marks.link]); + if (linksInSelection.length > 0) { + let attributes = this.getMarksInSelection(this.view.state, [schema.marks.link])[0].attrs; + this.link.href = attributes.href; + this.link.textContent = attributes.title; + this.link.style.visibility = "visible"; + } else this.link.style.visibility = "hidden"; + // Otherwise, reposition it and update its content this.tooltip.style.display = ""; let { from, to } = state.selection; @@ -421,8 +527,14 @@ export class TooltipTextMenu { let width = Math.abs(start.left - end.left) / 2 * this.editorProps.ScreenToLocalTransform().Scale; let mid = Math.min(start.left, end.left) + width; - this.tooltip.style.width = 225 + "px"; + //this.tooltip.style.width = 225 + "px"; this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px"; + this.tooltip.style.top = "-100px"; + //this.tooltip.style.height = "100px"; + + // let transform = this.editorProps.ScreenToLocalTransform(); + // this.tooltip.style.width = `${225 / transform.Scale}px`; + // Utils //UPDATE LIST ITEM DROPDOWN this.listTypeBtnDom = this.updateListItemDropdown(":", this.listTypeBtnDom!); diff --git a/src/client/util/request-image-size.js b/src/client/util/request-image-size.js new file mode 100644 index 000000000..0f9328872 --- /dev/null +++ b/src/client/util/request-image-size.js @@ -0,0 +1,73 @@ +/** + * request-image-size: Detect image dimensions via request. + * Licensed under the MIT license. + * + * https://github.com/FdezRomero/request-image-size + * © 2017 Rodrigo Fernández Romero + * + * Based on the work of Johannes J. Schmidt + * https://github.com/jo/http-image-size + */ + +const request = require('request'); +const imageSize = require('image-size'); +const HttpError = require('standard-http-error'); + +module.exports = function requestImageSize(options) { + let opts = { + encoding: null + }; + + if (options && typeof options === 'object') { + opts = Object.assign(options, opts); + } else if (options && typeof options === 'string') { + opts = Object.assign({ uri: options }, opts); + } else { + return Promise.reject(new Error('You should provide an URI string or a "request" options object.')); + } + + opts.encoding = null; + + return new Promise((resolve, reject) => { + const req = request(opts); + + req.on('response', res => { + if (res.statusCode >= 400) { + return reject(new HttpError(res.statusCode, res.statusMessage)); + } + + let buffer = new Buffer([]); + let size; + let imageSizeError; + + res.on('data', chunk => { + buffer = Buffer.concat([buffer, chunk]); + + try { + size = imageSize(buffer); + } catch (err) { + imageSizeError = err; + return; + } + + if (size) { + resolve(size); + return req.abort(); + } + }); + + res.on('error', err => reject(err)); + + res.on('end', () => { + if (!size) { + return reject(imageSizeError); + } + + size.downloaded = buffer.length; + return resolve(size); + }); + }); + + req.on('error', err => reject(err)); + }); +}; |
