Merge branch 'main' into feature/new-highlight-extension

# Conflicts:
#	docs/src/docPages/api/extensions.md
#	docs/src/links.yaml
#	packages/core/src/extensions/toggleMark.ts
This commit is contained in:
Philipp Kühn
2020-11-05 21:27:20 +01:00
245 changed files with 4197 additions and 2609 deletions

View File

@@ -0,0 +1,9 @@
import { Command } from '../Editor'
export default (): Command => ({ view }) => {
const element = view.dom as HTMLElement
element.blur()
return true
}

View File

@@ -0,0 +1,5 @@
import { Command } from '../Editor'
export default (emitUpdate: Boolean = false): Command => ({ commands }) => {
return commands.setContent('', emitUpdate)
}

View File

@@ -0,0 +1,29 @@
import { liftTarget } from 'prosemirror-transform'
import { Command } from '../Editor'
export default (): Command => ({ state, tr, dispatch }) => {
const { selection } = tr
const { from, to } = selection
state.doc.nodesBetween(from, to, (node, pos) => {
if (!node.type.isText) {
const fromPos = tr.doc.resolve(tr.mapping.map(pos + 1))
const toPos = tr.doc.resolve(tr.mapping.map(pos + node.nodeSize - 1))
const nodeRange = fromPos.blockRange(toPos)
if (nodeRange) {
const targetLiftDepth = liftTarget(nodeRange)
if (node.type.isTextblock && dispatch) {
tr.setNodeMarkup(nodeRange.start, state.schema.nodes.paragraph)
}
if ((targetLiftDepth || targetLiftDepth === 0) && dispatch) {
tr.lift(nodeRange, targetLiftDepth)
}
}
}
})
return true
}

View File

@@ -0,0 +1,6 @@
import { deleteSelection } from 'prosemirror-commands'
import { Command } from '../Editor'
export default (): Command => ({ state, dispatch }) => {
return deleteSelection(state, dispatch)
}

View File

@@ -0,0 +1,58 @@
import { TextSelection } from 'prosemirror-state'
import { Editor, Command } from '../Editor'
import minMax from '../utils/minMax'
type Position = 'start' | 'end' | number | boolean | null
interface ResolvedSelection {
from: number,
to: number,
}
function resolveSelection(editor: Editor, position: Position = null): ResolvedSelection {
if (position === null) {
return editor.selection
}
if (position === 'start' || position === true) {
return {
from: 0,
to: 0,
}
}
if (position === 'end') {
const { size } = editor.state.doc.content
return {
from: size,
to: size - 1, // TODO: -1 only for nodes with content
}
}
return {
from: position as number,
to: position as number,
}
}
export default (position: Position = null): Command => ({
editor, view, tr, dispatch,
}) => {
if ((view.hasFocus() && position === null) || position === false) {
return true
}
const { from, to } = resolveSelection(editor, position)
const { doc } = tr
const resolvedFrom = minMax(from, 0, doc.content.size)
const resolvedEnd = minMax(to, 0, doc.content.size)
const selection = TextSelection.create(doc, resolvedFrom, resolvedEnd)
if (dispatch) {
tr.setSelection(selection)
view.focus()
}
return true
}

View File

@@ -0,0 +1,31 @@
import { DOMParser } from 'prosemirror-model'
import { Selection, Transaction } from 'prosemirror-state'
import { ReplaceStep, ReplaceAroundStep } from 'prosemirror-transform'
import elementFromString from '../utils/elementFromString'
import { Command } from '../Editor'
// TODO: move to utils
// https://github.com/ProseMirror/prosemirror-state/blob/master/src/selection.js#L466
function selectionToInsertionEnd(tr: Transaction, startLen: number, bias: number) {
const last = tr.steps.length - 1
if (last < startLen) return
const step = tr.steps[last]
if (!(step instanceof ReplaceStep || step instanceof ReplaceAroundStep)) return
const map = tr.mapping.maps[last]
let end = 0
map.forEach((_from, _to, _newFrom, newTo) => { if (end === 0) end = newTo })
tr.setSelection(Selection.near(tr.doc.resolve(end as unknown as number), bias))
}
export default (value: string): Command => ({ tr, state, dispatch }) => {
const { selection } = tr
const element = elementFromString(value)
const slice = DOMParser.fromSchema(state.schema).parseSlice(element)
if (dispatch) {
tr.insert(selection.anchor, slice.content)
selectionToInsertionEnd(tr, tr.steps.length - 1, -1)
}
return true
}

