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>
|
||||
|
||||
<script>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import DynamicSection from './dynamic_section.vue'
|
||||
import { dynamicStylesheet, tiptapStylesheet } from './dynamic_editor.js'
|
||||
import { buildVariablesSchema, mergeSchemaProperties } from './dynamic_variables_schema.js'
|
||||
@@ -61,6 +61,11 @@ export default {
|
||||
DynamicSection
|
||||
},
|
||||
inject: ['baseFetch', 'template'],
|
||||
provide () {
|
||||
return {
|
||||
documentFonts: computed(() => this.documentFonts)
|
||||
}
|
||||
},
|
||||
props: {
|
||||
document: {
|
||||
type: Object,
|
||||
@@ -134,6 +139,40 @@ export default {
|
||||
styles () {
|
||||
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 () {
|
||||
if (this.isMounted) {
|
||||
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>
|
||||
<div
|
||||
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"
|
||||
:style="{ top: (coords.top - 42) + 'px', left: coords.left + 'px', transform: 'translateX(-50%)' }"
|
||||
ref="menu"
|
||||
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
|
||||
>
|
||||
<button
|
||||
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
||||
:class="isBold ? 'bg-base-200' : 'bg-transparent'"
|
||||
title="Bold"
|
||||
@click="toggleBold"
|
||||
>
|
||||
<IconBold
|
||||
:width="16"
|
||||
:height="16"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
||||
:class="isItalic ? 'bg-base-200' : 'bg-transparent'"
|
||||
title="Italic"
|
||||
@click="toggleItalic"
|
||||
>
|
||||
<IconItalic
|
||||
:width="16"
|
||||
:height="16"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
||||
:class="isUnderline ? 'bg-base-200' : 'bg-transparent'"
|
||||
title="Underline"
|
||||
@click="toggleUnderline"
|
||||
>
|
||||
<IconUnderline
|
||||
:width="16"
|
||||
:height="16"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center w-7 h-7 border-none rounded cursor-pointer text-gray-700"
|
||||
:class="isStrike ? 'bg-base-200' : 'bg-transparent'"
|
||||
title="Strikethrough"
|
||||
@click="toggleStrike"
|
||||
>
|
||||
<IconStrikethrough
|
||||
:width="16"
|
||||
:height="16"
|
||||
/>
|
||||
</button>
|
||||
<div class="w-px h-5 bg-base-300 mx-1" />
|
||||
<button
|
||||
class="inline-flex items-center justify-center text-xs h-7 border-none rounded cursor-pointer text-gray-700 bg-transparent"
|
||||
title="Wrap in variable"
|
||||
@click="wrapVariable"
|
||||
>
|
||||
<IconBracketsContain
|
||||
:width="16"
|
||||
:height="16"
|
||||
:stroke-width="1.6"
|
||||
/>
|
||||
<span class="px-0.5">
|
||||
Variable
|
||||
</span>
|
||||
</button>
|
||||
<div class="w-px h-5 bg-base-300 mx-1" />
|
||||
<button
|
||||
class="inline-flex items-center justify-center text-xs h-7 border-none rounded cursor-pointer text-gray-700 bg-transparent"
|
||||
title="Wrap in 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">
|
||||
Condition
|
||||
</span>
|
||||
</button>
|
||||
<div class="flex items-center gap-0.5">
|
||||
<div class="relative">
|
||||
<button
|
||||
class="flex items-center justify-between gap-1 w-36 h-7 px-2 border border-neutral-200 rounded-md hover:bg-neutral-100"
|
||||
:title="t('font')"
|
||||
@click="toggleDropdown('font')"
|
||||
>
|
||||
<span
|
||||
class="truncate"
|
||||
:style="{ fontFamily }"
|
||||
>{{ fontFamily }}</span>
|
||||
<IconChevronDown
|
||||
:width="14"
|
||||
:height="14"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
v-if="dropdown === 'font'"
|
||||
ref="dropdown"
|
||||
: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"
|
||||
>
|
||||
<template
|
||||
v-for="(group, index) in [documentFonts, standardFonts]"
|
||||
:key="index"
|
||||
>
|
||||
<div
|
||||
v-if="index && group.length && documentFonts.length"
|
||||
class="my-1 border-t border-neutral-200"
|
||||
/>
|
||||
<button
|
||||
v-for="font in group"
|
||||
:key="font.label"
|
||||
class="block w-full text-left px-2 py-1 rounded-md hover:bg-neutral-100 truncate"
|
||||
:class="{ 'bg-neutral-200': font.label === fontFamily }"
|
||||
:style="{ fontFamily: font.value }"
|
||||
@click="setSpanStyle('font-family', font.value)"
|
||||
>
|
||||
{{ font.label }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<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"
|
||||
:title="t('font_size')"
|
||||
@click="toggleDropdown('size')"
|
||||
>
|
||||
<span>{{ fontSize }}</span>
|
||||
<IconChevronDown
|
||||
:width="14"
|
||||
:height="14"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
v-if="dropdown === 'size'"
|
||||
ref="dropdown"
|
||||
:class="isDropdownAlignedRight ? 'right-0' : 'left-0'"
|
||||
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
|
||||
v-for="size in fontSizes"
|
||||
:key="size"
|
||||
class="block w-full text-left px-2 py-1 rounded-md hover:bg-neutral-100"
|
||||
:class="{ 'bg-neutral-200': size === fontSize }"
|
||||
@click="setFontSize(size)"
|
||||
>
|
||||
{{ size }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="inline-flex items-center justify-center w-7 h-7 rounded-md hover:bg-neutral-100"
|
||||
:title="t('increase_font_size')"
|
||||
@click="setFontSize(fontSizes.find((size) => size > fontSize) || fontSize)"
|
||||
>
|
||||
<IconTextIncrease
|
||||
:width="16"
|
||||
:height="16"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center w-7 h-7 rounded-md hover:bg-neutral-100"
|
||||
:title="t('decrease_font_size')"
|
||||
@click="setFontSize([...fontSizes].reverse().find((size) => size < fontSize) || fontSize)"
|
||||
>
|
||||
<IconTextDecrease
|
||||
:width="16"
|
||||
:height="16"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
name: 'DynamicMenu',
|
||||
@@ -110,7 +369,19 @@ export default {
|
||||
IconItalic,
|
||||
IconUnderline,
|
||||
IconStrikethrough,
|
||||
IconBracketsContain
|
||||
IconBracketsContain,
|
||||
IconChevronDown,
|
||||
IconTextIncrease,
|
||||
IconTextDecrease,
|
||||
IconLineHeight,
|
||||
IconLetterA,
|
||||
IconHighlight,
|
||||
IconList,
|
||||
IconX
|
||||
},
|
||||
inject: {
|
||||
t: 't',
|
||||
documentFonts: { default: () => [] }
|
||||
},
|
||||
props: {
|
||||
editor: {
|
||||
@@ -127,15 +398,83 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
isMouseDown: false,
|
||||
isBold: this.editor.isActive('bold'),
|
||||
isItalic: this.editor.isActive('italic'),
|
||||
isUnderline: this.editor.isActive('underline'),
|
||||
isStrike: this.editor.isActive('strike')
|
||||
dropdown: null,
|
||||
isDropdownAlignedRight: false,
|
||||
menuWidth: 0,
|
||||
isBold: false,
|
||||
isItalic: false,
|
||||
isUnderline: false,
|
||||
isStrike: false,
|
||||
fontFamily: '',
|
||||
fontSize: 12,
|
||||
color: '',
|
||||
backgroundColor: '',
|
||||
lineHeight: '',
|
||||
textAlign: 'left',
|
||||
listStyle: null,
|
||||
isCellSelection: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
visible () {
|
||||
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 () {
|
||||
@@ -154,11 +493,72 @@ export default {
|
||||
document.removeEventListener('mouseup', this.onMouseUp)
|
||||
},
|
||||
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 () {
|
||||
this.editor.chain().focus().toggleBold().run()
|
||||
this.toggleStyledMark('bold', 'font-weight', 'bold', this.isBold, (style) => parseInt(style.fontWeight) >= 600)
|
||||
},
|
||||
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 () {
|
||||
this.editor.chain().focus().toggleUnderline().run()
|
||||
@@ -166,6 +566,28 @@ export default {
|
||||
toggleStrike () {
|
||||
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 () {
|
||||
const { from, to } = this.editor.state.selection
|
||||
const replacement = '[[variable]]'
|
||||
@@ -201,10 +623,62 @@ export default {
|
||||
}, 1)
|
||||
},
|
||||
onTransaction () {
|
||||
this.isBold = this.editor.isActive('bold')
|
||||
this.isItalic = this.editor.isActive('italic')
|
||||
this.isUnderline = this.editor.isActive('underline')
|
||||
this.isStrike = this.editor.isActive('strike')
|
||||
if (this.visible) {
|
||||
this.readState()
|
||||
}
|
||||
},
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
ref="imageInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="hidden"
|
||||
@change="onImageSelected"
|
||||
>
|
||||
<Teleport
|
||||
v-if="editor"
|
||||
:to="container"
|
||||
@@ -45,6 +52,21 @@
|
||||
@add-variable="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
|
||||
v-if="contextMenu && contextMenuField"
|
||||
:context-menu="contextMenu"
|
||||
@@ -64,16 +86,21 @@
|
||||
<script>
|
||||
import { shallowRef } from 'vue'
|
||||
import { DOMSerializer, Fragment } from '@tiptap/pm/model'
|
||||
import { findTable } from '@tiptap/pm/tables'
|
||||
import { v4 } from 'uuid'
|
||||
import FieldContextMenu from './field_context_menu.vue'
|
||||
import AreaTitle from './area_title.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 {
|
||||
name: 'DynamicSection',
|
||||
components: {
|
||||
DynamicMenu,
|
||||
DynamicInsertMenu,
|
||||
DynamicBlockMenu,
|
||||
FieldContextMenu,
|
||||
AreaTitle
|
||||
},
|
||||
@@ -153,6 +180,8 @@ export default {
|
||||
isAreaDrag: false,
|
||||
areaToolbarCoords: null,
|
||||
dynamicMenuCoords: null,
|
||||
insertMenuCoords: null,
|
||||
blockMenuCoords: null,
|
||||
contextMenu: null,
|
||||
cursorHighlightCoords: null
|
||||
}
|
||||
@@ -235,6 +264,14 @@ export default {
|
||||
if (this.dynamicMenuCoords && this.editor && !this.editor.state.selection.empty) {
|
||||
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 () {
|
||||
@@ -258,9 +295,20 @@ export default {
|
||||
element: this.$refs.editorElement,
|
||||
editable: this.editable,
|
||||
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,
|
||||
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.insertMenuCoords = null
|
||||
this.blockMenuCoords = null
|
||||
} else {
|
||||
this.areaToolbarCoords = null
|
||||
this.selectedAreasRef.value = []
|
||||
|
||||
if (editor.state.selection.empty) {
|
||||
this.setBlockMenuCoords(editor)
|
||||
|
||||
if (selection.node) {
|
||||
this.dynamicMenuCoords = null
|
||||
this.insertMenuCoords = null
|
||||
} else if (selection.empty) {
|
||||
this.dynamicMenuCoords = null
|
||||
|
||||
if (editor.isFocused) {
|
||||
this.setInsertMenuCoords(editor)
|
||||
}
|
||||
} 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) {
|
||||
const { from, to } = editor.state.selection
|
||||
const view = editor.view
|
||||
const start = view.coordsAtPos(from)
|
||||
const end = view.coordsAtPos(to)
|
||||
const containerRect = this.container.getBoundingClientRect()
|
||||
const left = (start.left + end.right) / 2 - containerRect.left
|
||||
|
||||
this.dynamicMenuCoords = {
|
||||
top: Math.min(start.top, end.top) - containerRect.top,
|
||||
left: Math.max(80, Math.min(left, containerRect.width - 80))
|
||||
top: Math.max(start.bottom, end.bottom) - containerRect.top + 8,
|
||||
left: (start.left + end.right) / 2 - containerRect.left,
|
||||
width: containerRect.width
|
||||
}
|
||||
},
|
||||
onFieldDestroy (node) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
||||
@import "dynamic_lists";
|
||||
|
||||
*,
|
||||
::before,
|
||||
|
||||
@@ -249,7 +249,41 @@ const en = {
|
||||
no_revisions_yet: 'No revisions yet',
|
||||
viewing_revision_from: 'Viewing revision from {date}',
|
||||
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 = {
|
||||
@@ -503,7 +537,41 @@ const es = {
|
||||
no_revisions_yet: 'Aún no hay revisiones',
|
||||
viewing_revision_from: 'Viendo revisión del {date}',
|
||||
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 = {
|
||||
@@ -757,7 +825,41 @@ const it = {
|
||||
no_revisions_yet: 'Nessuna revisione ancora',
|
||||
viewing_revision_from: 'Visualizzazione revisione del {date}',
|
||||
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 = {
|
||||
@@ -1011,7 +1113,41 @@ const pt = {
|
||||
no_revisions_yet: 'Nenhuma revisão ainda',
|
||||
viewing_revision_from: 'Visualizando revisão de {date}',
|
||||
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 = {
|
||||
@@ -1265,7 +1401,41 @@ const fr = {
|
||||
no_revisions_yet: 'Aucune révision pour le moment',
|
||||
viewing_revision_from: 'Affichage de la révision du {date}',
|
||||
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 = {
|
||||
@@ -1519,7 +1689,41 @@ const de = {
|
||||
no_revisions_yet: 'Noch keine Revisionen',
|
||||
viewing_revision_from: 'Ansicht der Revision vom {date}',
|
||||
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 = {
|
||||
@@ -1773,7 +1977,41 @@ const nl = {
|
||||
no_revisions_yet: 'Nog geen revisies',
|
||||
viewing_revision_from: 'Revisie van {date} bekijken',
|
||||
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 }
|
||||
Reference in new issue
Block a user