mirror of
https://github.com/docusealco/docuseal
synced 2026-11-07 23:32:33 -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user