tabs to spaces whitespace
This commit is contained in:
@@ -4,4 +4,4 @@ This is a collection of commands for [tiptap](https://www.npmjs.com/package/tipt
|
||||
[](https://www.npmjs.com/package/tiptap-commands)
|
||||
[](https://npmcharts.com/compare/tiptap-commands?minimal=true)
|
||||
[](https://www.npmjs.com/package/tiptap-commands)
|
||||
[](https://www.npmjs.com/package/tiptap-commands)
|
||||
[](https://www.npmjs.com/package/tiptap-commands)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export default function (text = '') {
|
||||
return (state, dispatch) => {
|
||||
const { $from } = state.selection
|
||||
const { pos } = $from.pos
|
||||
return (state, dispatch) => {
|
||||
const { $from } = state.selection
|
||||
const { pos } = $from.pos
|
||||
|
||||
dispatch(state.tr.insertText(text, pos))
|
||||
dispatch(state.tr.insertText(text, pos))
|
||||
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { InputRule } from 'prosemirror-inputrules'
|
||||
|
||||
export default function (regexp, markType, getAttrs) {
|
||||
return new InputRule(regexp, (state, match, start, end) => {
|
||||
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs
|
||||
return new InputRule(regexp, (state, match, start, end) => {
|
||||
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs
|
||||
const { tr } = state
|
||||
let markEnd = end
|
||||
|
||||
if (match[1]) {
|
||||
const startSpaces = match[0].search(/\S/)
|
||||
const textStart = start + match[0].indexOf(match[1])
|
||||
const textEnd = textStart + match[1].length
|
||||
if (textEnd < end) {
|
||||
tr.delete(textEnd, end)
|
||||
}
|
||||
if (textStart > start) {
|
||||
tr.delete(start + startSpaces, textStart)
|
||||
}
|
||||
markEnd = start + startSpaces + match[1].length
|
||||
}
|
||||
if (match[1]) {
|
||||
const startSpaces = match[0].search(/\S/)
|
||||
const textStart = start + match[0].indexOf(match[1])
|
||||
const textEnd = textStart + match[1].length
|
||||
if (textEnd < end) {
|
||||
tr.delete(textEnd, end)
|
||||
}
|
||||
if (textStart > start) {
|
||||
tr.delete(start + startSpaces, textStart)
|
||||
}
|
||||
markEnd = start + startSpaces + match[1].length
|
||||
}
|
||||
|
||||
tr.addMark(start, markEnd, markType.create(attrs))
|
||||
tr.removeStoredMark(markType) // Do not continue with mark.
|
||||
return tr
|
||||
})
|
||||
tr.addMark(start, markEnd, markType.create(attrs))
|
||||
tr.removeStoredMark(markType) // Do not continue with mark.
|
||||
return tr
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default function (type) {
|
||||
return (state, dispatch) => {
|
||||
const { from, to } = state.selection
|
||||
return dispatch(state.tr.removeMark(from, to, type))
|
||||
}
|
||||
const { from, to } = state.selection
|
||||
return dispatch(state.tr.removeMark(from, to, type))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
export default function (range, type, attrs = {}) {
|
||||
return (state, dispatch) => {
|
||||
const { $from } = state.selection
|
||||
const index = $from.index()
|
||||
return (state, dispatch) => {
|
||||
const { $from } = state.selection
|
||||
const index = $from.index()
|
||||
|
||||
if (!$from.parent.canReplaceWith(index, index, type)) {
|
||||
return false
|
||||
}
|
||||
if (!$from.parent.canReplaceWith(index, index, type)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
dispatch(state.tr.replaceWith(range.from, range.to, type.create(attrs)))
|
||||
}
|
||||
if (dispatch) {
|
||||
dispatch(state.tr.replaceWith(range.from, range.to, type.create(attrs)))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
export default function (type, attrs = {}) {
|
||||
return (state, dispatch) => {
|
||||
const { $from } = state.selection
|
||||
const index = $from.index()
|
||||
return (state, dispatch) => {
|
||||
const { $from } = state.selection
|
||||
const index = $from.index()
|
||||
|
||||
if (!$from.parent.canReplaceWith(index, index, type)) {
|
||||
return false
|
||||
}
|
||||
if (!$from.parent.canReplaceWith(index, index, type)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
dispatch(state.tr.replaceSelectionWith(type.create(attrs)))
|
||||
}
|
||||
if (dispatch) {
|
||||
dispatch(state.tr.replaceSelectionWith(type.create(attrs)))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,13 @@ index = $pos.index(d)
|
||||
if (node.type.spec.isolating) return false
|
||||
let rest = node.content.cutByIndex(index, node.childCount)
|
||||
const after = (typesAfter && typesAfter[i]) || node
|
||||
if (after != node) rest = rest.replaceChild(0, after.type.create(after.attrs))
|
||||
if (after != node) rest = rest.replaceChild(0, after.type.create(after.attrs))
|
||||
|
||||
/* Change starts from here */
|
||||
// if (!node.canReplace(index + 1, node.childCount) || !after.type.validContent(rest))
|
||||
/* Change starts from here */
|
||||
// if (!node.canReplace(index + 1, node.childCount) || !after.type.validContent(rest))
|
||||
// return false
|
||||
if (!node.canReplace(index + 1, node.childCount)) return false
|
||||
/* Change ends here */
|
||||
/* Change ends here */
|
||||
}
|
||||
const index = $pos.indexAfter(base)
|
||||
const baseType = typesAfter && typesAfter[0]
|
||||
@@ -43,7 +43,7 @@ export default function splitListItem(itemType) {
|
||||
// list item should be split. Otherwise, bail out and let next
|
||||
// command handle lifting.
|
||||
if ($from.depth == 2 || $from.node(-3).type != itemType
|
||||
|| $from.index(-2) != $from.node(-2).childCount - 1) return false
|
||||
|| $from.index(-2) != $from.node(-2).childCount - 1) return false
|
||||
|
||||
if (dispatch) {
|
||||
let wrap = Fragment.empty; const
|
||||
@@ -52,23 +52,23 @@ keepItem = $from.index(-1) > 0
|
||||
// from the outer list item to the parent node of the cursor
|
||||
for (let d = $from.depth - (keepItem ? 1 : 2); d >= $from.depth - 3; d--) wrap = Fragment.from($from.node(d).copy(wrap))
|
||||
// Add a second list item with an empty default start node
|
||||
wrap = wrap.append(Fragment.from(itemType.createAndFill()))
|
||||
wrap = wrap.append(Fragment.from(itemType.createAndFill()))
|
||||
const tr = state.tr.replace($from.before(keepItem ? null : -1), $from.after(-3), new Slice(wrap, keepItem ? 3 : 2, 2))
|
||||
tr.setSelection(state.selection.constructor.near(tr.doc.resolve($from.pos + (keepItem ? 3 : 2))))
|
||||
dispatch(tr.scrollIntoView())
|
||||
}
|
||||
return true
|
||||
}
|
||||
const nextType = $to.pos == $from.end() ? grandParent.contentMatchAt($from.indexAfter(-1)).defaultType : null
|
||||
const tr = state.tr.delete($from.pos, $to.pos)
|
||||
const nextType = $to.pos == $from.end() ? grandParent.contentMatchAt($from.indexAfter(-1)).defaultType : null
|
||||
const tr = state.tr.delete($from.pos, $to.pos)
|
||||
|
||||
/* Change starts from here */
|
||||
// let types = nextType && [null, {type: nextType}]
|
||||
/* Change starts from here */
|
||||
// let types = nextType && [null, {type: nextType}]
|
||||
let types = nextType && [{ type: itemType }, { type: nextType }]
|
||||
if (!types) types = [{ type: itemType }, null]
|
||||
/* Change ends here */
|
||||
/* Change ends here */
|
||||
|
||||
if (!canSplit(tr.doc, $from.pos, 2, types)) return false
|
||||
if (!canSplit(tr.doc, $from.pos, 2, types)) return false
|
||||
if (dispatch) dispatch(tr.split($from.pos, 2, [{ type: state.schema.nodes.todo_item, attrs: { done: false } }]).scrollIntoView())
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ import { setBlockType } from 'prosemirror-commands'
|
||||
import { nodeIsActive } from 'tiptap-utils'
|
||||
|
||||
export default function (type, toggletype, attrs = {}) {
|
||||
return (state, dispatch, view) => {
|
||||
const isActive = nodeIsActive(state, type, attrs)
|
||||
return (state, dispatch, view) => {
|
||||
const isActive = nodeIsActive(state, type, attrs)
|
||||
|
||||
if (isActive) {
|
||||
return setBlockType(toggletype)(state, dispatch, view)
|
||||
}
|
||||
if (isActive) {
|
||||
return setBlockType(toggletype)(state, dispatch, view)
|
||||
}
|
||||
|
||||
return setBlockType(type, attrs)(state, dispatch, view)
|
||||
}
|
||||
return setBlockType(type, attrs)(state, dispatch, view)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ import { wrapIn, lift } from 'prosemirror-commands'
|
||||
import { nodeIsActive } from 'tiptap-utils'
|
||||
|
||||
export default function (type) {
|
||||
return (state, dispatch, view) => {
|
||||
const isActive = nodeIsActive(state, type)
|
||||
return (state, dispatch, view) => {
|
||||
const isActive = nodeIsActive(state, type)
|
||||
|
||||
if (isActive) {
|
||||
return lift(state, dispatch)
|
||||
}
|
||||
if (isActive) {
|
||||
return lift(state, dispatch)
|
||||
}
|
||||
|
||||
return wrapIn(type)(state, dispatch, view)
|
||||
}
|
||||
return wrapIn(type)(state, dispatch, view)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default function (type, attrs) {
|
||||
return (state, dispatch) => {
|
||||
const { from, to } = state.selection
|
||||
return dispatch(state.tr.addMark(from, to, type.create(attrs)))
|
||||
}
|
||||
const { from, to } = state.selection
|
||||
return dispatch(state.tr.addMark(from, to, type.create(attrs)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
import {
|
||||
chainCommands,
|
||||
deleteSelection,
|
||||
joinBackward,
|
||||
selectNodeBackward,
|
||||
joinForward,
|
||||
selectNodeForward,
|
||||
joinUp,
|
||||
joinDown,
|
||||
lift,
|
||||
newlineInCode,
|
||||
exitCode,
|
||||
createParagraphNear,
|
||||
liftEmptyBlock,
|
||||
splitBlock,
|
||||
splitBlockKeepMarks,
|
||||
selectParentNode,
|
||||
selectAll,
|
||||
wrapIn,
|
||||
setBlockType,
|
||||
toggleMark,
|
||||
autoJoin,
|
||||
baseKeymap,
|
||||
pcBaseKeymap,
|
||||
macBaseKeymap,
|
||||
chainCommands,
|
||||
deleteSelection,
|
||||
joinBackward,
|
||||
selectNodeBackward,
|
||||
joinForward,
|
||||
selectNodeForward,
|
||||
joinUp,
|
||||
joinDown,
|
||||
lift,
|
||||
newlineInCode,
|
||||
exitCode,
|
||||
createParagraphNear,
|
||||
liftEmptyBlock,
|
||||
splitBlock,
|
||||
splitBlockKeepMarks,
|
||||
selectParentNode,
|
||||
selectAll,
|
||||
wrapIn,
|
||||
setBlockType,
|
||||
toggleMark,
|
||||
autoJoin,
|
||||
baseKeymap,
|
||||
pcBaseKeymap,
|
||||
macBaseKeymap,
|
||||
} from 'prosemirror-commands'
|
||||
|
||||
import {
|
||||
addListNodes,
|
||||
wrapInList,
|
||||
splitListItem,
|
||||
liftListItem,
|
||||
sinkListItem,
|
||||
addListNodes,
|
||||
wrapInList,
|
||||
splitListItem,
|
||||
liftListItem,
|
||||
sinkListItem,
|
||||
} from 'prosemirror-schema-list'
|
||||
|
||||
import {
|
||||
wrappingInputRule,
|
||||
textblockTypeInputRule,
|
||||
wrappingInputRule,
|
||||
textblockTypeInputRule,
|
||||
} from 'prosemirror-inputrules'
|
||||
|
||||
import insertText from './commands/insertText'
|
||||
@@ -50,52 +50,52 @@ import toggleWrap from './commands/toggleWrap'
|
||||
import updateMark from './commands/updateMark'
|
||||
|
||||
export {
|
||||
// prosemirror-commands
|
||||
chainCommands,
|
||||
deleteSelection,
|
||||
joinBackward,
|
||||
selectNodeBackward,
|
||||
joinForward,
|
||||
selectNodeForward,
|
||||
joinUp,
|
||||
joinDown,
|
||||
lift,
|
||||
newlineInCode,
|
||||
exitCode,
|
||||
createParagraphNear,
|
||||
liftEmptyBlock,
|
||||
splitBlock,
|
||||
splitBlockKeepMarks,
|
||||
selectParentNode,
|
||||
selectAll,
|
||||
wrapIn,
|
||||
setBlockType,
|
||||
toggleMark,
|
||||
autoJoin,
|
||||
baseKeymap,
|
||||
pcBaseKeymap,
|
||||
macBaseKeymap,
|
||||
// prosemirror-commands
|
||||
chainCommands,
|
||||
deleteSelection,
|
||||
joinBackward,
|
||||
selectNodeBackward,
|
||||
joinForward,
|
||||
selectNodeForward,
|
||||
joinUp,
|
||||
joinDown,
|
||||
lift,
|
||||
newlineInCode,
|
||||
exitCode,
|
||||
createParagraphNear,
|
||||
liftEmptyBlock,
|
||||
splitBlock,
|
||||
splitBlockKeepMarks,
|
||||
selectParentNode,
|
||||
selectAll,
|
||||
wrapIn,
|
||||
setBlockType,
|
||||
toggleMark,
|
||||
autoJoin,
|
||||
baseKeymap,
|
||||
pcBaseKeymap,
|
||||
macBaseKeymap,
|
||||
|
||||
// prosemirror-schema-list
|
||||
addListNodes,
|
||||
wrapInList,
|
||||
splitListItem,
|
||||
liftListItem,
|
||||
sinkListItem,
|
||||
// prosemirror-schema-list
|
||||
addListNodes,
|
||||
wrapInList,
|
||||
splitListItem,
|
||||
liftListItem,
|
||||
sinkListItem,
|
||||
|
||||
// prosemirror-inputrules
|
||||
wrappingInputRule,
|
||||
textblockTypeInputRule,
|
||||
// prosemirror-inputrules
|
||||
wrappingInputRule,
|
||||
textblockTypeInputRule,
|
||||
|
||||
// custom
|
||||
insertText,
|
||||
markInputRule,
|
||||
removeMark,
|
||||
replaceText,
|
||||
setInlineBlockType,
|
||||
splitToDefaultListItem,
|
||||
toggleBlockType,
|
||||
toggleList,
|
||||
toggleWrap,
|
||||
updateMark,
|
||||
// custom
|
||||
insertText,
|
||||
markInputRule,
|
||||
removeMark,
|
||||
replaceText,
|
||||
setInlineBlockType,
|
||||
splitToDefaultListItem,
|
||||
toggleBlockType,
|
||||
toggleList,
|
||||
toggleWrap,
|
||||
updateMark,
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ This is a collection of extensions for [tiptap](https://www.npmjs.com/package/ti
|
||||
[](https://www.npmjs.com/package/tiptap-extensions)
|
||||
[](https://npmcharts.com/compare/tiptap-extensions?minimal=true)
|
||||
[](https://www.npmjs.com/package/tiptap-extensions)
|
||||
[](https://www.npmjs.com/package/tiptap-extensions)
|
||||
[](https://www.npmjs.com/package/tiptap-extensions)
|
||||
|
||||
@@ -3,35 +3,35 @@ import { history, undo, redo } from 'prosemirror-history'
|
||||
|
||||
export default class History extends Extension {
|
||||
|
||||
get name() {
|
||||
return 'history'
|
||||
}
|
||||
get name() {
|
||||
return 'history'
|
||||
}
|
||||
|
||||
keys() {
|
||||
const isMac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false
|
||||
const keymap = {
|
||||
'Mod-z': undo,
|
||||
'Shift-Mod-z': redo,
|
||||
}
|
||||
keys() {
|
||||
const isMac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false
|
||||
const keymap = {
|
||||
'Mod-z': undo,
|
||||
'Shift-Mod-z': redo,
|
||||
}
|
||||
|
||||
if (!isMac) {
|
||||
keymap['Mod-y'] = redo
|
||||
}
|
||||
if (!isMac) {
|
||||
keymap['Mod-y'] = redo
|
||||
}
|
||||
|
||||
return keymap
|
||||
}
|
||||
return keymap
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
return [
|
||||
history(),
|
||||
]
|
||||
}
|
||||
get plugins() {
|
||||
return [
|
||||
history(),
|
||||
]
|
||||
}
|
||||
|
||||
commands() {
|
||||
return {
|
||||
undo: () => undo,
|
||||
redo: () => redo,
|
||||
}
|
||||
}
|
||||
commands() {
|
||||
return {
|
||||
undo: () => undo,
|
||||
redo: () => redo,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,40 +3,40 @@ import { Decoration, DecorationSet } from 'prosemirror-view'
|
||||
|
||||
export default class Placeholder extends Extension {
|
||||
|
||||
get name() {
|
||||
return 'placeholder'
|
||||
}
|
||||
get name() {
|
||||
return 'placeholder'
|
||||
}
|
||||
|
||||
get defaultOptions() {
|
||||
return {
|
||||
emptyNodeClass: 'is-empty',
|
||||
}
|
||||
}
|
||||
get defaultOptions() {
|
||||
return {
|
||||
emptyNodeClass: 'is-empty',
|
||||
}
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
props: {
|
||||
decorations: ({ doc }) => {
|
||||
const decorations = []
|
||||
const completelyEmpty = doc.textContent === '' && doc.childCount <= 1 && doc.content.size <= 2
|
||||
get plugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
props: {
|
||||
decorations: ({ doc }) => {
|
||||
const decorations = []
|
||||
const completelyEmpty = doc.textContent === '' && doc.childCount <= 1 && doc.content.size <= 2
|
||||
|
||||
doc.descendants((node, pos) => {
|
||||
if (!completelyEmpty) {
|
||||
return
|
||||
}
|
||||
doc.descendants((node, pos) => {
|
||||
if (!completelyEmpty) {
|
||||
return
|
||||
}
|
||||
|
||||
const decoration = Decoration.node(pos, pos + node.nodeSize, {
|
||||
class: this.options.emptyNodeClass,
|
||||
})
|
||||
decorations.push(decoration)
|
||||
})
|
||||
const decoration = Decoration.node(pos, pos + node.nodeSize, {
|
||||
class: this.options.emptyNodeClass,
|
||||
})
|
||||
decorations.push(decoration)
|
||||
})
|
||||
|
||||
return DecorationSet.create(doc, decorations)
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
}
|
||||
return DecorationSet.create(doc, decorations)
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,43 +3,43 @@ import { toggleMark, markInputRule } from 'tiptap-commands'
|
||||
|
||||
export default class Bold extends Mark {
|
||||
|
||||
get name() {
|
||||
return 'bold'
|
||||
}
|
||||
get name() {
|
||||
return 'bold'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'strong',
|
||||
},
|
||||
{
|
||||
tag: 'b',
|
||||
getAttrs: node => node.style.fontWeight !== 'normal' && null,
|
||||
},
|
||||
{
|
||||
style: 'font-weight',
|
||||
getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null,
|
||||
},
|
||||
],
|
||||
toDOM: () => ['strong', 0],
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'strong',
|
||||
},
|
||||
{
|
||||
tag: 'b',
|
||||
getAttrs: node => node.style.fontWeight !== 'normal' && null,
|
||||
},
|
||||
{
|
||||
style: 'font-weight',
|
||||
getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null,
|
||||
},
|
||||
],
|
||||
toDOM: () => ['strong', 0],
|
||||
}
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Mod-b': toggleMark(type),
|
||||
}
|
||||
}
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Mod-b': toggleMark(type),
|
||||
}
|
||||
}
|
||||
|
||||
commands({ type }) {
|
||||
return () => toggleMark(type)
|
||||
}
|
||||
commands({ type }) {
|
||||
return () => toggleMark(type)
|
||||
}
|
||||
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
markInputRule(/(?:\*\*|__)([^*_]+)(?:\*\*|__)$/, type),
|
||||
]
|
||||
}
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
markInputRule(/(?:\*\*|__)([^*_]+)(?:\*\*|__)$/, type),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,33 +3,33 @@ import { toggleMark, markInputRule } from 'tiptap-commands'
|
||||
|
||||
export default class Code extends Mark {
|
||||
|
||||
get name() {
|
||||
return 'code'
|
||||
}
|
||||
get name() {
|
||||
return 'code'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
parseDOM: [
|
||||
{ tag: 'code' },
|
||||
],
|
||||
toDOM: () => ['code', 0],
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
parseDOM: [
|
||||
{ tag: 'code' },
|
||||
],
|
||||
toDOM: () => ['code', 0],
|
||||
}
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Mod-`': toggleMark(type),
|
||||
}
|
||||
}
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Mod-`': toggleMark(type),
|
||||
}
|
||||
}
|
||||
|
||||
commands({ type }) {
|
||||
return () => toggleMark(type)
|
||||
}
|
||||
commands({ type }) {
|
||||
return () => toggleMark(type)
|
||||
}
|
||||
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
markInputRule(/(?:`)([^`]+)(?:`)$/, type),
|
||||
]
|
||||
}
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
markInputRule(/(?:`)([^`]+)(?:`)$/, type),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,35 +3,35 @@ import { toggleMark, markInputRule } from 'tiptap-commands'
|
||||
|
||||
export default class Italic extends Mark {
|
||||
|
||||
get name() {
|
||||
return 'italic'
|
||||
}
|
||||
get name() {
|
||||
return 'italic'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
parseDOM: [
|
||||
{ tag: 'i' },
|
||||
{ tag: 'em' },
|
||||
{ style: 'font-style=italic' },
|
||||
],
|
||||
toDOM: () => ['em', 0],
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
parseDOM: [
|
||||
{ tag: 'i' },
|
||||
{ tag: 'em' },
|
||||
{ style: 'font-style=italic' },
|
||||
],
|
||||
toDOM: () => ['em', 0],
|
||||
}
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Mod-i': toggleMark(type),
|
||||
}
|
||||
}
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Mod-i': toggleMark(type),
|
||||
}
|
||||
}
|
||||
|
||||
commands({ type }) {
|
||||
return () => toggleMark(type)
|
||||
}
|
||||
commands({ type }) {
|
||||
return () => toggleMark(type)
|
||||
}
|
||||
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
markInputRule(/(?:^|[^*_])(?:\*|_)([^*_]+)(?:\*|_)$/, type),
|
||||
]
|
||||
}
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
markInputRule(/(?:^|[^*_])(?:\*|_)([^*_]+)(?:\*|_)$/, type),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,41 +3,41 @@ import { updateMark, removeMark } from 'tiptap-commands'
|
||||
|
||||
export default class Link extends Mark {
|
||||
|
||||
get name() {
|
||||
return 'link'
|
||||
}
|
||||
get name() {
|
||||
return 'link'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
href: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
inclusive: false,
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'a[href]',
|
||||
getAttrs: dom => ({
|
||||
href: dom.getAttribute('href'),
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: node => ['a', {
|
||||
...node.attrs,
|
||||
rel: 'noopener noreferrer nofollow',
|
||||
}, 0],
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
href: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
inclusive: false,
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'a[href]',
|
||||
getAttrs: dom => ({
|
||||
href: dom.getAttribute('href'),
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: node => ['a', {
|
||||
...node.attrs,
|
||||
rel: 'noopener noreferrer nofollow',
|
||||
}, 0],
|
||||
}
|
||||
}
|
||||
|
||||
commands({ type }) {
|
||||
return attrs => {
|
||||
if (attrs.href) {
|
||||
return updateMark(type, attrs)
|
||||
}
|
||||
commands({ type }) {
|
||||
return attrs => {
|
||||
if (attrs.href) {
|
||||
return updateMark(type, attrs)
|
||||
}
|
||||
|
||||
return removeMark(type)
|
||||
}
|
||||
}
|
||||
return removeMark(type)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,45 +3,45 @@ import { toggleMark, markInputRule } from 'tiptap-commands'
|
||||
|
||||
export default class Strike extends Mark {
|
||||
|
||||
get name() {
|
||||
return 'strike'
|
||||
}
|
||||
get name() {
|
||||
return 'strike'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 's',
|
||||
},
|
||||
{
|
||||
tag: 'del',
|
||||
},
|
||||
{
|
||||
tag: 'strike',
|
||||
},
|
||||
{
|
||||
style: 'text-decoration',
|
||||
getAttrs: value => value === 'line-through',
|
||||
},
|
||||
],
|
||||
toDOM: () => ['s', 0],
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 's',
|
||||
},
|
||||
{
|
||||
tag: 'del',
|
||||
},
|
||||
{
|
||||
tag: 'strike',
|
||||
},
|
||||
{
|
||||
style: 'text-decoration',
|
||||
getAttrs: value => value === 'line-through',
|
||||
},
|
||||
],
|
||||
toDOM: () => ['s', 0],
|
||||
}
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Mod-d': toggleMark(type),
|
||||
}
|
||||
}
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Mod-d': toggleMark(type),
|
||||
}
|
||||
}
|
||||
|
||||
commands({ type }) {
|
||||
return () => toggleMark(type)
|
||||
}
|
||||
commands({ type }) {
|
||||
return () => toggleMark(type)
|
||||
}
|
||||
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
markInputRule(/~([^~]+)~$/, type),
|
||||
]
|
||||
}
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
markInputRule(/~([^~]+)~$/, type),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,33 +3,33 @@ import { toggleMark } from 'tiptap-commands'
|
||||
|
||||
export default class Underline extends Mark {
|
||||
|
||||
get name() {
|
||||
return 'underline'
|
||||
}
|
||||
get name() {
|
||||
return 'underline'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'u',
|
||||
},
|
||||
{
|
||||
style: 'text-decoration',
|
||||
getAttrs: value => value === 'underline',
|
||||
},
|
||||
],
|
||||
toDOM: () => ['u', 0],
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'u',
|
||||
},
|
||||
{
|
||||
style: 'text-decoration',
|
||||
getAttrs: value => value === 'underline',
|
||||
},
|
||||
],
|
||||
toDOM: () => ['u', 0],
|
||||
}
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Mod-u': toggleMark(type),
|
||||
}
|
||||
}
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Mod-u': toggleMark(type),
|
||||
}
|
||||
}
|
||||
|
||||
commands({ type }) {
|
||||
return () => toggleMark(type)
|
||||
}
|
||||
commands({ type }) {
|
||||
return () => toggleMark(type)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,37 +3,37 @@ import { wrappingInputRule, toggleWrap } from 'tiptap-commands'
|
||||
|
||||
export default class Blockquote extends Node {
|
||||
|
||||
get name() {
|
||||
return 'blockquote'
|
||||
}
|
||||
get name() {
|
||||
return 'blockquote'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'block*',
|
||||
group: 'block',
|
||||
defining: true,
|
||||
draggable: false,
|
||||
parseDOM: [
|
||||
{ tag: 'blockquote' },
|
||||
],
|
||||
toDOM: () => ['blockquote', 0],
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
content: 'block*',
|
||||
group: 'block',
|
||||
defining: true,
|
||||
draggable: false,
|
||||
parseDOM: [
|
||||
{ tag: 'blockquote' },
|
||||
],
|
||||
toDOM: () => ['blockquote', 0],
|
||||
}
|
||||
}
|
||||
|
||||
commands({ type, schema }) {
|
||||
return () => toggleWrap(type, schema.nodes.paragraph)
|
||||
}
|
||||
commands({ type, schema }) {
|
||||
return () => toggleWrap(type, schema.nodes.paragraph)
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Ctrl->': toggleWrap(type),
|
||||
}
|
||||
}
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Ctrl->': toggleWrap(type),
|
||||
}
|
||||
}
|
||||
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
wrappingInputRule(/^\s*>\s$/, type),
|
||||
]
|
||||
}
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
wrappingInputRule(/^\s*>\s$/, type),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,35 +3,35 @@ import { wrappingInputRule, toggleList } from 'tiptap-commands'
|
||||
|
||||
export default class Bullet extends Node {
|
||||
|
||||
get name() {
|
||||
return 'bullet_list'
|
||||
}
|
||||
get name() {
|
||||
return 'bullet_list'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'list_item+',
|
||||
group: 'block',
|
||||
parseDOM: [
|
||||
{ tag: 'ul' },
|
||||
],
|
||||
toDOM: () => ['ul', 0],
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
content: 'list_item+',
|
||||
group: 'block',
|
||||
parseDOM: [
|
||||
{ tag: 'ul' },
|
||||
],
|
||||
toDOM: () => ['ul', 0],
|
||||
}
|
||||
}
|
||||
|
||||
commands({ type, schema }) {
|
||||
return () => toggleList(type, schema.nodes.list_item)
|
||||
}
|
||||
commands({ type, schema }) {
|
||||
return () => toggleList(type, schema.nodes.list_item)
|
||||
}
|
||||
|
||||
keys({ type, schema }) {
|
||||
return {
|
||||
'Shift-Ctrl-8': toggleList(type, schema.nodes.list_item),
|
||||
}
|
||||
}
|
||||
keys({ type, schema }) {
|
||||
return {
|
||||
'Shift-Ctrl-8': toggleList(type, schema.nodes.list_item),
|
||||
}
|
||||
}
|
||||
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
wrappingInputRule(/^\s*([-+*])\s$/, type),
|
||||
]
|
||||
}
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
wrappingInputRule(/^\s*([-+*])\s$/, type),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,39 +3,39 @@ import { toggleBlockType, setBlockType, textblockTypeInputRule } from 'tiptap-co
|
||||
|
||||
export default class CodeBlock extends Node {
|
||||
|
||||
get name() {
|
||||
return 'code_block'
|
||||
}
|
||||
get name() {
|
||||
return 'code_block'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'text*',
|
||||
marks: '',
|
||||
group: 'block',
|
||||
code: true,
|
||||
defining: true,
|
||||
draggable: false,
|
||||
parseDOM: [
|
||||
{ tag: 'pre', preserveWhitespace: 'full' },
|
||||
],
|
||||
toDOM: () => ['pre', ['code', 0]],
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
content: 'text*',
|
||||
marks: '',
|
||||
group: 'block',
|
||||
code: true,
|
||||
defining: true,
|
||||
draggable: false,
|
||||
parseDOM: [
|
||||
{ tag: 'pre', preserveWhitespace: 'full' },
|
||||
],
|
||||
toDOM: () => ['pre', ['code', 0]],
|
||||
}
|
||||
}
|
||||
|
||||
commands({ type, schema }) {
|
||||
return () => toggleBlockType(type, schema.nodes.paragraph)
|
||||
}
|
||||
commands({ type, schema }) {
|
||||
return () => toggleBlockType(type, schema.nodes.paragraph)
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Shift-Ctrl-\\': setBlockType(type),
|
||||
}
|
||||
}
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Shift-Ctrl-\\': setBlockType(type),
|
||||
}
|
||||
}
|
||||
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
textblockTypeInputRule(/^```$/, type),
|
||||
]
|
||||
}
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
textblockTypeInputRule(/^```$/, type),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,139 +5,139 @@ import { findBlockNodes } from 'prosemirror-utils'
|
||||
import low from 'lowlight/lib/core'
|
||||
|
||||
function getDecorations(doc) {
|
||||
const decorations = []
|
||||
const decorations = []
|
||||
|
||||
const blocks = findBlockNodes(doc)
|
||||
.filter(item => item.node.type.name === 'code_block')
|
||||
const blocks = findBlockNodes(doc)
|
||||
.filter(item => item.node.type.name === 'code_block')
|
||||
|
||||
const flatten = list => list.reduce(
|
||||
(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [],
|
||||
)
|
||||
const flatten = list => list.reduce(
|
||||
(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [],
|
||||
)
|
||||
|
||||
function parseNodes(nodes, className = []) {
|
||||
return nodes.map(node => {
|
||||
function parseNodes(nodes, className = []) {
|
||||
return nodes.map(node => {
|
||||
|
||||
const classes = [
|
||||
...className,
|
||||
...node.properties ? node.properties.className : [],
|
||||
]
|
||||
const classes = [
|
||||
...className,
|
||||
...node.properties ? node.properties.className : [],
|
||||
]
|
||||
|
||||
if (node.children) {
|
||||
return parseNodes(node.children, classes)
|
||||
}
|
||||
if (node.children) {
|
||||
return parseNodes(node.children, classes)
|
||||
}
|
||||
|
||||
return {
|
||||
text: node.value,
|
||||
classes,
|
||||
}
|
||||
})
|
||||
}
|
||||
return {
|
||||
text: node.value,
|
||||
classes,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
blocks.forEach(block => {
|
||||
let startPos = block.pos + 1
|
||||
const nodes = low.highlightAuto(block.node.textContent).value
|
||||
blocks.forEach(block => {
|
||||
let startPos = block.pos + 1
|
||||
const nodes = low.highlightAuto(block.node.textContent).value
|
||||
|
||||
flatten(parseNodes(nodes))
|
||||
.map(node => {
|
||||
const from = startPos
|
||||
const to = from + node.text.length
|
||||
flatten(parseNodes(nodes))
|
||||
.map(node => {
|
||||
const from = startPos
|
||||
const to = from + node.text.length
|
||||
|
||||
startPos = to
|
||||
startPos = to
|
||||
|
||||
return {
|
||||
...node,
|
||||
from,
|
||||
to,
|
||||
}
|
||||
})
|
||||
.forEach(node => {
|
||||
const decoration = Decoration.inline(node.from, node.to, {
|
||||
class: node.classes.join(' '),
|
||||
})
|
||||
decorations.push(decoration)
|
||||
})
|
||||
})
|
||||
return {
|
||||
...node,
|
||||
from,
|
||||
to,
|
||||
}
|
||||
})
|
||||
.forEach(node => {
|
||||
const decoration = Decoration.inline(node.from, node.to, {
|
||||
class: node.classes.join(' '),
|
||||
})
|
||||
decorations.push(decoration)
|
||||
})
|
||||
})
|
||||
|
||||
return DecorationSet.create(doc, decorations)
|
||||
return DecorationSet.create(doc, decorations)
|
||||
}
|
||||
|
||||
export default class CodeBlockHighlight extends Node {
|
||||
|
||||
constructor(options = {}) {
|
||||
super(options)
|
||||
try {
|
||||
Object.entries(this.options.languages).forEach(([name, mapping]) => {
|
||||
low.registerLanguage(name, mapping)
|
||||
})
|
||||
} catch (err) {
|
||||
throw new Error('Invalid syntax highlight definitions: define at least one highlight.js language mapping')
|
||||
}
|
||||
}
|
||||
constructor(options = {}) {
|
||||
super(options)
|
||||
try {
|
||||
Object.entries(this.options.languages).forEach(([name, mapping]) => {
|
||||
low.registerLanguage(name, mapping)
|
||||
})
|
||||
} catch (err) {
|
||||
throw new Error('Invalid syntax highlight definitions: define at least one highlight.js language mapping')
|
||||
}
|
||||
}
|
||||
|
||||
get defaultOptions() {
|
||||
return {
|
||||
languages: {},
|
||||
}
|
||||
}
|
||||
get defaultOptions() {
|
||||
return {
|
||||
languages: {},
|
||||
}
|
||||
}
|
||||
|
||||
get name() {
|
||||
return 'code_block'
|
||||
}
|
||||
get name() {
|
||||
return 'code_block'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'text*',
|
||||
marks: '',
|
||||
group: 'block',
|
||||
code: true,
|
||||
defining: true,
|
||||
draggable: false,
|
||||
parseDOM: [
|
||||
{ tag: 'pre', preserveWhitespace: 'full' },
|
||||
],
|
||||
toDOM: () => ['pre', ['code', 0]],
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
content: 'text*',
|
||||
marks: '',
|
||||
group: 'block',
|
||||
code: true,
|
||||
defining: true,
|
||||
draggable: false,
|
||||
parseDOM: [
|
||||
{ tag: 'pre', preserveWhitespace: 'full' },
|
||||
],
|
||||
toDOM: () => ['pre', ['code', 0]],
|
||||
}
|
||||
}
|
||||
|
||||
commands({ type, schema }) {
|
||||
return () => toggleBlockType(type, schema.nodes.paragraph)
|
||||
}
|
||||
commands({ type, schema }) {
|
||||
return () => toggleBlockType(type, schema.nodes.paragraph)
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Shift-Ctrl-\\': setBlockType(type),
|
||||
}
|
||||
}
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Shift-Ctrl-\\': setBlockType(type),
|
||||
}
|
||||
}
|
||||
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
textblockTypeInputRule(/^```$/, type),
|
||||
]
|
||||
}
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
textblockTypeInputRule(/^```$/, type),
|
||||
]
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
state: {
|
||||
init(_, { doc }) {
|
||||
return getDecorations(doc)
|
||||
},
|
||||
apply(tr, set) {
|
||||
// TODO: find way to cache decorations
|
||||
// see: https://discuss.prosemirror.net/t/how-to-update-multiple-inline-decorations-on-node-change/1493
|
||||
if (tr.docChanged) {
|
||||
return getDecorations(tr.doc)
|
||||
}
|
||||
return set.map(tr.mapping, tr.doc)
|
||||
},
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state)
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
}
|
||||
get plugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
state: {
|
||||
init(_, { doc }) {
|
||||
return getDecorations(doc)
|
||||
},
|
||||
apply(tr, set) {
|
||||
// TODO: find way to cache decorations
|
||||
// see: https://discuss.prosemirror.net/t/how-to-update-multiple-inline-decorations-on-node-change/1493
|
||||
if (tr.docChanged) {
|
||||
return getDecorations(tr.doc)
|
||||
}
|
||||
return set.map(tr.mapping, tr.doc)
|
||||
},
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state)
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,31 +3,31 @@ import { chainCommands, exitCode } from 'tiptap-commands'
|
||||
|
||||
export default class HardBreak extends Node {
|
||||
|
||||
get name() {
|
||||
return 'hard_break'
|
||||
}
|
||||
get name() {
|
||||
return 'hard_break'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
inline: true,
|
||||
group: 'inline',
|
||||
selectable: false,
|
||||
parseDOM: [
|
||||
{ tag: 'br' },
|
||||
],
|
||||
toDOM: () => ['br'],
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
inline: true,
|
||||
group: 'inline',
|
||||
selectable: false,
|
||||
parseDOM: [
|
||||
{ tag: 'br' },
|
||||
],
|
||||
toDOM: () => ['br'],
|
||||
}
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
const command = chainCommands(exitCode, (state, dispatch) => {
|
||||
dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView())
|
||||
return true
|
||||
})
|
||||
return {
|
||||
'Mod-Enter': command,
|
||||
'Shift-Enter': command,
|
||||
}
|
||||
}
|
||||
keys({ type }) {
|
||||
const command = chainCommands(exitCode, (state, dispatch) => {
|
||||
dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView())
|
||||
return true
|
||||
})
|
||||
return {
|
||||
'Mod-Enter': command,
|
||||
'Shift-Enter': command,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,55 +3,55 @@ import { setBlockType, textblockTypeInputRule, toggleBlockType } from 'tiptap-co
|
||||
|
||||
export default class Heading extends Node {
|
||||
|
||||
get name() {
|
||||
return 'heading'
|
||||
}
|
||||
get name() {
|
||||
return 'heading'
|
||||
}
|
||||
|
||||
get defaultOptions() {
|
||||
return {
|
||||
levels: [1, 2, 3, 4, 5, 6],
|
||||
}
|
||||
}
|
||||
get defaultOptions() {
|
||||
return {
|
||||
levels: [1, 2, 3, 4, 5, 6],
|
||||
}
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
level: {
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
content: 'inline*',
|
||||
group: 'block',
|
||||
defining: true,
|
||||
draggable: false,
|
||||
parseDOM: this.options.levels
|
||||
.map(level => ({
|
||||
tag: `h${level}`,
|
||||
attrs: { level },
|
||||
})),
|
||||
toDOM: node => [`h${node.attrs.level}`, 0],
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
level: {
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
content: 'inline*',
|
||||
group: 'block',
|
||||
defining: true,
|
||||
draggable: false,
|
||||
parseDOM: this.options.levels
|
||||
.map(level => ({
|
||||
tag: `h${level}`,
|
||||
attrs: { level },
|
||||
})),
|
||||
toDOM: node => [`h${node.attrs.level}`, 0],
|
||||
}
|
||||
}
|
||||
|
||||
commands({ type, schema }) {
|
||||
return attrs => toggleBlockType(type, schema.nodes.paragraph, attrs)
|
||||
}
|
||||
commands({ type, schema }) {
|
||||
return attrs => toggleBlockType(type, schema.nodes.paragraph, attrs)
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return this.options.levels.reduce((items, level) => ({
|
||||
...items,
|
||||
...{
|
||||
[`Shift-Ctrl-${level}`]: setBlockType(type, { level }),
|
||||
},
|
||||
}), {})
|
||||
}
|
||||
keys({ type }) {
|
||||
return this.options.levels.reduce((items, level) => ({
|
||||
...items,
|
||||
...{
|
||||
[`Shift-Ctrl-${level}`]: setBlockType(type, { level }),
|
||||
},
|
||||
}), {})
|
||||
}
|
||||
|
||||
inputRules({ type }) {
|
||||
return this.options.levels.map(level => textblockTypeInputRule(
|
||||
new RegExp(`^(#{1,${level}})\\s$`),
|
||||
type,
|
||||
match => ({ level }),
|
||||
))
|
||||
}
|
||||
inputRules({ type }) {
|
||||
return this.options.levels.map(level => textblockTypeInputRule(
|
||||
new RegExp(`^(#{1,${level}})\\s$`),
|
||||
type,
|
||||
match => ({ level }),
|
||||
))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,92 +2,92 @@ import { Node, Plugin } from 'tiptap'
|
||||
|
||||
export default class Image extends Node {
|
||||
|
||||
get name() {
|
||||
return 'image'
|
||||
}
|
||||
get name() {
|
||||
return 'image'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
inline: true,
|
||||
attrs: {
|
||||
src: {},
|
||||
alt: {
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
group: 'inline',
|
||||
draggable: true,
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'img[src]',
|
||||
getAttrs: dom => ({
|
||||
src: dom.getAttribute('src'),
|
||||
title: dom.getAttribute('title'),
|
||||
alt: dom.getAttribute('alt'),
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: node => ['img', node.attrs],
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
inline: true,
|
||||
attrs: {
|
||||
src: {},
|
||||
alt: {
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
group: 'inline',
|
||||
draggable: true,
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'img[src]',
|
||||
getAttrs: dom => ({
|
||||
src: dom.getAttribute('src'),
|
||||
title: dom.getAttribute('title'),
|
||||
alt: dom.getAttribute('alt'),
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: node => ['img', node.attrs],
|
||||
}
|
||||
}
|
||||
|
||||
commands({ type }) {
|
||||
return attrs => (state, dispatch) => {
|
||||
const { selection } = state
|
||||
const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos
|
||||
const node = type.create(attrs)
|
||||
const transaction = state.tr.insert(position, node)
|
||||
dispatch(transaction)
|
||||
}
|
||||
}
|
||||
commands({ type }) {
|
||||
return attrs => (state, dispatch) => {
|
||||
const { selection } = state
|
||||
const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos
|
||||
const node = type.create(attrs)
|
||||
const transaction = state.tr.insert(position, node)
|
||||
dispatch(transaction)
|
||||
}
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
drop(view, event) {
|
||||
const hasFiles = event.dataTransfer
|
||||
&& event.dataTransfer.files
|
||||
&& event.dataTransfer.files.length
|
||||
get plugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
drop(view, event) {
|
||||
const hasFiles = event.dataTransfer
|
||||
&& event.dataTransfer.files
|
||||
&& event.dataTransfer.files.length
|
||||
|
||||
if (!hasFiles) {
|
||||
return
|
||||
}
|
||||
if (!hasFiles) {
|
||||
return
|
||||
}
|
||||
|
||||
const images = Array
|
||||
.from(event.dataTransfer.files)
|
||||
.filter(file => (/image/i).test(file.type))
|
||||
const images = Array
|
||||
.from(event.dataTransfer.files)
|
||||
.filter(file => (/image/i).test(file.type))
|
||||
|
||||
if (images.length === 0) {
|
||||
return
|
||||
}
|
||||
if (images.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.preventDefault()
|
||||
|
||||
const { schema } = view.state
|
||||
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY })
|
||||
const { schema } = view.state
|
||||
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY })
|
||||
|
||||
images.forEach(image => {
|
||||
const reader = new FileReader()
|
||||
images.forEach(image => {
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = readerEvent => {
|
||||
const node = schema.nodes.image.create({
|
||||
src: readerEvent.target.result,
|
||||
})
|
||||
const transaction = view.state.tr.insert(coordinates.pos, node)
|
||||
view.dispatch(transaction)
|
||||
}
|
||||
reader.readAsDataURL(image)
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
}
|
||||
reader.onload = readerEvent => {
|
||||
const node = schema.nodes.image.create({
|
||||
src: readerEvent.target.result,
|
||||
})
|
||||
const transaction = view.state.tr.insert(coordinates.pos, node)
|
||||
view.dispatch(transaction)
|
||||
}
|
||||
reader.readAsDataURL(image)
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,28 +3,28 @@ import { splitListItem, liftListItem, sinkListItem } from 'tiptap-commands'
|
||||
|
||||
export default class ListItem extends Node {
|
||||
|
||||
get name() {
|
||||
return 'list_item'
|
||||
}
|
||||
get name() {
|
||||
return 'list_item'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'paragraph block*',
|
||||
defining: true,
|
||||
draggable: false,
|
||||
parseDOM: [
|
||||
{ tag: 'li' },
|
||||
],
|
||||
toDOM: () => ['li', 0],
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
content: 'paragraph block*',
|
||||
defining: true,
|
||||
draggable: false,
|
||||
parseDOM: [
|
||||
{ tag: 'li' },
|
||||
],
|
||||
toDOM: () => ['li', 0],
|
||||
}
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
Enter: splitListItem(type),
|
||||
Tab: sinkListItem(type),
|
||||
'Shift-Tab': liftListItem(type),
|
||||
}
|
||||
}
|
||||
keys({ type }) {
|
||||
return {
|
||||
Enter: splitListItem(type),
|
||||
Tab: sinkListItem(type),
|
||||
'Shift-Tab': liftListItem(type),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,68 +4,68 @@ import SuggestionsPlugin from '../plugins/Suggestions'
|
||||
|
||||
export default class Mention extends Node {
|
||||
|
||||
get name() {
|
||||
return 'mention'
|
||||
}
|
||||
get name() {
|
||||
return 'mention'
|
||||
}
|
||||
|
||||
get defaultOptions() {
|
||||
return {
|
||||
matcher: {
|
||||
char: '@',
|
||||
allowSpaces: false,
|
||||
startOfLine: false,
|
||||
},
|
||||
mentionClass: 'mention',
|
||||
suggestionClass: 'mention-suggestion',
|
||||
}
|
||||
}
|
||||
get defaultOptions() {
|
||||
return {
|
||||
matcher: {
|
||||
char: '@',
|
||||
allowSpaces: false,
|
||||
startOfLine: false,
|
||||
},
|
||||
mentionClass: 'mention',
|
||||
suggestionClass: 'mention-suggestion',
|
||||
}
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
id: {},
|
||||
label: {},
|
||||
},
|
||||
group: 'inline',
|
||||
inline: true,
|
||||
selectable: false,
|
||||
atom: true,
|
||||
toDOM: node => [
|
||||
'span',
|
||||
{
|
||||
class: this.options.mentionClass,
|
||||
'data-mention-id': node.attrs.id,
|
||||
},
|
||||
`${this.options.matcher.char}${node.attrs.label}`,
|
||||
],
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'span[data-mention-id]',
|
||||
getAttrs: dom => {
|
||||
const id = dom.getAttribute('data-mention-id')
|
||||
const label = dom.innerText.split(this.options.matcher.char).join('')
|
||||
return { id, label }
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
id: {},
|
||||
label: {},
|
||||
},
|
||||
group: 'inline',
|
||||
inline: true,
|
||||
selectable: false,
|
||||
atom: true,
|
||||
toDOM: node => [
|
||||
'span',
|
||||
{
|
||||
class: this.options.mentionClass,
|
||||
'data-mention-id': node.attrs.id,
|
||||
},
|
||||
`${this.options.matcher.char}${node.attrs.label}`,
|
||||
],
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'span[data-mention-id]',
|
||||
getAttrs: dom => {
|
||||
const id = dom.getAttribute('data-mention-id')
|
||||
const label = dom.innerText.split(this.options.matcher.char).join('')
|
||||
return { id, label }
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
return [
|
||||
SuggestionsPlugin({
|
||||
command: ({ range, attrs, schema }) => replaceText(range, schema.nodes.mention, attrs),
|
||||
appendText: ' ',
|
||||
matcher: this.options.matcher,
|
||||
items: this.options.items,
|
||||
onEnter: this.options.onEnter,
|
||||
onChange: this.options.onChange,
|
||||
onExit: this.options.onExit,
|
||||
onKeyDown: this.options.onKeyDown,
|
||||
onFilter: this.options.onFilter,
|
||||
suggestionClass: this.options.suggestionClass,
|
||||
}),
|
||||
]
|
||||
}
|
||||
get plugins() {
|
||||
return [
|
||||
SuggestionsPlugin({
|
||||
command: ({ range, attrs, schema }) => replaceText(range, schema.nodes.mention, attrs),
|
||||
appendText: ' ',
|
||||
matcher: this.options.matcher,
|
||||
items: this.options.items,
|
||||
onEnter: this.options.onEnter,
|
||||
onChange: this.options.onChange,
|
||||
onExit: this.options.onExit,
|
||||
onKeyDown: this.options.onKeyDown,
|
||||
onFilter: this.options.onFilter,
|
||||
suggestionClass: this.options.suggestionClass,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,50 +3,50 @@ import { wrappingInputRule, toggleList } from 'tiptap-commands'
|
||||
|
||||
export default class OrderedList extends Node {
|
||||
|
||||
get name() {
|
||||
return 'ordered_list'
|
||||
}
|
||||
get name() {
|
||||
return 'ordered_list'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
order: {
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
content: 'list_item+',
|
||||
group: 'block',
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'ol',
|
||||
getAttrs: dom => ({
|
||||
order: dom.hasAttribute('start') ? +dom.getAttribute('start') : 1,
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: node => (node.attrs.order === 1 ? ['ol', 0] : ['ol', { start: node.attrs.order }, 0]),
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
order: {
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
content: 'list_item+',
|
||||
group: 'block',
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'ol',
|
||||
getAttrs: dom => ({
|
||||
order: dom.hasAttribute('start') ? +dom.getAttribute('start') : 1,
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: node => (node.attrs.order === 1 ? ['ol', 0] : ['ol', { start: node.attrs.order }, 0]),
|
||||
}
|
||||
}
|
||||
|
||||
commands({ type, schema }) {
|
||||
return () => toggleList(type, schema.nodes.list_item)
|
||||
}
|
||||
commands({ type, schema }) {
|
||||
return () => toggleList(type, schema.nodes.list_item)
|
||||
}
|
||||
|
||||
keys({ type, schema }) {
|
||||
return {
|
||||
'Shift-Ctrl-9': toggleList(type, schema.nodes.list_item),
|
||||
}
|
||||
}
|
||||
keys({ type, schema }) {
|
||||
return {
|
||||
'Shift-Ctrl-9': toggleList(type, schema.nodes.list_item),
|
||||
}
|
||||
}
|
||||
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
wrappingInputRule(
|
||||
/^(\d+)\.\s$/,
|
||||
type,
|
||||
match => ({ order: +match[1] }),
|
||||
(match, node) => node.childCount + node.attrs.order === +match[1],
|
||||
),
|
||||
]
|
||||
}
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
wrappingInputRule(
|
||||
/^(\d+)\.\s$/,
|
||||
type,
|
||||
match => ({ order: +match[1] }),
|
||||
(match, node) => node.childCount + node.attrs.order === +match[1],
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,64 +3,64 @@ import { splitToDefaultListItem, liftListItem } from 'tiptap-commands'
|
||||
|
||||
export default class TodoItem extends Node {
|
||||
|
||||
get name() {
|
||||
return 'todo_item'
|
||||
}
|
||||
get name() {
|
||||
return 'todo_item'
|
||||
}
|
||||
|
||||
get view() {
|
||||
return {
|
||||
props: ['node', 'updateAttrs', 'editable'],
|
||||
methods: {
|
||||
onChange() {
|
||||
this.updateAttrs({
|
||||
done: !this.node.attrs.done,
|
||||
})
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<li data-type="todo_item" :data-done="node.attrs.done.toString()">
|
||||
<span class="todo-checkbox" contenteditable="false" @click="onChange"></span>
|
||||
<div class="todo-content" ref="content" :contenteditable="editable.toString()"></div>
|
||||
</li>
|
||||
`,
|
||||
}
|
||||
}
|
||||
get view() {
|
||||
return {
|
||||
props: ['node', 'updateAttrs', 'editable'],
|
||||
methods: {
|
||||
onChange() {
|
||||
this.updateAttrs({
|
||||
done: !this.node.attrs.done,
|
||||
})
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<li data-type="todo_item" :data-done="node.attrs.done.toString()">
|
||||
<span class="todo-checkbox" contenteditable="false" @click="onChange"></span>
|
||||
<div class="todo-content" ref="content" :contenteditable="editable.toString()"></div>
|
||||
</li>
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
done: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
draggable: false,
|
||||
content: 'paragraph',
|
||||
toDOM(node) {
|
||||
const { done } = node.attrs
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
done: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
draggable: false,
|
||||
content: 'paragraph',
|
||||
toDOM(node) {
|
||||
const { done } = node.attrs
|
||||
|
||||
return ['li', {
|
||||
'data-type': 'todo_item',
|
||||
'data-done': done.toString(),
|
||||
},
|
||||
['span', { class: 'todo-checkbox', contenteditable: 'false' }],
|
||||
['div', { class: 'todo-content' }, 0],
|
||||
]
|
||||
},
|
||||
parseDOM: [{
|
||||
priority: 51,
|
||||
tag: '[data-type="todo_item"]',
|
||||
getAttrs: dom => ({
|
||||
done: dom.getAttribute('data-done') === 'true',
|
||||
}),
|
||||
}],
|
||||
}
|
||||
}
|
||||
return ['li', {
|
||||
'data-type': 'todo_item',
|
||||
'data-done': done.toString(),
|
||||
},
|
||||
['span', { class: 'todo-checkbox', contenteditable: 'false' }],
|
||||
['div', { class: 'todo-content' }, 0],
|
||||
]
|
||||
},
|
||||
parseDOM: [{
|
||||
priority: 51,
|
||||
tag: '[data-type="todo_item"]',
|
||||
getAttrs: dom => ({
|
||||
done: dom.getAttribute('data-done') === 'true',
|
||||
}),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
Enter: splitToDefaultListItem(type),
|
||||
'Shift-Tab': liftListItem(type),
|
||||
}
|
||||
}
|
||||
keys({ type }) {
|
||||
return {
|
||||
Enter: splitToDefaultListItem(type),
|
||||
'Shift-Tab': liftListItem(type),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,30 +3,30 @@ import { wrapInList, wrappingInputRule } from 'tiptap-commands'
|
||||
|
||||
export default class TodoList extends Node {
|
||||
|
||||
get name() {
|
||||
return 'todo_list'
|
||||
}
|
||||
get name() {
|
||||
return 'todo_list'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
group: 'block',
|
||||
content: 'todo_item+',
|
||||
toDOM: () => ['ul', { 'data-type': 'todo_list' }, 0],
|
||||
parseDOM: [{
|
||||
priority: 51,
|
||||
tag: '[data-type="todo_list"]',
|
||||
}],
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
group: 'block',
|
||||
content: 'todo_item+',
|
||||
toDOM: () => ['ul', { 'data-type': 'todo_list' }, 0],
|
||||
parseDOM: [{
|
||||
priority: 51,
|
||||
tag: '[data-type="todo_list"]',
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
commands({ type }) {
|
||||
return () => wrapInList(type)
|
||||
}
|
||||
commands({ type }) {
|
||||
return () => wrapInList(type)
|
||||
}
|
||||
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
wrappingInputRule(/^\s*(\[ \])\s$/, type),
|
||||
]
|
||||
}
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
wrappingInputRule(/^\s*(\[ \])\s$/, type),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,239 +4,239 @@ import { insertText } from 'tiptap-commands'
|
||||
|
||||
// Create a matcher that matches when a specific character is typed. Useful for @mentions and #tags.
|
||||
function triggerCharacter({
|
||||
char = '@',
|
||||
allowSpaces = false,
|
||||
startOfLine = false,
|
||||
char = '@',
|
||||
allowSpaces = false,
|
||||
startOfLine = false,
|
||||
}) {
|
||||
|
||||
return $position => {
|
||||
// Matching expressions used for later
|
||||
const suffix = new RegExp(`\\s${char}$`)
|
||||
const prefix = startOfLine ? '^' : ''
|
||||
const regexp = allowSpaces
|
||||
? new RegExp(`${prefix}${char}.*?(?=\\s${char}|$)`, 'gm')
|
||||
: new RegExp(`${prefix}(?:^)?${char}[^\\s${char}]*`, 'gm')
|
||||
return $position => {
|
||||
// Matching expressions used for later
|
||||
const suffix = new RegExp(`\\s${char}$`)
|
||||
const prefix = startOfLine ? '^' : ''
|
||||
const regexp = allowSpaces
|
||||
? new RegExp(`${prefix}${char}.*?(?=\\s${char}|$)`, 'gm')
|
||||
: new RegExp(`${prefix}(?:^)?${char}[^\\s${char}]*`, 'gm')
|
||||
|
||||
// Lookup the boundaries of the current node
|
||||
const textFrom = $position.before()
|
||||
const textTo = $position.end()
|
||||
const text = $position.doc.textBetween(textFrom, textTo, '\0', '\0')
|
||||
// Lookup the boundaries of the current node
|
||||
const textFrom = $position.before()
|
||||
const textTo = $position.end()
|
||||
const text = $position.doc.textBetween(textFrom, textTo, '\0', '\0')
|
||||
|
||||
let match = regexp.exec(text)
|
||||
let position
|
||||
while (match !== null) {
|
||||
// JavaScript doesn't have lookbehinds; this hacks a check that first character is " "
|
||||
// or the line beginning
|
||||
const matchPrefix = match.input.slice(Math.max(0, match.index - 1), match.index)
|
||||
let match = regexp.exec(text)
|
||||
let position
|
||||
while (match !== null) {
|
||||
// JavaScript doesn't have lookbehinds; this hacks a check that first character is " "
|
||||
// or the line beginning
|
||||
const matchPrefix = match.input.slice(Math.max(0, match.index - 1), match.index)
|
||||
|
||||
if (/^[\s\0]?$/.test(matchPrefix)) {
|
||||
// The absolute position of the match in the document
|
||||
const from = match.index + $position.start()
|
||||
let to = from + match[0].length
|
||||
if (/^[\s\0]?$/.test(matchPrefix)) {
|
||||
// The absolute position of the match in the document
|
||||
const from = match.index + $position.start()
|
||||
let to = from + match[0].length
|
||||
|
||||
// Edge case handling; if spaces are allowed and we're directly in between
|
||||
// two triggers
|
||||
if (allowSpaces && suffix.test(text.slice(to - 1, to + 1))) {
|
||||
match[0] += ' '
|
||||
to += 1
|
||||
}
|
||||
// Edge case handling; if spaces are allowed and we're directly in between
|
||||
// two triggers
|
||||
if (allowSpaces && suffix.test(text.slice(to - 1, to + 1))) {
|
||||
match[0] += ' '
|
||||
to += 1
|
||||
}
|
||||
|
||||
// If the $position is located within the matched substring, return that range
|
||||
if (from < $position.pos && to >= $position.pos) {
|
||||
position = {
|
||||
range: {
|
||||
from,
|
||||
to,
|
||||
},
|
||||
query: match[0].slice(char.length),
|
||||
text: match[0],
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the $position is located within the matched substring, return that range
|
||||
if (from < $position.pos && to >= $position.pos) {
|
||||
position = {
|
||||
range: {
|
||||
from,
|
||||
to,
|
||||
},
|
||||
query: match[0].slice(char.length),
|
||||
text: match[0],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match = regexp.exec(text)
|
||||
}
|
||||
}
|
||||
|
||||
return position
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function SuggestionsPlugin({
|
||||
matcher = {
|
||||
char: '@',
|
||||
allowSpaces: false,
|
||||
startOfLine: false,
|
||||
},
|
||||
appendText = null,
|
||||
suggestionClass = 'suggestion',
|
||||
command = () => false,
|
||||
items = [],
|
||||
onEnter = () => false,
|
||||
onChange = () => false,
|
||||
onExit = () => false,
|
||||
onKeyDown = () => false,
|
||||
onFilter = (searchItems, query) => {
|
||||
if (!query) {
|
||||
return searchItems
|
||||
}
|
||||
matcher = {
|
||||
char: '@',
|
||||
allowSpaces: false,
|
||||
startOfLine: false,
|
||||
},
|
||||
appendText = null,
|
||||
suggestionClass = 'suggestion',
|
||||
command = () => false,
|
||||
items = [],
|
||||
onEnter = () => false,
|
||||
onChange = () => false,
|
||||
onExit = () => false,
|
||||
onKeyDown = () => false,
|
||||
onFilter = (searchItems, query) => {
|
||||
if (!query) {
|
||||
return searchItems
|
||||
}
|
||||
|
||||
return searchItems
|
||||
.filter(item => JSON.stringify(item).toLowerCase().includes(query.toLowerCase()))
|
||||
},
|
||||
return searchItems
|
||||
.filter(item => JSON.stringify(item).toLowerCase().includes(query.toLowerCase()))
|
||||
},
|
||||
}) {
|
||||
return new Plugin({
|
||||
key: new PluginKey('suggestions'),
|
||||
return new Plugin({
|
||||
key: new PluginKey('suggestions'),
|
||||
|
||||
view() {
|
||||
return {
|
||||
update: (view, prevState) => {
|
||||
const prev = this.key.getState(prevState)
|
||||
const next = this.key.getState(view.state)
|
||||
view() {
|
||||
return {
|
||||
update: (view, prevState) => {
|
||||
const prev = this.key.getState(prevState)
|
||||
const next = this.key.getState(view.state)
|
||||
|
||||
// See how the state changed
|
||||
const moved = prev.active && next.active && prev.range.from !== next.range.from
|
||||
const started = !prev.active && next.active
|
||||
const stopped = prev.active && !next.active
|
||||
const changed = !started && !stopped && prev.query !== next.query
|
||||
const handleStart = started || moved
|
||||
const handleChange = changed && !moved
|
||||
const handleExit = stopped || moved
|
||||
// See how the state changed
|
||||
const moved = prev.active && next.active && prev.range.from !== next.range.from
|
||||
const started = !prev.active && next.active
|
||||
const stopped = prev.active && !next.active
|
||||
const changed = !started && !stopped && prev.query !== next.query
|
||||
const handleStart = started || moved
|
||||
const handleChange = changed && !moved
|
||||
const handleExit = stopped || moved
|
||||
|
||||
// Cancel when suggestion isn't active
|
||||
if (!handleStart && !handleChange && !handleExit) {
|
||||
return
|
||||
}
|
||||
// Cancel when suggestion isn't active
|
||||
if (!handleStart && !handleChange && !handleExit) {
|
||||
return
|
||||
}
|
||||
|
||||
const state = handleExit ? prev : next
|
||||
const decorationNode = document.querySelector(`[data-decoration-id="${state.decorationId}"]`)
|
||||
const state = handleExit ? prev : next
|
||||
const decorationNode = document.querySelector(`[data-decoration-id="${state.decorationId}"]`)
|
||||
|
||||
// build a virtual node for popper.js or tippy.js
|
||||
// this can be used for building popups without a DOM node
|
||||
const virtualNode = decorationNode ? {
|
||||
getBoundingClientRect() {
|
||||
return decorationNode.getBoundingClientRect()
|
||||
},
|
||||
clientWidth: decorationNode.clientWidth,
|
||||
clientHeight: decorationNode.clientHeight,
|
||||
} : null
|
||||
// build a virtual node for popper.js or tippy.js
|
||||
// this can be used for building popups without a DOM node
|
||||
const virtualNode = decorationNode ? {
|
||||
getBoundingClientRect() {
|
||||
return decorationNode.getBoundingClientRect()
|
||||
},
|
||||
clientWidth: decorationNode.clientWidth,
|
||||
clientHeight: decorationNode.clientHeight,
|
||||
} : null
|
||||
|
||||
const props = {
|
||||
view,
|
||||
range: state.range,
|
||||
query: state.query,
|
||||
text: state.text,
|
||||
decorationNode,
|
||||
virtualNode,
|
||||
items: onFilter(Array.isArray(items) ? items : items(), state.query),
|
||||
command: ({ range, attrs }) => {
|
||||
command({
|
||||
range,
|
||||
attrs,
|
||||
schema: view.state.schema,
|
||||
})(view.state, view.dispatch, view)
|
||||
const props = {
|
||||
view,
|
||||
range: state.range,
|
||||
query: state.query,
|
||||
text: state.text,
|
||||
decorationNode,
|
||||
virtualNode,
|
||||
items: onFilter(Array.isArray(items) ? items : items(), state.query),
|
||||
command: ({ range, attrs }) => {
|
||||
command({
|
||||
range,
|
||||
attrs,
|
||||
schema: view.state.schema,
|
||||
})(view.state, view.dispatch, view)
|
||||
|
||||
if (appendText) {
|
||||
insertText(appendText)(view.state, view.dispatch, view)
|
||||
}
|
||||
},
|
||||
}
|
||||
if (appendText) {
|
||||
insertText(appendText)(view.state, view.dispatch, view)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Trigger the hooks when necessary
|
||||
if (handleExit) {
|
||||
onExit(props)
|
||||
}
|
||||
// Trigger the hooks when necessary
|
||||
if (handleExit) {
|
||||
onExit(props)
|
||||
}
|
||||
|
||||
if (handleChange) {
|
||||
onChange(props)
|
||||
}
|
||||
if (handleChange) {
|
||||
onChange(props)
|
||||
}
|
||||
|
||||
if (handleStart) {
|
||||
onEnter(props)
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
if (handleStart) {
|
||||
onEnter(props)
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
state: {
|
||||
state: {
|
||||
|
||||
// Initialize the plugin's internal state.
|
||||
init() {
|
||||
return {
|
||||
active: false,
|
||||
range: {},
|
||||
query: null,
|
||||
text: null,
|
||||
}
|
||||
},
|
||||
// Initialize the plugin's internal state.
|
||||
init() {
|
||||
return {
|
||||
active: false,
|
||||
range: {},
|
||||
query: null,
|
||||
text: null,
|
||||
}
|
||||
},
|
||||
|
||||
// Apply changes to the plugin state from a view transaction.
|
||||
apply(tr, prev) {
|
||||
const { selection } = tr
|
||||
const next = { ...prev }
|
||||
// Apply changes to the plugin state from a view transaction.
|
||||
apply(tr, prev) {
|
||||
const { selection } = tr
|
||||
const next = { ...prev }
|
||||
|
||||
// We can only be suggesting if there is no selection
|
||||
if (selection.from === selection.to) {
|
||||
// Reset active state if we just left the previous suggestion range
|
||||
if (selection.from < prev.range.from || selection.from > prev.range.to) {
|
||||
next.active = false
|
||||
}
|
||||
// We can only be suggesting if there is no selection
|
||||
if (selection.from === selection.to) {
|
||||
// Reset active state if we just left the previous suggestion range
|
||||
if (selection.from < prev.range.from || selection.from > prev.range.to) {
|
||||
next.active = false
|
||||
}
|
||||
|
||||
// Try to match against where our cursor currently is
|
||||
const $position = selection.$from
|
||||
const match = triggerCharacter(matcher)($position)
|
||||
const decorationId = (Math.random() + 1).toString(36).substr(2, 5)
|
||||
// Try to match against where our cursor currently is
|
||||
const $position = selection.$from
|
||||
const match = triggerCharacter(matcher)($position)
|
||||
const decorationId = (Math.random() + 1).toString(36).substr(2, 5)
|
||||
|
||||
// If we found a match, update the current state to show it
|
||||
if (match) {
|
||||
next.active = true
|
||||
next.decorationId = prev.decorationId ? prev.decorationId : decorationId
|
||||
next.range = match.range
|
||||
next.query = match.query
|
||||
next.text = match.text
|
||||
} else {
|
||||
next.active = false
|
||||
}
|
||||
} else {
|
||||
next.active = false
|
||||
}
|
||||
// If we found a match, update the current state to show it
|
||||
if (match) {
|
||||
next.active = true
|
||||
next.decorationId = prev.decorationId ? prev.decorationId : decorationId
|
||||
next.range = match.range
|
||||
next.query = match.query
|
||||
next.text = match.text
|
||||
} else {
|
||||
next.active = false
|
||||
}
|
||||
} else {
|
||||
next.active = false
|
||||
}
|
||||
|
||||
// Make sure to empty the range if suggestion is inactive
|
||||
if (!next.active) {
|
||||
next.decorationId = null
|
||||
next.range = {}
|
||||
next.query = null
|
||||
next.text = null
|
||||
}
|
||||
// Make sure to empty the range if suggestion is inactive
|
||||
if (!next.active) {
|
||||
next.decorationId = null
|
||||
next.range = {}
|
||||
next.query = null
|
||||
next.text = null
|
||||
}
|
||||
|
||||
return next
|
||||
},
|
||||
},
|
||||
return next
|
||||
},
|
||||
},
|
||||
|
||||
props: {
|
||||
props: {
|
||||
|
||||
// Call the keydown hook if suggestion is active.
|
||||
handleKeyDown(view, event) {
|
||||
const { active, range } = this.getState(view.state)
|
||||
// Call the keydown hook if suggestion is active.
|
||||
handleKeyDown(view, event) {
|
||||
const { active, range } = this.getState(view.state)
|
||||
|
||||
if (!active) return false
|
||||
if (!active) return false
|
||||
|
||||
return onKeyDown({ view, event, range })
|
||||
},
|
||||
return onKeyDown({ view, event, range })
|
||||
},
|
||||
|
||||
// Setup decorator on the currently active suggestion.
|
||||
decorations(editorState) {
|
||||
const { active, range, decorationId } = this.getState(editorState)
|
||||
// Setup decorator on the currently active suggestion.
|
||||
decorations(editorState) {
|
||||
const { active, range, decorationId } = this.getState(editorState)
|
||||
|
||||
if (!active) return null
|
||||
if (!active) return null
|
||||
|
||||
return DecorationSet.create(editorState.doc, [
|
||||
Decoration.inline(range.from, range.to, {
|
||||
nodeName: 'span',
|
||||
class: suggestionClass,
|
||||
'data-decoration-id': decorationId,
|
||||
}),
|
||||
])
|
||||
},
|
||||
},
|
||||
})
|
||||
return DecorationSet.create(editorState.doc, [
|
||||
Decoration.inline(range.from, range.to, {
|
||||
nodeName: 'span',
|
||||
class: suggestionClass,
|
||||
'data-decoration-id': decorationId,
|
||||
}),
|
||||
])
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ This is a collection of utility functions for [tiptap](https://www.npmjs.com/pac
|
||||
[](https://www.npmjs.com/package/tiptap-utils)
|
||||
[](https://npmcharts.com/compare/tiptap-utils?minimal=true)
|
||||
[](https://www.npmjs.com/package/tiptap-utils)
|
||||
[](https://www.npmjs.com/package/tiptap-utils)
|
||||
[](https://www.npmjs.com/package/tiptap-utils)
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
export default function (state, type) {
|
||||
const { from, to } = state.selection
|
||||
let marks = []
|
||||
const { from, to } = state.selection
|
||||
let marks = []
|
||||
|
||||
state.doc.nodesBetween(from, to, node => {
|
||||
marks = [...marks, ...node.marks]
|
||||
})
|
||||
state.doc.nodesBetween(from, to, node => {
|
||||
marks = [...marks, ...node.marks]
|
||||
})
|
||||
|
||||
const mark = marks.find(markItem => markItem.type.name === type.name)
|
||||
const mark = marks.find(markItem => markItem.type.name === type.name)
|
||||
|
||||
if (mark) {
|
||||
return mark.attrs
|
||||
}
|
||||
if (mark) {
|
||||
return mark.attrs
|
||||
}
|
||||
|
||||
return {}
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
export default function (state, type) {
|
||||
const {
|
||||
from,
|
||||
$from,
|
||||
to,
|
||||
empty,
|
||||
} = state.selection
|
||||
const {
|
||||
from,
|
||||
$from,
|
||||
to,
|
||||
empty,
|
||||
} = state.selection
|
||||
|
||||
if (empty) {
|
||||
return !!type.isInSet(state.storedMarks || $from.marks())
|
||||
}
|
||||
if (empty) {
|
||||
return !!type.isInSet(state.storedMarks || $from.marks())
|
||||
}
|
||||
|
||||
return !!state.doc.rangeHasMark(from, to, type)
|
||||
return !!state.doc.rangeHasMark(from, to, type)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { findParentNode } from 'prosemirror-utils'
|
||||
|
||||
export default function (state, type, attrs = {}) {
|
||||
const predicate = node => node.type === type
|
||||
const parent = findParentNode(predicate)(state.selection)
|
||||
const predicate = node => node.type === type
|
||||
const parent = findParentNode(predicate)(state.selection)
|
||||
|
||||
if (!Object.keys(attrs).length || !parent) {
|
||||
return !!parent
|
||||
}
|
||||
if (!Object.keys(attrs).length || !parent) {
|
||||
return !!parent
|
||||
}
|
||||
|
||||
return parent.node.hasMarkup(type, attrs)
|
||||
return parent.node.hasMarkup(type, attrs)
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ This is the core package of [tiptap](https://www.npmjs.com/package/tiptap).
|
||||
[](https://www.npmjs.com/package/tiptap)
|
||||
[](https://npmcharts.com/compare/tiptap?minimal=true)
|
||||
[](https://www.npmjs.com/package/tiptap)
|
||||
[](https://www.npmjs.com/package/tiptap)
|
||||
[](https://www.npmjs.com/package/tiptap)
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
export default {
|
||||
props: {
|
||||
editor: {
|
||||
default: null,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'editor.element': {
|
||||
immediate: true,
|
||||
handler(element) {
|
||||
if (element) {
|
||||
this.$nextTick(() => this.$el.append(element.firstChild))
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('div')
|
||||
},
|
||||
props: {
|
||||
editor: {
|
||||
default: null,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'editor.element': {
|
||||
immediate: true,
|
||||
handler(element) {
|
||||
if (element) {
|
||||
this.$nextTick(() => this.$el.append(element.firstChild))
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('div')
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
import FloatingMenu from '../Utils/FloatingMenu'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
editor: {
|
||||
default: null,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
menu: {
|
||||
isActive: false,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
editor: {
|
||||
immediate: true,
|
||||
handler(editor) {
|
||||
if (editor) {
|
||||
this.$nextTick(() => {
|
||||
editor.registerPlugin(FloatingMenu({
|
||||
element: this.$el,
|
||||
onUpdate: menu => {
|
||||
this.menu = menu
|
||||
},
|
||||
}))
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
render() {
|
||||
if (!this.editor) {
|
||||
return null
|
||||
}
|
||||
props: {
|
||||
editor: {
|
||||
default: null,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
menu: {
|
||||
isActive: false,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
editor: {
|
||||
immediate: true,
|
||||
handler(editor) {
|
||||
if (editor) {
|
||||
this.$nextTick(() => {
|
||||
editor.registerPlugin(FloatingMenu({
|
||||
element: this.$el,
|
||||
onUpdate: menu => {
|
||||
this.menu = menu
|
||||
},
|
||||
}))
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
render() {
|
||||
if (!this.editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.$scopedSlots.default({
|
||||
focused: this.editor.view.focused,
|
||||
focus: this.editor.focus,
|
||||
commands: this.editor.commands,
|
||||
isActive: this.editor.isActive.bind(this.editor),
|
||||
markAttrs: this.editor.markAttrs.bind(this.editor),
|
||||
menu: this.menu,
|
||||
})
|
||||
},
|
||||
return this.$scopedSlots.default({
|
||||
focused: this.editor.view.focused,
|
||||
focus: this.editor.focus,
|
||||
commands: this.editor.commands,
|
||||
isActive: this.editor.isActive.bind(this.editor),
|
||||
markAttrs: this.editor.markAttrs.bind(this.editor),
|
||||
menu: this.menu,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
export default {
|
||||
props: {
|
||||
editor: {
|
||||
default: null,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
render() {
|
||||
if (!this.editor) {
|
||||
return null
|
||||
}
|
||||
props: {
|
||||
editor: {
|
||||
default: null,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
render() {
|
||||
if (!this.editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.$scopedSlots.default({
|
||||
focused: this.editor.view.focused,
|
||||
focus: this.editor.focus,
|
||||
commands: this.editor.commands,
|
||||
isActive: this.editor.isActive.bind(this.editor),
|
||||
markAttrs: this.editor.markAttrs.bind(this.editor),
|
||||
})
|
||||
},
|
||||
return this.$scopedSlots.default({
|
||||
focused: this.editor.view.focused,
|
||||
focus: this.editor.focus,
|
||||
commands: this.editor.commands,
|
||||
isActive: this.editor.isActive.bind(this.editor),
|
||||
markAttrs: this.editor.markAttrs.bind(this.editor),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
import MenuBubble from '../Utils/MenuBubble'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
editor: {
|
||||
default: null,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
menu: {
|
||||
isActive: false,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
editor: {
|
||||
immediate: true,
|
||||
handler(editor) {
|
||||
if (editor) {
|
||||
this.$nextTick(() => {
|
||||
editor.registerPlugin(MenuBubble({
|
||||
element: this.$el,
|
||||
onUpdate: menu => {
|
||||
this.menu = menu
|
||||
},
|
||||
}))
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
render() {
|
||||
if (!this.editor) {
|
||||
return null
|
||||
}
|
||||
props: {
|
||||
editor: {
|
||||
default: null,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
menu: {
|
||||
isActive: false,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
editor: {
|
||||
immediate: true,
|
||||
handler(editor) {
|
||||
if (editor) {
|
||||
this.$nextTick(() => {
|
||||
editor.registerPlugin(MenuBubble({
|
||||
element: this.$el,
|
||||
onUpdate: menu => {
|
||||
this.menu = menu
|
||||
},
|
||||
}))
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
render() {
|
||||
if (!this.editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.$scopedSlots.default({
|
||||
focused: this.editor.view.focused,
|
||||
focus: this.editor.focus,
|
||||
commands: this.editor.commands,
|
||||
isActive: this.editor.isActive.bind(this.editor),
|
||||
markAttrs: this.editor.markAttrs.bind(this.editor),
|
||||
menu: this.menu,
|
||||
})
|
||||
},
|
||||
return this.$scopedSlots.default({
|
||||
focused: this.editor.view.focused,
|
||||
focus: this.editor.focus,
|
||||
commands: this.editor.commands,
|
||||
isActive: this.editor.isActive.bind(this.editor),
|
||||
markAttrs: this.editor.markAttrs.bind(this.editor),
|
||||
menu: this.menu,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -7,4 +7,4 @@ export { default as FloatingMenu } from './Components/FloatingMenu'
|
||||
export { default as Extension } from './Utils/Extension'
|
||||
export { default as Node } from './Utils/Node'
|
||||
export { default as Mark } from './Utils/Mark'
|
||||
export { default as Plugin } from './Utils/Plugin'
|
||||
export { default as Plugin } from './Utils/Plugin'
|
||||
|
||||
@@ -2,14 +2,14 @@ import Node from '../Utils/Node'
|
||||
|
||||
export default class Doc extends Node {
|
||||
|
||||
get name() {
|
||||
return 'doc'
|
||||
}
|
||||
get name() {
|
||||
return 'doc'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'block+',
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
content: 'block+',
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,24 +3,24 @@ import Node from '../Utils/Node'
|
||||
|
||||
export default class Paragraph extends Node {
|
||||
|
||||
get name() {
|
||||
return 'paragraph'
|
||||
}
|
||||
get name() {
|
||||
return 'paragraph'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'inline*',
|
||||
group: 'block',
|
||||
draggable: false,
|
||||
parseDOM: [{
|
||||
tag: 'p',
|
||||
}],
|
||||
toDOM: () => ['p', 0],
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
content: 'inline*',
|
||||
group: 'block',
|
||||
draggable: false,
|
||||
parseDOM: [{
|
||||
tag: 'p',
|
||||
}],
|
||||
toDOM: () => ['p', 0],
|
||||
}
|
||||
}
|
||||
|
||||
commands({ type }) {
|
||||
return () => setBlockType(type)
|
||||
}
|
||||
commands({ type }) {
|
||||
return () => setBlockType(type)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ import Node from '../Utils/Node'
|
||||
|
||||
export default class Text extends Node {
|
||||
|
||||
get name() {
|
||||
return 'text'
|
||||
}
|
||||
get name() {
|
||||
return 'text'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
group: 'inline',
|
||||
}
|
||||
}
|
||||
get schema() {
|
||||
return {
|
||||
group: 'inline',
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Paragraph from './Paragraph'
|
||||
import Text from './Text'
|
||||
|
||||
export default [
|
||||
new Doc(),
|
||||
new Text(),
|
||||
new Paragraph(),
|
||||
new Doc(),
|
||||
new Text(),
|
||||
new Paragraph(),
|
||||
]
|
||||
|
||||
@@ -1,92 +1,92 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
export default class ComponentView {
|
||||
constructor(component, {
|
||||
parent,
|
||||
node,
|
||||
view,
|
||||
getPos,
|
||||
decorations,
|
||||
editable,
|
||||
}) {
|
||||
this.parent = parent
|
||||
this.component = component
|
||||
this.node = node
|
||||
this.view = view
|
||||
this.getPos = getPos
|
||||
this.decorations = decorations
|
||||
this.editable = editable
|
||||
constructor(component, {
|
||||
parent,
|
||||
node,
|
||||
view,
|
||||
getPos,
|
||||
decorations,
|
||||
editable,
|
||||
}) {
|
||||
this.parent = parent
|
||||
this.component = component
|
||||
this.node = node
|
||||
this.view = view
|
||||
this.getPos = getPos
|
||||
this.decorations = decorations
|
||||
this.editable = editable
|
||||
|
||||
this.dom = this.createDOM()
|
||||
this.contentDOM = this.vm.$refs.content
|
||||
}
|
||||
this.dom = this.createDOM()
|
||||
this.contentDOM = this.vm.$refs.content
|
||||
}
|
||||
|
||||
createDOM() {
|
||||
const Component = Vue.extend(this.component)
|
||||
this.vm = new Component({
|
||||
parent: this.parent,
|
||||
propsData: {
|
||||
node: this.node,
|
||||
view: this.view,
|
||||
getPos: this.getPos,
|
||||
decorations: this.decorations,
|
||||
editable: this.editable,
|
||||
updateAttrs: attrs => this.updateAttrs(attrs),
|
||||
updateContent: content => this.updateContent(content),
|
||||
},
|
||||
}).$mount()
|
||||
return this.vm.$el
|
||||
}
|
||||
createDOM() {
|
||||
const Component = Vue.extend(this.component)
|
||||
this.vm = new Component({
|
||||
parent: this.parent,
|
||||
propsData: {
|
||||
node: this.node,
|
||||
view: this.view,
|
||||
getPos: this.getPos,
|
||||
decorations: this.decorations,
|
||||
editable: this.editable,
|
||||
updateAttrs: attrs => this.updateAttrs(attrs),
|
||||
updateContent: content => this.updateContent(content),
|
||||
},
|
||||
}).$mount()
|
||||
return this.vm.$el
|
||||
}
|
||||
|
||||
updateAttrs(attrs) {
|
||||
if (!this.editable) {
|
||||
return
|
||||
}
|
||||
updateAttrs(attrs) {
|
||||
if (!this.editable) {
|
||||
return
|
||||
}
|
||||
|
||||
const transaction = this.view.state.tr.setNodeMarkup(this.getPos(), null, {
|
||||
...this.node.attrs,
|
||||
...attrs,
|
||||
})
|
||||
this.view.dispatch(transaction)
|
||||
}
|
||||
const transaction = this.view.state.tr.setNodeMarkup(this.getPos(), null, {
|
||||
...this.node.attrs,
|
||||
...attrs,
|
||||
})
|
||||
this.view.dispatch(transaction)
|
||||
}
|
||||
|
||||
updateContent(content) {
|
||||
if (!this.editable) {
|
||||
return
|
||||
}
|
||||
updateContent(content) {
|
||||
if (!this.editable) {
|
||||
return
|
||||
}
|
||||
|
||||
const transaction = this.view.state.tr.setNodeMarkup(this.getPos(), this.node.type, { content })
|
||||
this.view.dispatch(transaction)
|
||||
}
|
||||
const transaction = this.view.state.tr.setNodeMarkup(this.getPos(), this.node.type, { content })
|
||||
this.view.dispatch(transaction)
|
||||
}
|
||||
|
||||
ignoreMutation() {
|
||||
return true
|
||||
}
|
||||
ignoreMutation() {
|
||||
return true
|
||||
}
|
||||
|
||||
stopEvent(event) {
|
||||
// TODO: find a way to pass full extensions to ComponentView
|
||||
// so we could check for schema.draggable
|
||||
// for now we're allowing all drag events for node views
|
||||
return !/drag/.test(event.type)
|
||||
}
|
||||
stopEvent(event) {
|
||||
// TODO: find a way to pass full extensions to ComponentView
|
||||
// so we could check for schema.draggable
|
||||
// for now we're allowing all drag events for node views
|
||||
return !/drag/.test(event.type)
|
||||
}
|
||||
|
||||
update(node, decorations) {
|
||||
if (node.type !== this.node.type) {
|
||||
return false
|
||||
}
|
||||
update(node, decorations) {
|
||||
if (node.type !== this.node.type) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (node === this.node && this.decorations === decorations) {
|
||||
return true
|
||||
}
|
||||
if (node === this.node && this.decorations === decorations) {
|
||||
return true
|
||||
}
|
||||
|
||||
this.node = node
|
||||
this.decorations = decorations
|
||||
this.vm._props.node = node
|
||||
this.vm._props.decorations = decorations
|
||||
return true
|
||||
}
|
||||
this.node = node
|
||||
this.decorations = decorations
|
||||
this.vm._props.node = node
|
||||
this.vm._props.decorations = decorations
|
||||
return true
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.vm.$destroy()
|
||||
}
|
||||
destroy() {
|
||||
this.vm.$destroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,278 +9,278 @@ import { inputRules } from 'prosemirror-inputrules'
|
||||
import { markIsActive, nodeIsActive, getMarkAttrs } from 'tiptap-utils'
|
||||
|
||||
import {
|
||||
ExtensionManager,
|
||||
initNodeViews,
|
||||
builtInKeymap,
|
||||
ExtensionManager,
|
||||
initNodeViews,
|
||||
builtInKeymap,
|
||||
} from '.'
|
||||
|
||||
import builtInNodes from '../Nodes'
|
||||
|
||||
export default class Editor {
|
||||
|
||||
constructor(options = {}) {
|
||||
this.setOptions(options)
|
||||
this.init()
|
||||
}
|
||||
constructor(options = {}) {
|
||||
this.setOptions(options)
|
||||
this.init()
|
||||
}
|
||||
|
||||
setOptions(options) {
|
||||
const defaultOptions = {
|
||||
editable: true,
|
||||
content: '',
|
||||
onUpdate: () => {},
|
||||
}
|
||||
setOptions(options) {
|
||||
const defaultOptions = {
|
||||
editable: true,
|
||||
content: '',
|
||||
onUpdate: () => {},
|
||||
}
|
||||
|
||||
this.options = {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
}
|
||||
}
|
||||
this.options = {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.bus = new Vue()
|
||||
this.element = document.createElement('div')
|
||||
this.extensions = this.createExtensions()
|
||||
this.nodes = this.createNodes()
|
||||
this.marks = this.createMarks()
|
||||
this.views = this.createViews()
|
||||
this.schema = this.createSchema()
|
||||
this.plugins = this.createPlugins()
|
||||
this.keymaps = this.createKeymaps()
|
||||
this.inputRules = this.createInputRules()
|
||||
this.state = this.createState()
|
||||
this.view = this.createView()
|
||||
this.commands = this.createCommands()
|
||||
this.getActiveNodesAndMarks()
|
||||
this.emit('init')
|
||||
}
|
||||
init() {
|
||||
this.bus = new Vue()
|
||||
this.element = document.createElement('div')
|
||||
this.extensions = this.createExtensions()
|
||||
this.nodes = this.createNodes()
|
||||
this.marks = this.createMarks()
|
||||
this.views = this.createViews()
|
||||
this.schema = this.createSchema()
|
||||
this.plugins = this.createPlugins()
|
||||
this.keymaps = this.createKeymaps()
|
||||
this.inputRules = this.createInputRules()
|
||||
this.state = this.createState()
|
||||
this.view = this.createView()
|
||||
this.commands = this.createCommands()
|
||||
this.getActiveNodesAndMarks()
|
||||
this.emit('init')
|
||||
}
|
||||
|
||||
createExtensions() {
|
||||
return new ExtensionManager([
|
||||
...builtInNodes,
|
||||
...this.options.extensions,
|
||||
])
|
||||
}
|
||||
createExtensions() {
|
||||
return new ExtensionManager([
|
||||
...builtInNodes,
|
||||
...this.options.extensions,
|
||||
])
|
||||
}
|
||||
|
||||
createPlugins() {
|
||||
return this.extensions.plugins
|
||||
}
|
||||
createPlugins() {
|
||||
return this.extensions.plugins
|
||||
}
|
||||
|
||||
createKeymaps() {
|
||||
return this.extensions.keymaps({
|
||||
schema: this.schema,
|
||||
})
|
||||
}
|
||||
createKeymaps() {
|
||||
return this.extensions.keymaps({
|
||||
schema: this.schema,
|
||||
})
|
||||
}
|
||||
|
||||
createInputRules() {
|
||||
return this.extensions.inputRules({
|
||||
schema: this.schema,
|
||||
})
|
||||
}
|
||||
createInputRules() {
|
||||
return this.extensions.inputRules({
|
||||
schema: this.schema,
|
||||
})
|
||||
}
|
||||
|
||||
createCommands() {
|
||||
return this.extensions.commands({
|
||||
schema: this.schema,
|
||||
view: this.view,
|
||||
editable: this.options.editable,
|
||||
})
|
||||
}
|
||||
createCommands() {
|
||||
return this.extensions.commands({
|
||||
schema: this.schema,
|
||||
view: this.view,
|
||||
editable: this.options.editable,
|
||||
})
|
||||
}
|
||||
|
||||
createNodes() {
|
||||
return this.extensions.nodes
|
||||
}
|
||||
createNodes() {
|
||||
return this.extensions.nodes
|
||||
}
|
||||
|
||||
createMarks() {
|
||||
return this.extensions.marks
|
||||
}
|
||||
createMarks() {
|
||||
return this.extensions.marks
|
||||
}
|
||||
|
||||
createViews() {
|
||||
return this.extensions.views
|
||||
}
|
||||
createViews() {
|
||||
return this.extensions.views
|
||||
}
|
||||
|
||||
createSchema() {
|
||||
return new Schema({
|
||||
nodes: this.nodes,
|
||||
marks: this.marks,
|
||||
})
|
||||
}
|
||||
createSchema() {
|
||||
return new Schema({
|
||||
nodes: this.nodes,
|
||||
marks: this.marks,
|
||||
})
|
||||
}
|
||||
|
||||
createState() {
|
||||
return EditorState.create({
|
||||
schema: this.schema,
|
||||
doc: this.createDocument(this.options.content),
|
||||
plugins: [
|
||||
...this.plugins,
|
||||
inputRules({
|
||||
rules: this.inputRules,
|
||||
}),
|
||||
...this.keymaps,
|
||||
keymap(builtInKeymap),
|
||||
keymap(baseKeymap),
|
||||
gapCursor(),
|
||||
new Plugin({
|
||||
props: {
|
||||
editable: () => this.options.editable,
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
}
|
||||
createState() {
|
||||
return EditorState.create({
|
||||
schema: this.schema,
|
||||
doc: this.createDocument(this.options.content),
|
||||
plugins: [
|
||||
...this.plugins,
|
||||
inputRules({
|
||||
rules: this.inputRules,
|
||||
}),
|
||||
...this.keymaps,
|
||||
keymap(builtInKeymap),
|
||||
keymap(baseKeymap),
|
||||
gapCursor(),
|
||||
new Plugin({
|
||||
props: {
|
||||
editable: () => this.options.editable,
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
createDocument(content) {
|
||||
if (typeof content === 'object') {
|
||||
return this.schema.nodeFromJSON(content)
|
||||
}
|
||||
createDocument(content) {
|
||||
if (typeof content === 'object') {
|
||||
return this.schema.nodeFromJSON(content)
|
||||
}
|
||||
|
||||
if (typeof content === 'string') {
|
||||
const element = document.createElement('div')
|
||||
element.innerHTML = content.trim()
|
||||
if (typeof content === 'string') {
|
||||
const element = document.createElement('div')
|
||||
element.innerHTML = content.trim()
|
||||
|
||||
return DOMParser.fromSchema(this.schema).parse(element)
|
||||
}
|
||||
return DOMParser.fromSchema(this.schema).parse(element)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
createView() {
|
||||
const view = new EditorView(this.element, {
|
||||
state: this.state,
|
||||
dispatchTransaction: this.dispatchTransaction.bind(this),
|
||||
nodeViews: initNodeViews({
|
||||
nodes: this.views,
|
||||
editable: this.options.editable,
|
||||
}),
|
||||
})
|
||||
createView() {
|
||||
const view = new EditorView(this.element, {
|
||||
state: this.state,
|
||||
dispatchTransaction: this.dispatchTransaction.bind(this),
|
||||
nodeViews: initNodeViews({
|
||||
nodes: this.views,
|
||||
editable: this.options.editable,
|
||||
}),
|
||||
})
|
||||
|
||||
view.dom.style.whiteSpace = 'pre-wrap'
|
||||
view.dom.style.whiteSpace = 'pre-wrap'
|
||||
|
||||
return view
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
dispatchTransaction(transaction) {
|
||||
this.state = this.state.apply(transaction)
|
||||
this.view.updateState(this.state)
|
||||
this.getActiveNodesAndMarks()
|
||||
dispatchTransaction(transaction) {
|
||||
this.state = this.state.apply(transaction)
|
||||
this.view.updateState(this.state)
|
||||
this.getActiveNodesAndMarks()
|
||||
|
||||
if (!transaction.docChanged) {
|
||||
return
|
||||
}
|
||||
if (!transaction.docChanged) {
|
||||
return
|
||||
}
|
||||
|
||||
this.emitUpdate()
|
||||
}
|
||||
this.emitUpdate()
|
||||
}
|
||||
|
||||
emitUpdate() {
|
||||
this.options.onUpdate({
|
||||
getHTML: this.getHTML.bind(this),
|
||||
getJSON: this.getJSON.bind(this),
|
||||
state: this.state,
|
||||
})
|
||||
}
|
||||
emitUpdate() {
|
||||
this.options.onUpdate({
|
||||
getHTML: this.getHTML.bind(this),
|
||||
getJSON: this.getJSON.bind(this),
|
||||
state: this.state,
|
||||
})
|
||||
}
|
||||
|
||||
getHTML() {
|
||||
const div = document.createElement('div')
|
||||
const fragment = DOMSerializer
|
||||
.fromSchema(this.schema)
|
||||
.serializeFragment(this.state.doc.content)
|
||||
getHTML() {
|
||||
const div = document.createElement('div')
|
||||
const fragment = DOMSerializer
|
||||
.fromSchema(this.schema)
|
||||
.serializeFragment(this.state.doc.content)
|
||||
|
||||
div.appendChild(fragment)
|
||||
div.appendChild(fragment)
|
||||
|
||||
return div.innerHTML
|
||||
}
|
||||
return div.innerHTML
|
||||
}
|
||||
|
||||
getJSON() {
|
||||
return this.state.doc.toJSON()
|
||||
}
|
||||
getJSON() {
|
||||
return this.state.doc.toJSON()
|
||||
}
|
||||
|
||||
setContent(content = {}, emitUpdate = false) {
|
||||
this.state = EditorState.create({
|
||||
schema: this.state.schema,
|
||||
doc: this.createDocument(content),
|
||||
plugins: this.state.plugins,
|
||||
})
|
||||
setContent(content = {}, emitUpdate = false) {
|
||||
this.state = EditorState.create({
|
||||
schema: this.state.schema,
|
||||
doc: this.createDocument(content),
|
||||
plugins: this.state.plugins,
|
||||
})
|
||||
|
||||
this.view.updateState(this.state)
|
||||
this.view.updateState(this.state)
|
||||
|
||||
if (emitUpdate) {
|
||||
this.emitUpdate()
|
||||
}
|
||||
}
|
||||
if (emitUpdate) {
|
||||
this.emitUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
clearContent(emitUpdate = false) {
|
||||
this.setContent({
|
||||
type: 'doc',
|
||||
content: [{
|
||||
type: 'paragraph',
|
||||
}],
|
||||
}, emitUpdate)
|
||||
}
|
||||
clearContent(emitUpdate = false) {
|
||||
this.setContent({
|
||||
type: 'doc',
|
||||
content: [{
|
||||
type: 'paragraph',
|
||||
}],
|
||||
}, emitUpdate)
|
||||
}
|
||||
|
||||
getActiveNodesAndMarks() {
|
||||
this.activeMarks = Object
|
||||
.entries(this.schema.marks)
|
||||
.reduce((marks, [name, mark]) => ({
|
||||
...marks,
|
||||
[name]: (attrs = {}) => markIsActive(this.state, mark, attrs),
|
||||
}), {})
|
||||
getActiveNodesAndMarks() {
|
||||
this.activeMarks = Object
|
||||
.entries(this.schema.marks)
|
||||
.reduce((marks, [name, mark]) => ({
|
||||
...marks,
|
||||
[name]: (attrs = {}) => markIsActive(this.state, mark, attrs),
|
||||
}), {})
|
||||
|
||||
this.activeMarkAttrs = Object
|
||||
.entries(this.schema.marks)
|
||||
.reduce((marks, [name, mark]) => ({
|
||||
...marks,
|
||||
[name]: getMarkAttrs(this.state, mark),
|
||||
}), {})
|
||||
this.activeMarkAttrs = Object
|
||||
.entries(this.schema.marks)
|
||||
.reduce((marks, [name, mark]) => ({
|
||||
...marks,
|
||||
[name]: getMarkAttrs(this.state, mark),
|
||||
}), {})
|
||||
|
||||
this.activeNodes = Object
|
||||
.entries(this.schema.nodes)
|
||||
.reduce((nodes, [name, node]) => ({
|
||||
...nodes,
|
||||
[name]: (attrs = {}) => nodeIsActive(this.state, node, attrs),
|
||||
}), {})
|
||||
}
|
||||
this.activeNodes = Object
|
||||
.entries(this.schema.nodes)
|
||||
.reduce((nodes, [name, node]) => ({
|
||||
...nodes,
|
||||
[name]: (attrs = {}) => nodeIsActive(this.state, node, attrs),
|
||||
}), {})
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.view.focus()
|
||||
}
|
||||
focus() {
|
||||
this.view.focus()
|
||||
}
|
||||
|
||||
emit(event, ...data) {
|
||||
this.bus.$emit(event, ...data)
|
||||
}
|
||||
emit(event, ...data) {
|
||||
this.bus.$emit(event, ...data)
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
this.bus.$on(event, callback)
|
||||
}
|
||||
on(event, callback) {
|
||||
this.bus.$on(event, callback)
|
||||
}
|
||||
|
||||
registerPlugin(plugin = null) {
|
||||
if (plugin) {
|
||||
this.state = this.state.reconfigure({
|
||||
plugins: this.state.plugins.concat([plugin]),
|
||||
})
|
||||
this.view.updateState(this.state)
|
||||
}
|
||||
}
|
||||
registerPlugin(plugin = null) {
|
||||
if (plugin) {
|
||||
this.state = this.state.reconfigure({
|
||||
plugins: this.state.plugins.concat([plugin]),
|
||||
})
|
||||
this.view.updateState(this.state)
|
||||
}
|
||||
}
|
||||
|
||||
markAttrs(type = null) {
|
||||
return this.activeMarkAttrs[type]
|
||||
}
|
||||
markAttrs(type = null) {
|
||||
return this.activeMarkAttrs[type]
|
||||
}
|
||||
|
||||
isActive(type = null, attrs = {}) {
|
||||
const types = {
|
||||
...this.activeMarks,
|
||||
...this.activeNodes,
|
||||
}
|
||||
isActive(type = null, attrs = {}) {
|
||||
const types = {
|
||||
...this.activeMarks,
|
||||
...this.activeNodes,
|
||||
}
|
||||
|
||||
if (!types[type]) {
|
||||
return false
|
||||
}
|
||||
if (!types[type]) {
|
||||
return false
|
||||
}
|
||||
|
||||
return types[type](attrs)
|
||||
}
|
||||
return types[type](attrs)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.emit('destroy')
|
||||
destroy() {
|
||||
this.emit('destroy')
|
||||
|
||||
if (this.view) {
|
||||
this.view.destroy()
|
||||
}
|
||||
}
|
||||
if (this.view) {
|
||||
this.view.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,148 +2,148 @@ import { keymap } from 'prosemirror-keymap'
|
||||
|
||||
export default class ExtensionManager {
|
||||
|
||||
constructor(extensions = []) {
|
||||
this.extensions = extensions
|
||||
}
|
||||
constructor(extensions = []) {
|
||||
this.extensions = extensions
|
||||
}
|
||||
|
||||
get nodes() {
|
||||
return this.extensions
|
||||
.filter(extension => extension.type === 'node')
|
||||
.reduce((nodes, { name, schema }) => ({
|
||||
...nodes,
|
||||
[name]: schema,
|
||||
}), {})
|
||||
}
|
||||
get nodes() {
|
||||
return this.extensions
|
||||
.filter(extension => extension.type === 'node')
|
||||
.reduce((nodes, { name, schema }) => ({
|
||||
...nodes,
|
||||
[name]: schema,
|
||||
}), {})
|
||||
}
|
||||
|
||||
get marks() {
|
||||
return this.extensions
|
||||
.filter(extension => extension.type === 'mark')
|
||||
.reduce((marks, { name, schema }) => ({
|
||||
...marks,
|
||||
[name]: schema,
|
||||
}), {})
|
||||
}
|
||||
get marks() {
|
||||
return this.extensions
|
||||
.filter(extension => extension.type === 'mark')
|
||||
.reduce((marks, { name, schema }) => ({
|
||||
...marks,
|
||||
[name]: schema,
|
||||
}), {})
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
return this.extensions
|
||||
.filter(extension => extension.plugins)
|
||||
.reduce((allPlugins, { plugins }) => ([
|
||||
...allPlugins,
|
||||
...plugins,
|
||||
]), [])
|
||||
}
|
||||
get plugins() {
|
||||
return this.extensions
|
||||
.filter(extension => extension.plugins)
|
||||
.reduce((allPlugins, { plugins }) => ([
|
||||
...allPlugins,
|
||||
...plugins,
|
||||
]), [])
|
||||
}
|
||||
|
||||
get views() {
|
||||
return this.extensions
|
||||
.filter(extension => ['node', 'mark'].includes(extension.type))
|
||||
.filter(extension => extension.view)
|
||||
.reduce((views, { name, view }) => ({
|
||||
...views,
|
||||
[name]: view,
|
||||
}), {})
|
||||
}
|
||||
get views() {
|
||||
return this.extensions
|
||||
.filter(extension => ['node', 'mark'].includes(extension.type))
|
||||
.filter(extension => extension.view)
|
||||
.reduce((views, { name, view }) => ({
|
||||
...views,
|
||||
[name]: view,
|
||||
}), {})
|
||||
}
|
||||
|
||||
keymaps({ schema }) {
|
||||
const extensionKeymaps = this.extensions
|
||||
.filter(extension => ['extension'].includes(extension.type))
|
||||
.filter(extension => extension.keys)
|
||||
.map(extension => extension.keys({ schema }))
|
||||
keymaps({ schema }) {
|
||||
const extensionKeymaps = this.extensions
|
||||
.filter(extension => ['extension'].includes(extension.type))
|
||||
.filter(extension => extension.keys)
|
||||
.map(extension => extension.keys({ schema }))
|
||||
|
||||
const nodeMarkKeymaps = this.extensions
|
||||
.filter(extension => ['node', 'mark'].includes(extension.type))
|
||||
.filter(extension => extension.keys)
|
||||
.map(extension => extension.keys({
|
||||
type: schema[`${extension.type}s`][extension.name],
|
||||
schema,
|
||||
}))
|
||||
const nodeMarkKeymaps = this.extensions
|
||||
.filter(extension => ['node', 'mark'].includes(extension.type))
|
||||
.filter(extension => extension.keys)
|
||||
.map(extension => extension.keys({
|
||||
type: schema[`${extension.type}s`][extension.name],
|
||||
schema,
|
||||
}))
|
||||
|
||||
return [
|
||||
...extensionKeymaps,
|
||||
...nodeMarkKeymaps,
|
||||
].map(keys => keymap(keys))
|
||||
}
|
||||
return [
|
||||
...extensionKeymaps,
|
||||
...nodeMarkKeymaps,
|
||||
].map(keys => keymap(keys))
|
||||
}
|
||||
|
||||
inputRules({ schema }) {
|
||||
const extensionInputRules = this.extensions
|
||||
.filter(extension => ['extension'].includes(extension.type))
|
||||
.filter(extension => extension.inputRules)
|
||||
.map(extension => extension.inputRules({ schema }))
|
||||
inputRules({ schema }) {
|
||||
const extensionInputRules = this.extensions
|
||||
.filter(extension => ['extension'].includes(extension.type))
|
||||
.filter(extension => extension.inputRules)
|
||||
.map(extension => extension.inputRules({ schema }))
|
||||
|
||||
const nodeMarkInputRules = this.extensions
|
||||
.filter(extension => ['node', 'mark'].includes(extension.type))
|
||||
.filter(extension => extension.inputRules)
|
||||
.map(extension => extension.inputRules({
|
||||
type: schema[`${extension.type}s`][extension.name],
|
||||
schema,
|
||||
}))
|
||||
const nodeMarkInputRules = this.extensions
|
||||
.filter(extension => ['node', 'mark'].includes(extension.type))
|
||||
.filter(extension => extension.inputRules)
|
||||
.map(extension => extension.inputRules({
|
||||
type: schema[`${extension.type}s`][extension.name],
|
||||
schema,
|
||||
}))
|
||||
|
||||
return [
|
||||
...extensionInputRules,
|
||||
...nodeMarkInputRules,
|
||||
].reduce((allInputRules, inputRules) => ([
|
||||
...allInputRules,
|
||||
...inputRules,
|
||||
]), [])
|
||||
}
|
||||
return [
|
||||
...extensionInputRules,
|
||||
...nodeMarkInputRules,
|
||||
].reduce((allInputRules, inputRules) => ([
|
||||
...allInputRules,
|
||||
...inputRules,
|
||||
]), [])
|
||||
}
|
||||
|
||||
commands({ schema, view, editable }) {
|
||||
return this.extensions
|
||||
.filter(extension => extension.commands)
|
||||
.reduce((allCommands, { name, type, commands: provider }) => {
|
||||
commands({ schema, view, editable }) {
|
||||
return this.extensions
|
||||
.filter(extension => extension.commands)
|
||||
.reduce((allCommands, { name, type, commands: provider }) => {
|
||||
|
||||
const commands = {}
|
||||
const value = provider({
|
||||
schema,
|
||||
...['node', 'mark'].includes(type) ? {
|
||||
type: schema[`${type}s`][name],
|
||||
} : {},
|
||||
})
|
||||
const commands = {}
|
||||
const value = provider({
|
||||
schema,
|
||||
...['node', 'mark'].includes(type) ? {
|
||||
type: schema[`${type}s`][name],
|
||||
} : {},
|
||||
})
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
commands[name] = attrs => value
|
||||
.forEach(callback => {
|
||||
if (!editable) {
|
||||
return false
|
||||
}
|
||||
view.focus()
|
||||
return callback(attrs)(view.state, view.dispatch, view)
|
||||
})
|
||||
} else if (typeof value === 'function') {
|
||||
commands[name] = attrs => {
|
||||
if (!editable) {
|
||||
return false
|
||||
}
|
||||
view.focus()
|
||||
return value(attrs)(view.state, view.dispatch, view)
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
Object.entries(value).forEach(([commandName, commandValue]) => {
|
||||
if (Array.isArray(commandValue)) {
|
||||
commands[commandName] = attrs => commandValue
|
||||
.forEach(callback => {
|
||||
if (!editable) {
|
||||
return false
|
||||
}
|
||||
view.focus()
|
||||
return callback(attrs)(view.state, view.dispatch, view)
|
||||
})
|
||||
} else {
|
||||
commands[commandName] = attrs => {
|
||||
if (!editable) {
|
||||
return false
|
||||
}
|
||||
view.focus()
|
||||
return commandValue(attrs)(view.state, view.dispatch, view)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
commands[name] = attrs => value
|
||||
.forEach(callback => {
|
||||
if (!editable) {
|
||||
return false
|
||||
}
|
||||
view.focus()
|
||||
return callback(attrs)(view.state, view.dispatch, view)
|
||||
})
|
||||
} else if (typeof value === 'function') {
|
||||
commands[name] = attrs => {
|
||||
if (!editable) {
|
||||
return false
|
||||
}
|
||||
view.focus()
|
||||
return value(attrs)(view.state, view.dispatch, view)
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
Object.entries(value).forEach(([commandName, commandValue]) => {
|
||||
if (Array.isArray(commandValue)) {
|
||||
commands[commandName] = attrs => commandValue
|
||||
.forEach(callback => {
|
||||
if (!editable) {
|
||||
return false
|
||||
}
|
||||
view.focus()
|
||||
return callback(attrs)(view.state, view.dispatch, view)
|
||||
})
|
||||
} else {
|
||||
commands[commandName] = attrs => {
|
||||
if (!editable) {
|
||||
return false
|
||||
}
|
||||
view.focus()
|
||||
return commandValue(attrs)(view.state, view.dispatch, view)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...allCommands,
|
||||
...commands,
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
return {
|
||||
...allCommands,
|
||||
...commands,
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,81 +2,81 @@ import { Plugin } from 'prosemirror-state'
|
||||
|
||||
class Menu {
|
||||
|
||||
constructor({ options, editorView }) {
|
||||
this.options = {
|
||||
...{
|
||||
element: null,
|
||||
onUpdate: () => false,
|
||||
},
|
||||
...options,
|
||||
}
|
||||
this.editorView = editorView
|
||||
this.isActive = false
|
||||
this.top = 0
|
||||
constructor({ options, editorView }) {
|
||||
this.options = {
|
||||
...{
|
||||
element: null,
|
||||
onUpdate: () => false,
|
||||
},
|
||||
...options,
|
||||
}
|
||||
this.editorView = editorView
|
||||
this.isActive = false
|
||||
this.top = 0
|
||||
|
||||
this.editorView.dom.addEventListener('blur', this.hide.bind(this))
|
||||
}
|
||||
this.editorView.dom.addEventListener('blur', this.hide.bind(this))
|
||||
}
|
||||
|
||||
update(view, lastState) {
|
||||
const { state } = view
|
||||
update(view, lastState) {
|
||||
const { state } = view
|
||||
|
||||
// Don't do anything if the document/selection didn't change
|
||||
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
|
||||
return
|
||||
}
|
||||
// Don't do anything if the document/selection didn't change
|
||||
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!state.selection.empty) {
|
||||
this.hide()
|
||||
return
|
||||
}
|
||||
if (!state.selection.empty) {
|
||||
this.hide()
|
||||
return
|
||||
}
|
||||
|
||||
const currentDom = view.domAtPos(state.selection.$anchor.pos)
|
||||
const currentDom = view.domAtPos(state.selection.$anchor.pos)
|
||||
|
||||
const isActive = currentDom.node.innerHTML === '<br>'
|
||||
&& currentDom.node.tagName === 'P'
|
||||
&& currentDom.node.parentNode === view.dom
|
||||
const isActive = currentDom.node.innerHTML === '<br>'
|
||||
&& currentDom.node.tagName === 'P'
|
||||
&& currentDom.node.parentNode === view.dom
|
||||
|
||||
if (!isActive) {
|
||||
this.hide()
|
||||
return
|
||||
}
|
||||
if (!isActive) {
|
||||
this.hide()
|
||||
return
|
||||
}
|
||||
|
||||
const editorBoundings = this.options.element.offsetParent.getBoundingClientRect()
|
||||
const cursorBoundings = view.coordsAtPos(state.selection.$anchor.pos)
|
||||
const top = cursorBoundings.top - editorBoundings.top
|
||||
const editorBoundings = this.options.element.offsetParent.getBoundingClientRect()
|
||||
const cursorBoundings = view.coordsAtPos(state.selection.$anchor.pos)
|
||||
const top = cursorBoundings.top - editorBoundings.top
|
||||
|
||||
this.isActive = true
|
||||
this.top = top
|
||||
this.isActive = true
|
||||
this.top = top
|
||||
|
||||
this.sendUpdate()
|
||||
}
|
||||
this.sendUpdate()
|
||||
}
|
||||
|
||||
sendUpdate() {
|
||||
this.options.onUpdate({
|
||||
isActive: this.isActive,
|
||||
top: this.top,
|
||||
})
|
||||
}
|
||||
sendUpdate() {
|
||||
this.options.onUpdate({
|
||||
isActive: this.isActive,
|
||||
top: this.top,
|
||||
})
|
||||
}
|
||||
|
||||
hide(event) {
|
||||
if (event && event.relatedTarget) {
|
||||
return
|
||||
}
|
||||
hide(event) {
|
||||
if (event && event.relatedTarget) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isActive = false
|
||||
this.sendUpdate()
|
||||
}
|
||||
this.isActive = false
|
||||
this.sendUpdate()
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.editorView.dom.removeEventListener('blur', this.hide)
|
||||
}
|
||||
destroy() {
|
||||
this.editorView.dom.removeEventListener('blur', this.hide)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default function (options) {
|
||||
return new Plugin({
|
||||
view(editorView) {
|
||||
return new Menu({ editorView, options })
|
||||
},
|
||||
})
|
||||
return new Plugin({
|
||||
view(editorView) {
|
||||
return new Menu({ editorView, options })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,84 +2,84 @@ import { Plugin } from 'prosemirror-state'
|
||||
|
||||
class Menu {
|
||||
|
||||
constructor({ options, editorView }) {
|
||||
this.options = {
|
||||
...{
|
||||
element: null,
|
||||
onUpdate: () => false,
|
||||
},
|
||||
...options,
|
||||
}
|
||||
this.editorView = editorView
|
||||
this.isActive = false
|
||||
this.left = 0
|
||||
this.bottom = 0
|
||||
constructor({ options, editorView }) {
|
||||
this.options = {
|
||||
...{
|
||||
element: null,
|
||||
onUpdate: () => false,
|
||||
},
|
||||
...options,
|
||||
}
|
||||
this.editorView = editorView
|
||||
this.isActive = false
|
||||
this.left = 0
|
||||
this.bottom = 0
|
||||
|
||||
this.editorView.dom.addEventListener('blur', this.hide.bind(this))
|
||||
}
|
||||
this.editorView.dom.addEventListener('blur', this.hide.bind(this))
|
||||
}
|
||||
|
||||
update(view, lastState) {
|
||||
const { state } = view
|
||||
update(view, lastState) {
|
||||
const { state } = view
|
||||
|
||||
// Don't do anything if the document/selection didn't change
|
||||
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
|
||||
return
|
||||
}
|
||||
// Don't do anything if the document/selection didn't change
|
||||
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Hide the tooltip if the selection is empty
|
||||
if (state.selection.empty) {
|
||||
this.hide()
|
||||
return
|
||||
}
|
||||
// Hide the tooltip if the selection is empty
|
||||
if (state.selection.empty) {
|
||||
this.hide()
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, reposition it and update its content
|
||||
const { from, to } = state.selection
|
||||
// Otherwise, reposition it and update its content
|
||||
const { from, to } = state.selection
|
||||
|
||||
// These are in screen coordinates
|
||||
const start = view.coordsAtPos(from)
|
||||
const end = view.coordsAtPos(to)
|
||||
// These are in screen coordinates
|
||||
const start = view.coordsAtPos(from)
|
||||
const end = view.coordsAtPos(to)
|
||||
|
||||
// The box in which the tooltip is positioned, to use as base
|
||||
const box = this.options.element.offsetParent.getBoundingClientRect()
|
||||
// The box in which the tooltip is positioned, to use as base
|
||||
const box = this.options.element.offsetParent.getBoundingClientRect()
|
||||
|
||||
// Find a center-ish x position from the selection endpoints (when
|
||||
// crossing lines, end may be more to the left)
|
||||
const left = Math.max((start.left + end.left) / 2, start.left + 3)
|
||||
// Find a center-ish x position from the selection endpoints (when
|
||||
// crossing lines, end may be more to the left)
|
||||
const left = Math.max((start.left + end.left) / 2, start.left + 3)
|
||||
|
||||
this.isActive = true
|
||||
this.left = parseInt(left - box.left, 10)
|
||||
this.bottom = parseInt(box.bottom - start.top, 10)
|
||||
this.isActive = true
|
||||
this.left = parseInt(left - box.left, 10)
|
||||
this.bottom = parseInt(box.bottom - start.top, 10)
|
||||
|
||||
this.sendUpdate()
|
||||
}
|
||||
this.sendUpdate()
|
||||
}
|
||||
|
||||
sendUpdate() {
|
||||
this.options.onUpdate({
|
||||
isActive: this.isActive,
|
||||
left: this.left,
|
||||
bottom: this.bottom,
|
||||
})
|
||||
}
|
||||
sendUpdate() {
|
||||
this.options.onUpdate({
|
||||
isActive: this.isActive,
|
||||
left: this.left,
|
||||
bottom: this.bottom,
|
||||
})
|
||||
}
|
||||
|
||||
hide(event) {
|
||||
if (event && event.relatedTarget) {
|
||||
return
|
||||
}
|
||||
hide(event) {
|
||||
if (event && event.relatedTarget) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isActive = false
|
||||
this.sendUpdate()
|
||||
}
|
||||
this.isActive = false
|
||||
this.sendUpdate()
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.editorView.dom.removeEventListener('blur', this.hide)
|
||||
}
|
||||
destroy() {
|
||||
this.editorView.dom.removeEventListener('blur', this.hide)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default function (options) {
|
||||
return new Plugin({
|
||||
view(editorView) {
|
||||
return new Menu({ editorView, options })
|
||||
},
|
||||
})
|
||||
return new Plugin({
|
||||
view(editorView) {
|
||||
return new Menu({ editorView, options })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import { lift, selectParentNode } from 'prosemirror-commands'
|
||||
import { undoInputRule } from 'prosemirror-inputrules'
|
||||
|
||||
const keymap = {
|
||||
'Mod-BracketLeft': lift,
|
||||
Backspace: undoInputRule,
|
||||
Escape: selectParentNode,
|
||||
'Mod-BracketLeft': lift,
|
||||
Backspace: undoInputRule,
|
||||
Escape: selectParentNode,
|
||||
}
|
||||
|
||||
export default keymap
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
export default class Extension {
|
||||
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
...this.defaultOptions,
|
||||
...options,
|
||||
}
|
||||
}
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
...this.defaultOptions,
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
get name() {
|
||||
return null
|
||||
}
|
||||
get name() {
|
||||
return null
|
||||
}
|
||||
|
||||
get type() {
|
||||
return 'extension'
|
||||
}
|
||||
get type() {
|
||||
return 'extension'
|
||||
}
|
||||
|
||||
get defaultOptions() {
|
||||
return {}
|
||||
}
|
||||
get defaultOptions() {
|
||||
return {}
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
return []
|
||||
}
|
||||
get plugins() {
|
||||
return []
|
||||
}
|
||||
|
||||
inputRules() {
|
||||
return []
|
||||
}
|
||||
inputRules() {
|
||||
return []
|
||||
}
|
||||
|
||||
keys() {
|
||||
return {}
|
||||
}
|
||||
keys() {
|
||||
return {}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,24 +2,24 @@ import Extension from './Extension'
|
||||
|
||||
export default class Mark extends Extension {
|
||||
|
||||
constructor(options = {}) {
|
||||
super(options)
|
||||
}
|
||||
constructor(options = {}) {
|
||||
super(options)
|
||||
}
|
||||
|
||||
get type() {
|
||||
return 'mark'
|
||||
}
|
||||
get type() {
|
||||
return 'mark'
|
||||
}
|
||||
|
||||
get view() {
|
||||
return null
|
||||
}
|
||||
get view() {
|
||||
return null
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return null
|
||||
}
|
||||
get schema() {
|
||||
return null
|
||||
}
|
||||
|
||||
command() {
|
||||
return () => {}
|
||||
}
|
||||
command() {
|
||||
return () => {}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,24 +2,24 @@ import Extension from './Extension'
|
||||
|
||||
export default class Node extends Extension {
|
||||
|
||||
constructor(options = {}) {
|
||||
super(options)
|
||||
}
|
||||
constructor(options = {}) {
|
||||
super(options)
|
||||
}
|
||||
|
||||
get type() {
|
||||
return 'node'
|
||||
}
|
||||
get type() {
|
||||
return 'node'
|
||||
}
|
||||
|
||||
get view() {
|
||||
return null
|
||||
}
|
||||
get view() {
|
||||
return null
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return null
|
||||
}
|
||||
get schema() {
|
||||
return null
|
||||
}
|
||||
|
||||
command() {
|
||||
return () => {}
|
||||
}
|
||||
command() {
|
||||
return () => {}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user