mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 06:42:32 -05:00
improve dynamic editor
This commit is contained in:
1 parent
5896c9a2aa
commit
d793f00b37
9 files changed
+2473
-222
No files matched your search
@@ -0,0 +1,164 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="absolute z-[5] pointer-events-none select-none"
|
||||||
|
:style="{ top: coords.top + 'px', left: coords.left + 'px', width: coords.width + 'px', height: coords.height + 'px' }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute left-0 flex items-center gap-0.5 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg pointer-events-auto"
|
||||||
|
:style="{ top: Math.max(coords.top - 40, 0) - coords.top + 'px' }"
|
||||||
|
@mousedown.prevent
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="(icon, align) in alignIcons"
|
||||||
|
:key="align"
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 rounded-md hover:bg-neutral-100"
|
||||||
|
:class="{ 'bg-neutral-200': coords.align === align }"
|
||||||
|
:title="t(`align_${align}`)"
|
||||||
|
@click="$emit('command', 'setBlockAlign', align)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="icon"
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div class="w-px h-5 bg-neutral-200 mx-1" />
|
||||||
|
<template v-if="coords.type === 'table'">
|
||||||
|
<button
|
||||||
|
v-for="action in tableActions"
|
||||||
|
:key="action.title"
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 rounded-md"
|
||||||
|
:class="[{ 'text-red-600': action.danger }, isDisabled(action) ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-100']"
|
||||||
|
:title="action.structural && !coords.isRegular ? t('irregular_table_structure') : t(action.title)"
|
||||||
|
:disabled="isDisabled(action)"
|
||||||
|
@click="$emit('command', action.command, action.arg)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="action.icon"
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 rounded-md hover:bg-neutral-100 text-red-600"
|
||||||
|
:title="t('delete_image')"
|
||||||
|
@click="$emit('command', 'deleteSelection')"
|
||||||
|
>
|
||||||
|
<IconTrash
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="size && coords.type === 'table'"
|
||||||
|
class="absolute top-0 left-0 border border-dashed border-blue-500"
|
||||||
|
:style="{ width: size.width + 'px', height: size.height + 'px' }"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="coords.type === 'table'"
|
||||||
|
class="absolute w-3 h-3 bg-white border-2 border-blue-500 rounded-sm pointer-events-auto cursor-nwse-resize"
|
||||||
|
:style="{ left: (size?.width ?? coords.width) - 6 + 'px', top: (size?.height ?? coords.height) - 6 + 'px' }"
|
||||||
|
:title="t('resize')"
|
||||||
|
@mousedown.prevent.stop="startResize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { h } from 'vue'
|
||||||
|
import { IconAlignLeft, IconAlignCenter, IconAlignRight, IconRowInsertTop, IconRowInsertBottom, IconColumnInsertLeft, IconColumnInsertRight, IconRowRemove, IconColumnRemove, IconTrash } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
const IconMergeCells = (props) => h('svg', { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', ...props }, [
|
||||||
|
h('path', { d: 'M5 4h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2z' }),
|
||||||
|
h('path', { d: 'M6 12h4m-2 -2l2 2l-2 2' }),
|
||||||
|
h('path', { d: 'M18 12h-4m2 -2l-2 2l2 2' })
|
||||||
|
])
|
||||||
|
|
||||||
|
const IconSplitCell = (props) => h('svg', { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', ...props }, [
|
||||||
|
h('path', { d: 'M5 4h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2z' }),
|
||||||
|
h('path', { d: 'M12 4v16' }),
|
||||||
|
h('path', { d: 'M10 12h-4m2 -2l-2 2l2 2' }),
|
||||||
|
h('path', { d: 'M14 12h4m-2 -2l2 2l-2 2' })
|
||||||
|
])
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DynamicBlockMenu',
|
||||||
|
components: {
|
||||||
|
IconTrash
|
||||||
|
},
|
||||||
|
inject: ['t'],
|
||||||
|
props: {
|
||||||
|
coords: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['command', 'resize'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
size: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
alignIcons () {
|
||||||
|
return {
|
||||||
|
left: IconAlignLeft,
|
||||||
|
center: IconAlignCenter,
|
||||||
|
right: IconAlignRight
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tableActions () {
|
||||||
|
return [
|
||||||
|
{ command: 'addTableRow', arg: false, icon: IconRowInsertTop, title: 'insert_row_above', structural: true },
|
||||||
|
{ command: 'addTableRow', arg: true, icon: IconRowInsertBottom, title: 'insert_row_below', structural: true },
|
||||||
|
{ command: 'addTableColumn', arg: false, icon: IconColumnInsertLeft, title: 'insert_column_left', structural: true },
|
||||||
|
{ command: 'addTableColumn', arg: true, icon: IconColumnInsertRight, title: 'insert_column_right', structural: true },
|
||||||
|
{ command: 'mergeTableCells', icon: IconMergeCells, title: 'merge_cells', structural: true, enabled: 'canMerge' },
|
||||||
|
{ command: 'splitTableCell', icon: IconSplitCell, title: 'split_cell', structural: true, enabled: 'canSplit' },
|
||||||
|
{ command: 'deleteTableRow', icon: IconRowRemove, title: 'delete_row', danger: true, structural: true },
|
||||||
|
{ command: 'deleteTableColumn', icon: IconColumnRemove, title: 'delete_column', danger: true, structural: true },
|
||||||
|
{ command: 'deleteTable', icon: IconTrash, title: 'delete_table', danger: true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
window.removeEventListener('mousemove', this.onResizeMove)
|
||||||
|
window.removeEventListener('mouseup', this.onResizeEnd)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isDisabled (action) {
|
||||||
|
return (action.structural && !this.coords.isRegular) || (action.enabled && !this.coords[action.enabled])
|
||||||
|
},
|
||||||
|
startResize (event) {
|
||||||
|
this.resizeStart = { x: event.clientX, y: event.clientY, width: this.coords.width, height: this.coords.height }
|
||||||
|
this.size = { width: this.coords.width, height: this.coords.height }
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', this.onResizeMove)
|
||||||
|
window.addEventListener('mouseup', this.onResizeEnd)
|
||||||
|
},
|
||||||
|
onResizeMove (event) {
|
||||||
|
const { x, y, width, height } = this.resizeStart
|
||||||
|
this.size = {
|
||||||
|
width: Math.max(width + event.clientX - x, 20),
|
||||||
|
height: Math.max(height + event.clientY - y, this.coords.minHeight)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onResizeEnd () {
|
||||||
|
window.removeEventListener('mousemove', this.onResizeMove)
|
||||||
|
window.removeEventListener('mouseup', this.onResizeEnd)
|
||||||
|
|
||||||
|
if (this.size.width !== this.coords.width || this.size.height !== this.coords.height) {
|
||||||
|
this.$emit('resize', this.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.size = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import DynamicSection from './dynamic_section.vue'
|
import DynamicSection from './dynamic_section.vue'
|
||||||
import { dynamicStylesheet, tiptapStylesheet } from './dynamic_editor.js'
|
import { dynamicStylesheet, tiptapStylesheet } from './dynamic_editor.js'
|
||||||
import { buildVariablesSchema, mergeSchemaProperties } from './dynamic_variables_schema.js'
|
import { buildVariablesSchema, mergeSchemaProperties } from './dynamic_variables_schema.js'
|
||||||
@@ -61,6 +61,11 @@ export default {
|
|||||||
DynamicSection
|
DynamicSection
|
||||||
},
|
},
|
||||||
inject: ['baseFetch', 'template'],
|
inject: ['baseFetch', 'template'],
|
||||||
|
provide () {
|
||||||
|
return {
|
||||||
|
documentFonts: computed(() => this.documentFonts)
|
||||||
|
}
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
document: {
|
document: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -134,6 +139,40 @@ export default {
|
|||||||
styles () {
|
styles () {
|
||||||
return this.headDom.querySelectorAll('style')
|
return this.headDom.querySelectorAll('style')
|
||||||
},
|
},
|
||||||
|
documentFonts () {
|
||||||
|
const fonts = []
|
||||||
|
const firstFamily = (value) => value.split(',')[0].trim().replace(/^["']|["']$/g, '')
|
||||||
|
const add = (label, value) => {
|
||||||
|
if (label && !fonts.some((font) => font.label === label)) fonts.push({ label, value })
|
||||||
|
}
|
||||||
|
|
||||||
|
this.styles.forEach((style) => {
|
||||||
|
const css = style.textContent
|
||||||
|
|
||||||
|
for (const type of ['minor', 'major']) {
|
||||||
|
for (const [, name, value] of css.matchAll(new RegExp(`(--[\\w-]+-${type}HAnsi-font):\\s*([^;}]+)`, 'g'))) {
|
||||||
|
add(firstFamily(value), `var(${name})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [, value] of css.matchAll(/@font-face\s*\{[^}]*?font-family:\s*([^;}]+)/g)) {
|
||||||
|
add(firstFamily(value), value.trim())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const skipFamilies = ['symbol', 'wingdings', 'wingdings 2', 'wingdings 3', 'webdings', 'serif', 'sans-serif', 'monospace', 'inherit', 'initial']
|
||||||
|
const addUsed = (value) => {
|
||||||
|
if (value && !value.startsWith('var(') && !skipFamilies.includes(firstFamily(value).toLowerCase())) add(firstFamily(value), value.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
this.styles.forEach((style) => {
|
||||||
|
for (const [, value] of style.textContent.replace(/@font-face\s*\{[^}]*\}/g, '').matchAll(/font-family:\s*([^;}]+)/g)) addUsed(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.bodyDom.querySelectorAll('[style*="font-family"]').forEach((el) => addUsed(el.style.fontFamily))
|
||||||
|
|
||||||
|
return fonts
|
||||||
|
},
|
||||||
shadow () {
|
shadow () {
|
||||||
if (this.isMounted) {
|
if (this.isMounted) {
|
||||||
return this.$refs.shadow.attachShadow({ mode: 'open' })
|
return this.$refs.shadow.attachShadow({ mode: 'open' })
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,204 @@
|
|||||||
|
<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
|
||||||
|
v-if="isOpen"
|
||||||
|
class="absolute top-7 left-0 min-w-36 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative"
|
||||||
|
@mouseenter="submenu = 'field'"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-2 w-full px-2 py-1 rounded-md"
|
||||||
|
:class="{ 'bg-neutral-100': submenu === '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"
|
||||||
|
@mouseenter="submenu = 'table'"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-2 w-full px-2 py-1 rounded-md"
|
||||||
|
:class="{ 'bg-neutral-100': submenu === '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"
|
||||||
|
@mouseleave="tableSize = { rows: 0, cols: 0 }"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-8 gap-0.5">
|
||||||
|
<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'"
|
||||||
|
@mouseenter="tableSize = { rows: row, cols: col }"
|
||||||
|
@click="[$emit('insert-table', { rows: row, cols: col }), isOpen = false]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 text-xs text-center text-base-content/60">
|
||||||
|
{{ tableSize.rows ? `${tableSize.rows} × ${tableSize.cols}` : t('table_size') }}
|
||||||
|
</div>
|
||||||
|
</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]"
|
||||||
|
>
|
||||||
|
<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'), isOpen = false]"
|
||||||
|
>
|
||||||
|
<IconPhoto
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
{{ t('image') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconPlus, IconX, IconForms, IconTable, IconPhoto, IconChevronRight, IconBracketsContain } from '@tabler/icons-vue'
|
||||||
|
import FieldType from './field_type.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DynamicInsertMenu',
|
||||||
|
components: {
|
||||||
|
IconPlus,
|
||||||
|
IconX,
|
||||||
|
IconForms,
|
||||||
|
IconTable,
|
||||||
|
IconPhoto,
|
||||||
|
IconChevronRight,
|
||||||
|
IconBracketsContain
|
||||||
|
},
|
||||||
|
inject: ['withPhone', 'withPayment', 'withVerification', 'withKba', 't', 'fieldTypes'],
|
||||||
|
props: {
|
||||||
|
coords: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['insert-field', 'insert-table', 'insert-variable', 'insert-image'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isOpen: false,
|
||||||
|
submenu: null,
|
||||||
|
tableSize: { rows: 0, cols: 0 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
skipTypes: FieldType.computed.skipTypes,
|
||||||
|
fieldIconsSorted: FieldType.computed.fieldIconsSorted,
|
||||||
|
fieldTypesList () {
|
||||||
|
return Object.fromEntries(Object.entries(this.fieldIconsSorted).filter(([type]) => {
|
||||||
|
return this.fieldTypes.includes(type) || ((this.withPhone || type !== 'phone') && (this.withPayment || type !== 'payment') && (this.withVerification || type !== 'verification') && (this.withKba || type !== 'kba'))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
isOpen (value) {
|
||||||
|
this.submenu = null
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
document.addEventListener('mousedown', this.onDocumentMouseDown)
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('mousedown', this.onDocumentMouseDown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
document.removeEventListener('mousedown', this.onDocumentMouseDown)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onDocumentMouseDown (event) {
|
||||||
|
if (!event.composedPath().includes(this.$el)) {
|
||||||
|
this.isOpen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
[class*="doc-list-"]::before {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 0.25in;
|
||||||
|
text-indent: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
[class*="doc-list-dash-"]::before {
|
||||||
|
content: "–";
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-disc-0,
|
||||||
|
.doc-list-dash-0,
|
||||||
|
.doc-list-decimal-0 {
|
||||||
|
margin-left: 0.5in !important;
|
||||||
|
text-indent: -0.25in !important;
|
||||||
|
counter-set: doc-list-1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-disc-0::before {
|
||||||
|
content: "•";
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-decimal-0 {
|
||||||
|
counter-increment: doc-list-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-decimal-0::before {
|
||||||
|
content: counter(doc-list-0) ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-disc-1,
|
||||||
|
.doc-list-dash-1,
|
||||||
|
.doc-list-decimal-1 {
|
||||||
|
margin-left: 0.75in !important;
|
||||||
|
text-indent: -0.25in !important;
|
||||||
|
counter-set: doc-list-2 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-disc-1::before {
|
||||||
|
content: "◦";
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-decimal-1 {
|
||||||
|
counter-increment: doc-list-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-decimal-1::before {
|
||||||
|
content: counter(doc-list-1, lower-alpha) ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-disc-2,
|
||||||
|
.doc-list-dash-2,
|
||||||
|
.doc-list-decimal-2 {
|
||||||
|
margin-left: 1in !important;
|
||||||
|
text-indent: -0.25in !important;
|
||||||
|
counter-set: doc-list-3 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-disc-2::before {
|
||||||
|
content: "▪";
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-decimal-2 {
|
||||||
|
counter-increment: doc-list-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-decimal-2::before {
|
||||||
|
content: counter(doc-list-2, lower-roman) ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-disc-3,
|
||||||
|
.doc-list-dash-3,
|
||||||
|
.doc-list-decimal-3 {
|
||||||
|
margin-left: 1.25in !important;
|
||||||
|
text-indent: -0.25in !important;
|
||||||
|
counter-set: doc-list-4 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-disc-3::before {
|
||||||
|
content: "•";
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-decimal-3 {
|
||||||
|
counter-increment: doc-list-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-decimal-3::before {
|
||||||
|
content: counter(doc-list-3) ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-disc-4,
|
||||||
|
.doc-list-dash-4,
|
||||||
|
.doc-list-decimal-4 {
|
||||||
|
margin-left: 1.5in !important;
|
||||||
|
text-indent: -0.25in !important;
|
||||||
|
counter-set: doc-list-5 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-disc-4::before {
|
||||||
|
content: "◦";
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-decimal-4 {
|
||||||
|
counter-increment: doc-list-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-decimal-4::before {
|
||||||
|
content: counter(doc-list-4, lower-alpha) ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-disc-5,
|
||||||
|
.doc-list-dash-5,
|
||||||
|
.doc-list-decimal-5 {
|
||||||
|
margin-left: 1.75in !important;
|
||||||
|
text-indent: -0.25in !important;
|
||||||
|
counter-set: doc-list-6 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-disc-5::before {
|
||||||
|
content: "▪";
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-decimal-5 {
|
||||||
|
counter-increment: doc-list-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-decimal-5::before {
|
||||||
|
content: counter(doc-list-5, lower-roman) ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-disc-6,
|
||||||
|
.doc-list-dash-6,
|
||||||
|
.doc-list-decimal-6 {
|
||||||
|
margin-left: 2in !important;
|
||||||
|
text-indent: -0.25in !important;
|
||||||
|
counter-set: doc-list-7 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-disc-6::before {
|
||||||
|
content: "•";
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-decimal-6 {
|
||||||
|
counter-increment: doc-list-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-decimal-6::before {
|
||||||
|
content: counter(doc-list-6) ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-disc-7,
|
||||||
|
.doc-list-dash-7,
|
||||||
|
.doc-list-decimal-7 {
|
||||||
|
margin-left: 2.25in !important;
|
||||||
|
text-indent: -0.25in !important;
|
||||||
|
counter-set: doc-list-8 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-disc-7::before {
|
||||||
|
content: "◦";
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-decimal-7 {
|
||||||
|
counter-increment: doc-list-7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-decimal-7::before {
|
||||||
|
content: counter(doc-list-7, lower-alpha) ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-disc-8,
|
||||||
|
.doc-list-dash-8,
|
||||||
|
.doc-list-decimal-8 {
|
||||||
|
margin-left: 2.5in !important;
|
||||||
|
text-indent: -0.25in !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-disc-8::before {
|
||||||
|
content: "▪";
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-decimal-8 {
|
||||||
|
counter-increment: doc-list-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-list-decimal-8::before {
|
||||||
|
content: counter(doc-list-8, lower-roman) ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
:not([class*="doc-list-"]) + .doc-list-decimal-0,
|
||||||
|
.doc-list-decimal-0:first-child,
|
||||||
|
.doc-list-disc-0 + .doc-list-decimal-0,
|
||||||
|
.doc-list-dash-0 + .doc-list-decimal-0 {
|
||||||
|
counter-set: doc-list-0 1 doc-list-1 0;
|
||||||
|
}
|
||||||
@@ -1,107 +1,366 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="visible"
|
v-if="visible"
|
||||||
class="absolute z-10 flex items-center gap-0.5 px-1.5 py-1 bg-white border border-base-300 rounded-lg shadow select-none"
|
ref="menu"
|
||||||
:style="{ top: (coords.top - 42) + 'px', left: coords.left + 'px', transform: 'translateX(-50%)' }"
|
class="absolute z-10 flex flex-col gap-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg select-none text-sm"
|
||||||
|
:style="{ top: coords.top + 'px', left: menuLeft + 'px' }"
|
||||||
@mousedown.prevent
|
@mousedown.prevent
|
||||||
>
|
>
|
||||||
<button
|
<div class="flex items-center gap-0.5">
|
||||||
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
<div class="relative">
|
||||||
:class="isBold ? 'bg-base-200' : 'bg-transparent'"
|
<button
|
||||||
title="Bold"
|
class="flex items-center justify-between gap-1 w-36 h-7 px-2 border border-neutral-200 rounded-md hover:bg-neutral-100"
|
||||||
@click="toggleBold"
|
:title="t('font')"
|
||||||
>
|
@click="toggleDropdown('font')"
|
||||||
<IconBold
|
>
|
||||||
:width="16"
|
<span
|
||||||
:height="16"
|
class="truncate"
|
||||||
/>
|
:style="{ fontFamily }"
|
||||||
</button>
|
>{{ fontFamily }}</span>
|
||||||
<button
|
<IconChevronDown
|
||||||
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
:width="14"
|
||||||
:class="isItalic ? 'bg-base-200' : 'bg-transparent'"
|
:height="14"
|
||||||
title="Italic"
|
/>
|
||||||
@click="toggleItalic"
|
</button>
|
||||||
>
|
<div
|
||||||
<IconItalic
|
v-if="dropdown === 'font'"
|
||||||
:width="16"
|
ref="dropdown"
|
||||||
:height="16"
|
:class="isDropdownAlignedRight ? 'right-0' : 'left-0'"
|
||||||
/>
|
class="absolute z-10 top-full mt-1 w-48 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg max-h-64 overflow-y-auto"
|
||||||
</button>
|
>
|
||||||
<button
|
<template
|
||||||
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
v-for="(group, index) in [documentFonts, standardFonts]"
|
||||||
:class="isUnderline ? 'bg-base-200' : 'bg-transparent'"
|
:key="index"
|
||||||
title="Underline"
|
>
|
||||||
@click="toggleUnderline"
|
<div
|
||||||
>
|
v-if="index && group.length && documentFonts.length"
|
||||||
<IconUnderline
|
class="my-1 border-t border-neutral-200"
|
||||||
:width="16"
|
/>
|
||||||
:height="16"
|
<button
|
||||||
/>
|
v-for="font in group"
|
||||||
</button>
|
:key="font.label"
|
||||||
<button
|
class="block w-full text-left px-2 py-1 rounded-md hover:bg-neutral-100 truncate"
|
||||||
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
:class="{ 'bg-neutral-200': font.label === fontFamily }"
|
||||||
:class="isStrike ? 'bg-base-200' : 'bg-transparent'"
|
:style="{ fontFamily: font.value }"
|
||||||
title="Strikethrough"
|
@click="setSpanStyle('font-family', font.value)"
|
||||||
@click="toggleStrike"
|
>
|
||||||
>
|
{{ font.label }}
|
||||||
<IconStrikethrough
|
</button>
|
||||||
:width="16"
|
</template>
|
||||||
:height="16"
|
</div>
|
||||||
/>
|
</div>
|
||||||
</button>
|
<div class="relative">
|
||||||
<div class="w-px h-5 bg-base-300 mx-1" />
|
<button
|
||||||
<button
|
class="flex items-center justify-between gap-1 w-16 h-7 px-2 border border-neutral-200 rounded-md hover:bg-neutral-100"
|
||||||
class="inline-flex items-center justify-center text-xs h-7 border-none rounded cursor-pointer text-gray-700 bg-transparent"
|
:title="t('font_size')"
|
||||||
title="Wrap in variable"
|
@click="toggleDropdown('size')"
|
||||||
@click="wrapVariable"
|
>
|
||||||
>
|
<span>{{ fontSize }}</span>
|
||||||
<IconBracketsContain
|
<IconChevronDown
|
||||||
:width="16"
|
:width="14"
|
||||||
:height="16"
|
:height="14"
|
||||||
:stroke-width="1.6"
|
/>
|
||||||
/>
|
</button>
|
||||||
<span class="px-0.5">
|
<div
|
||||||
Variable
|
v-if="dropdown === 'size'"
|
||||||
</span>
|
ref="dropdown"
|
||||||
</button>
|
:class="isDropdownAlignedRight ? 'right-0' : 'left-0'"
|
||||||
<div class="w-px h-5 bg-base-300 mx-1" />
|
class="absolute z-10 top-full mt-1 w-16 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg max-h-64 overflow-y-auto"
|
||||||
<button
|
>
|
||||||
class="inline-flex items-center justify-center text-xs h-7 border-none rounded cursor-pointer text-gray-700 bg-transparent"
|
<button
|
||||||
title="Wrap in condition"
|
v-for="size in fontSizes"
|
||||||
@click="wrapCondition"
|
:key="size"
|
||||||
>
|
class="block w-full text-left px-2 py-1 rounded-md hover:bg-neutral-100"
|
||||||
<svg
|
:class="{ 'bg-neutral-200': size === fontSize }"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
@click="setFontSize(size)"
|
||||||
width="16"
|
>
|
||||||
height="16"
|
{{ size }}
|
||||||
viewBox="0 0 24 24"
|
</button>
|
||||||
fill="none"
|
</div>
|
||||||
stroke="currentColor"
|
</div>
|
||||||
stroke-width="1.6"
|
<button
|
||||||
stroke-linecap="round"
|
class="inline-flex items-center justify-center w-7 h-7 rounded-md hover:bg-neutral-100"
|
||||||
stroke-linejoin="round"
|
:title="t('increase_font_size')"
|
||||||
class="tabler-icon tabler-icon-brackets-contain"
|
@click="setFontSize(fontSizes.find((size) => size > fontSize) || fontSize)"
|
||||||
><path d="M7 4h-4v16h4" /><path d="M17 4h4v16h-4" />
|
>
|
||||||
<text
|
<IconTextIncrease
|
||||||
x="12"
|
:width="16"
|
||||||
y="16.5"
|
:height="16"
|
||||||
text-anchor="middle"
|
/>
|
||||||
fill="currentColor"
|
</button>
|
||||||
stroke="none"
|
<button
|
||||||
font-size="14"
|
class="inline-flex items-center justify-center w-7 h-7 rounded-md hover:bg-neutral-100"
|
||||||
font-weight="600"
|
:title="t('decrease_font_size')"
|
||||||
font-family="ui-sans-serif, system-ui, sans-serif"
|
@click="setFontSize([...fontSizes].reverse().find((size) => size < fontSize) || fontSize)"
|
||||||
>if</text>
|
>
|
||||||
</svg>
|
<IconTextDecrease
|
||||||
<span class="px-0.5">
|
:width="16"
|
||||||
Condition
|
:height="16"
|
||||||
</span>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
<div class="w-px h-5 bg-neutral-200 mx-1" />
|
||||||
|
<div class="relative">
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center h-7 px-1 rounded-md hover:bg-neutral-100"
|
||||||
|
:title="t('line_spacing')"
|
||||||
|
@click="toggleDropdown('lineHeight')"
|
||||||
|
>
|
||||||
|
<IconLineHeight
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
<IconChevronDown
|
||||||
|
:width="12"
|
||||||
|
:height="12"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="dropdown === 'lineHeight'"
|
||||||
|
ref="dropdown"
|
||||||
|
:class="isDropdownAlignedRight ? 'right-0' : 'left-0'"
|
||||||
|
class="absolute z-10 top-full mt-1 w-20 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="value in lineHeights"
|
||||||
|
:key="value"
|
||||||
|
class="block w-full text-left px-2 py-1 rounded-md hover:bg-neutral-100"
|
||||||
|
:class="{ 'bg-neutral-200': value === lineHeight }"
|
||||||
|
@click="setBlockStyle('line-height', value)"
|
||||||
|
>
|
||||||
|
{{ value }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center h-7 px-1 rounded-md hover:bg-neutral-100"
|
||||||
|
:title="t('align')"
|
||||||
|
@click="toggleDropdown('align')"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="alignIcons[textAlign]"
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
<IconChevronDown
|
||||||
|
:width="12"
|
||||||
|
:height="12"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="dropdown === 'align'"
|
||||||
|
ref="dropdown"
|
||||||
|
:class="isDropdownAlignedRight ? 'right-0' : 'left-0'"
|
||||||
|
class="absolute z-10 top-full mt-1 w-max flex gap-0.5 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="(icon, align) in alignIcons"
|
||||||
|
:key="align"
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 rounded-md hover:bg-neutral-100"
|
||||||
|
:class="{ 'bg-neutral-200': align === textAlign }"
|
||||||
|
:title="t(align)"
|
||||||
|
@click="setBlockStyle('text-align', align)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="icon"
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center h-7 px-1 rounded-md hover:bg-neutral-100"
|
||||||
|
:class="{ 'bg-neutral-200': listStyle }"
|
||||||
|
:title="t('bullet_list')"
|
||||||
|
@click="toggleDropdown('list')"
|
||||||
|
>
|
||||||
|
<IconList
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
<IconChevronDown
|
||||||
|
:width="12"
|
||||||
|
:height="12"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="dropdown === 'list'"
|
||||||
|
ref="dropdown"
|
||||||
|
:class="isDropdownAlignedRight ? 'right-0' : 'left-0'"
|
||||||
|
class="absolute z-10 top-full mt-1 w-28 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="(label, style) in listStyles"
|
||||||
|
:key="style"
|
||||||
|
class="flex items-center gap-2 w-full text-left px-2 py-1 rounded-md hover:bg-neutral-100"
|
||||||
|
:class="{ 'bg-neutral-200': style === listStyle }"
|
||||||
|
@click="toggleList(style)"
|
||||||
|
>
|
||||||
|
<span class="w-4 text-center">{{ listMarkers[style] }}</span>
|
||||||
|
{{ label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-0.5">
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 rounded-md hover:bg-neutral-100"
|
||||||
|
:class="{ 'bg-neutral-200': isBold }"
|
||||||
|
:title="t('bold')"
|
||||||
|
@click="toggleBold"
|
||||||
|
>
|
||||||
|
<IconBold
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 rounded-md hover:bg-neutral-100"
|
||||||
|
:class="{ 'bg-neutral-200': isItalic }"
|
||||||
|
:title="t('italic')"
|
||||||
|
@click="toggleItalic"
|
||||||
|
>
|
||||||
|
<IconItalic
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 rounded-md hover:bg-neutral-100"
|
||||||
|
:class="{ 'bg-neutral-200': isUnderline }"
|
||||||
|
:title="t('underline')"
|
||||||
|
@click="toggleUnderline"
|
||||||
|
>
|
||||||
|
<IconUnderline
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center w-7 h-7 rounded-md hover:bg-neutral-100"
|
||||||
|
:class="{ 'bg-neutral-200': isStrike }"
|
||||||
|
:title="t('strikethrough')"
|
||||||
|
@click="toggleStrike"
|
||||||
|
>
|
||||||
|
<IconStrikethrough
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div class="w-px h-5 bg-neutral-200 mx-1" />
|
||||||
|
<div
|
||||||
|
v-for="colorProperty in ['color', 'background-color']"
|
||||||
|
:key="colorProperty"
|
||||||
|
class="relative"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center h-7 px-1 rounded-md hover:bg-neutral-100"
|
||||||
|
:title="colorProperty === 'color' ? t('text_color') : t('background_color')"
|
||||||
|
@click="toggleDropdown(colorProperty)"
|
||||||
|
>
|
||||||
|
<span class="flex flex-col items-center">
|
||||||
|
<component
|
||||||
|
:is="colorProperty === 'color' ? 'IconLetterA' : 'IconHighlight'"
|
||||||
|
:width="16"
|
||||||
|
:height="14"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="w-4 h-1 rounded-sm border border-neutral-200"
|
||||||
|
:style="{ backgroundColor: colorProperty === 'color' ? color : backgroundColor }"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<IconChevronDown
|
||||||
|
:width="12"
|
||||||
|
:height="12"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="dropdown === colorProperty"
|
||||||
|
ref="dropdown"
|
||||||
|
:class="isDropdownAlignedRight ? 'right-0' : 'left-0'"
|
||||||
|
class="absolute z-10 top-full mt-1 w-max p-2 bg-white border border-neutral-200 rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-1 w-full mb-2 px-2 py-1 rounded-md hover:bg-neutral-100"
|
||||||
|
@click="setSpanStyle(colorProperty, null)"
|
||||||
|
>
|
||||||
|
<IconX
|
||||||
|
:width="14"
|
||||||
|
:height="14"
|
||||||
|
/>
|
||||||
|
{{ colorProperty === 'color' ? t('default') : t('none') }}
|
||||||
|
</button>
|
||||||
|
<div class="grid grid-cols-10 gap-1">
|
||||||
|
<button
|
||||||
|
v-for="value in colors"
|
||||||
|
:key="value"
|
||||||
|
class="w-4 h-4 rounded-sm border border-neutral-200 hover:scale-125 transition-transform"
|
||||||
|
:style="{ backgroundColor: value }"
|
||||||
|
:title="value"
|
||||||
|
@click="setSpanStyle(colorProperty, value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="!isCellSelection">
|
||||||
|
<div class="w-px h-5 bg-neutral-200 mx-1" />
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center text-xs h-7 rounded-md hover:bg-neutral-100"
|
||||||
|
:title="t('create_variable')"
|
||||||
|
@click="wrapVariable"
|
||||||
|
>
|
||||||
|
<IconBracketsContain
|
||||||
|
:width="16"
|
||||||
|
:height="16"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
<span class="px-0.5">
|
||||||
|
{{ t('variable') }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center text-xs h-7 rounded-md hover:bg-neutral-100"
|
||||||
|
:title="t('create_condition')"
|
||||||
|
@click="wrapCondition"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.6"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="tabler-icon tabler-icon-brackets-contain"
|
||||||
|
><path d="M7 4h-4v16h4" /><path d="M17 4h4v16h-4" />
|
||||||
|
<text
|
||||||
|
x="12"
|
||||||
|
y="16.5"
|
||||||
|
text-anchor="middle"
|
||||||
|
fill="currentColor"
|
||||||
|
stroke="none"
|
||||||
|
font-size="14"
|
||||||
|
font-weight="600"
|
||||||
|
font-family="ui-sans-serif, system-ui, sans-serif"
|
||||||
|
>if</text>
|
||||||
|
</svg>
|
||||||
|
<span class="px-0.5">
|
||||||
|
{{ t('condition') }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconBold, IconItalic, IconUnderline, IconStrikethrough, IconBracketsContain } from '@tabler/icons-vue'
|
import { IconBold, IconItalic, IconUnderline, IconStrikethrough, IconBracketsContain, IconChevronDown, IconTextIncrease, IconTextDecrease, IconLineHeight, IconAlignLeft, IconAlignCenter, IconAlignRight, IconAlignJustified, IconLetterA, IconHighlight, IconList, IconX } from '@tabler/icons-vue'
|
||||||
|
import { CellSelection } from '@tiptap/pm/tables'
|
||||||
|
import { findNumbering, numberingListStyle } from './dynamic_editor.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DynamicMenu',
|
name: 'DynamicMenu',
|
||||||
@@ -110,7 +369,19 @@ export default {
|
|||||||
IconItalic,
|
IconItalic,
|
||||||
IconUnderline,
|
IconUnderline,
|
||||||
IconStrikethrough,
|
IconStrikethrough,
|
||||||
IconBracketsContain
|
IconBracketsContain,
|
||||||
|
IconChevronDown,
|
||||||
|
IconTextIncrease,
|
||||||
|
IconTextDecrease,
|
||||||
|
IconLineHeight,
|
||||||
|
IconLetterA,
|
||||||
|
IconHighlight,
|
||||||
|
IconList,
|
||||||
|
IconX
|
||||||
|
},
|
||||||
|
inject: {
|
||||||
|
t: 't',
|
||||||
|
documentFonts: { default: () => [] }
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
editor: {
|
editor: {
|
||||||
@@ -127,15 +398,83 @@ export default {
|
|||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isMouseDown: false,
|
isMouseDown: false,
|
||||||
isBold: this.editor.isActive('bold'),
|
dropdown: null,
|
||||||
isItalic: this.editor.isActive('italic'),
|
isDropdownAlignedRight: false,
|
||||||
isUnderline: this.editor.isActive('underline'),
|
menuWidth: 0,
|
||||||
isStrike: this.editor.isActive('strike')
|
isBold: false,
|
||||||
|
isItalic: false,
|
||||||
|
isUnderline: false,
|
||||||
|
isStrike: false,
|
||||||
|
fontFamily: '',
|
||||||
|
fontSize: 12,
|
||||||
|
color: '',
|
||||||
|
backgroundColor: '',
|
||||||
|
lineHeight: '',
|
||||||
|
textAlign: 'left',
|
||||||
|
listStyle: null,
|
||||||
|
isCellSelection: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
visible () {
|
visible () {
|
||||||
return !!this.coords && !this.isMouseDown
|
return !!this.coords && !this.isMouseDown
|
||||||
|
},
|
||||||
|
menuLeft () {
|
||||||
|
return Math.max(4, Math.min(this.coords.left - this.menuWidth / 2, this.coords.width - this.menuWidth - 4))
|
||||||
|
},
|
||||||
|
standardFonts () {
|
||||||
|
return [
|
||||||
|
{ label: 'Arial', value: 'Arial' },
|
||||||
|
{ label: 'Times New Roman', value: "'Times New Roman'" },
|
||||||
|
{ label: 'Courier New', value: "'Courier New'" }
|
||||||
|
].filter((font) => !this.documentFonts.some((documentFont) => documentFont.label === font.label))
|
||||||
|
},
|
||||||
|
fontSizes () {
|
||||||
|
return [8, 9, 10, 10.5, 11, 12, 14, 16, 18, 20, 24, 28, 32, 36, 48, 72]
|
||||||
|
},
|
||||||
|
lineHeights () {
|
||||||
|
return ['1', '1.15', '1.5', '2', '2.5', '3']
|
||||||
|
},
|
||||||
|
alignIcons () {
|
||||||
|
return {
|
||||||
|
left: IconAlignLeft,
|
||||||
|
center: IconAlignCenter,
|
||||||
|
right: IconAlignRight,
|
||||||
|
justify: IconAlignJustified
|
||||||
|
}
|
||||||
|
},
|
||||||
|
listStyles () {
|
||||||
|
return {
|
||||||
|
disc: this.t('dots'),
|
||||||
|
dash: this.t('dash'),
|
||||||
|
decimal: this.t('numbers')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
listMarkers () {
|
||||||
|
return {
|
||||||
|
disc: '•',
|
||||||
|
dash: '–',
|
||||||
|
decimal: '1.'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors () {
|
||||||
|
return [
|
||||||
|
'#000000', '#434343', '#666666', '#999999', '#b7b7b7', '#cccccc', '#d9d9d9', '#efefef', '#f3f3f3', '#ffffff',
|
||||||
|
'#980000', '#ff0000', '#ff9900', '#ffff00', '#00ff00', '#00ffff', '#4a86e8', '#0000ff', '#9900ff', '#ff00ff',
|
||||||
|
'#e6b8af', '#f4cccc', '#fce5cd', '#fff2cc', '#d9ead3', '#d0e0e3', '#c9daf8', '#cfe2f3', '#d9d2e9', '#ead1dc',
|
||||||
|
'#cc4125', '#e06666', '#f6b26b', '#ffd966', '#93c47d', '#76a5af', '#6d9eeb', '#6fa8dc', '#8e7cc3', '#c27ba0',
|
||||||
|
'#85200c', '#990000', '#b45f06', '#bf9000', '#38761d', '#134f5c', '#1155cc', '#0b5394', '#351c75', '#741b47'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible (value) {
|
||||||
|
this.dropdown = null
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
this.readState()
|
||||||
|
this.$nextTick(this.measure)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
@@ -154,11 +493,72 @@ export default {
|
|||||||
document.removeEventListener('mouseup', this.onMouseUp)
|
document.removeEventListener('mouseup', this.onMouseUp)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
measure () {
|
||||||
|
if (this.$refs.menu) {
|
||||||
|
this.menuWidth = this.$refs.menu.offsetWidth
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleDropdown (name) {
|
||||||
|
this.dropdown = this.dropdown === name ? null : name
|
||||||
|
this.isDropdownAlignedRight = false
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const dropdownEl = [this.$refs.dropdown].flat()[0]
|
||||||
|
|
||||||
|
if (!dropdownEl) return
|
||||||
|
|
||||||
|
const dropdownRect = dropdownEl.getBoundingClientRect()
|
||||||
|
const containerRect = this.$el.parentElement.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.isDropdownAlignedRight = dropdownRect.right > containerRect.right
|
||||||
|
})
|
||||||
|
},
|
||||||
toggleBold () {
|
toggleBold () {
|
||||||
this.editor.chain().focus().toggleBold().run()
|
this.toggleStyledMark('bold', 'font-weight', 'bold', this.isBold, (style) => parseInt(style.fontWeight) >= 600)
|
||||||
},
|
},
|
||||||
toggleItalic () {
|
toggleItalic () {
|
||||||
this.editor.chain().focus().toggleItalic().run()
|
this.toggleStyledMark('italic', 'font-style', 'italic', this.isItalic, (style) => style.fontStyle === 'italic')
|
||||||
|
},
|
||||||
|
toggleStyledMark (markName, property, value, isActive, isStyled) {
|
||||||
|
const isSegmentStyled = ({ from }) => isStyled(getComputedStyle(this.textElementAt(from)))
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
this.editor.chain().focus().unsetMark(markName).setListMarkerStyle(property, null).run()
|
||||||
|
|
||||||
|
if (this.selectedTextSegments().some(isSegmentStyled)) {
|
||||||
|
this.editor.chain().setSpanStyle(property, 'normal').run()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.editor.chain().focus().setSpanStyle(property, null).run()
|
||||||
|
|
||||||
|
const { state, view } = this.editor
|
||||||
|
const tr = state.tr
|
||||||
|
|
||||||
|
this.selectedTextSegments().forEach((segment) => {
|
||||||
|
if (!isSegmentStyled(segment)) tr.addMark(segment.from, segment.to, state.schema.marks[markName].create())
|
||||||
|
})
|
||||||
|
|
||||||
|
if (tr.docChanged) view.dispatch(tr)
|
||||||
|
|
||||||
|
this.editor.commands.setListMarkerStyle(property, value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedTextSegments () {
|
||||||
|
const { state } = this.editor
|
||||||
|
const segments = []
|
||||||
|
|
||||||
|
state.selection.ranges.forEach(({ $from, $to }) => state.doc.nodesBetween($from.pos, $to.pos, (node, pos) => {
|
||||||
|
if (node.isText) segments.push({ from: Math.max(pos, $from.pos), to: Math.min(pos + node.nodeSize, $to.pos) })
|
||||||
|
}))
|
||||||
|
|
||||||
|
return segments
|
||||||
|
},
|
||||||
|
textElementAt (pos) {
|
||||||
|
const { node, offset } = this.editor.view.domAtPos(pos, 1)
|
||||||
|
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) return node.parentElement
|
||||||
|
|
||||||
|
return node.childNodes[offset]?.nodeType === Node.ELEMENT_NODE ? node.childNodes[offset] : node
|
||||||
},
|
},
|
||||||
toggleUnderline () {
|
toggleUnderline () {
|
||||||
this.editor.chain().focus().toggleUnderline().run()
|
this.editor.chain().focus().toggleUnderline().run()
|
||||||
@@ -166,6 +566,28 @@ export default {
|
|||||||
toggleStrike () {
|
toggleStrike () {
|
||||||
this.editor.chain().focus().toggleStrike().run()
|
this.editor.chain().focus().toggleStrike().run()
|
||||||
},
|
},
|
||||||
|
setSpanStyle (property, value) {
|
||||||
|
this.dropdown = null
|
||||||
|
|
||||||
|
const chain = this.editor.chain().focus().setSpanStyle(property, value)
|
||||||
|
|
||||||
|
if (property !== 'background-color') chain.setListMarkerStyle(property, value)
|
||||||
|
|
||||||
|
chain.run()
|
||||||
|
},
|
||||||
|
setFontSize (size) {
|
||||||
|
this.setSpanStyle('font-size', `${size}pt`)
|
||||||
|
},
|
||||||
|
setBlockStyle (property, value) {
|
||||||
|
this.dropdown = null
|
||||||
|
|
||||||
|
this.editor.chain().focus().setBlockStyle(property, value).run()
|
||||||
|
},
|
||||||
|
toggleList (style) {
|
||||||
|
this.dropdown = null
|
||||||
|
|
||||||
|
this.editor.chain().focus().toggleList(style).run()
|
||||||
|
},
|
||||||
wrapVariable () {
|
wrapVariable () {
|
||||||
const { from, to } = this.editor.state.selection
|
const { from, to } = this.editor.state.selection
|
||||||
const replacement = '[[variable]]'
|
const replacement = '[[variable]]'
|
||||||
@@ -201,10 +623,62 @@ export default {
|
|||||||
}, 1)
|
}, 1)
|
||||||
},
|
},
|
||||||
onTransaction () {
|
onTransaction () {
|
||||||
this.isBold = this.editor.isActive('bold')
|
if (this.visible) {
|
||||||
this.isItalic = this.editor.isActive('italic')
|
this.readState()
|
||||||
this.isUnderline = this.editor.isActive('underline')
|
}
|
||||||
this.isStrike = this.editor.isActive('strike')
|
},
|
||||||
|
readState () {
|
||||||
|
const { view, state } = this.editor
|
||||||
|
const { ranges } = state.selection
|
||||||
|
const selectionFrom = Math.min(...ranges.map((range) => range.$from.pos))
|
||||||
|
const selectionTo = Math.max(...ranges.map((range) => range.$to.pos))
|
||||||
|
let from = null
|
||||||
|
|
||||||
|
state.doc.nodesBetween(selectionFrom, selectionTo, (node, pos) => {
|
||||||
|
if (from === null && node.isText) from = Math.max(pos, selectionFrom)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (from === null) from = selectionFrom
|
||||||
|
|
||||||
|
const $from = state.doc.resolve(from)
|
||||||
|
const el = this.textElementAt(from)
|
||||||
|
const style = getComputedStyle(el)
|
||||||
|
const blockDom = $from.parent.isTextblock ? view.nodeDOM($from.before()) : view.dom
|
||||||
|
const decorations = []
|
||||||
|
|
||||||
|
this.isCellSelection = state.selection instanceof CellSelection
|
||||||
|
this.isBold = parseInt(style.fontWeight) >= 600
|
||||||
|
this.isItalic = style.fontStyle === 'italic'
|
||||||
|
|
||||||
|
this.fontFamily = style.fontFamily.split(',')[0].replace(/["']/g, '').trim()
|
||||||
|
this.fontSize = Math.round(parseFloat(style.fontSize) * 0.75 * 2) / 2
|
||||||
|
this.color = style.color
|
||||||
|
this.backgroundColor = 'transparent'
|
||||||
|
|
||||||
|
for (let inlineEl = el; inlineEl && view.dom.contains(inlineEl); inlineEl = inlineEl.parentElement) {
|
||||||
|
const { textDecorationLine, backgroundColor } = getComputedStyle(inlineEl)
|
||||||
|
|
||||||
|
decorations.push(textDecorationLine)
|
||||||
|
|
||||||
|
if (inlineEl === blockDom) break
|
||||||
|
|
||||||
|
if (this.backgroundColor === 'transparent' && backgroundColor !== 'rgba(0, 0, 0, 0)' && backgroundColor !== 'transparent') {
|
||||||
|
this.backgroundColor = backgroundColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isUnderline = this.editor.isActive('underline') || decorations.some((line) => line.includes('underline'))
|
||||||
|
this.isStrike = this.editor.isActive('strike') || decorations.some((line) => line.includes('line-through'))
|
||||||
|
|
||||||
|
if ($from.parent.isTextblock) {
|
||||||
|
const blockStyle = getComputedStyle(blockDom)
|
||||||
|
const align = blockStyle.textAlign.replace('start', 'left').replace('end', 'right')
|
||||||
|
|
||||||
|
this.textAlign = this.alignIcons[align] ? align : 'left'
|
||||||
|
this.lineHeight = String(Math.round(parseFloat(blockStyle.lineHeight) / parseFloat(blockStyle.fontSize) * 100) / 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listStyle = findNumbering($from.parent) ? numberingListStyle(view, $from.before()) : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,13 @@
|
|||||||
:style="section.style.cssText"
|
:style="section.style.cssText"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<input
|
||||||
|
ref="imageInput"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden"
|
||||||
|
@change="onImageSelected"
|
||||||
|
>
|
||||||
<Teleport
|
<Teleport
|
||||||
v-if="editor"
|
v-if="editor"
|
||||||
:to="container"
|
:to="container"
|
||||||
@@ -45,6 +52,21 @@
|
|||||||
@add-variable="dynamicMenuCoords = null"
|
@add-variable="dynamicMenuCoords = null"
|
||||||
@add-condition="dynamicMenuCoords = null"
|
@add-condition="dynamicMenuCoords = null"
|
||||||
/>
|
/>
|
||||||
|
<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()"
|
||||||
|
/>
|
||||||
|
<DynamicBlockMenu
|
||||||
|
v-if="editable && blockMenuCoords && !isDrawMode"
|
||||||
|
:coords="blockMenuCoords"
|
||||||
|
@command="onBlockCommand"
|
||||||
|
@resize="onBlockResize"
|
||||||
|
/>
|
||||||
<FieldContextMenu
|
<FieldContextMenu
|
||||||
v-if="contextMenu && contextMenuField"
|
v-if="contextMenu && contextMenuField"
|
||||||
:context-menu="contextMenu"
|
:context-menu="contextMenu"
|
||||||
@@ -64,16 +86,21 @@
|
|||||||
<script>
|
<script>
|
||||||
import { shallowRef } from 'vue'
|
import { shallowRef } from 'vue'
|
||||||
import { DOMSerializer, Fragment } from '@tiptap/pm/model'
|
import { DOMSerializer, Fragment } from '@tiptap/pm/model'
|
||||||
|
import { findTable } from '@tiptap/pm/tables'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import FieldContextMenu from './field_context_menu.vue'
|
import FieldContextMenu from './field_context_menu.vue'
|
||||||
import AreaTitle from './area_title.vue'
|
import AreaTitle from './area_title.vue'
|
||||||
import DynamicMenu from './dynamic_menu.vue'
|
import DynamicMenu from './dynamic_menu.vue'
|
||||||
import { buildEditor } from './dynamic_editor.js'
|
import DynamicInsertMenu from './dynamic_insert_menu.vue'
|
||||||
|
import DynamicBlockMenu from './dynamic_block_menu.vue'
|
||||||
|
import { buildEditor, isRegularTable, resizeTable, tableRowMinHeights } from './dynamic_editor.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DynamicSection',
|
name: 'DynamicSection',
|
||||||
components: {
|
components: {
|
||||||
DynamicMenu,
|
DynamicMenu,
|
||||||
|
DynamicInsertMenu,
|
||||||
|
DynamicBlockMenu,
|
||||||
FieldContextMenu,
|
FieldContextMenu,
|
||||||
AreaTitle
|
AreaTitle
|
||||||
},
|
},
|
||||||
@@ -153,6 +180,8 @@ export default {
|
|||||||
isAreaDrag: false,
|
isAreaDrag: false,
|
||||||
areaToolbarCoords: null,
|
areaToolbarCoords: null,
|
||||||
dynamicMenuCoords: null,
|
dynamicMenuCoords: null,
|
||||||
|
insertMenuCoords: null,
|
||||||
|
blockMenuCoords: null,
|
||||||
contextMenu: null,
|
contextMenu: null,
|
||||||
cursorHighlightCoords: null
|
cursorHighlightCoords: null
|
||||||
}
|
}
|
||||||
@@ -235,6 +264,14 @@ export default {
|
|||||||
if (this.dynamicMenuCoords && this.editor && !this.editor.state.selection.empty) {
|
if (this.dynamicMenuCoords && this.editor && !this.editor.state.selection.empty) {
|
||||||
this.$nextTick(() => this.setDynamicMenuCoords(this.editor))
|
this.$nextTick(() => this.setDynamicMenuCoords(this.editor))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.insertMenuCoords && this.editor) {
|
||||||
|
this.$nextTick(() => this.setInsertMenuCoords(this.editor))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.blockMenuCoords && this.editor) {
|
||||||
|
this.$nextTick(() => this.setBlockMenuCoords(this.editor))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
@@ -258,9 +295,20 @@ export default {
|
|||||||
element: this.$refs.editorElement,
|
element: this.$refs.editorElement,
|
||||||
editable: this.editable,
|
editable: this.editable,
|
||||||
content: this.section.innerHTML,
|
content: this.section.innerHTML,
|
||||||
onUpdate: (event) => this.$emit('update', event),
|
onUpdate: (event) => {
|
||||||
|
this.$emit('update', event)
|
||||||
|
|
||||||
|
if (this.blockMenuCoords) {
|
||||||
|
this.$nextTick(() => this.setBlockMenuCoords(this.editor))
|
||||||
|
}
|
||||||
|
},
|
||||||
onSelectionUpdate: this.onSelectionUpdate,
|
onSelectionUpdate: this.onSelectionUpdate,
|
||||||
onBlur: () => { this.dynamicMenuCoords = null }
|
onFocus: this.onSelectionUpdate,
|
||||||
|
onBlur: () => {
|
||||||
|
this.dynamicMenuCoords = null
|
||||||
|
this.insertMenuCoords = null
|
||||||
|
this.blockMenuCoords = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -402,28 +450,150 @@ export default {
|
|||||||
this.selectedAreasRef.value = [area]
|
this.selectedAreasRef.value = [area]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.insertMenuCoords = null
|
||||||
|
this.blockMenuCoords = null
|
||||||
} else {
|
} else {
|
||||||
this.areaToolbarCoords = null
|
this.areaToolbarCoords = null
|
||||||
this.selectedAreasRef.value = []
|
this.selectedAreasRef.value = []
|
||||||
|
|
||||||
if (editor.state.selection.empty) {
|
this.setBlockMenuCoords(editor)
|
||||||
|
|
||||||
|
if (selection.node) {
|
||||||
this.dynamicMenuCoords = null
|
this.dynamicMenuCoords = null
|
||||||
|
this.insertMenuCoords = null
|
||||||
|
} else if (selection.empty) {
|
||||||
|
this.dynamicMenuCoords = null
|
||||||
|
|
||||||
|
if (editor.isFocused) {
|
||||||
|
this.setInsertMenuCoords(editor)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.setDynamicMenuCoords(editor)
|
this.insertMenuCoords = null
|
||||||
|
|
||||||
|
if (editor.isFocused) {
|
||||||
|
this.setDynamicMenuCoords(editor)
|
||||||
|
} else {
|
||||||
|
this.dynamicMenuCoords = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setInsertMenuCoords (editor) {
|
||||||
|
const coords = editor.view.coordsAtPos(editor.state.selection.head)
|
||||||
|
const containerRect = this.container.getBoundingClientRect()
|
||||||
|
const sectionEl = this.$refs.editorElement
|
||||||
|
const sectionRect = sectionEl.getBoundingClientRect()
|
||||||
|
const paddingLeft = parseFloat(getComputedStyle(sectionEl).paddingLeft) * this.zoom
|
||||||
|
|
||||||
|
this.insertMenuCoords = {
|
||||||
|
top: (coords.top + coords.bottom) / 2 - containerRect.top,
|
||||||
|
left: sectionRect.left - containerRect.left + Math.max(paddingLeft / 2, 14)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setBlockMenuCoords (editor) {
|
||||||
|
const { selection } = editor.state
|
||||||
|
const isImage = selection.node?.type.name === 'image'
|
||||||
|
const table = isImage ? null : findTable(selection.$from)
|
||||||
|
|
||||||
|
if (!isImage && !table) {
|
||||||
|
this.blockMenuCoords = null
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = isImage ? selection.from : table.pos
|
||||||
|
const dom = editor.view.nodeDOM(pos)
|
||||||
|
const rect = dom.getBoundingClientRect()
|
||||||
|
const containerRect = this.container.getBoundingClientRect()
|
||||||
|
const coords = {
|
||||||
|
type: isImage ? 'image' : 'table',
|
||||||
|
pos,
|
||||||
|
align: 'left',
|
||||||
|
top: rect.top - containerRect.top,
|
||||||
|
left: rect.left - containerRect.left,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isImage) {
|
||||||
|
coords.align = getComputedStyle(dom.parentElement).textAlign.replace('start', 'left').replace('end', 'right')
|
||||||
|
} else {
|
||||||
|
const { marginLeft, marginRight, float } = dom.style
|
||||||
|
|
||||||
|
if (float === 'left' || float === 'right') {
|
||||||
|
coords.align = float
|
||||||
|
} else if (marginLeft === 'auto') {
|
||||||
|
coords.align = marginRight === 'auto' ? 'center' : 'right'
|
||||||
|
} else if (dom.getAttribute('align')) {
|
||||||
|
coords.align = dom.getAttribute('align')
|
||||||
|
}
|
||||||
|
|
||||||
|
coords.isRegular = isRegularTable(table.node)
|
||||||
|
coords.canMerge = editor.can().mergeTableCells()
|
||||||
|
coords.canSplit = editor.can().splitTableCell()
|
||||||
|
coords.minHeight = tableRowMinHeights(dom, this.zoom, false).reduce((acc, value) => acc + value, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.blockMenuCoords = coords
|
||||||
|
},
|
||||||
|
onBlockResize ({ width, height }) {
|
||||||
|
resizeTable(this.editor.view, this.blockMenuCoords.pos, { width: width / this.zoom, height: height / this.zoom }, this.zoom)
|
||||||
|
|
||||||
|
this.editor.commands.focus()
|
||||||
|
|
||||||
|
this.$nextTick(() => this.setBlockMenuCoords(this.editor))
|
||||||
|
},
|
||||||
|
onBlockCommand (command, arg) {
|
||||||
|
this.editor.chain().focus()[command](arg).run()
|
||||||
|
|
||||||
|
this.$nextTick(() => this.setBlockMenuCoords(this.editor))
|
||||||
|
},
|
||||||
|
onInsertField (type) {
|
||||||
|
this.insertFieldAtRange({ sourceField: { type }, from: this.editor.state.selection.head })
|
||||||
|
},
|
||||||
|
onInsertTable ({ rows, cols }) {
|
||||||
|
const $pos = this.editor.state.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
|
||||||
|
const from = isEmpty ? $pos.before() : $pos.after()
|
||||||
|
const to = isEmpty ? $pos.after() : from
|
||||||
|
|
||||||
|
this.editor.chain().focus()
|
||||||
|
.insertContentAt({ from, to }, html)
|
||||||
|
.setTextSelection(from + 5)
|
||||||
|
.run()
|
||||||
|
},
|
||||||
|
onImageSelected (event) {
|
||||||
|
const file = event.target.files[0]
|
||||||
|
const pos = this.editor.state.selection.head
|
||||||
|
|
||||||
|
event.target.value = ''
|
||||||
|
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
const reader = new FileReader()
|
||||||
|
|
||||||
|
reader.onload = () => {
|
||||||
|
this.editor.chain().focus()
|
||||||
|
.insertContentAt(pos, { type: 'image', attrs: { htmlAttrs: { src: reader.result, style: 'max-width: 100%' } } })
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
},
|
||||||
setDynamicMenuCoords (editor) {
|
setDynamicMenuCoords (editor) {
|
||||||
const { from, to } = editor.state.selection
|
const { from, to } = editor.state.selection
|
||||||
const view = editor.view
|
const view = editor.view
|
||||||
const start = view.coordsAtPos(from)
|
const start = view.coordsAtPos(from)
|
||||||
const end = view.coordsAtPos(to)
|
const end = view.coordsAtPos(to)
|
||||||
const containerRect = this.container.getBoundingClientRect()
|
const containerRect = this.container.getBoundingClientRect()
|
||||||
const left = (start.left + end.right) / 2 - containerRect.left
|
|
||||||
|
|
||||||
this.dynamicMenuCoords = {
|
this.dynamicMenuCoords = {
|
||||||
top: Math.min(start.top, end.top) - containerRect.top,
|
top: Math.max(start.bottom, end.bottom) - containerRect.top + 8,
|
||||||
left: Math.max(80, Math.min(left, containerRect.width - 80))
|
left: (start.left + end.right) / 2 - containerRect.left,
|
||||||
|
width: containerRect.width
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onFieldDestroy (node) {
|
onFieldDestroy (node) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
@import "tailwindcss/components";
|
@import "tailwindcss/components";
|
||||||
@import "tailwindcss/utilities";
|
@import "tailwindcss/utilities";
|
||||||
|
@import "dynamic_lists";
|
||||||
|
|
||||||
*,
|
*,
|
||||||
::before,
|
::before,
|
||||||
|
|||||||
@@ -249,7 +249,41 @@ const en = {
|
|||||||
no_revisions_yet: 'No revisions yet',
|
no_revisions_yet: 'No revisions yet',
|
||||||
viewing_revision_from: 'Viewing revision from {date}',
|
viewing_revision_from: 'Viewing revision from {date}',
|
||||||
connect_google_drive: 'Connect Google Drive',
|
connect_google_drive: 'Connect Google Drive',
|
||||||
submitting: 'Submitting'
|
submitting: 'Submitting',
|
||||||
|
font_size: 'Font size',
|
||||||
|
increase_font_size: 'Increase font size',
|
||||||
|
decrease_font_size: 'Decrease font size',
|
||||||
|
line_spacing: 'Line spacing',
|
||||||
|
justify: 'Justify',
|
||||||
|
bullet_list: 'List',
|
||||||
|
dots: 'Dots',
|
||||||
|
dash: 'Dash',
|
||||||
|
numbers: 'Numbers',
|
||||||
|
bold: 'Bold',
|
||||||
|
italic: 'Italic',
|
||||||
|
underline: 'Underline',
|
||||||
|
strikethrough: 'Strikethrough',
|
||||||
|
text_color: 'Text color',
|
||||||
|
background_color: 'Background color',
|
||||||
|
variable: 'Variable',
|
||||||
|
create_variable: 'Wrap in variable',
|
||||||
|
create_condition: 'Wrap in condition',
|
||||||
|
insert: 'Insert',
|
||||||
|
field: 'Field',
|
||||||
|
table: 'Table',
|
||||||
|
table_size: 'Table size',
|
||||||
|
align_center: 'Align Center',
|
||||||
|
insert_row_above: 'Insert row above',
|
||||||
|
insert_row_below: 'Insert row below',
|
||||||
|
insert_column_left: 'Insert column left',
|
||||||
|
insert_column_right: 'Insert column right',
|
||||||
|
merge_cells: 'Merge cells',
|
||||||
|
split_cell: 'Split cell',
|
||||||
|
delete_row: 'Delete row',
|
||||||
|
delete_column: 'Delete column',
|
||||||
|
delete_table: 'Delete table',
|
||||||
|
delete_image: 'Delete image',
|
||||||
|
irregular_table_structure: 'Rows and columns can\'t be edited: irregular table layout'
|
||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
@@ -503,7 +537,41 @@ const es = {
|
|||||||
no_revisions_yet: 'Aún no hay revisiones',
|
no_revisions_yet: 'Aún no hay revisiones',
|
||||||
viewing_revision_from: 'Viendo revisión del {date}',
|
viewing_revision_from: 'Viendo revisión del {date}',
|
||||||
connect_google_drive: 'Conectar Google Drive',
|
connect_google_drive: 'Conectar Google Drive',
|
||||||
submitting: 'Enviando'
|
submitting: 'Enviando',
|
||||||
|
font_size: 'Tamaño',
|
||||||
|
increase_font_size: 'Aumentar tamaño',
|
||||||
|
decrease_font_size: 'Reducir tamaño',
|
||||||
|
line_spacing: 'Interlineado',
|
||||||
|
justify: 'Justificar',
|
||||||
|
bullet_list: 'Lista',
|
||||||
|
dots: 'Puntos',
|
||||||
|
dash: 'Guiones',
|
||||||
|
numbers: 'Números',
|
||||||
|
bold: 'Negrita',
|
||||||
|
italic: 'Cursiva',
|
||||||
|
underline: 'Subrayado',
|
||||||
|
strikethrough: 'Tachado',
|
||||||
|
text_color: 'Color de texto',
|
||||||
|
background_color: 'Color de fondo',
|
||||||
|
variable: 'Variable',
|
||||||
|
create_variable: 'Crear variable',
|
||||||
|
create_condition: 'Crear condición',
|
||||||
|
insert: 'Insertar',
|
||||||
|
field: 'Campo',
|
||||||
|
table: 'Tabla',
|
||||||
|
table_size: 'Tamaño de tabla',
|
||||||
|
align_center: 'Centrar',
|
||||||
|
insert_row_above: 'Fila arriba',
|
||||||
|
insert_row_below: 'Fila abajo',
|
||||||
|
insert_column_left: 'Columna izquierda',
|
||||||
|
insert_column_right: 'Columna derecha',
|
||||||
|
merge_cells: 'Combinar celdas',
|
||||||
|
split_cell: 'Dividir celda',
|
||||||
|
delete_row: 'Eliminar fila',
|
||||||
|
delete_column: 'Eliminar columna',
|
||||||
|
delete_table: 'Eliminar tabla',
|
||||||
|
delete_image: 'Eliminar imagen',
|
||||||
|
irregular_table_structure: 'No se pueden editar filas ni columnas: tabla irregular'
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
@@ -757,7 +825,41 @@ const it = {
|
|||||||
no_revisions_yet: 'Nessuna revisione ancora',
|
no_revisions_yet: 'Nessuna revisione ancora',
|
||||||
viewing_revision_from: 'Visualizzazione revisione del {date}',
|
viewing_revision_from: 'Visualizzazione revisione del {date}',
|
||||||
connect_google_drive: 'Connetti Google Drive',
|
connect_google_drive: 'Connetti Google Drive',
|
||||||
submitting: 'Invio'
|
submitting: 'Invio',
|
||||||
|
font_size: 'Dimensione',
|
||||||
|
increase_font_size: 'Aumenta dimensione',
|
||||||
|
decrease_font_size: 'Riduci dimensione',
|
||||||
|
line_spacing: 'Interlinea',
|
||||||
|
justify: 'Giustifica',
|
||||||
|
bullet_list: 'Elenco',
|
||||||
|
dots: 'Punti',
|
||||||
|
dash: 'Trattini',
|
||||||
|
numbers: 'Numeri',
|
||||||
|
bold: 'Grassetto',
|
||||||
|
italic: 'Corsivo',
|
||||||
|
underline: 'Sottolineato',
|
||||||
|
strikethrough: 'Barrato',
|
||||||
|
text_color: 'Colore testo',
|
||||||
|
background_color: 'Colore sfondo',
|
||||||
|
variable: 'Variabile',
|
||||||
|
create_variable: 'Crea variabile',
|
||||||
|
create_condition: 'Crea condizione',
|
||||||
|
insert: 'Inserisci',
|
||||||
|
field: 'Campo',
|
||||||
|
table: 'Tabella',
|
||||||
|
table_size: 'Dimensione tabella',
|
||||||
|
align_center: 'Centra',
|
||||||
|
insert_row_above: 'Riga sopra',
|
||||||
|
insert_row_below: 'Riga sotto',
|
||||||
|
insert_column_left: 'Colonna a sinistra',
|
||||||
|
insert_column_right: 'Colonna a destra',
|
||||||
|
merge_cells: 'Unisci celle',
|
||||||
|
split_cell: 'Dividi cella',
|
||||||
|
delete_row: 'Elimina riga',
|
||||||
|
delete_column: 'Elimina colonna',
|
||||||
|
delete_table: 'Elimina tabella',
|
||||||
|
delete_image: 'Elimina immagine',
|
||||||
|
irregular_table_structure: 'Righe e colonne non modificabili: tabella irregolare'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
@@ -1011,7 +1113,41 @@ const pt = {
|
|||||||
no_revisions_yet: 'Nenhuma revisão ainda',
|
no_revisions_yet: 'Nenhuma revisão ainda',
|
||||||
viewing_revision_from: 'Visualizando revisão de {date}',
|
viewing_revision_from: 'Visualizando revisão de {date}',
|
||||||
connect_google_drive: 'Conectar Google Drive',
|
connect_google_drive: 'Conectar Google Drive',
|
||||||
submitting: 'Enviando'
|
submitting: 'Enviando',
|
||||||
|
font_size: 'Tamanho',
|
||||||
|
increase_font_size: 'Aumentar tamanho',
|
||||||
|
decrease_font_size: 'Diminuir tamanho',
|
||||||
|
line_spacing: 'Espaçamento',
|
||||||
|
justify: 'Justificar',
|
||||||
|
bullet_list: 'Lista',
|
||||||
|
dots: 'Pontos',
|
||||||
|
dash: 'Traços',
|
||||||
|
numbers: 'Números',
|
||||||
|
bold: 'Negrito',
|
||||||
|
italic: 'Itálico',
|
||||||
|
underline: 'Sublinhado',
|
||||||
|
strikethrough: 'Tachado',
|
||||||
|
text_color: 'Cor do texto',
|
||||||
|
background_color: 'Cor de fundo',
|
||||||
|
variable: 'Variável',
|
||||||
|
create_variable: 'Criar variável',
|
||||||
|
create_condition: 'Criar condição',
|
||||||
|
insert: 'Inserir',
|
||||||
|
field: 'Campo',
|
||||||
|
table: 'Tabela',
|
||||||
|
table_size: 'Tamanho da tabela',
|
||||||
|
align_center: 'Centralizar',
|
||||||
|
insert_row_above: 'Linha acima',
|
||||||
|
insert_row_below: 'Linha abaixo',
|
||||||
|
insert_column_left: 'Coluna à esquerda',
|
||||||
|
insert_column_right: 'Coluna à direita',
|
||||||
|
merge_cells: 'Mesclar células',
|
||||||
|
split_cell: 'Dividir célula',
|
||||||
|
delete_row: 'Excluir linha',
|
||||||
|
delete_column: 'Excluir coluna',
|
||||||
|
delete_table: 'Excluir tabela',
|
||||||
|
delete_image: 'Excluir imagem',
|
||||||
|
irregular_table_structure: 'Linhas e colunas não editáveis: tabela irregular'
|
||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
@@ -1265,7 +1401,41 @@ const fr = {
|
|||||||
no_revisions_yet: 'Aucune révision pour le moment',
|
no_revisions_yet: 'Aucune révision pour le moment',
|
||||||
viewing_revision_from: 'Affichage de la révision du {date}',
|
viewing_revision_from: 'Affichage de la révision du {date}',
|
||||||
connect_google_drive: 'Connecter Google Drive',
|
connect_google_drive: 'Connecter Google Drive',
|
||||||
submitting: 'Soumission'
|
submitting: 'Soumission',
|
||||||
|
font_size: 'Taille',
|
||||||
|
increase_font_size: 'Augmenter la taille',
|
||||||
|
decrease_font_size: 'Réduire la taille',
|
||||||
|
line_spacing: 'Interligne',
|
||||||
|
justify: 'Justifier',
|
||||||
|
bullet_list: 'Liste',
|
||||||
|
dots: 'Points',
|
||||||
|
dash: 'Tirets',
|
||||||
|
numbers: 'Numéros',
|
||||||
|
bold: 'Gras',
|
||||||
|
italic: 'Italique',
|
||||||
|
underline: 'Souligné',
|
||||||
|
strikethrough: 'Barré',
|
||||||
|
text_color: 'Couleur du texte',
|
||||||
|
background_color: 'Couleur de fond',
|
||||||
|
variable: 'Variable',
|
||||||
|
create_variable: 'Créer une variable',
|
||||||
|
create_condition: 'Créer une condition',
|
||||||
|
insert: 'Insérer',
|
||||||
|
field: 'Champ',
|
||||||
|
table: 'Tableau',
|
||||||
|
table_size: 'Taille du tableau',
|
||||||
|
align_center: 'Centrer',
|
||||||
|
insert_row_above: 'Ligne au-dessus',
|
||||||
|
insert_row_below: 'Ligne en dessous',
|
||||||
|
insert_column_left: 'Colonne à gauche',
|
||||||
|
insert_column_right: 'Colonne à droite',
|
||||||
|
merge_cells: 'Fusionner',
|
||||||
|
split_cell: 'Scinder',
|
||||||
|
delete_row: 'Supprimer la ligne',
|
||||||
|
delete_column: 'Supprimer la colonne',
|
||||||
|
delete_table: 'Supprimer le tableau',
|
||||||
|
delete_image: 'Supprimer l\'image',
|
||||||
|
irregular_table_structure: 'Lignes et colonnes non modifiables : tableau irrégulier'
|
||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
@@ -1519,7 +1689,41 @@ const de = {
|
|||||||
no_revisions_yet: 'Noch keine Revisionen',
|
no_revisions_yet: 'Noch keine Revisionen',
|
||||||
viewing_revision_from: 'Ansicht der Revision vom {date}',
|
viewing_revision_from: 'Ansicht der Revision vom {date}',
|
||||||
connect_google_drive: 'Google Drive verbinden',
|
connect_google_drive: 'Google Drive verbinden',
|
||||||
submitting: 'Übermittlung'
|
submitting: 'Übermittlung',
|
||||||
|
font_size: 'Schriftgröße',
|
||||||
|
increase_font_size: 'Schrift vergrößern',
|
||||||
|
decrease_font_size: 'Schrift verkleinern',
|
||||||
|
line_spacing: 'Zeilenabstand',
|
||||||
|
justify: 'Blocksatz',
|
||||||
|
bullet_list: 'Liste',
|
||||||
|
dots: 'Punkte',
|
||||||
|
dash: 'Striche',
|
||||||
|
numbers: 'Zahlen',
|
||||||
|
bold: 'Fett',
|
||||||
|
italic: 'Kursiv',
|
||||||
|
underline: 'Unterstrichen',
|
||||||
|
strikethrough: 'Durchgestrichen',
|
||||||
|
text_color: 'Textfarbe',
|
||||||
|
background_color: 'Hintergrundfarbe',
|
||||||
|
variable: 'Variable',
|
||||||
|
create_variable: 'Variable erstellen',
|
||||||
|
create_condition: 'Bedingung erstellen',
|
||||||
|
insert: 'Einfügen',
|
||||||
|
field: 'Feld',
|
||||||
|
table: 'Tabelle',
|
||||||
|
table_size: 'Tabellengröße',
|
||||||
|
align_center: 'Zentrieren',
|
||||||
|
insert_row_above: 'Zeile darüber',
|
||||||
|
insert_row_below: 'Zeile darunter',
|
||||||
|
insert_column_left: 'Spalte links',
|
||||||
|
insert_column_right: 'Spalte rechts',
|
||||||
|
merge_cells: 'Zellen verbinden',
|
||||||
|
split_cell: 'Zelle teilen',
|
||||||
|
delete_row: 'Zeile löschen',
|
||||||
|
delete_column: 'Spalte löschen',
|
||||||
|
delete_table: 'Tabelle löschen',
|
||||||
|
delete_image: 'Bild löschen',
|
||||||
|
irregular_table_structure: 'Zeilen und Spalten nicht bearbeitbar: unregelmäßige Tabelle'
|
||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
@@ -1773,7 +1977,41 @@ const nl = {
|
|||||||
no_revisions_yet: 'Nog geen revisies',
|
no_revisions_yet: 'Nog geen revisies',
|
||||||
viewing_revision_from: 'Revisie van {date} bekijken',
|
viewing_revision_from: 'Revisie van {date} bekijken',
|
||||||
connect_google_drive: 'Verbind Google Drive',
|
connect_google_drive: 'Verbind Google Drive',
|
||||||
submitting: 'Indienen'
|
submitting: 'Indienen',
|
||||||
|
font_size: 'Tekstgrootte',
|
||||||
|
increase_font_size: 'Tekst vergroten',
|
||||||
|
decrease_font_size: 'Tekst verkleinen',
|
||||||
|
line_spacing: 'Regelafstand',
|
||||||
|
justify: 'Uitvullen',
|
||||||
|
bullet_list: 'Lijst',
|
||||||
|
dots: 'Punten',
|
||||||
|
dash: 'Streepjes',
|
||||||
|
numbers: 'Nummers',
|
||||||
|
bold: 'Vet',
|
||||||
|
italic: 'Cursief',
|
||||||
|
underline: 'Onderstrepen',
|
||||||
|
strikethrough: 'Doorhalen',
|
||||||
|
text_color: 'Tekstkleur',
|
||||||
|
background_color: 'Achtergrondkleur',
|
||||||
|
variable: 'Variabele',
|
||||||
|
create_variable: 'Maak variabele',
|
||||||
|
create_condition: 'Maak voorwaarde',
|
||||||
|
insert: 'Invoegen',
|
||||||
|
field: 'Veld',
|
||||||
|
table: 'Tabel',
|
||||||
|
table_size: 'Tabelgrootte',
|
||||||
|
align_center: 'Centreren',
|
||||||
|
insert_row_above: 'Rij erboven',
|
||||||
|
insert_row_below: 'Rij eronder',
|
||||||
|
insert_column_left: 'Kolom links',
|
||||||
|
insert_column_right: 'Kolom rechts',
|
||||||
|
merge_cells: 'Cellen samenvoegen',
|
||||||
|
split_cell: 'Cel splitsen',
|
||||||
|
delete_row: 'Rij verwijderen',
|
||||||
|
delete_column: 'Kolom verwijderen',
|
||||||
|
delete_table: 'Tabel verwijderen',
|
||||||
|
delete_image: 'Afbeelding verwijderen',
|
||||||
|
irregular_table_structure: 'Rijen en kolommen niet bewerkbaar: onregelmatige tabel'
|
||||||
}
|
}
|
||||||
|
|
||||||
export { en, es, it, pt, fr, de, nl }
|
export { en, es, it, pt, fr, de, nl }
|
||||||
Reference in new issue
Block a user