View File

@@ -0,0 +1,9 @@
import { Command } from '../Editor'
export default (value: string): Command => ({ tr, dispatch }) => {
if (dispatch) {
tr.insertText(value)
}
return true
}

View File

@@ -0,0 +1,10 @@
import { liftListItem } from 'prosemirror-schema-list'
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import getNodeType from '../utils/getNodeType'
export default (typeOrName: string | NodeType): Command => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return liftListItem(type)(state, dispatch)
}

View File

@@ -0,0 +1,26 @@
import { MarkType } from 'prosemirror-model'
import { Command } from '../Editor'
import getMarkType from '../utils/getMarkType'
import getMarkRange from '../utils/getMarkRange'
export default (typeOrName: string | MarkType): Command => ({ tr, state, dispatch }) => {
const { selection } = tr
const type = getMarkType(typeOrName, state.schema)
let { from, to } = selection
const { $from, empty } = selection
if (empty) {
const range = getMarkRange($from, type)
if (range) {
from = range.from
to = range.to
}
}
if (dispatch) {
tr.removeMark(from, to, type)
}
return true
}

View File

@@ -0,0 +1,20 @@
import { Command } from '../Editor'
export default (): Command => ({ tr, state, dispatch }) => {
const { selection } = tr
const { from, to, empty } = selection
if (empty) {
return true
}
if (dispatch) {
Object
.entries(state.schema.marks)
.forEach(([, mark]) => {
tr.removeMark(from, to, mark as any)
})
}
return true
}

View File

@@ -0,0 +1,24 @@
import { Command } from '../Editor'
export default (attributeNames: string[] = []): Command => ({ tr, state, dispatch }) => {
const { selection } = tr
const { from, to } = selection
state.doc.nodesBetween(from, to, (node, pos) => {
if (!node.type.isText) {
attributeNames.forEach(name => {
const attribute = node.type.spec.attrs?.[name]
const defaultValue = attribute?.default
if (attribute && defaultValue !== undefined && dispatch) {
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
[name]: defaultValue,
})
}
})
}
})
return true
}

View File

@@ -0,0 +1,9 @@
import { Command } from '../Editor'
export default (): Command => ({ tr, dispatch }) => {
if (dispatch) {
tr.scrollIntoView()
}
return true
}

View File

@@ -0,0 +1,6 @@
import { selectAll } from 'prosemirror-commands'
import { Command } from '../Editor'
export default (): Command => ({ state, dispatch }) => {
return selectAll(state, dispatch)
}

View File

@@ -0,0 +1,6 @@
import { selectParentNode } from 'prosemirror-commands'
import { Command } from '../Editor'
export default (): Command => ({ state, dispatch }) => {
return selectParentNode(state, dispatch)
}

View File

@@ -0,0 +1,10 @@
import { NodeType } from 'prosemirror-model'
import { setBlockType } from 'prosemirror-commands'
import { Command } from '../Editor'
import getNodeType from '../utils/getNodeType'
export default (typeOrName: string | NodeType, attrs = {}): Command => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return setBlockType(type, attrs)(state, dispatch)
}

View File

@@ -0,0 +1,17 @@
import { TextSelection } from 'prosemirror-state'
import { Command } from '../Editor'
export default (content: string, emitUpdate: Boolean = false, parseOptions = {}): Command => ({ tr, editor, dispatch }) => {
const { createDocument } = editor
const { doc } = tr
const document = createDocument(content, parseOptions)
const selection = TextSelection.create(doc, 0, doc.content.size)
if (dispatch) {
tr.setSelection(selection)
.replaceSelectionWith(document, false)
.setMeta('preventUpdate', !emitUpdate)
}
return true
}

View File

