Merge branch 'master' into feature/suggestions
# Conflicts: # packages/tiptap-extensions/package.json
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tiptap-commands",
|
||||
"version": "0.2.4",
|
||||
"version": "0.3.0",
|
||||
"description": "Commands for tiptap",
|
||||
"homepage": "https://tiptap.scrumpy.io",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// this is a copy of canSplit
|
||||
// see https://github.com/ProseMirror/prosemirror-transform/blob/master/src/structure.js
|
||||
|
||||
function canSplit(doc, pos, depth = 1, typesAfter) {
|
||||
let $pos = doc.resolve(pos), base = $pos.depth - depth
|
||||
let innerType = (typesAfter && typesAfter[typesAfter.length - 1]) || $pos.parent
|
||||
if (base < 0 || $pos.parent.type.spec.isolating ||
|
||||
!$pos.parent.canReplace($pos.index(), $pos.parent.childCount) ||
|
||||
!innerType.type.validContent($pos.parent.content.cutByIndex($pos.index(), $pos.parent.childCount)))
|
||||
return false
|
||||
for (let d = $pos.depth - 1, i = depth - 2; d > base; d--, i--) {
|
||||
let node = $pos.node(d), index = $pos.index(d)
|
||||
if (node.type.spec.isolating) return false
|
||||
let rest = node.content.cutByIndex(index, node.childCount)
|
||||
let after = (typesAfter && typesAfter[i]) || node
|
||||
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))
|
||||
// return false
|
||||
if (!node.canReplace(index + 1, node.childCount))
|
||||
return false
|
||||
/* Change ends here */
|
||||
}
|
||||
let index = $pos.indexAfter(base)
|
||||
let baseType = typesAfter && typesAfter[0]
|
||||
return $pos.node(base).canReplaceWith(index, index, baseType ? baseType.type : $pos.node(base + 1).type)
|
||||
}
|
||||
|
||||
// this is a copy of splitListItem
|
||||
// see https://github.com/ProseMirror/prosemirror-schema-list/blob/master/src/schema-list.js
|
||||
|
||||
export default function (itemType) {
|
||||
return function(state, dispatch) {
|
||||
let {$from, $to, node} = state.selection
|
||||
if ((node && node.isBlock) || $from.depth < 2 || !$from.sameParent($to)) return false
|
||||
let grandParent = $from.node(-1)
|
||||
if (grandParent.type != itemType) return false
|
||||
if ($from.parent.content.size == 0) {
|
||||
// In an empty block. If this is a nested list, the wrapping
|
||||
// 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
|
||||
|
||||
if (dispatch) {
|
||||
let wrap = Fragment.empty, keepItem = $from.index(-1) > 0
|
||||
// Build a fragment containing empty versions of the structure
|
||||
// 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()))
|
||||
let 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
|
||||
}
|
||||
let nextType = $to.pos == $from.end() ? grandParent.contentMatchAt($from.indexAfter(-1)).defaultType : null
|
||||
let tr = state.tr.delete($from.pos, $to.pos)
|
||||
|
||||
/* 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 */
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
|
||||
import markInputRule from './commands/markInputRule'
|
||||
import removeMark from './commands/removeMark'
|
||||
import splitToDefaultListItem from './commands/splitToDefaultListItem'
|
||||
import toggleBlockType from './commands/toggleBlockType'
|
||||
import toggleList from './commands/toggleList'
|
||||
import updateMark from './commands/updateMark'
|
||||
@@ -85,6 +86,7 @@ export {
|
||||
// custom
|
||||
markInputRule,
|
||||
removeMark,
|
||||
splitToDefaultListItem,
|
||||
toggleBlockType,
|
||||
toggleList,
|
||||
updateMark,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tiptap-extensions",
|
||||
"version": "0.6.1",
|
||||
"version": "0.8.0",
|
||||
"description": "Extensions for tiptap",
|
||||
"homepage": "https://tiptap.scrumpy.io",
|
||||
"license": "MIT",
|
||||
@@ -20,10 +20,11 @@
|
||||
"url": "https://github.com/heyscrumpy/tiptap/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"lowlight": "^1.10.0",
|
||||
"prosemirror-history": "^1.0.2",
|
||||
"prosemirror-state": "^1.2.2",
|
||||
"prosemirror-view": "^1.5.1",
|
||||
"tiptap": "^0.8.0",
|
||||
"tiptap-commands": "^0.2.4"
|
||||
"tiptap": "^0.10.0",
|
||||
"tiptap-commands": "^0.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export { default as BlockquoteNode } from './nodes/Blockquote'
|
||||
export { default as BulletListNode } from './nodes/BulletList'
|
||||
export { default as CodeBlockNode } from './nodes/CodeBlock'
|
||||
export { default as CodeBlockHighlightNode } from './nodes/CodeBlockHighlight'
|
||||
export { default as HardBreakNode } from './nodes/HardBreak'
|
||||
export { default as HeadingNode } from './nodes/Heading'
|
||||
export { default as ImageNode } from './nodes/Image'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Node } from 'tiptap'
|
||||
import { wrappingInputRule, setBlockType, wrapIn } from 'tiptap-commands'
|
||||
import { Node, Plugin } from 'tiptap'
|
||||
import { wrappingInputRule, wrapIn } from 'tiptap-commands'
|
||||
|
||||
export default class BlockquoteNode extends Node {
|
||||
|
||||
|
||||
126
packages/tiptap-extensions/src/nodes/CodeBlockHighlight.js
Normal file
126
packages/tiptap-extensions/src/nodes/CodeBlockHighlight.js
Normal file
@@ -0,0 +1,126 @@
|
||||
import { Node, Plugin } from 'tiptap'
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view'
|
||||
import { toggleBlockType, setBlockType, textblockTypeInputRule } from 'tiptap-commands'
|
||||
import { findBlockNodes } from 'prosemirror-utils'
|
||||
import low from 'lowlight'
|
||||
|
||||
function getDecorations(doc) {
|
||||
const decorations = []
|
||||
|
||||
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), [],
|
||||
)
|
||||
|
||||
function parseNodes(nodes, className = []) {
|
||||
return nodes.map(node => {
|
||||
|
||||
const classes = [
|
||||
...className,
|
||||
...node.properties ? node.properties.className : [],
|
||||
]
|
||||
|
||||
if (node.children) {
|
||||
return parseNodes(node.children, classes)
|
||||
}
|
||||
|
||||
return {
|
||||
text: node.value,
|
||||
classes,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
startPos = to
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
export default class CodeBlockHighlightNode extends Node {
|
||||
|
||||
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]],
|
||||
}
|
||||
}
|
||||
|
||||
command({ type, schema }) {
|
||||
return toggleBlockType(type, schema.nodes.paragraph)
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Shift-Ctrl-\\': setBlockType(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)
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Node } from 'tiptap'
|
||||
import { splitListItem, liftListItem } from 'tiptap-commands'
|
||||
import { splitToDefaultListItem, liftListItem } from 'tiptap-commands'
|
||||
|
||||
export default class TodoItemNode extends Node {
|
||||
|
||||
@@ -58,7 +58,7 @@ export default class TodoItemNode extends Node {
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
Enter: splitListItem(type),
|
||||
Enter: splitToDefaultListItem(type),
|
||||
'Shift-Tab': liftListItem(type),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tiptap",
|
||||
"version": "0.8.0",
|
||||
"version": "0.10.0",
|
||||
"description": "A rich-text editor for Vue.js",
|
||||
"homepage": "https://tiptap.scrumpy.io",
|
||||
"license": "MIT",
|
||||
@@ -31,7 +31,7 @@
|
||||
"prosemirror-model": "^1.5.0",
|
||||
"prosemirror-state": "^1.2.1",
|
||||
"prosemirror-view": "^1.4.3",
|
||||
"tiptap-commands": "^0.2.4",
|
||||
"tiptap-commands": "^0.3.0",
|
||||
"tiptap-utils": "^0.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,10 @@ export default {
|
||||
this.view = this.createView()
|
||||
this.commands = this.createCommands()
|
||||
this.updateMenuActions()
|
||||
this.$emit('init', {
|
||||
view: this.view,
|
||||
state: this.state,
|
||||
})
|
||||
},
|
||||
|
||||
createSchema() {
|
||||
@@ -202,16 +206,21 @@ export default {
|
||||
dispatchTransaction(transaction) {
|
||||
this.state = this.state.apply(transaction)
|
||||
this.view.updateState(this.state)
|
||||
this.updateMenuActions()
|
||||
|
||||
if (!transaction.docChanged) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$emit('update', {
|
||||
getHTML: this.getHTML,
|
||||
getJSON: this.getJSON,
|
||||
state: this.state,
|
||||
})
|
||||
this.updateMenuActions()
|
||||
},
|
||||
|
||||
getHTML() {
|
||||
return this.contentNode.elm.innerHTML
|
||||
return this.view.dom.innerHTML
|
||||
},
|
||||
|
||||
getJSON() {
|
||||
|
||||
@@ -61,15 +61,6 @@ export default class ExtensionManager {
|
||||
...extensionKeymaps,
|
||||
...nodeMarkKeymaps,
|
||||
].map(keys => keymap(keys))
|
||||
|
||||
// return 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,
|
||||
// }))
|
||||
// .map(keys => keymap(keys))
|
||||
}
|
||||
|
||||
inputRules({ schema }) {
|
||||
|
||||
@@ -8,19 +8,13 @@ export default function ({ schema, state, commands }) {
|
||||
const command = commands[name] ? commands[name] : () => {}
|
||||
return { name, active, command }
|
||||
})
|
||||
.reduce((actions, { name, active, command }) => Object.assign({}, actions, {
|
||||
.reduce((actions, { name, active, command }) => ({
|
||||
...actions,
|
||||
[name]: {
|
||||
active,
|
||||
command,
|
||||
},
|
||||
}), {})
|
||||
// .reduce((actions, { name, active, command }) => ({
|
||||
// ...actions,
|
||||
// [name]: {
|
||||
// active,
|
||||
// command,
|
||||
// },
|
||||
// }), {})
|
||||
|
||||
const marks = Object.entries(schema.marks)
|
||||
.map(([name]) => {
|
||||
@@ -34,21 +28,14 @@ export default function ({ schema, state, commands }) {
|
||||
command,
|
||||
}
|
||||
})
|
||||
.reduce((actions, { name, active, attrs, command }) => Object.assign({}, actions, {
|
||||
.reduce((actions, { name, active, attrs, command }) => ({
|
||||
...actions,
|
||||
[name]: {
|
||||
active,
|
||||
attrs,
|
||||
command,
|
||||
},
|
||||
}), {})
|
||||
// .reduce((actions, { name, active, attrs, command }) => ({
|
||||
// ...actions,
|
||||
// [name]: {
|
||||
// active,
|
||||
// attrs,
|
||||
// command,
|
||||
// },
|
||||
// }), {})
|
||||
|
||||
return {
|
||||
nodes,
|
||||
|
||||
@@ -2,9 +2,11 @@ import ComponentView from './ComponentView'
|
||||
|
||||
export default function initNodeViews({ nodes, editable }) {
|
||||
const nodeViews = {}
|
||||
|
||||
Object.keys(nodes).forEach(nodeName => {
|
||||
nodeViews[nodeName] = (node, view, getPos, decorations) => {
|
||||
const component = nodes[nodeName]
|
||||
|
||||
return new ComponentView(component, {
|
||||
node,
|
||||
view,
|
||||
@@ -14,5 +16,6 @@ export default function initNodeViews({ nodes, editable }) {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return nodeViews
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ class Toolbar {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Otherwise, reposition it and update its content
|
||||
this.show()
|
||||
const { from, to } = state.selection
|
||||
@@ -54,6 +53,7 @@ class Toolbar {
|
||||
if (event && event.relatedTarget) {
|
||||
return
|
||||
}
|
||||
|
||||
this.element.style.visibility = 'hidden'
|
||||
this.element.style.opacity = 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user