Merge branch 'master' into feature/suggestions

# Conflicts:
#	packages/tiptap-extensions/package.json
This commit is contained in:
Philipp Kühn
2018-09-04 23:12:00 +02:00
24 changed files with 1045 additions and 652 deletions

View File

@@ -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",

View File

@@ -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
}
}

View File

@@ -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,

View File

@@ -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"
}
}

View File

@@ -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'

View File

@@ -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 {

View 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)
},
},
}),
]
}
}

View File

@@ -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),
}
}

View File

@@ -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"
}
}

View File

@@ -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() {

View File

@@ -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 }) {

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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
}