@@ -0,0 +1,17 @@
import { Command } from '../Editor'
export default (attributes: {}): Command => ({ tr, state, dispatch }) => {
const { selection } = tr
const { from, to } = selection
state.doc.nodesBetween(from, to, (node, pos) => {
if (!node.type.isText && dispatch) {
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
...attributes,
})
}
})
return true
}

View File

@@ -0,0 +1,10 @@
import { sinkListItem as originalSinkListItem } from 'prosemirror-schema-list'
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import getNodeType from '../utils/getNodeType'
export default (typeOrName: string | NodeType): Command => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return originalSinkListItem(type)(state, dispatch)
}

View File

@@ -0,0 +1,117 @@
import { canSplit } from 'prosemirror-transform'
import { ContentMatch, Fragment } from 'prosemirror-model'
import { EditorState, NodeSelection, TextSelection } from 'prosemirror-state'
import { Command } from '../Editor'
function defaultBlockAt(match: ContentMatch) {
for (let i = 0; i < match.edgeCount; i + 1) {
const { type } = match.edge(i)
// @ts-ignore
if (type.isTextblock && !type.hasRequiredAttrs()) return type
}
return null
}
export interface SplitBlockOptions {
withAttributes: boolean,
withMarks: boolean,
}
function keepMarks(state: EditorState) {
const marks = state.storedMarks
|| (state.selection.$to.parentOffset && state.selection.$from.marks())
if (marks) {
state.tr.ensureMarks(marks)
}
}
export default (options: Partial<SplitBlockOptions> = {}): Command => ({ tr, state, dispatch }) => {
const defaultOptions: SplitBlockOptions = {
withAttributes: false,
withMarks: true,
}
const config = { ...defaultOptions, ...options }
const { selection, doc } = tr
const { $from, $to } = selection
if (selection instanceof NodeSelection && selection.node.isBlock) {
if (!$from.parentOffset || !canSplit(doc, $from.pos)) {
return false
}
if (dispatch) {
if (config.withMarks) {
keepMarks(state)
}
tr.split($from.pos).scrollIntoView()
}
return true
}
if (!$from.parent.isBlock) {
return false
}
if (dispatch) {
const atEnd = $to.parentOffset === $to.parent.content.size
if (selection instanceof TextSelection) {
tr.deleteSelection()
}
const deflt = $from.depth === 0
? undefined
: defaultBlockAt($from.node(-1).contentMatchAt($from.indexAfter(-1)))
let types = atEnd && deflt
? [{
type: deflt,
attrs: config.withAttributes
? $from.node().attrs
: {},
}]
: undefined
let can = canSplit(tr.doc, tr.mapping.map($from.pos), 1, types)
if (
!types
&& !can
&& canSplit(tr.doc, tr.mapping.map($from.pos), 1, deflt ? [{ type: deflt }] : undefined)
) {
can = true
types = deflt
? [{
type: deflt,
attrs: config.withAttributes
? $from.node().attrs
: {},
}]
: undefined
}
if (can) {
tr.split(tr.mapping.map($from.pos), 1, types)
if (
!atEnd
&& !$from.parentOffset
&& $from.parent.type !== deflt
&& $from.node(-1).canReplace($from.index(-1), $from.indexAfter(-1), Fragment.from(deflt?.create()))
) {
tr.setNodeMarkup(tr.mapping.map($from.before()), deflt || undefined)
}
}
if (config.withMarks) {
keepMarks(state)
}
tr.scrollIntoView()
}
return true
}

View File

@@ -0,0 +1,10 @@
import { splitListItem } from 'prosemirror-schema-list'
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import getNodeType from '../utils/getNodeType'
export default (typeOrName: string | NodeType): Command => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return splitListItem(type)(state, dispatch)
}

View File

@@ -0,0 +1,16 @@
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import nodeIsActive from '../utils/nodeIsActive'
import getNodeType from '../utils/getNodeType'
export default (typeOrName: string | NodeType, toggleTypeOrName: string | NodeType, attrs = {}): Command => ({ state, commands }) => {
const type = getNodeType(typeOrName, state.schema)
const toggleType = getNodeType(toggleTypeOrName, state.schema)
const isActive = nodeIsActive(state, type, attrs)
if (isActive) {
return commands.setBlockType(toggleType)
}
return commands.setBlockType(type, attrs)
}

