| import type { Node, NodeType, ResolvedPos, Mark, MarkType, Schema } from 'prosemirror-model' | |
| import type { EditorState, Selection } from 'prosemirror-state' | |
| import type { EditorView } from 'prosemirror-view' | |
| import { selectAll } from 'prosemirror-commands' | |
| export const isList = (node: Node, schema: Schema) => { | |
| return ( | |
| node.type === schema.nodes.bullet_list || | |
| node.type === schema.nodes.ordered_list | |
| ) | |
| } | |
| export const autoSelectAll = (view: EditorView) => { | |
| const { empty } = view.state.selection | |
| if (empty) selectAll(view.state, view.dispatch) | |
| } | |
| export const addMark = (editorView: EditorView, mark: Mark, selection?: { from: number; to: number; }) => { | |
| if (selection) { | |
| editorView.dispatch(editorView.state.tr.addMark(selection.from, selection.to, mark)) | |
| } | |
| else { | |
| const { $from, $to } = editorView.state.selection | |
| editorView.dispatch(editorView.state.tr.addMark($from.pos, $to.pos, mark)) | |
| } | |
| } | |
| export const findNodesWithSameMark = (doc: Node, from: number, to: number, markType: MarkType) => { | |
| let ii = from | |
| const finder = (mark: Mark) => mark.type === markType | |
| let firstMark = null | |
| let fromNode = null | |
| let toNode = null | |
| while (ii <= to) { | |
| const node = doc.nodeAt(ii) | |
| if (!node || !node.marks) return null | |
| const mark = node.marks.find(finder) | |
| if (!mark) return null | |
| if (firstMark && mark !== firstMark) return null | |
| fromNode = fromNode || node | |
| firstMark = firstMark || mark | |
| toNode = node | |
| ii++ | |
| } | |
| let fromPos = from | |
| let toPos = to | |
| let jj = 0 | |
| ii = from - 1 | |
| while (ii > jj) { | |
| const node = doc.nodeAt(ii) | |
| const mark = node && node.marks.find(finder) | |
| if (!mark || mark !== firstMark) break | |
| fromPos = ii | |
| fromNode = node | |
| ii-- | |
| } | |
| ii = to + 1 | |
| jj = doc.nodeSize - 2 | |
| while (ii < jj) { | |
| const node = doc.nodeAt(ii) | |
| const mark = node && node.marks.find(finder) | |
| if (!mark || mark !== firstMark) break | |
| toPos = ii | |
| toNode = node | |
| ii++ | |
| } | |
| return { | |
| mark: firstMark, | |
| from: { | |
| node: fromNode, | |
| pos: fromPos, | |
| }, | |
| to: { | |
| node: toNode, | |
| pos: toPos, | |
| }, | |
| } | |
| } | |
| const equalNodeType = (nodeType: NodeType, node: Node) => { | |
| return Array.isArray(nodeType) && nodeType.indexOf(node.type) > -1 || node.type === nodeType | |
| } | |
| const findParentNodeClosestToPos = ($pos: ResolvedPos, predicate: (node: Node) => boolean) => { | |
| for (let i = $pos.depth; i > 0; i--) { | |
| const node = $pos.node(i) | |
| if (predicate(node)) { | |
| return { | |
| pos: i > 0 ? $pos.before(i) : 0, | |
| start: $pos.start(i), | |
| depth: i, | |
| node, | |
| } | |
| } | |
| } | |
| } | |
| export const findParentNode = (predicate: (node: Node) => boolean) => { | |
| return (_ref: Selection) => findParentNodeClosestToPos(_ref.$from, predicate) | |
| } | |
| export const findParentNodeOfType = (nodeType: NodeType) => { | |
| return (selection: Selection) => { | |
| return findParentNode((node: Node) => { | |
| return equalNodeType(nodeType, node) | |
| })(selection) | |
| } | |
| } | |
| export const isActiveOfParentNodeType = (nodeType: string, state: EditorState) => { | |
| const node = state.schema.nodes[nodeType] | |
| return !!findParentNodeOfType(node)(state.selection) | |
| } | |
| export const getLastTextNode = (node: Node | null): Node | null => { | |
| if (!node) return null | |
| if (node.type.name === 'text') return node | |
| if (!node.lastChild) return null | |
| return getLastTextNode(node.lastChild) | |
| } | |
| export const getMarkAttrs = (view: EditorView) => { | |
| const { selection, doc } = view.state | |
| const { from } = selection | |
| let node = doc.nodeAt(from) || doc.nodeAt(from - 1) | |
| node = getLastTextNode(node) | |
| return node?.marks || [] | |
| } | |
| export const getAttrValue = (marks: readonly Mark[], markType: string, attr: string): string | null => { | |
| for (const mark of marks) { | |
| if (mark.type.name === markType && mark.attrs[attr]) return mark.attrs[attr] | |
| } | |
| return null | |
| } | |
| export const isActiveMark = (marks: readonly Mark[], markType: string) => { | |
| for (const mark of marks) { | |
| if (mark.type.name === markType) return true | |
| } | |
| return false | |
| } | |
| export const markActive = (state: EditorState, type: MarkType) => { | |
| const { from, $from, to, empty } = state.selection | |
| if (empty) return type.isInSet(state.storedMarks || $from.marks()) | |
| return state.doc.rangeHasMark(from, to, type) | |
| } | |
| export const getAttrValueInSelection = (view: EditorView, attr: string) => { | |
| const { selection, doc } = view.state | |
| const { from, to } = selection | |
| let keepChecking = true | |
| let value = '' | |
| doc.nodesBetween(from, to, node => { | |
| if (keepChecking && node.attrs[attr]) { | |
| keepChecking = false | |
| value = node.attrs[attr] | |
| } | |
| return keepChecking | |
| }) | |
| return value | |
| } | |
| type Align = 'left' | 'right' | 'center' | |
| interface DefaultAttrs { | |
| color: string | |
| backcolor: string | |
| fontsize: string | |
| fontname: string | |
| align: Align | |
| } | |
| const _defaultAttrs: DefaultAttrs = { | |
| color: '#000000', | |
| backcolor: '', | |
| fontsize: '16px', | |
| fontname: '', | |
| align: 'left', | |
| } | |
| export const getTextAttrs = (view: EditorView, attrs: Partial<DefaultAttrs> = {}) => { | |
| const defaultAttrs: DefaultAttrs = { ..._defaultAttrs, ...attrs } | |
| const marks = getMarkAttrs(view) | |
| const isBold = isActiveMark(marks, 'strong') | |
| const isEm = isActiveMark(marks, 'em') | |
| const isUnderline = isActiveMark(marks, 'underline') | |
| const isStrikethrough = isActiveMark(marks, 'strikethrough') | |
| const isSuperscript = isActiveMark(marks, 'superscript') | |
| const isSubscript = isActiveMark(marks, 'subscript') | |
| const isCode = isActiveMark(marks, 'code') | |
| const color = getAttrValue(marks, 'forecolor', 'color') || defaultAttrs.color | |
| const backcolor = getAttrValue(marks, 'backcolor', 'backcolor') || defaultAttrs.backcolor | |
| const fontsize = getAttrValue(marks, 'fontsize', 'fontsize') || defaultAttrs.fontsize | |
| const fontname = getAttrValue(marks, 'fontname', 'fontname') || defaultAttrs.fontname | |
| const link = getAttrValue(marks, 'link', 'href') || '' | |
| const align = (getAttrValueInSelection(view, 'align') || defaultAttrs.align) as Align | |
| const isBulletList = isActiveOfParentNodeType('bullet_list', view.state) | |
| const isOrderedList = isActiveOfParentNodeType('ordered_list', view.state) | |
| const isBlockquote = isActiveOfParentNodeType('blockquote', view.state) | |
| return { | |
| bold: isBold, | |
| em: isEm, | |
| underline: isUnderline, | |
| strikethrough: isStrikethrough, | |
| superscript: isSuperscript, | |
| subscript: isSubscript, | |
| code: isCode, | |
| color: color, | |
| backcolor: backcolor, | |
| fontsize: fontsize, | |
| fontname: fontname, | |
| link: link, | |
| align: align, | |
| bulletList: isBulletList, | |
| orderedList: isOrderedList, | |
| blockquote: isBlockquote, | |
| } | |
| } | |
| export type TextAttrs = ReturnType<typeof getTextAttrs> | |
| export const getFontsize = (view: EditorView) => { | |
| const marks = getMarkAttrs(view) | |
| const fontsize = getAttrValue(marks, 'fontsize', 'fontsize') || _defaultAttrs.fontsize | |
| return parseInt(fontsize) | |
| } | |
| export const defaultRichTextAttrs: TextAttrs = { | |
| bold: false, | |
| em: false, | |
| underline: false, | |
| strikethrough: false, | |
| superscript: false, | |
| subscript: false, | |
| code: false, | |
| color: '#000000', | |
| backcolor: '', | |
| fontsize: '16px', | |
| fontname: '', | |
| link: '', | |
| align: 'left', | |
| bulletList: false, | |
| orderedList: false, | |
| blockquote: false, | |
| } |