Nithin David
4 years ago
10 changed files with 860 additions and 0 deletions
@ -0,0 +1,48 @@ |
|||||||
|
/* eslint-disable class-methods-use-this */ |
||||||
|
// ::- 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() {} |
||||||
|
|
||||||
|
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; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
import { Plugin } from 'prosemirror-state'; |
||||||
|
import { Decoration, DecorationSet } from 'prosemirror-view'; |
||||||
|
|
||||||
|
export default (placeholderText = '') => { |
||||||
|
return new Plugin({ |
||||||
|
props: { |
||||||
|
decorations: state => { |
||||||
|
const decorations = []; |
||||||
|
|
||||||
|
const decorate = (node, pos) => { |
||||||
|
if (node.type.isBlock && node.childCount === 0) { |
||||||
|
decorations.push( |
||||||
|
Decoration.node(pos, pos + node.nodeSize, { |
||||||
|
class: 'empty-node', |
||||||
|
'data-placeholder': placeholderText, |
||||||
|
}) |
||||||
|
); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
state.doc.descendants(decorate); |
||||||
|
|
||||||
|
return DecorationSet.create(state.doc, decorations); |
||||||
|
}, |
||||||
|
}, |
||||||
|
}); |
||||||
|
}; |
@ -0,0 +1,18 @@ |
|||||||
|
import Field from './Field'; |
||||||
|
|
||||||
|
// ::- 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; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
import { Field } from './Field'; |
||||||
|
|
||||||
|
// ::- 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.className = this.options.class; |
||||||
|
input.value = this.options.value || ''; |
||||||
|
input.autocomplete = 'off'; |
||||||
|
return input; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,77 @@ |
|||||||
|
import { keymap } from 'prosemirror-keymap'; |
||||||
|
import { history } from 'prosemirror-history'; |
||||||
|
import { baseKeymap } from 'prosemirror-commands'; |
||||||
|
import { Plugin } from 'prosemirror-state'; |
||||||
|
import { dropCursor } from 'prosemirror-dropcursor'; |
||||||
|
import { gapCursor } from 'prosemirror-gapcursor'; |
||||||
|
import { menuBar } from 'prosemirror-menu'; |
||||||
|
|
||||||
|
import { buildMenuItems } from './menu'; |
||||||
|
import { buildKeymap } from './keymap'; |
||||||
|
import { buildInputRules } from './inputrules'; |
||||||
|
import Placeholder from './Placeholder'; |
||||||
|
|
||||||
|
export { buildMenuItems, buildKeymap, buildInputRules }; |
||||||
|
|
||||||
|
// !! This module exports helper functions for deriving a set of basic
|
||||||
|
// menu items, input rules, or key bindings from a schema. These
|
||||||
|
// values need to know about the schema for two reasons—they need
|
||||||
|
// access to specific instances of node and mark types, and they need
|
||||||
|
// to know which of the node and mark types that they know about are
|
||||||
|
// actually present in the schema.
|
||||||
|
//
|
||||||
|
// The `exampleSetup` plugin ties these together into a plugin that
|
||||||
|
// will automatically enable this basic functionality in an editor.
|
||||||
|
|
||||||
|
// :: (Object) → [Plugin]
|
||||||
|
// A convenience plugin that bundles together a simple menu with basic
|
||||||
|
// key bindings, input rules, and styling for the example schema.
|
||||||
|
// Probably only useful for quickly setting up a passable
|
||||||
|
// editor—you'll need more control over your settings in most
|
||||||
|
// real-world situations.
|
||||||
|
//
|
||||||
|
// options::- The following options are recognized:
|
||||||
|
//
|
||||||
|
// schema:: Schema
|
||||||
|
// The schema to generate key bindings and menu items for.
|
||||||
|
//
|
||||||
|
// mapKeys:: ?Object
|
||||||
|
// Can be used to [adjust](#example-setup.buildKeymap) the key bindings created.
|
||||||
|
//
|
||||||
|
// menuBar:: ?bool
|
||||||
|
// Set to false to disable the menu bar.
|
||||||
|
//
|
||||||
|
// history:: ?bool
|
||||||
|
// Set to false to disable the history plugin.
|
||||||
|
//
|
||||||
|
// floatingMenu:: ?bool
|
||||||
|
// Set to false to make the menu bar non-floating.
|
||||||
|
//
|
||||||
|
// menuContent:: [[MenuItem]]
|
||||||
|
// Can be used to override the menu content.
|
||||||
|
export function wootWriterSetup(options) { |
||||||
|
let plugins = [ |
||||||
|
buildInputRules(options.schema), |
||||||
|
keymap(buildKeymap(options.schema, options.mapKeys)), |
||||||
|
keymap(baseKeymap), |
||||||
|
dropCursor(), |
||||||
|
gapCursor(), |
||||||
|
Placeholder(options.placeholder), |
||||||
|
]; |
||||||
|
if (options.menuBar !== false) |
||||||
|
plugins.push( |
||||||
|
menuBar({ |
||||||
|
floating: options.floatingMenu !== false, |
||||||
|
content: options.menuContent || buildMenuItems(options.schema).fullMenu, |
||||||
|
}) |
||||||
|
); |
||||||
|
if (options.history !== false) plugins.push(history()); |
||||||
|
|
||||||
|
return plugins.concat( |
||||||
|
new Plugin({ |
||||||
|
props: { |
||||||
|
attributes: { class: 'ProseMirror-woot-style' }, |
||||||
|
}, |
||||||
|
}) |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,70 @@ |
|||||||
|
/* eslint-disable no-cond-assign */ |
||||||
|
import { |
||||||
|
inputRules, |
||||||
|
wrappingInputRule, |
||||||
|
textblockTypeInputRule, |
||||||
|
smartQuotes, |
||||||
|
emDash, |
||||||
|
ellipsis, |
||||||
|
} from 'prosemirror-inputrules'; |
||||||
|
|
||||||
|
// : (NodeType) → InputRule
|
||||||
|
// Given a blockquote node type, returns an input rule that turns `"> "`
|
||||||
|
// at the start of a textblock into a blockquote.
|
||||||
|
export function blockQuoteRule(nodeType) { |
||||||
|
return wrappingInputRule(/^\s*>\s$/, nodeType); |
||||||
|
} |
||||||
|
|
||||||
|
// : (NodeType) → InputRule
|
||||||
|
// Given a list node type, returns an input rule that turns a number
|
||||||
|
// followed by a dot at the start of a textblock into an ordered list.
|
||||||
|
export function orderedListRule(nodeType) { |
||||||
|
return wrappingInputRule( |
||||||
|
/^(\d+)\.\s$/, |
||||||
|
nodeType, |
||||||
|
match => ({ order: +match[1] }), |
||||||
|
(match, node) => node.childCount + node.attrs.order === +match[1] |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// : (NodeType) → InputRule
|
||||||
|
// Given a list node type, returns an input rule that turns a bullet
|
||||||
|
// (dash, plush, or asterisk) at the start of a textblock into a
|
||||||
|
// bullet list.
|
||||||
|
export function bulletListRule(nodeType) { |
||||||
|
return wrappingInputRule(/^\s*([-+*])\s$/, nodeType); |
||||||
|
} |
||||||
|
|
||||||
|
// : (NodeType) → InputRule
|
||||||
|
// Given a code block node type, returns an input rule that turns a
|
||||||
|
// textblock starting with three backticks into a code block.
|
||||||
|
export function codeBlockRule(nodeType) { |
||||||
|
return textblockTypeInputRule(/^```$/, nodeType); |
||||||
|
} |
||||||
|
|
||||||
|
// : (NodeType, number) → InputRule
|
||||||
|
// Given a node type and a maximum level, creates an input rule that
|
||||||
|
// turns up to that number of `#` characters followed by a space at
|
||||||
|
// the start of a textblock into a heading whose level corresponds to
|
||||||
|
// the number of `#` signs.
|
||||||
|
export function headingRule(nodeType, maxLevel) { |
||||||
|
return textblockTypeInputRule( |
||||||
|
new RegExp('^(#{1,' + maxLevel + '})\\s$'), |
||||||
|
nodeType, |
||||||
|
match => ({ level: match[1].length }) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// : (Schema) → Plugin
|
||||||
|
// A set of input rules for creating the basic block quotes, lists,
|
||||||
|
// code blocks, and heading.
|
||||||
|
export function buildInputRules(schema) { |
||||||
|
let rules = smartQuotes.concat(ellipsis, emDash); |
||||||
|
let type; |
||||||
|
if ((type = schema.nodes.blockquote)) rules.push(blockQuoteRule(type)); |
||||||
|
if ((type = schema.nodes.ordered_list)) rules.push(orderedListRule(type)); |
||||||
|
if ((type = schema.nodes.bullet_list)) rules.push(bulletListRule(type)); |
||||||
|
if ((type = schema.nodes.code_block)) rules.push(codeBlockRule(type)); |
||||||
|
if ((type = schema.nodes.heading)) rules.push(headingRule(type, 6)); |
||||||
|
return inputRules({ rules }); |
||||||
|
} |
@ -0,0 +1,120 @@ |
|||||||
|
/* eslint-disable no-plusplus */ |
||||||
|
/* eslint-disable no-cond-assign */ |
||||||
|
import { |
||||||
|
wrapIn, |
||||||
|
setBlockType, |
||||||
|
chainCommands, |
||||||
|
toggleMark, |
||||||
|
exitCode, |
||||||
|
joinUp, |
||||||
|
joinDown, |
||||||
|
lift, |
||||||
|
selectParentNode, |
||||||
|
} from 'prosemirror-commands'; |
||||||
|
import { |
||||||
|
wrapInList, |
||||||
|
splitListItem, |
||||||
|
liftListItem, |
||||||
|
sinkListItem, |
||||||
|
} from 'prosemirror-schema-list'; |
||||||
|
import { undo, redo } from 'prosemirror-history'; |
||||||
|
import { undoInputRule } from 'prosemirror-inputrules'; |
||||||
|
|
||||||
|
const mac = |
||||||
|
typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false; |
||||||
|
|
||||||
|
// :: (Schema, ?Object) → Object
|
||||||
|
// Inspect the given schema looking for marks and nodes from the
|
||||||
|
// basic schema, and if found, add key bindings related to them.
|
||||||
|
// This will add:
|
||||||
|
//
|
||||||
|
// * **Mod-b** for toggling [strong](#schema-basic.StrongMark)
|
||||||
|
// * **Mod-i** for toggling [emphasis](#schema-basic.EmMark)
|
||||||
|
// * **Mod-`** for toggling [code font](#schema-basic.CodeMark)
|
||||||
|
// * **Ctrl-Shift-0** for making the current textblock a paragraph
|
||||||
|
// * **Ctrl-Shift-1** to **Ctrl-Shift-Digit6** for making the current
|
||||||
|
// textblock a heading of the corresponding level
|
||||||
|
// * **Ctrl-Shift-Backslash** to make the current textblock a code block
|
||||||
|
// * **Ctrl-Shift-8** to wrap the selection in an ordered list
|
||||||
|
// * **Ctrl-Shift-9** to wrap the selection in a bullet list
|
||||||
|
// * **Ctrl->** to wrap the selection in a block quote
|
||||||
|
// * **Enter** to split a non-empty textblock in a list item while at
|
||||||
|
// the same time splitting the list item
|
||||||
|
// * **Mod-Enter** to insert a hard break
|
||||||
|
// * **Mod-_** to insert a horizontal rule
|
||||||
|
// * **Backspace** to undo an input rule
|
||||||
|
// * **Alt-ArrowUp** to `joinUp`
|
||||||
|
// * **Alt-ArrowDown** to `joinDown`
|
||||||
|
// * **Mod-BracketLeft** to `lift`
|
||||||
|
// * **Escape** to `selectParentNode`
|
||||||
|
//
|
||||||
|
// You can suppress or map these bindings by passing a `mapKeys`
|
||||||
|
// argument, which maps key names (say `"Mod-B"` to either `false`, to
|
||||||
|
// remove the binding, or a new key name string.
|
||||||
|
export function buildKeymap(schema, mapKeys) { |
||||||
|
let keys = {}; |
||||||
|
let type; |
||||||
|
function bind(key, cmd) { |
||||||
|
if (mapKeys) { |
||||||
|
let mapped = mapKeys[key]; |
||||||
|
if (mapped === false) return; |
||||||
|
if (mapped) key = mapped; |
||||||
|
} |
||||||
|
keys[key] = cmd; |
||||||
|
} |
||||||
|
|
||||||
|
bind('Mod-z', undo); |
||||||
|
bind('Shift-Mod-z', redo); |
||||||
|
bind('Backspace', undoInputRule); |
||||||
|
if (!mac) bind('Mod-y', redo); |
||||||
|
|
||||||
|
bind('Alt-ArrowUp', joinUp); |
||||||
|
bind('Alt-ArrowDown', joinDown); |
||||||
|
bind('Mod-BracketLeft', lift); |
||||||
|
bind('Escape', selectParentNode); |
||||||
|
|
||||||
|
if ((type = schema.marks.strong)) { |
||||||
|
bind('Mod-b', toggleMark(type)); |
||||||
|
bind('Mod-B', toggleMark(type)); |
||||||
|
} |
||||||
|
if ((type = schema.marks.em)) { |
||||||
|
bind('Mod-i', toggleMark(type)); |
||||||
|
bind('Mod-I', toggleMark(type)); |
||||||
|
} |
||||||
|
if ((type = schema.marks.code)) bind('Mod-`', toggleMark(type)); |
||||||
|
|
||||||
|
if ((type = schema.nodes.bullet_list)) bind('Shift-Ctrl-8', wrapInList(type)); |
||||||
|
if ((type = schema.nodes.ordered_list)) |
||||||
|
bind('Shift-Ctrl-9', wrapInList(type)); |
||||||
|
if ((type = schema.nodes.blockquote)) bind('Ctrl->', wrapIn(type)); |
||||||
|
if ((type = schema.nodes.hard_break)) { |
||||||
|
let br = type; |
||||||
|
let cmd = chainCommands(exitCode, (state, dispatch) => { |
||||||
|
dispatch(state.tr.replaceSelectionWith(br.create()).scrollIntoView()); |
||||||
|
return true; |
||||||
|
}); |
||||||
|
bind('Mod-Enter', cmd); |
||||||
|
bind('Shift-Enter', cmd); |
||||||
|
if (mac) bind('Ctrl-Enter', cmd); |
||||||
|
} |
||||||
|
if ((type = schema.nodes.list_item)) { |
||||||
|
bind('Enter', splitListItem(type)); |
||||||
|
bind('Mod-[', liftListItem(type)); |
||||||
|
bind('Mod-]', sinkListItem(type)); |
||||||
|
} |
||||||
|
if ((type = schema.nodes.paragraph)) bind('Shift-Ctrl-0', setBlockType(type)); |
||||||
|
if ((type = schema.nodes.code_block)) |
||||||
|
bind('Shift-Ctrl-\\', setBlockType(type)); |
||||||
|
if ((type = schema.nodes.heading)) |
||||||
|
for (let i = 1; i <= 6; i++) |
||||||
|
bind('Shift-Ctrl-' + i, setBlockType(type, { level: i })); |
||||||
|
if ((type = schema.nodes.horizontal_rule)) { |
||||||
|
let hr = type; |
||||||
|
bind('Mod-_', (state, dispatch) => { |
||||||
|
dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()); |
||||||
|
return true; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
return keys; |
||||||
|
} |
@ -0,0 +1,121 @@ |
|||||||
|
/* eslint-disable no-cond-assign */ |
||||||
|
/* eslint-disable no-plusplus */ |
||||||
|
import { undoItem, redoItem, icons, MenuItem } from 'prosemirror-menu'; |
||||||
|
import { toggleMark } from 'prosemirror-commands'; |
||||||
|
import { wrapInList } from 'prosemirror-schema-list'; |
||||||
|
import { openPrompt } from './prompt'; |
||||||
|
import { TextField } from './TextField'; |
||||||
|
|
||||||
|
// Helpers to create specific types of items
|
||||||
|
|
||||||
|
function cmdItem(cmd, options) { |
||||||
|
let passedOptions = { |
||||||
|
label: options.title, |
||||||
|
run: cmd, |
||||||
|
}; |
||||||
|
Object.keys(options).reduce((acc, optionKey) => { |
||||||
|
acc[optionKey] = options[optionKey]; |
||||||
|
return acc; |
||||||
|
}, passedOptions); |
||||||
|
if ((!options.enable || options.enable === true) && !options.select) |
||||||
|
passedOptions[options.enable ? 'enable' : 'select'] = state => cmd(state); |
||||||
|
|
||||||
|
return new MenuItem(passedOptions); |
||||||
|
} |
||||||
|
|
||||||
|
function markActive(state, type) { |
||||||
|
let { from, $from, to, empty } = state.selection; |
||||||
|
if (empty) return type.isInSet(state.storedMarks || $from.marks()); |
||||||
|
return state.doc.rangeHasMark(from, to, type); |
||||||
|
} |
||||||
|
|
||||||
|
function markItem(markType, options) { |
||||||
|
let passedOptions = { |
||||||
|
active(state) { |
||||||
|
return markActive(state, markType); |
||||||
|
}, |
||||||
|
enable: true, |
||||||
|
}; |
||||||
|
Object.keys(options).reduce((acc, optionKey) => { |
||||||
|
acc[optionKey] = options[optionKey]; |
||||||
|
return acc; |
||||||
|
}, passedOptions); |
||||||
|
return cmdItem(toggleMark(markType), passedOptions); |
||||||
|
} |
||||||
|
|
||||||
|
function linkItem(markType) { |
||||||
|
return new MenuItem({ |
||||||
|
title: 'Add or remove link', |
||||||
|
icon: icons.link, |
||||||
|
active(state) { |
||||||
|
return markActive(state, markType); |
||||||
|
}, |
||||||
|
enable(state) { |
||||||
|
return !state.selection.empty; |
||||||
|
}, |
||||||
|
run(state, dispatch, view) { |
||||||
|
if (markActive(state, markType)) { |
||||||
|
toggleMark(markType)(state, dispatch); |
||||||
|
return true; |
||||||
|
} |
||||||
|
openPrompt({ |
||||||
|
title: 'Create a link', |
||||||
|
fields: { |
||||||
|
href: new TextField({ |
||||||
|
label: 'https://example.com', |
||||||
|
class: 'small', |
||||||
|
required: true, |
||||||
|
}), |
||||||
|
}, |
||||||
|
callback(attrs) { |
||||||
|
toggleMark(markType, attrs)(view.state, view.dispatch); |
||||||
|
view.focus(); |
||||||
|
}, |
||||||
|
}); |
||||||
|
return false; |
||||||
|
}, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
function wrapListItem(nodeType, options) { |
||||||
|
return cmdItem(wrapInList(nodeType, options.attrs), options); |
||||||
|
} |
||||||
|
|
||||||
|
export function buildMenuItems(schema) { |
||||||
|
let r = {}; |
||||||
|
let type; |
||||||
|
if ((type = schema.marks.strong)) |
||||||
|
r.toggleStrong = markItem(type, { |
||||||
|
title: 'Toggle strong style', |
||||||
|
icon: icons.strong, |
||||||
|
}); |
||||||
|
if ((type = schema.marks.em)) |
||||||
|
r.toggleEm = markItem(type, { title: 'Toggle emphasis', icon: icons.em }); |
||||||
|
if ((type = schema.marks.code)) |
||||||
|
r.toggleCode = markItem(type, { |
||||||
|
title: 'Toggle code font', |
||||||
|
icon: icons.code, |
||||||
|
}); |
||||||
|
if ((type = schema.marks.link)) r.toggleLink = linkItem(type); |
||||||
|
|
||||||
|
if ((type = schema.nodes.bullet_list)) |
||||||
|
r.wrapBulletList = wrapListItem(type, { |
||||||
|
title: 'Wrap in bullet list', |
||||||
|
icon: icons.bulletList, |
||||||
|
}); |
||||||
|
if ((type = schema.nodes.ordered_list)) |
||||||
|
r.wrapOrderedList = wrapListItem(type, { |
||||||
|
title: 'Wrap in ordered list', |
||||||
|
icon: icons.orderedList, |
||||||
|
}); |
||||||
|
|
||||||
|
let cut = arr => arr.filter(x => x); |
||||||
|
|
||||||
|
r.inlineMenu = [ |
||||||
|
cut([r.toggleStrong, r.toggleEm, r.toggleCode, r.toggleLink]), |
||||||
|
]; |
||||||
|
r.blockMenu = [cut([r.wrapBulletList, r.wrapOrderedList])]; |
||||||
|
r.fullMenu = r.inlineMenu.concat([[undoItem, redoItem]], r.blockMenu); |
||||||
|
|
||||||
|
return r; |
||||||
|
} |
@ -0,0 +1,114 @@ |
|||||||
|
/* eslint-disable no-plusplus */ |
||||||
|
const prefix = 'ProseMirror-prompt'; |
||||||
|
|
||||||
|
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); |
||||||
|
} |
||||||
|
|
||||||
|
function getValues(fields, domFields) { |
||||||
|
let result = Object.keys(fields) |
||||||
|
.filter((name, index) => { |
||||||
|
let field = fields[name]; |
||||||
|
let dom = domFields[index]; |
||||||
|
let value = field.read(dom); |
||||||
|
let bad = field.validate(value); |
||||||
|
|
||||||
|
if (bad) reportInvalid(dom, bad); |
||||||
|
return !bad; |
||||||
|
}) |
||||||
|
.reduce((acc, name, index) => { |
||||||
|
let field = fields[name]; |
||||||
|
let dom = domFields[index]; |
||||||
|
let value = field.read(dom); |
||||||
|
acc[name] = field.clean(value); |
||||||
|
return acc; |
||||||
|
}, {}); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
export function openPrompt(options) { |
||||||
|
let wrapper = document.body.appendChild(document.createElement('div')); |
||||||
|
wrapper.className = prefix; |
||||||
|
|
||||||
|
const close = () => { |
||||||
|
// eslint-disable-next-line no-use-before-define
|
||||||
|
window.removeEventListener('mousedown', mouseOutside); |
||||||
|
if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper); |
||||||
|
}; |
||||||
|
|
||||||
|
let mouseOutside = e => { |
||||||
|
if (!wrapper.contains(e.target)) close(); |
||||||
|
}; |
||||||
|
setTimeout(() => window.addEventListener('mousedown', mouseOutside), 50); |
||||||
|
|
||||||
|
let domFields = []; |
||||||
|
|
||||||
|
Object.values(options.fields).map(field => domFields.push(field.render())); |
||||||
|
|
||||||
|
let submitButton = document.createElement('button'); |
||||||
|
submitButton.type = 'submit'; |
||||||
|
submitButton.className = |
||||||
|
'button tiny button--save-link ' + prefix + '-submit'; |
||||||
|
submitButton.textContent = 'Create Link'; |
||||||
|
let cancelButton = document.createElement('button'); |
||||||
|
cancelButton.type = 'button'; |
||||||
|
cancelButton.className = 'button tiny hollow secondary' + prefix + '-cancel'; |
||||||
|
cancelButton.textContent = 'Cancel'; |
||||||
|
cancelButton.addEventListener('click', close); |
||||||
|
|
||||||
|
let form = wrapper.appendChild(document.createElement('form')); |
||||||
|
if (options.title) { |
||||||
|
const titleDom = document.createElement('h5'); |
||||||
|
titleDom.className = 'sub-block-title'; |
||||||
|
form.appendChild(titleDom).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.key === 'Esc') { |
||||||
|
e.preventDefault(); |
||||||
|
close(); |
||||||
|
} else if (e.key === 'Enter' && !(e.ctrlKey || e.metaKey || e.shiftKey)) { |
||||||
|
e.preventDefault(); |
||||||
|
submit(); |
||||||
|
} else if (e.key === 'Tab') { |
||||||
|
window.setTimeout(() => { |
||||||
|
if (!wrapper.contains(document.activeElement)) close(); |
||||||
|
}, 500); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
let input = form.elements[0]; |
||||||
|
if (input) input.focus(); |
||||||
|
} |
@ -0,0 +1,251 @@ |
|||||||
|
.ProseMirror { |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror { |
||||||
|
word-wrap: break-word; |
||||||
|
white-space: pre-wrap; |
||||||
|
-webkit-font-variant-ligatures: none; |
||||||
|
font-variant-ligatures: none; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror pre { |
||||||
|
white-space: pre-wrap; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror li { |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-hideselection *::selection { |
||||||
|
background: transparent; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-hideselection *::-moz-selection { |
||||||
|
background: transparent; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-hideselection { |
||||||
|
caret-color: transparent; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-selectednode { |
||||||
|
outline: 2px solid var(--w-200); |
||||||
|
} |
||||||
|
|
||||||
|
/* Make sure li selections wrap around markers */ |
||||||
|
li.ProseMirror-selectednode { |
||||||
|
outline: none; |
||||||
|
} |
||||||
|
|
||||||
|
li.ProseMirror-selectednode:after { |
||||||
|
content: ''; |
||||||
|
position: absolute; |
||||||
|
left: -32px; |
||||||
|
right: -2px; |
||||||
|
top: -2px; |
||||||
|
bottom: -2px; |
||||||
|
border: 2px solid var(--w-200); |
||||||
|
pointer-events: none; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-textblock-dropdown { |
||||||
|
min-width: 3em; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-menu { |
||||||
|
margin: 0 -4px; |
||||||
|
line-height: 1; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-tooltip .ProseMirror-menu { |
||||||
|
width: -webkit-fit-content; |
||||||
|
width: fit-content; |
||||||
|
white-space: pre; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-menuitem { |
||||||
|
margin-right: 3px; |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-menu-active { |
||||||
|
background: #eee; |
||||||
|
border-radius: 4px; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-menu-active { |
||||||
|
background: #eee; |
||||||
|
border-radius: 4px; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-menu-disabled { |
||||||
|
opacity: 0.3; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-menubar { |
||||||
|
position: relative; |
||||||
|
min-height: var(--space-two); |
||||||
|
color: var(--color-heading); |
||||||
|
padding: var(--space-small) 0; |
||||||
|
top: 0; |
||||||
|
left: var(--space-minus-small); |
||||||
|
right: 0; |
||||||
|
background: transparent; |
||||||
|
z-index: 10; |
||||||
|
-moz-box-sizing: border-box; |
||||||
|
box-sizing: border-box; |
||||||
|
overflow: visible; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-icon { |
||||||
|
display: inline-block; |
||||||
|
line-height: 0.8; |
||||||
|
vertical-align: -2px; |
||||||
|
/* Compensate for padding */ |
||||||
|
padding: 2px 8px; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-menu-disabled.ProseMirror-icon { |
||||||
|
cursor: default; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-icon svg { |
||||||
|
fill: currentColor; |
||||||
|
height: 2em; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-icon span { |
||||||
|
vertical-align: text-top; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-gapcursor { |
||||||
|
display: none; |
||||||
|
pointer-events: none; |
||||||
|
position: absolute; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-gapcursor:after { |
||||||
|
content: ''; |
||||||
|
display: block; |
||||||
|
position: absolute; |
||||||
|
top: -2px; |
||||||
|
width: 20px; |
||||||
|
border-top: 1px solid black; |
||||||
|
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite; |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes ProseMirror-cursor-blink { |
||||||
|
to { |
||||||
|
visibility: hidden; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-focused .ProseMirror-gapcursor { |
||||||
|
display: block; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror ul, |
||||||
|
.ProseMirror ol { |
||||||
|
padding-left: 30px; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror blockquote { |
||||||
|
padding-left: 1em; |
||||||
|
border-left: 3px solid #eee; |
||||||
|
margin-left: 0; |
||||||
|
margin-right: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-prompt { |
||||||
|
position: fixed; |
||||||
|
z-index: 11; |
||||||
|
padding: var(--space-normal); |
||||||
|
background: white; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-prompt input[type='text'] { |
||||||
|
padding: 0 4px; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-prompt-close { |
||||||
|
position: absolute; |
||||||
|
left: 2px; |
||||||
|
top: 1px; |
||||||
|
color: #666; |
||||||
|
border: none; |
||||||
|
background: transparent; |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-prompt-close:after { |
||||||
|
content: '✕'; |
||||||
|
font-size: 12px; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-invalid { |
||||||
|
background: #ffc; |
||||||
|
border: 1px solid #cc7; |
||||||
|
border-radius: 4px; |
||||||
|
padding: 5px 10px; |
||||||
|
position: absolute; |
||||||
|
min-width: 10em; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-prompt-buttons { |
||||||
|
display: flex; |
||||||
|
flex-direction: row-reverse; |
||||||
|
align-items: center; |
||||||
|
justify-content: flex-start; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror-prompt-buttons .button--save-link { |
||||||
|
margin-left: var(--space-smaller); |
||||||
|
} |
||||||
|
|
||||||
|
#editor, |
||||||
|
.editor { |
||||||
|
background: white; |
||||||
|
color: black; |
||||||
|
background-clip: padding-box; |
||||||
|
border-radius: 4px; |
||||||
|
border: 2px solid rgba(0, 0, 0, 0.2); |
||||||
|
padding: 5px 0; |
||||||
|
margin-bottom: 23px; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror p:first-child, |
||||||
|
.ProseMirror h1:first-child, |
||||||
|
.ProseMirror h2:first-child, |
||||||
|
.ProseMirror h3:first-child, |
||||||
|
.ProseMirror h4:first-child, |
||||||
|
.ProseMirror h5:first-child, |
||||||
|
.ProseMirror h6:first-child { |
||||||
|
margin-top: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror { |
||||||
|
padding: 4px 8px 4px 14px; |
||||||
|
line-height: 1.2; |
||||||
|
outline: none; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror p { |
||||||
|
margin-bottom: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror .empty-node::before { |
||||||
|
position: absolute; |
||||||
|
color: #aaa; |
||||||
|
cursor: text; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror .empty-node:hover::before { |
||||||
|
color: #777; |
||||||
|
} |
||||||
|
|
||||||
|
.ProseMirror p.empty-node:first-child::before { |
||||||
|
content: attr(data-placeholder); |
||||||
|
cursor: text; |
||||||
|
} |
Loading…
Reference in new issue