View File

@@ -0,0 +1,52 @@
import { findParentNode } from 'prosemirror-utils'
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import getNodeType from '../utils/getNodeType'
import isList from '../utils/isList'
export default (listTypeOrName: string | NodeType, itemTypeOrName: string | NodeType): Command => ({
editor, tr, state, dispatch, chain, commands, can,
}) => {
const { extensions } = editor.options
const listType = getNodeType(listTypeOrName, state.schema)
const itemType = getNodeType(itemTypeOrName, state.schema)
const { selection } = state
const { $from, $to } = selection
const range = $from.blockRange($to)
if (!range) {
return false
}
const parentList = findParentNode(node => isList(node.type.name, extensions))(selection)
if (range.depth >= 1 && parentList && range.depth - parentList.depth <= 1) {
// remove list
if (parentList.node.type === listType) {
return commands.liftListItem(itemType)
}
// change list type
if (
isList(parentList.node.type.name, extensions)
&& listType.validContent(parentList.node.content)
&& dispatch
) {
tr.setNodeMarkup(parentList.pos, listType)
return true
}
}
const canWrapInList = can().wrapInList(listType)
// try to convert node to paragraph if needed
if (!canWrapInList) {
return chain()
.clearNodes()
.wrapInList(listType)
.run()
}
return commands.wrapInList(listType)
}

View File

@@ -0,0 +1,19 @@
import { toggleMark } from 'prosemirror-commands'
import { MarkType } from 'prosemirror-model'
import { Command } from '../Editor'
import getMarkType from '../utils/getMarkType'
import markIsActive from '../utils/markIsActive'
export default (typeOrName: string | MarkType, attrs?: {}): Command => ({ state, dispatch, commands }) => {
const type = getMarkType(typeOrName, state.schema)
const hasMarkWithDifferentAttributes = attrs
&& markIsActive(state, type)
&& !markIsActive(state, type, attrs)
if (attrs && hasMarkWithDifferentAttributes) {
return commands.updateMark(type, attrs)
}
return toggleMark(type)(state, dispatch)
}

View File

@@ -0,0 +1,16 @@
import { wrapIn, lift } from 'prosemirror-commands'
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import nodeIsActive from '../utils/nodeIsActive'
import getNodeType from '../utils/getNodeType'
export default (typeOrName: string | NodeType, attrs = {}): Command => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
const isActive = nodeIsActive(state, type, attrs)
if (isActive) {
return lift(state, dispatch)
}
return wrapIn(type, attrs)(state, dispatch)
}

View File

@@ -0,0 +1,15 @@
import { Command } from '../Editor'
export default (commands: Command[] | ((props: Parameters<Command>[0]) => Command[])): Command => props => {
const items = typeof commands === 'function'
? commands(props)
: commands
for (let i = 0; i < items.length; i += 1) {
if (items[i](props)) {
return true
}
}
return false
}

View File

@@ -0,0 +1,32 @@
import { MarkType } from 'prosemirror-model'
import { Command } from '../Editor'
import getMarkType from '../utils/getMarkType'
import getMarkRange from '../utils/getMarkRange'
export default (typeOrName: string | MarkType, attrs: {}): Command => ({ tr, state, dispatch }) => {
const { selection, doc } = tr
let { from, to } = selection
const { $from, empty } = selection
const type = getMarkType(typeOrName, state.schema)
if (empty) {
const range = getMarkRange($from, type)
if (range) {
from = range.from
to = range.to
}
}
const hasMark = doc.rangeHasMark(from, to, type)
if (hasMark && dispatch) {
tr.removeMark(from, to, type)
}
if (dispatch) {
tr.addMark(from, to, type.create(attrs))
}
return true
}

View File

@@ -0,0 +1,10 @@
import { wrapInList } from 'prosemirror-schema-list'
import { NodeType } from 'prosemirror-model'
import { Command } from '../Editor'
import getNodeType from '../utils/getNodeType'
export default (typeOrName: string | NodeType, attrs?: {}): Command => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return wrapInList(type, attrs)(state, dispatch)
}