optimize add list style

This commit is contained in:
Pete Matsyburka committed 2026-10-03 14:05:38 +03:00
1 parent a990d22831
commit bef825c4e0
4 files changed
+218 -234

No files matched your search

@@ -51,6 +51,7 @@
@add-custom-field="$emit('add-custom-field', $event)"
@set-draw="$emit('set-draw', $event)"
@add-attachment="document.attachments.push($event)"
@add-list="ensureListsStyle"
/>
</Teleport>
</div>
@@ -189,15 +190,8 @@ export default {
}
}
},
watch: {
bodyDom: {
immediate: true,
handler () {
this.listsStyle = this.bodyDom.querySelector('body > style[data-lists]')?.textContent || null
this.ensureListsStyle(this.bodyDom)
}
}
created () {
this.loadListsStyle()
},
mounted () {
this.isMounted = true
@@ -244,6 +238,8 @@ export default {
}
},
reloadContent () {
this.loadListsStyle()
this.sectionRefs.forEach((ref) => ref.reloadContent())
},
onBeforeUnload (event) {
@@ -269,8 +265,6 @@ export default {
})
},
onSectionUpdate (section, { editor }) {
this.ensureListsStyle(editor.view.dom)
clearTimeout(this.saveTimer)
this.saveTimer = setTimeout(async () => {
@@ -279,8 +273,11 @@ export default {
delete this.saveTimer
}, 1000)
},
ensureListsStyle (root) {
if (this.listsStyle || !root.querySelector('[class*="doc-list-"]')) return
loadListsStyle () {
this.listsStyle = this.bodyDom.querySelector('body > style[data-lists]')?.textContent || null
},
ensureListsStyle () {
if (this.listsStyle) return
const style = this.bodyDom.createElement('style')
@@ -1,185 +1,159 @@
<template>
<div
class="absolute z-10 select-none text-sm"
:style="{ top: coords.top - 12 + 'px', left: coords.left - 12 + 'px' }"
@mousedown.prevent
>
<button
class="flex items-center justify-center w-6 h-6 rounded-md border border-neutral-200"
:class="isOpen ? 'bg-neutral-100 text-base-content' : 'bg-white text-base-content/40 hover:bg-neutral-100 hover:text-base-content'"
:title="t('insert')"
@click="isOpen = !isOpen"
>
<IconX
v-if="isOpen"
:width="14"
:height="14"
/>
<IconPlus
v-else
:width="14"
:height="14"
/>
</button>
<div class="absolute top-7 left-0 min-w-36 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg">
<div
v-if="isOpen"
class="absolute top-7 left-0 min-w-36 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg"
class="relative"
@pointerenter="$event.pointerType === 'mouse' ? submenu = 'field' : null"
>
<div
class="relative"
@pointerenter="$event.pointerType === 'mouse' ? submenu = 'field' : null"
>
<button
class="flex items-center gap-2 w-full px-2 py-1 rounded-md"
:class="{ 'bg-neutral-100': submenu === 'field' }"
@click="submenu = submenu === 'field' ? null : 'field'"
>
<IconForms
:width="16"
:height="16"
:stroke-width="1.6"
/>
{{ t('field') }}
<IconChevronRight
:width="14"
:height="14"
class="ml-auto"
/>
</button>
<div
v-if="submenu === 'field'"
class="absolute top-0 left-full min-w-40 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg"
>
<button
v-for="(icon, type) in fieldTypesList"
:key="type"
class="flex items-center gap-2 w-full px-2 py-1 rounded-md hover:bg-neutral-100"
@click="[$emit('insert-field', type), isOpen = false]"
>
<component
:is="icon"
:width="16"
:height="16"
:stroke-width="1.6"
/>
{{ fieldNames[type] }}
</button>
</div>
</div>
<div
class="relative"
@pointerenter="$event.pointerType === 'mouse' ? submenu = 'table' : null"
>
<button
class="flex items-center gap-2 w-full px-2 py-1 rounded-md"
:class="{ 'bg-neutral-100': submenu === 'table' }"
@click="submenu = submenu === 'table' ? null : 'table'"
>
<IconTable
:width="16"
:height="16"
:stroke-width="1.6"
/>
{{ t('table') }}
<IconChevronRight
:width="14"
:height="14"
class="ml-auto"
/>
</button>
<div
v-if="submenu === 'table'"
class="absolute top-0 left-full w-max p-2 bg-white border border-neutral-200 rounded-lg shadow-lg"
>
<div
v-if="!isMobile"
class="grid grid-cols-8 gap-0.5 mb-2"
>
<template
v-for="row in 8"
:key="row"
>
<button
v-for="col in 8"
:key="col"
class="w-4 h-4 border rounded-sm"
:class="row <= tableSize.rows && col <= tableSize.cols ? 'bg-neutral-200 border-neutral-400' : 'bg-white border-neutral-200'"
:aria-label="`${row} × ${col}`"
@pointerenter="$event.pointerType === 'mouse' ? tableSize = { rows: row, cols: col } : null"
@click="[$emit('insert-table', { rows: row, cols: col }), isOpen = false]"
/>
</template>
</div>
<div class="flex items-center justify-center gap-1 text-xs">
<template
v-for="key in ['rows', 'cols']"
:key="key"
>
<span v-if="key === 'cols'">×</span>
<div class="flex items-center border border-neutral-200 rounded-md">
<button
class="inline-flex items-center justify-center w-6 h-6 rounded-md hover:bg-neutral-100"
:aria-label="t(`decrease_${key}`)"
@click="tableSize[key] = Math.max(tableSize[key] - 1, 1)"
>
<IconMinus
:width="12"
:height="12"
/>
</button>
<span class="w-5 text-center">{{ tableSize[key] }}</span>
<button
class="inline-flex items-center justify-center w-6 h-6 rounded-md hover:bg-neutral-100"
:aria-label="t(`increase_${key}`)"
@click="tableSize[key] = Math.min(tableSize[key] + 1, 20)"
>
<IconPlus
:width="12"
:height="12"
/>
</button>
</div>
</template>
</div>
<button
v-if="isMobile || tableSize.rows > 8 || tableSize.cols > 8"
class="w-full mt-2 px-2 py-1 rounded-md bg-neutral-100 hover:bg-neutral-200"
@click="[$emit('insert-table', { ...tableSize }), isOpen = false]"
>
{{ t('insert') }}
</button>
</div>
</div>
<button
class="flex items-center gap-2 w-full px-2 py-1 rounded-md hover:bg-neutral-100"
@mouseenter="submenu = null"
@click="[$emit('insert-variable'), isOpen = false]"
class="flex items-center gap-2 w-full px-2 py-1 rounded-md"
:class="{ 'bg-neutral-100': submenu === 'field' }"
@click="submenu = submenu === 'field' ? null : 'field'"
>
<IconBracketsContain
<IconForms
:width="16"
:height="16"
:stroke-width="1.6"
/>
{{ t('variable') }}
</button>
<button
class="flex items-center gap-2 w-full px-2 py-1 rounded-md hover:bg-neutral-100"
@mouseenter="submenu = null"
@click="[$emit('insert-image'), isOpen = false]"
>
<IconPhoto
:width="16"
:height="16"
:stroke-width="1.6"
{{ t('field') }}
<IconChevronRight
:width="14"
:height="14"
class="ml-auto"
/>
{{ t('image') }}
</button>
<div
v-if="submenu === 'field'"
class="absolute top-0 left-full min-w-40 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg"
>
<button
v-for="(icon, type) in fieldTypesList"
:key="type"
class="flex items-center gap-2 w-full px-2 py-1 rounded-md hover:bg-neutral-100"
@click="[$emit('insert-field', type), $emit('close')]"
>
<component
:is="icon"
:width="16"
:height="16"
:stroke-width="1.6"
/>
{{ fieldNames[type] }}
</button>
</div>
</div>
<div
class="relative"
@pointerenter="$event.pointerType === 'mouse' ? submenu = 'table' : null"
>
<button
class="flex items-center gap-2 w-full px-2 py-1 rounded-md"
:class="{ 'bg-neutral-100': submenu === 'table' }"
@click="submenu = submenu === 'table' ? null : 'table'"
>
<IconTable
:width="16"
:height="16"
:stroke-width="1.6"
/>
{{ t('table') }}
<IconChevronRight
:width="14"
:height="14"
class="ml-auto"
/>
</button>
<div
v-if="submenu === 'table'"
class="absolute top-0 left-full w-max p-2 bg-white border border-neutral-200 rounded-lg shadow-lg"
>
<div
v-if="!isMobile"
class="grid grid-cols-8 gap-0.5 mb-2"
>
<template
v-for="row in 8"
:key="row"
>
<button
v-for="col in 8"
:key="col"
class="w-4 h-4 border rounded-sm"
:class="row <= tableSize.rows && col <= tableSize.cols ? 'bg-neutral-200 border-neutral-400' : 'bg-white border-neutral-200'"
:aria-label="`${row} × ${col}`"
@pointerenter="$event.pointerType === 'mouse' ? tableSize = { rows: row, cols: col } : null"
@click="[$emit('insert-table', { rows: row, cols: col }), $emit('close')]"
/>
</template>
</div>
<div class="flex items-center justify-center gap-1 text-xs">
<template
v-for="key in ['rows', 'cols']"
:key="key"
>
<span v-if="key === 'cols'">×</span>
<div class="flex items-center border border-neutral-200 rounded-md">
<button
class="inline-flex items-center justify-center w-6 h-6 rounded-md hover:bg-neutral-100"
:aria-label="t(`decrease_${key}`)"
@click="tableSize[key] = Math.max(tableSize[key] - 1, 1)"
>
<IconMinus
:width="12"
:height="12"
/>
</button>
<span class="w-5 text-center">{{ tableSize[key] }}</span>
<button
class="inline-flex items-center justify-center w-6 h-6 rounded-md hover:bg-neutral-100"
:aria-label="t(`increase_${key}`)"
@click="tableSize[key] = Math.min(tableSize[key] + 1, 20)"
>
<IconPlus
:width="12"
:height="12"
/>
</button>
</div>
</template>
</div>
<button
v-if="isMobile || tableSize.rows > 8 || tableSize.cols > 8"
class="w-full mt-2 px-2 py-1 rounded-md bg-neutral-100 hover:bg-neutral-200"
@click="[$emit('insert-table', { ...tableSize }), $emit('close')]"
>
{{ t('insert') }}
</button>
</div>
</div>
<button
class="flex items-center gap-2 w-full px-2 py-1 rounded-md hover:bg-neutral-100"
@mouseenter="submenu = null"
@click="[$emit('insert-variable'), $emit('close')]"
>
<IconBracketsContain
:width="16"
:height="16"
:stroke-width="1.6"
/>
{{ t('variable') }}
</button>
<button
class="flex items-center gap-2 w-full px-2 py-1 rounded-md hover:bg-neutral-100"
@mouseenter="submenu = null"
@click="[$emit('insert-image'), $emit('close')]"
>
<IconPhoto
:width="16"
:height="16"
:stroke-width="1.6"
/>
{{ t('image') }}
</button>
</div>
</template>
<script>
import { IconPlus, IconMinus, IconX, IconForms, IconTable, IconPhoto, IconChevronRight, IconBracketsContain } from '@tabler/icons-vue'
import { IconPlus, IconMinus, IconForms, IconTable, IconPhoto, IconChevronRight, IconBracketsContain } from '@tabler/icons-vue'
import FieldType from './field_type.vue'
export default {
@@ -187,7 +161,6 @@ export default {
components: {
IconPlus,
IconMinus,
IconX,
IconForms,
IconTable,
IconPhoto,
@@ -195,16 +168,9 @@ export default {
IconBracketsContain
},
inject: ['withPhone', 'withPayment', 'withVerification', 'withKba', 't', 'fieldTypes', 'isMobile'],
props: {
coords: {
type: Object,
required: true
}
},
emits: ['insert-field', 'insert-table', 'insert-variable', 'insert-image'],
emits: ['insert-field', 'insert-table', 'insert-variable', 'insert-image', 'close'],
data () {
return {
isOpen: false,
submenu: null,
tableSize: { rows: 3, cols: 3 }
}
@@ -220,24 +186,18 @@ export default {
}))
}
},
watch: {
isOpen (value) {
this.submenu = null
if (value) {
document.addEventListener('mousedown', this.onDocumentMouseDown)
} else {
document.removeEventListener('mousedown', this.onDocumentMouseDown)
}
}
mounted () {
document.addEventListener('mousedown', this.onDocumentMouseDown)
},
beforeUnmount () {
document.removeEventListener('mousedown', this.onDocumentMouseDown)
this.$emit('close')
},
methods: {
onDocumentMouseDown (event) {
if (!event.composedPath().includes(this.$el)) {
this.isOpen = false
if (!event.composedPath().includes(this.$el.parentElement)) {
this.$emit('close')
}
}
}
@@ -385,7 +385,7 @@ export default {
default: null
}
},
emits: ['add-variable', 'add-condition'],
emits: ['add-variable', 'add-condition', 'add-list'],
data () {
return {
isMouseDown: false,
@@ -576,6 +576,8 @@ export default {
toggleList (style) {
this.dropdown = null
if (style !== this.listStyle) this.$emit('add-list')
this.editor.chain().focus().toggleList(style).run()
},
wrapVariable () {
@@ -51,19 +51,44 @@
v-if="editable"
v-show="!selectedAreasRef.value.length"
:editor="editor"
:coords="dynamicMenuCoords"
:coords="isInsertMenuOpen ? null : dynamicMenuCoords"
@add-variable="dynamicMenuCoords = null"
@add-condition="dynamicMenuCoords = null"
@add-list="$emit('add-list')"
/>
<DynamicInsertMenu
v-if="editable && insertMenuCoords && !isDrawMode && !selectedAreasRef.value.length"
:key="insertMenuCoords.top"
:coords="insertMenuCoords"
@insert-field="onInsertField"
@insert-table="onInsertTable"
@insert-variable="editor.chain().focus().insertContent('[[variable]]').run()"
@insert-image="$refs.imageInput.click()"
/>
<div
v-if="editable && insertMenuCoords && !isDrawMode"
class="absolute z-10 select-none text-sm"
:style="{ top: insertMenuCoords.top - 12 + 'px', left: insertMenuCoords.left - 12 + 'px' }"
@mousedown.prevent
>
<button
class="flex items-center justify-center w-6 h-6 rounded-md border border-neutral-200"
:class="isInsertMenuOpen ? 'bg-neutral-100 text-base-content' : 'bg-white text-base-content/40 hover:bg-neutral-100 hover:text-base-content'"
:title="t('insert')"
@click="isInsertMenuOpen = !isInsertMenuOpen"
>
<IconX
v-if="isInsertMenuOpen"
:width="14"
:height="14"
/>
<IconPlus
v-else
:width="14"
:height="14"
/>
</button>
<DynamicInsertMenu
v-if="isInsertMenuOpen"
:key="insertMenuCoords.top"
@close="isInsertMenuOpen = false"
@insert-field="onInsertField"
@insert-table="onInsertTable"
@insert-variable="editor.chain().focus().insertContent('[[variable]]').run()"
@insert-image="$refs.imageInput.click()"
/>
</div>
<DynamicBlockMenu
v-if="editable && blockMenuCoords && !isDrawMode"
:coords="blockMenuCoords"
@@ -90,8 +115,9 @@
<script>
import { shallowRef } from 'vue'
import { DOMSerializer, Fragment } from '@tiptap/pm/model'
import { findTable } from '@tiptap/pm/tables'
import { findTable, CellSelection } from '@tiptap/pm/tables'
import { v4 } from 'uuid'
import { IconPlus, IconX } from '@tabler/icons-vue'
import FieldContextMenu from './field_context_menu.vue'
import AreaTitle from './area_title.vue'
import DynamicMenu from './dynamic_menu.vue'
@@ -106,7 +132,9 @@ export default {
DynamicInsertMenu,
DynamicBlockMenu,
FieldContextMenu,
AreaTitle
AreaTitle,
IconPlus,
IconX
},
inject: ['template', 'save', 'baseFetch', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'selectedAreasRef', 'getFieldTypeIndex', 'fieldTypes', 'withPhone', 'withPayment', 'withVerification', 'withKba', 'backgroundColor'],
props: {
@@ -178,13 +206,14 @@ export default {
default: null
}
},
emits: ['update', 'draw', 'set-draw', 'add-custom-field', 'add-attachment'],
emits: ['update', 'draw', 'set-draw', 'add-custom-field', 'add-attachment', 'add-list'],
data () {
return {
isAreaDrag: false,
areaToolbarCoords: null,
dynamicMenuCoords: null,
insertMenuCoords: null,
isInsertMenuOpen: false,
blockMenuCoords: null,
contextMenu: null,
cursorHighlightCoords: null
@@ -455,7 +484,6 @@ export default {
}
}
this.insertMenuCoords = null
this.blockMenuCoords = null
} else {
this.areaToolbarCoords = null
@@ -463,25 +491,18 @@ export default {
this.setBlockMenuCoords(editor)
if (selection.node) {
if (selection.node || selection.empty || !editor.isFocused) {
this.dynamicMenuCoords = null
this.insertMenuCoords = null
} else if (selection.empty) {
this.dynamicMenuCoords = null
if (editor.isFocused) {
this.setInsertMenuCoords(editor)
}
} else {
this.insertMenuCoords = null
if (editor.isFocused) {
this.setDynamicMenuCoords(editor)
} else {
this.dynamicMenuCoords = null
}
this.setDynamicMenuCoords(editor)
}
}
if (selection instanceof CellSelection) {
this.insertMenuCoords = null
} else if (editor.isFocused) {
this.setInsertMenuCoords(editor)
}
},
setInsertMenuCoords (editor) {
const coords = editor.view.coordsAtPos(editor.state.selection.head)
@@ -562,10 +583,12 @@ export default {
this.$nextTick(() => this.setBlockMenuCoords(this.editor))
},
onInsertField (type) {
this.insertFieldAtRange({ sourceField: { type }, from: this.editor.state.selection.head })
const { from, to } = this.editor.state.selection
this.insertFieldAtRange({ sourceField: { type }, from, to })
},
onInsertTable ({ rows, cols }) {
const $pos = this.editor.state.selection.$head
const $pos = this.editor.state.tr.deleteSelection().selection.$head
const cell = '<td style="border: 1px solid #000000; padding: 4pt; vertical-align: top"><p style="margin: 0"></p></td>'
const html = `<table style="width: 100%; border-collapse: collapse"><tbody>${`<tr>${cell.repeat(cols)}</tr>`.repeat(rows)}</tbody></table>`
const isEmpty = $pos.parent.isTextblock && !$pos.parent.content.size
@@ -573,13 +596,13 @@ export default {
const to = isEmpty ? $pos.after() : from
this.editor.chain().focus()
.deleteSelection()
.insertContentAt({ from, to }, html)
.setTextSelection(from + 4)
.run()
},
async onImageSelected (event) {
const file = event.target.files[0]
const pos = this.editor.state.selection.head
event.target.value = ''
@@ -603,7 +626,7 @@ export default {
await this.$nextTick()
this.editor.chain().focus()
.insertContentAt(pos, { type: 'image', attrs: { htmlAttrs: { src: `blob:${data.uuid}`, style: 'max-width: 100%' } } })
.insertContent({ type: 'image', attrs: { htmlAttrs: { src: `blob:${data.uuid}`, style: 'max-width: 100%' } } })
.run()
},
setDynamicMenuCoords (editor) {
@@ -835,6 +858,8 @@ export default {
const text = clipboardData.getData('text/plain')
const clipboardHtml = html || (text.includes('<dynamic-field') ? text : '')
if (/class="[^"]*doc-list-/.test(html)) this.$emit('add-list')
if (!clipboardHtml || !clipboardHtml.includes('data-field=')) {
return
}