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

@@ -59,6 +59,7 @@ export default {
| `editable` | `Boolean` | `true` | When set to `false` the editor is read-only. |
| `doc` | `Object` | `null` | The editor state object used by Prosemirror. You can also pass HTML to the `content` slot. When used both, the `content` slot will be ignored. |
| `extensions` | `Array` | `[]` | A list of extensions used, by the editor. This can be `Nodes`, `Marks` or `Plugins`. |
| `@init` | `Object` | `undefined` | This will return an Object with the current `state` and `view` of Prosemirror on init. |
| `@update` | `Object` | `undefined` | This will return an Object with the current `state` of Prosemirror, a `getJSON()` and `getHTML()` function on every change. |
## Scoped Slots

View File

@@ -4,6 +4,7 @@ import webpack from 'webpack'
import httpProxyMiddleware from 'http-proxy-middleware'
import webpackDevMiddleware from 'webpack-dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware'
import historyApiFallbackMiddleware from 'connect-history-api-fallback'
import config from './webpack.config'
import { sassImport } from './utilities'
import { srcPath, sassImportPath } from './paths'
@@ -11,6 +12,8 @@ import { srcPath, sassImportPath } from './paths'
const bundler = webpack(config)
const middlewares = []
middlewares.push(historyApiFallbackMiddleware())
// add webpack stuff
middlewares.push(webpackDevMiddleware(bundler, {
publicPath: config.output.publicPath,

View File

@@ -37,6 +37,7 @@
background: $color-black;
color: $color-white;
font-size: 0.8rem;
overflow-x: auto;
code {
display: block;

View File

@@ -0,0 +1,28 @@
export const javascript = `function $initHighlight(block, flags) {
try {
if (block.className.search(/\bno\-highlight\b/) != -1)
return processBlock(block, true, 0x0F) + ' class=""';
} catch (e) {
/* handle exception */
}
for (var i = 0 / 2; i < classes.length; i++) { // "0 / 2" should not be parsed as regexp
if (checkCondition(classes[i]) === undefined)
return /\d+/g;
}
}`
export const css = `@font-face {
font-family: Chunkfive; src: url('Chunkfive.otf');
}
body, .usertext {
color: #F0F0F0; background: #600;
font-family: Chunkfive, sans;
}
@import url(print.css);
@media print {
a[href^=http]::after {
content: attr(href)
}
}`

View File

@@ -0,0 +1,139 @@
<template>
<div>
<editor class="editor" :extensions="extensions">
<div class="editor__content" slot="content" slot-scope="props">
<h2>
Code Highlighting
</h2>
<p>
These are code blocks with <strong>automatic syntax highlighting</strong> based on highlight.js.
</p>
<pre><code v-html="javascript"></code></pre>
<pre><code v-html="css"></code></pre>
</div>
</editor>
</div>
</template>
<script>
import Icon from 'Components/Icon'
import { Editor } from 'tiptap'
import {
BlockquoteNode,
BulletListNode,
CodeBlockHighlightNode,
HardBreakNode,
HeadingNode,
ListItemNode,
OrderedListNode,
TodoItemNode,
TodoListNode,
BoldMark,
CodeMark,
ItalicMark,
LinkMark,
HistoryExtension,
} from 'tiptap-extensions'
import { javascript, css } from './examples'
export default {
components: {
Editor,
Icon,
},
data() {
return {
extensions: [
new BlockquoteNode(),
new BulletListNode(),
new CodeBlockHighlightNode(),
new HardBreakNode(),
new HeadingNode({ maxLevel: 3 }),
new ListItemNode(),
new OrderedListNode(),
new TodoItemNode(),
new TodoListNode(),
new BoldMark(),
new CodeMark(),
new ItalicMark(),
new LinkMark(),
new HistoryExtension(),
],
javascript,
css,
}
},
}
</script>
<style lang="scss">
pre {
&::before {
content: attr(data-language);
text-transform: uppercase;
display: block;
text-align: right;
font-weight: bold;
font-size: 0.6rem;
}
code {
.hljs-comment,
.hljs-quote {
color: #999999;
}
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-name,
.hljs-regexp,
.hljs-link,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #f2777a;
}
.hljs-number,
.hljs-meta,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params {
color: #f99157;
}
.hljs-string,
.hljs-symbol,
.hljs-bullet {
color: #99cc99;
}
.hljs-title,
.hljs-section {
color: #ffcc66;
}
.hljs-keyword,
.hljs-selector-tag {
color: #6699cc;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
}
}
</style>

View File

@@ -21,6 +21,9 @@
<router-link class="subnavigation__link" to="/markdown-shortcuts">
Markdown Shortcuts
</router-link>
<router-link class="subnavigation__link" to="/code-highlighting">
Code Highlighting
</router-link>
<router-link class="subnavigation__link" to="/read-only">
Read-Only
</router-link>

View File

@@ -10,6 +10,7 @@ import RouteImages from 'Components/Routes/Images'
import RouteHidingMenuBar from 'Components/Routes/HidingMenuBar'
import RouteTodoList from 'Components/Routes/TodoList'
import RouteMarkdownShortcuts from 'Components/Routes/MarkdownShortcuts'
import RouteCodeHighlighting from 'Components/Routes/CodeHighlighting'
import RouteReadOnly from 'Components/Routes/ReadOnly'
import RouteEmbeds from 'Components/Routes/Embeds'
import RouteMentions from 'Components/Routes/Mentions'
@@ -72,6 +73,13 @@ const routes = [
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/MarkdownShortcuts',
},
},
{
path: '/code-highlighting',
component: RouteCodeHighlighting,
meta: {
githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/CodeHighlighting',
},
},
{
path: '/read-only',
component: RouteReadOnly,
@@ -104,6 +112,7 @@ const routes = [
const router = new VueRouter({
routes,
mode: 'history',
linkActiveClass: 'is-active',
linkExactActiveClass: 'is-exact-active',
})

4
netlify.toml Normal file
View File

@@ -0,0 +1,4 @@
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

View File

@@ -29,36 +29,37 @@
"ie >= 9"
],
"devDependencies": {
"@babel/core": "^7.0.0-rc.2",
"@babel/node": "^7.0.0-rc.2",
"@babel/plugin-syntax-dynamic-import": "^7.0.0-rc.2",
"@babel/plugin-transform-runtime": "^7.0.0-rc.2",
"@babel/polyfill": "^7.0.0-rc.2",
"@babel/preset-env": "^7.0.0-rc.2",
"@babel/preset-stage-2": "^7.0.0-rc.2",
"@babel/runtime": "^7.0.0-rc.2",
"@babel/core": "^7.0.0",
"@babel/node": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-transform-runtime": "^7.0.0",
"@babel/polyfill": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-stage-2": "^7.0.0",
"@babel/runtime": "^7.0.0",
"autoprefixer": "^9.1.3",
"babel-eslint": "^8.2.5",
"babel-loader": "^8.0.0-beta.6",
"browser-sync": "^2.24.5",
"babel-eslint": "^9.0.0",
"babel-loader": "^8.0.2",
"browser-sync": "^2.24.7",
"connect-history-api-fallback": "^1.5.0",
"copy-webpack-plugin": "^4.5.2",
"css-loader": "^1.0.0",
"eslint": "^5.4.0",
"eslint": "^5.5.0",
"eslint-config-airbnb-base": "^13.0.0",
"eslint-plugin-html": "^4.0.5",
"eslint-plugin-import": "^2.13.0",
"eslint-plugin-vue": "4.7.1",
"file-loader": "^2.0.0",
"glob": "^7.1.2",
"glob": "^7.1.3",
"html-webpack-plugin": "^3.2.0",
"http-proxy-middleware": "^0.18.0",
"http-proxy-middleware": "^0.19.0",
"http-server": "^0.11.1",
"imagemin-webpack-plugin": "^2.1.5",
"lerna": "^3.1.4",
"lerna": "^3.2.1",
"mini-css-extract-plugin": "^0.4.2",
"minimist": "^1.2.0",
"node-sass": "^4.9.1",
"optimize-css-assets-webpack-plugin": "^5.0.0",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"ora": "^3.0.0",
"postcss": "^7.0.2",
"postcss-loader": "^3.0.0",
@@ -71,16 +72,16 @@
"rollup-plugin-replace": "^2.0.0",
"rollup-plugin-vue": "^4.3.2",
"sass-loader": "^7.0.3",
"style-loader": "^0.22.1",
"uglify-js": "^3.4.7",
"style-loader": "^0.23.0",
"uglify-js": "^3.4.9",
"vue": "^2.5.17",
"vue-loader": "^15.4.0",
"vue-loader": "^15.4.1",
"vue-router": "^3.0.1",
"vue-style-loader": "^4.1.0",
"vue-template-compiler": "^2.5.17",
"webpack": "^4.17.1",
"webpack": "^4.17.2",
"webpack-dev-middleware": "^3.1.3",
"webpack-hot-middleware": "^2.22.2",
"webpack-hot-middleware": "^2.23.1",
"webpack-manifest-plugin": "^2.0.3",
"webpack-svgstore-plugin": "^4.0.3",
"zlib": "^1.0.5"

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
}

1195
yarn.lock

File diff suppressed because it is too large Load Diff