tabs to spaces whitespace

This commit is contained in:
Philipp Kühn
2018-11-08 22:03:10 +01:00
parent b8b82220ba
commit f04a6be6c1
114 changed files with 4214 additions and 4214 deletions

View File

@@ -4,4 +4,4 @@ This is a collection of extensions for [tiptap](https://www.npmjs.com/package/ti
[![](https://img.shields.io/npm/v/tiptap-extensions.svg?label=version)](https://www.npmjs.com/package/tiptap-extensions)
[![](https://img.shields.io/npm/dm/tiptap-extensions.svg)](https://npmcharts.com/compare/tiptap-extensions?minimal=true)
[![](https://img.shields.io/npm/l/tiptap-extensions.svg)](https://www.npmjs.com/package/tiptap-extensions)
[![](http://img.badgesize.io/https://unpkg.com/tiptap-extensions/dist/extensions.min.js?compression=gzip&label=size&colorB=000000)](https://www.npmjs.com/package/tiptap-extensions)
[![](http://img.badgesize.io/https://unpkg.com/tiptap-extensions/dist/extensions.min.js?compression=gzip&label=size&colorB=000000)](https://www.npmjs.com/package/tiptap-extensions)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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