diff options
author | Sam Wilkins <abdullah_ahmed@brown.edu> | 2019-04-16 18:12:46 -0400 |
---|---|---|
committer | Sam Wilkins <abdullah_ahmed@brown.edu> | 2019-04-16 18:12:46 -0400 |
commit | 6a0a1528a9fd90bfcdd1c9283db4c717bc2d6975 (patch) | |
tree | 41a33538bc7074f42854dbd7b8b55246ae3bb46a /src | |
parent | c135d7ff8af5b3cf73b8789452f655d8d312e878 (diff) |
basic linking and unlinking working
Diffstat (limited to 'src')
-rw-r--r-- | src/client/util/ProsemirrorCopy/prompt.js | 168 | ||||
-rw-r--r-- | src/client/util/ProsemirrorExampleTransfer.ts (renamed from src/client/util/ProsemirrorKeymap.ts) | 3 | ||||
-rw-r--r-- | src/client/util/TooltipTextMenu.tsx | 46 | ||||
-rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 2 |
4 files changed, 217 insertions, 2 deletions
diff --git a/src/client/util/ProsemirrorCopy/prompt.js b/src/client/util/ProsemirrorCopy/prompt.js new file mode 100644 index 000000000..35dd7b872 --- /dev/null +++ b/src/client/util/ProsemirrorCopy/prompt.js @@ -0,0 +1,168 @@ +const prefix = "ProseMirror-prompt" + +export function openPrompt(options) { + let wrapper = document.body.appendChild(document.createElement("div")) + wrapper.className = prefix + + 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")) + if (options.title) form.appendChild(document.createElement("h5")).textContent = options.title + domFields.forEach(field => { + form.appendChild(document.createElement("div")).appendChild(field) + }) + let buttons = form.appendChild(document.createElement("div")) + buttons.className = prefix + "-buttons" + buttons.appendChild(submitButton) + buttons.appendChild(document.createTextNode(" ")) + buttons.appendChild(cancelButton) + + let box = wrapper.getBoundingClientRect() + wrapper.style.top = ((window.innerHeight - box.height) / 2) + "px" + wrapper.style.left = ((window.innerWidth - box.width) / 2) + "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" + 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..d2665b0fc 100644 --- a/src/client/util/ProsemirrorKeymap.ts +++ b/src/client/util/ProsemirrorExampleTransfer.ts @@ -7,6 +7,7 @@ import { wrapInList, splitListItem, liftListItem, sinkListItem } from "prosemirr import { undo, redo } from "prosemirror-history"; import { undoInputRule } from "prosemirror-inputrules"; import { Transaction, EditorState } from "prosemirror-state"; +import { MenuItem } from "prosemirror-menu"; const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; @@ -97,4 +98,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 a92cbd263..d81679ef3 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -18,6 +18,7 @@ import { import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FieldViewProps } from "../views/nodes/FieldView"; import { throwStatement } from "babel-types"; +const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js"); const SVG = "http://www.w3.org/2000/svg"; @@ -123,6 +124,9 @@ export class TooltipTextMenu { this.tooltip.appendChild(dd_fontStyle.render(this.view).dom); this.tooltip.appendChild(this.fontSizeIndicator); this.tooltip.appendChild(dd_fontSize.render(this.view).dom); + + this.tooltip.appendChild(this.createLink().render(this.view).dom); + dd_fontStyle.render(this.view).dom.nodeValue = "TEST"; console.log(dd_fontStyle.render(this.view).dom.nodeValue); } @@ -173,6 +177,48 @@ 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(); + } + }); + } + }); + } + + 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"); diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index bff8ca7a4..bd98622fb 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -9,7 +9,7 @@ import { KeyStore } from "../../../fields/KeyStore"; import { RichTextField } from "../../../fields/RichTextField"; import { TextField } from "../../../fields/TextField"; import { Document } from "../../../fields/Document"; -import buildKeymap from "../../util/ProsemirrorKeymap"; +import buildKeymap from "../../util/ProsemirrorExampleTransfer"; import { inpRules } from "../../util/RichTextRules"; import { schema } from "../../util/RichTextSchema"; import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu"; |