diff options
Diffstat (limited to 'src')
-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) | 2 | ||||
-rw-r--r-- | src/client/util/TooltipTextMenu.tsx | 88 | ||||
-rw-r--r-- | src/client/views/PresentationView.tsx | 10 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 7 | ||||
-rw-r--r-- | src/debug/Test.tsx | 1 | ||||
-rw-r--r-- | src/new_fields/Doc.ts | 1 | ||||
-rw-r--r-- | src/server/index.ts | 3 |
9 files changed, 272 insertions, 21 deletions
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..964720b06 100644 --- a/src/client/util/ProsemirrorKeymap.ts +++ b/src/client/util/ProsemirrorExampleTransfer.ts @@ -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/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index 6eb654319..b860f6758 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -14,11 +14,12 @@ 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"; @@ -46,6 +47,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; @@ -120,6 +123,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); } @@ -175,7 +185,7 @@ export class TooltipTextMenu { this.linkText.style.width = "150px"; this.linkText.style.overflow = "hidden"; this.linkText.style.color = "white"; - this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); } + this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); }; let linkBtn = document.createElement("div"); linkBtn.textContent = ">>"; linkBtn.style.width = "20px"; @@ -192,8 +202,9 @@ export class TooltipTextMenu { let docid = href.replace(DocServer.prepend("/doc/"), ""); DocServer.GetRefField(docid).then(action((f: Opt<Field>) => { if (f instanceof Doc) { - if (DocumentManager.Instance.getDocumentView(f)) + if (DocumentManager.Instance.getDocumentView(f)) { DocumentManager.Instance.getDocumentView(f)!.props.focus(f); + } else CollectionDockingView.Instance.AddRightSplit(f); } })); @@ -201,7 +212,7 @@ export class TooltipTextMenu { 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 = "20px"; @@ -221,7 +232,7 @@ export class TooltipTextMenu { }), }, hideSource: false - }) + }); }; this.linkEditor.appendChild(this.linkDrag); this.linkEditor.appendChild(this.linkText); @@ -239,7 +250,7 @@ export class TooltipTextMenu { e.stopPropagation(); e.preventDefault(); } - } + }; this.tooltip.appendChild(this.linkEditor); } @@ -331,6 +342,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) { @@ -347,6 +395,12 @@ 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) { let span = document.createElement("span"); @@ -394,6 +448,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; @@ -407,6 +475,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; diff --git a/src/client/views/PresentationView.tsx b/src/client/views/PresentationView.tsx index 4853eb151..2a456324c 100644 --- a/src/client/views/PresentationView.tsx +++ b/src/client/views/PresentationView.tsx @@ -1,7 +1,7 @@ import { observer } from "mobx-react"; -import React = require("react") +import React = require("react"); import { observable, action, runInAction, reaction } from "mobx"; -import "./PresentationView.scss" +import "./PresentationView.scss"; import "./Main.tsx"; import { DocumentManager } from "../util/DocumentManager"; import { Utils } from "../../Utils"; @@ -16,7 +16,6 @@ export interface PresViewProps { //Document: Doc; } - @observer /** * Component that takes in a document prop and a boolean whether it's collapsed or not. @@ -142,7 +141,7 @@ export class PresentationView extends React.Component<PresViewProps> { if (activeW && activeW instanceof Doc) { PromiseValue(Cast(activeW.presentationView, Doc)). then(pv => runInAction(() => - self.Document = pv ? pv : (activeW.presentationView = new Doc()))) + self.Document = pv ? pv : (activeW.presentationView = new Doc()))); } }, { fireImmediately: true }); @@ -166,8 +165,9 @@ export class PresentationView extends React.Component<PresViewProps> { } render() { - if (!this.Document) + if (!this.Document) { return (null); + } let titleStr = this.Document.Title; let width = NumCast(this.Document.width); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx index 642118d75..40ec8a325 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx @@ -1,4 +1,3 @@ -import { computed } from "mobx"; import { observer } from "mobx-react"; import { CollectionViewProps } from "../CollectionSubView"; import "./CollectionFreeFormView.scss"; @@ -6,7 +5,6 @@ import React = require("react"); import v5 = require("uuid/v5"); import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; import CursorField from "../../../../new_fields/CursorField"; -import { List } from "../../../../new_fields/List"; import { Cast } from "../../../../new_fields/Types"; import { listSpec } from "../../../../new_fields/Schema"; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 37dc05ca5..df3eb159b 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -12,7 +12,7 @@ import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; import { DocServer } from "../../DocServer"; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager } from "../../util/DragManager"; -import buildKeymap from "../../util/ProsemirrorKeymap"; +import buildKeymap from "../../util/ProsemirrorExampleTransfer"; import { inpRules } from "../../util/RichTextRules"; import { ImageResizeView, schema } from "../../util/RichTextSchema"; import { SelectionManager } from "../../util/SelectionManager"; @@ -184,7 +184,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config), dispatchTransaction: this.dispatchTransaction, nodeViews: { - image(node, view, getPos) { return new ImageResizeView(node, view, getPos) } + image(node, view, getPos) { return new ImageResizeView(node, view, getPos); } } }); let text = StrCast(this.props.Document.documentText); @@ -232,8 +232,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let docid = href.replace(DocServer.prepend("/doc/"), ""); DocServer.GetRefField(docid).then(action((f: Opt<Field>) => { if (f instanceof Doc) { - if (DocumentManager.Instance.getDocumentView(f)) + if (DocumentManager.Instance.getDocumentView(f)) { DocumentManager.Instance.getDocumentView(f)!.props.focus(f); + } else CollectionDockingView.Instance.AddRightSplit(f); } })); diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx index 57221aa39..0dca4b4b1 100644 --- a/src/debug/Test.tsx +++ b/src/debug/Test.tsx @@ -6,7 +6,6 @@ import { ImageField } from '../new_fields/URLField'; import { Doc } from '../new_fields/Doc'; import { List } from '../new_fields/List'; - const schema1 = createSchema({ hello: "number", test: "string", diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index f844dad6e..3ccc06d44 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -96,7 +96,6 @@ export class Doc extends RefField { public [HeightSym] = () => NumCast(this.__fields.height); public [HandleUpdate](diff: any) { - console.log(diff); const set = diff.$set; if (set) { for (const key in set) { diff --git a/src/server/index.ts b/src/server/index.ts index 93e4cafbf..f816af48a 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -7,7 +7,6 @@ import * as expressValidator from 'express-validator'; import * as formidable from 'formidable'; import * as fs from 'fs'; import * as mobileDetect from 'mobile-detect'; -import { ObservableMap } from 'mobx'; import * as passport from 'passport'; import * as path from 'path'; import * as request from 'request'; @@ -334,7 +333,7 @@ function ToSearchTerm(val: any): { suffix: string, value: any } | undefined { suffix = suffix[0]; } - return { suffix, value: val } + return { suffix, value: val }; } function UpdateField(socket: Socket, diff: Diff) { |