mirror of https://github.com/docusealco/docuseal
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
769 lines
17 KiB
769 lines
17 KiB
import { Editor, Extension, Node, Mark } from '@tiptap/core'
|
|
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
|
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
|
import Document from '@tiptap/extension-document'
|
|
import Text from '@tiptap/extension-text'
|
|
import HardBreak from '@tiptap/extension-hard-break'
|
|
import History from '@tiptap/extension-history'
|
|
import Gapcursor from '@tiptap/extension-gapcursor'
|
|
import Dropcursor from '@tiptap/extension-dropcursor'
|
|
import { createApp, reactive } from 'vue'
|
|
import DynamicArea from './dynamic_area.vue'
|
|
import styles from './dynamic_styles.scss'
|
|
|
|
export const dynamicStylesheet = new CSSStyleSheet()
|
|
|
|
dynamicStylesheet.replaceSync(styles[0][1])
|
|
|
|
export const tiptapStylesheet = new CSSStyleSheet()
|
|
|
|
tiptapStylesheet.replaceSync(
|
|
`.ProseMirror {
|
|
position: relative;
|
|
}
|
|
|
|
.ProseMirror {
|
|
word-wrap: break-word;
|
|
white-space: pre-wrap;
|
|
white-space: break-spaces;
|
|
-webkit-font-variant-ligatures: none;
|
|
font-variant-ligatures: none;
|
|
font-feature-settings: "liga" 0;
|
|
}
|
|
|
|
.ProseMirror [contenteditable="false"] {
|
|
white-space: normal;
|
|
}
|
|
|
|
.ProseMirror [contenteditable="false"] [contenteditable="true"] {
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
.ProseMirror pre {
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
img.ProseMirror-separator {
|
|
display: inline !important;
|
|
border: none !important;
|
|
margin: 0 !important;
|
|
width: 0 !important;
|
|
height: 0 !important;
|
|
}
|
|
|
|
.ProseMirror-gapcursor {
|
|
display: none;
|
|
pointer-events: none;
|
|
position: absolute;
|
|
margin: 0;
|
|
}
|
|
|
|
.ProseMirror-gapcursor:after {
|
|
content: "";
|
|
display: block;
|
|
position: absolute;
|
|
top: -2px;
|
|
width: 20px;
|
|
border-top: 1px solid black;
|
|
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
|
|
}
|
|
|
|
@keyframes ProseMirror-cursor-blink {
|
|
to {
|
|
visibility: hidden;
|
|
}
|
|
}
|
|
|
|
.ProseMirror-hideselection *::selection {
|
|
background: transparent;
|
|
}
|
|
|
|
.ProseMirror-hideselection *::-moz-selection {
|
|
background: transparent;
|
|
}
|
|
|
|
.ProseMirror-hideselection * {
|
|
caret-color: transparent;
|
|
}
|
|
|
|
.ProseMirror-focused .ProseMirror-gapcursor {
|
|
display: block;
|
|
}
|
|
dynamic-variable {
|
|
background-color: #fef3c7;
|
|
}`)
|
|
|
|
function collectDomAttrs (dom) {
|
|
const attrs = {}
|
|
|
|
for (let i = 0; i < dom.attributes.length; i++) {
|
|
attrs[dom.attributes[i].name] = dom.attributes[i].value
|
|
}
|
|
|
|
return { htmlAttrs: attrs }
|
|
}
|
|
|
|
function collectSpanDomAttrs (dom) {
|
|
const result = collectDomAttrs(dom)
|
|
|
|
if (result.htmlAttrs.style) {
|
|
const temp = document.createElement('span')
|
|
|
|
temp.style.cssText = result.htmlAttrs.style
|
|
|
|
if (['bold', '700'].includes(temp.style.fontWeight)) {
|
|
temp.style.removeProperty('font-weight')
|
|
}
|
|
|
|
if (temp.style.fontStyle === 'italic') {
|
|
temp.style.removeProperty('font-style')
|
|
}
|
|
|
|
if (temp.style.textDecoration === 'underline') {
|
|
temp.style.removeProperty('text-decoration')
|
|
}
|
|
|
|
if (temp.style.cssText) {
|
|
result.htmlAttrs.style = temp.style.cssText
|
|
} else {
|
|
delete result.htmlAttrs.style
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
function createBlockNode (name, tag, content) {
|
|
return Node.create({
|
|
name,
|
|
group: 'block',
|
|
content: content || 'block+',
|
|
addAttributes () {
|
|
return {
|
|
htmlAttrs: { default: {} }
|
|
}
|
|
},
|
|
parseHTML () {
|
|
return [{ tag, getAttrs: collectDomAttrs }]
|
|
},
|
|
renderHTML ({ node }) {
|
|
return [tag, node.attrs.htmlAttrs, 0]
|
|
}
|
|
})
|
|
}
|
|
|
|
const CustomParagraph = Node.create({
|
|
name: 'paragraph',
|
|
group: 'block',
|
|
content: 'inline*',
|
|
addAttributes () {
|
|
return {
|
|
htmlAttrs: { default: {} }
|
|
}
|
|
},
|
|
parseHTML () {
|
|
return [{ tag: 'p', getAttrs: collectDomAttrs }]
|
|
},
|
|
renderHTML ({ node }) {
|
|
return ['p', node.attrs.htmlAttrs, 0]
|
|
}
|
|
})
|
|
|
|
const CustomHeading = Node.create({
|
|
name: 'heading',
|
|
group: 'block',
|
|
content: 'inline*',
|
|
addAttributes () {
|
|
return {
|
|
htmlAttrs: { default: {} },
|
|
level: { default: 1 }
|
|
}
|
|
},
|
|
parseHTML () {
|
|
return [1, 2, 3, 4, 5, 6].map((level) => ({
|
|
tag: `h${level}`,
|
|
getAttrs: (dom) => ({ ...collectDomAttrs(dom), level })
|
|
}))
|
|
},
|
|
renderHTML ({ node }) {
|
|
return [`h${node.attrs.level}`, node.attrs.htmlAttrs, 0]
|
|
}
|
|
})
|
|
|
|
const SectionNode = createBlockNode('section', 'section')
|
|
const ArticleNode = createBlockNode('article', 'article')
|
|
const DivNode = createBlockNode('div', 'div')
|
|
const BlockquoteNode = createBlockNode('blockquote', 'blockquote')
|
|
const PreNode = createBlockNode('pre', 'pre')
|
|
const OrderedListNode = createBlockNode('orderedList', 'ol', '(listItem | block)+')
|
|
const BulletListNode = createBlockNode('bulletList', 'ul', '(listItem | block)+')
|
|
|
|
const ListItemNode = Node.create({
|
|
name: 'listItem',
|
|
content: 'block+',
|
|
addAttributes () {
|
|
return {
|
|
htmlAttrs: { default: {} }
|
|
}
|
|
},
|
|
parseHTML () {
|
|
return [{ tag: 'li', getAttrs: collectDomAttrs }]
|
|
},
|
|
renderHTML ({ node }) {
|
|
return ['li', node.attrs.htmlAttrs, 0]
|
|
}
|
|
})
|
|
|
|
const TableNode = Node.create({
|
|
name: 'table',
|
|
group: 'block',
|
|
content: '(colgroup | tableHead | tableBody | tableRow)+',
|
|
addAttributes () {
|
|
return { htmlAttrs: { default: {} } }
|
|
},
|
|
parseHTML () {
|
|
return [{ tag: 'table', getAttrs: collectDomAttrs }]
|
|
},
|
|
renderHTML ({ node }) {
|
|
return ['table', node.attrs.htmlAttrs, 0]
|
|
}
|
|
})
|
|
|
|
const TableHead = Node.create({
|
|
name: 'tableHead',
|
|
content: 'tableRow+',
|
|
addAttributes () {
|
|
return { htmlAttrs: { default: {} } }
|
|
},
|
|
parseHTML () {
|
|
return [{ tag: 'thead', getAttrs: collectDomAttrs }]
|
|
},
|
|
renderHTML ({ node }) {
|
|
return ['thead', node.attrs.htmlAttrs, 0]
|
|
}
|
|
})
|
|
|
|
const TableBody = Node.create({
|
|
name: 'tableBody',
|
|
content: 'tableRow+',
|
|
addAttributes () {
|
|
return { htmlAttrs: { default: {} } }
|
|
},
|
|
parseHTML () {
|
|
return [{ tag: 'tbody', getAttrs: collectDomAttrs }]
|
|
},
|
|
renderHTML ({ node }) {
|
|
return ['tbody', node.attrs.htmlAttrs, 0]
|
|
}
|
|
})
|
|
|
|
const TableRow = Node.create({
|
|
name: 'tableRow',
|
|
content: '(tableCell | tableHeader)+',
|
|
addAttributes () {
|
|
return { htmlAttrs: { default: {} } }
|
|
},
|
|
parseHTML () {
|
|
return [{ tag: 'tr', getAttrs: collectDomAttrs }]
|
|
},
|
|
renderHTML ({ node }) {
|
|
return ['tr', node.attrs.htmlAttrs, 0]
|
|
}
|
|
})
|
|
|
|
const TableCell = Node.create({
|
|
name: 'tableCell',
|
|
content: 'block*',
|
|
addAttributes () {
|
|
return { htmlAttrs: { default: {} } }
|
|
},
|
|
parseHTML () {
|
|
return [{ tag: 'td', getAttrs: collectDomAttrs }]
|
|
},
|
|
renderHTML ({ node }) {
|
|
return ['td', node.attrs.htmlAttrs, 0]
|
|
}
|
|
})
|
|
|
|
const TableHeader = Node.create({
|
|
name: 'tableHeader',
|
|
content: 'block*',
|
|
addAttributes () {
|
|
return { htmlAttrs: { default: {} } }
|
|
},
|
|
parseHTML () {
|
|
return [{ tag: 'th', getAttrs: collectDomAttrs }]
|
|
},
|
|
renderHTML ({ node }) {
|
|
return ['th', node.attrs.htmlAttrs, 0]
|
|
}
|
|
})
|
|
|
|
const ImageNode = Node.create({
|
|
name: 'image',
|
|
inline: true,
|
|
group: 'inline',
|
|
draggable: true,
|
|
addAttributes () {
|
|
return { htmlAttrs: { default: {} } }
|
|
},
|
|
parseHTML () {
|
|
return [{ tag: 'img', getAttrs: collectDomAttrs }]
|
|
},
|
|
renderHTML ({ node }) {
|
|
return ['img', node.attrs.htmlAttrs]
|
|
}
|
|
})
|
|
|
|
const ColGroupNode = Node.create({
|
|
name: 'colgroup',
|
|
group: 'block',
|
|
content: 'col*',
|
|
addAttributes () {
|
|
return { htmlAttrs: { default: {} } }
|
|
},
|
|
parseHTML () {
|
|
return [{ tag: 'colgroup', getAttrs: collectDomAttrs }]
|
|
},
|
|
renderHTML ({ node }) {
|
|
return ['colgroup', node.attrs.htmlAttrs, 0]
|
|
}
|
|
})
|
|
|
|
const ColNode = Node.create({
|
|
name: 'col',
|
|
group: 'block',
|
|
atom: true,
|
|
addAttributes () {
|
|
return { htmlAttrs: { default: {} } }
|
|
},
|
|
parseHTML () {
|
|
return [{ tag: 'col', getAttrs: collectDomAttrs }]
|
|
},
|
|
renderHTML ({ node }) {
|
|
return ['col', node.attrs.htmlAttrs]
|
|
}
|
|
})
|
|
|
|
const CustomBold = Mark.create({
|
|
name: 'bold',
|
|
parseHTML () {
|
|
return [{ tag: 'strong' }, { tag: 'b' }, { style: 'font-weight=bold' }, { style: 'font-weight=700' }]
|
|
},
|
|
renderHTML () {
|
|
return ['strong', 0]
|
|
},
|
|
addCommands () {
|
|
return {
|
|
toggleBold: () => ({ commands }) => commands.toggleMark(this.name)
|
|
}
|
|
},
|
|
addKeyboardShortcuts () {
|
|
return {
|
|
'Mod-b': () => this.editor.commands.toggleBold()
|
|
}
|
|
}
|
|
})
|
|
|
|
const CustomItalic = Mark.create({
|
|
name: 'italic',
|
|
parseHTML () {
|
|
return [{ tag: 'em' }, { tag: 'i' }, { style: 'font-style=italic' }]
|
|
},
|
|
renderHTML () {
|
|
return ['em', 0]
|
|
},
|
|
addCommands () {
|
|
return {
|
|
toggleItalic: () => ({ commands }) => commands.toggleMark(this.name)
|
|
}
|
|
},
|
|
addKeyboardShortcuts () {
|
|
return {
|
|
'Mod-i': () => this.editor.commands.toggleItalic()
|
|
}
|
|
}
|
|
})
|
|
|
|
const CustomUnderline = Mark.create({
|
|
name: 'underline',
|
|
parseHTML () {
|
|
return [{ tag: 'u' }, { style: 'text-decoration=underline' }]
|
|
},
|
|
renderHTML () {
|
|
return ['u', 0]
|
|
},
|
|
addCommands () {
|
|
return {
|
|
toggleUnderline: () => ({ commands }) => commands.toggleMark(this.name)
|
|
}
|
|
},
|
|
addKeyboardShortcuts () {
|
|
return {
|
|
'Mod-u': () => this.editor.commands.toggleUnderline()
|
|
}
|
|
}
|
|
})
|
|
|
|
const CustomStrike = Mark.create({
|
|
name: 'strike',
|
|
parseHTML () {
|
|
return [{ tag: 's' }, { tag: 'del' }, { tag: 'strike' }, { style: 'text-decoration=line-through' }]
|
|
},
|
|
renderHTML () {
|
|
return ['s', 0]
|
|
},
|
|
addCommands () {
|
|
return {
|
|
toggleStrike: () => ({ commands }) => commands.toggleMark(this.name)
|
|
}
|
|
},
|
|
addKeyboardShortcuts () {
|
|
return {
|
|
'Mod-Shift-s': () => this.editor.commands.toggleStrike()
|
|
}
|
|
}
|
|
})
|
|
|
|
const EmptySpanNode = Node.create({
|
|
name: 'emptySpan',
|
|
inline: true,
|
|
group: 'inline',
|
|
atom: true,
|
|
addAttributes () {
|
|
return { htmlAttrs: { default: {} } }
|
|
},
|
|
parseHTML () {
|
|
return [{
|
|
tag: 'span',
|
|
priority: 60,
|
|
getAttrs (dom) {
|
|
if (dom.childNodes.length === 0 && dom.attributes.length > 0) {
|
|
return collectDomAttrs(dom)
|
|
}
|
|
|
|
return false
|
|
}
|
|
}]
|
|
},
|
|
renderHTML ({ node }) {
|
|
return ['span', node.attrs.htmlAttrs]
|
|
}
|
|
})
|
|
|
|
const SpanMark = Mark.create({
|
|
name: 'span',
|
|
excludes: '',
|
|
addAttributes () {
|
|
return { htmlAttrs: { default: {} } }
|
|
},
|
|
parseHTML () {
|
|
return [{ tag: 'span', getAttrs: collectSpanDomAttrs }]
|
|
},
|
|
renderHTML ({ mark }) {
|
|
return ['span', mark.attrs.htmlAttrs, 0]
|
|
}
|
|
})
|
|
|
|
const LinkMark = Mark.create({
|
|
name: 'link',
|
|
excludes: '',
|
|
addAttributes () {
|
|
return { htmlAttrs: { default: {} } }
|
|
},
|
|
parseHTML () {
|
|
return [{ tag: 'a', getAttrs: collectDomAttrs }]
|
|
},
|
|
renderHTML ({ mark }) {
|
|
return ['a', mark.attrs.htmlAttrs, 0]
|
|
}
|
|
})
|
|
|
|
const SubscriptMark = Mark.create({
|
|
name: 'subscript',
|
|
addAttributes () {
|
|
return { htmlAttrs: { default: {} } }
|
|
},
|
|
parseHTML () {
|
|
return [{ tag: 'sub', getAttrs: collectDomAttrs }]
|
|
},
|
|
renderHTML ({ mark }) {
|
|
return ['sub', mark.attrs.htmlAttrs, 0]
|
|
}
|
|
})
|
|
|
|
const SuperscriptMark = Mark.create({
|
|
name: 'superscript',
|
|
addAttributes () {
|
|
return { htmlAttrs: { default: {} } }
|
|
},
|
|
parseHTML () {
|
|
return [{ tag: 'sup', getAttrs: collectDomAttrs }]
|
|
},
|
|
renderHTML ({ mark }) {
|
|
return ['sup', mark.attrs.htmlAttrs, 0]
|
|
}
|
|
})
|
|
|
|
const TabHandler = Extension.create({
|
|
name: 'tabHandler',
|
|
addKeyboardShortcuts () {
|
|
return {
|
|
Tab: () => {
|
|
this.editor.commands.insertContent('\t')
|
|
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
const variableHighlightKey = new PluginKey('variableHighlight')
|
|
|
|
function buildDecorations (doc) {
|
|
const decorations = []
|
|
const regex = /\[\[[^\]]*\]\]/g
|
|
|
|
doc.descendants((node, pos) => {
|
|
if (!node.isText) return
|
|
|
|
let match
|
|
|
|
while ((match = regex.exec(node.text)) !== null) {
|
|
const from = pos + match.index
|
|
const to = from + match[0].length
|
|
|
|
decorations.push(Decoration.inline(from, to, { nodeName: 'dynamic-variable' }))
|
|
}
|
|
})
|
|
|
|
return DecorationSet.create(doc, decorations)
|
|
}
|
|
|
|
const VariableHighlight = Extension.create({
|
|
name: 'variableHighlight',
|
|
addProseMirrorPlugins () {
|
|
return [
|
|
new Plugin({
|
|
key: variableHighlightKey,
|
|
state: {
|
|
init (_, { doc }) {
|
|
return buildDecorations(doc)
|
|
},
|
|
apply (tr, oldSet) {
|
|
if (tr.docChanged) {
|
|
return buildDecorations(tr.doc)
|
|
}
|
|
|
|
return oldSet
|
|
}
|
|
},
|
|
props: {
|
|
decorations (state) {
|
|
return this.getState(state)
|
|
},
|
|
handleTextInput (view, from, to, text) {
|
|
if (text !== '[') return false
|
|
|
|
const { state } = view
|
|
const charBefore = state.doc.textBetween(Math.max(from - 1, 0), from)
|
|
|
|
if (charBefore !== '[') return false
|
|
|
|
const tr = state.tr.insertText('[]]', from, to)
|
|
|
|
tr.setSelection(state.selection.constructor.create(tr.doc, from + 1))
|
|
|
|
view.dispatch(tr)
|
|
|
|
return true
|
|
}
|
|
}
|
|
})
|
|
]
|
|
}
|
|
})
|
|
|
|
export function buildEditor ({ dynamicAreaProps, attachmentsIndex, onFieldDrop, onFieldDestroy, editorOptions }) {
|
|
const FieldNode = Node.create({
|
|
name: 'fieldNode',
|
|
inline: true,
|
|
group: 'inline',
|
|
atom: true,
|
|
draggable: true,
|
|
addAttributes () {
|
|
return {
|
|
uuid: { default: null },
|
|
areaUuid: { default: null },
|
|
width: { default: '124px' },
|
|
height: { default: null },
|
|
verticalAlign: { default: 'text-bottom' },
|
|
display: { default: 'inline-flex' }
|
|
}
|
|
},
|
|
parseHTML () {
|
|
return [{
|
|
tag: 'dynamic-field',
|
|
getAttrs (dom) {
|
|
return {
|
|
uuid: dom.getAttribute('uuid'),
|
|
areaUuid: dom.getAttribute('area-uuid'),
|
|
width: dom.style.width,
|
|
height: dom.style.height,
|
|
display: dom.style.display,
|
|
verticalAlign: dom.style.verticalAlign
|
|
}
|
|
}
|
|
}]
|
|
},
|
|
renderHTML ({ node }) {
|
|
return ['dynamic-field', {
|
|
uuid: node.attrs.uuid,
|
|
'area-uuid': node.attrs.areaUuid,
|
|
style: `width: ${node.attrs.width}; height: ${node.attrs.height}; display: ${node.attrs.display}; vertical-align: ${node.attrs.verticalAlign};`
|
|
}]
|
|
},
|
|
addNodeView () {
|
|
return ({ node, getPos, editor }) => {
|
|
const dom = document.createElement('span')
|
|
|
|
const nodeStyle = reactive({
|
|
width: node.attrs.width,
|
|
height: node.attrs.height,
|
|
verticalAlign: node.attrs.verticalAlign,
|
|
display: node.attrs.display
|
|
})
|
|
|
|
dom.dataset.areaUuid = node.attrs.areaUuid
|
|
|
|
const shadow = dom.attachShadow({ mode: 'open' })
|
|
|
|
shadow.adoptedStyleSheets = [dynamicStylesheet]
|
|
|
|
const app = createApp(DynamicArea, {
|
|
fieldUuid: node.attrs.uuid,
|
|
areaUuid: node.attrs.areaUuid,
|
|
nodeStyle,
|
|
getPos,
|
|
editor,
|
|
editable: editorOptions.editable,
|
|
...dynamicAreaProps
|
|
})
|
|
|
|
app.mount(shadow)
|
|
|
|
return {
|
|
dom,
|
|
update (updatedNode) {
|
|
if (updatedNode.attrs.areaUuid === node.attrs.areaUuid) {
|
|
nodeStyle.width = updatedNode.attrs.width
|
|
nodeStyle.height = updatedNode.attrs.height
|
|
nodeStyle.verticalAlign = updatedNode.attrs.verticalAlign
|
|
nodeStyle.display = updatedNode.attrs.display
|
|
}
|
|
},
|
|
destroy () {
|
|
onFieldDestroy(node)
|
|
|
|
app.unmount()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
const FieldDropPlugin = Extension.create({
|
|
name: 'fieldDrop',
|
|
addProseMirrorPlugins () {
|
|
return [
|
|
new Plugin({
|
|
key: new PluginKey('fieldDrop'),
|
|
props: {
|
|
handleDrop: onFieldDrop
|
|
}
|
|
})
|
|
]
|
|
}
|
|
})
|
|
|
|
const DynamicImageNode = ImageNode.extend({
|
|
renderHTML ({ node }) {
|
|
const { loading, ...attrs } = node.attrs.htmlAttrs
|
|
|
|
return ['img', attrs]
|
|
},
|
|
addNodeView () {
|
|
return ({ node }) => {
|
|
const dom = document.createElement('img')
|
|
|
|
const attrs = { ...node.attrs.htmlAttrs }
|
|
|
|
const blobUuid = attrs.src?.startsWith('blob:') && attrs.src.slice(5)
|
|
|
|
if (blobUuid && attachmentsIndex[blobUuid]) {
|
|
attrs.src = attachmentsIndex[blobUuid]
|
|
}
|
|
|
|
dom.setAttribute('loading', 'lazy')
|
|
|
|
Object.entries(attrs).forEach(([k, v]) => dom.setAttribute(k, v))
|
|
|
|
return { dom }
|
|
}
|
|
}
|
|
})
|
|
|
|
return new Editor({
|
|
extensions: [
|
|
Document,
|
|
Text,
|
|
HardBreak,
|
|
History,
|
|
Gapcursor,
|
|
Dropcursor,
|
|
CustomBold,
|
|
CustomItalic,
|
|
CustomUnderline,
|
|
CustomStrike,
|
|
CustomParagraph,
|
|
CustomHeading,
|
|
SectionNode,
|
|
ArticleNode,
|
|
DivNode,
|
|
BlockquoteNode,
|
|
PreNode,
|
|
OrderedListNode,
|
|
BulletListNode,
|
|
ListItemNode,
|
|
TableNode,
|
|
TableHead,
|
|
TableBody,
|
|
TableRow,
|
|
TableCell,
|
|
TableHeader,
|
|
ColGroupNode,
|
|
ColNode,
|
|
DynamicImageNode,
|
|
EmptySpanNode,
|
|
LinkMark,
|
|
SpanMark,
|
|
SubscriptMark,
|
|
SuperscriptMark,
|
|
VariableHighlight,
|
|
TabHandler,
|
|
FieldNode,
|
|
FieldDropPlugin
|
|
],
|
|
editorProps: {
|
|
attributes: {
|
|
style: 'outline: none'
|
|
}
|
|
},
|
|
parseOptions: {
|
|
preserveWhitespace: true
|
|
},
|
|
injectCSS: false,
|
|
...editorOptions
|
|
})
|
|
}
|