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