@bagelink/vue 1.19.1 → 1.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Dropdown.vue.d.ts +73 -48
- package/dist/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
- package/dist/components/dataTable/DataTable.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts +1 -0
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/config.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts +6 -0
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/richTextTypes.d.ts +1 -0
- package/dist/components/form/inputs/RichText/richTextTypes.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/contentMetrics.d.ts +7 -0
- package/dist/components/form/inputs/RichText/utils/contentMetrics.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/image.d.ts +17 -0
- package/dist/components/form/inputs/RichText/utils/image.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/sanitizePaste.d.ts +15 -0
- package/dist/components/form/inputs/RichText/utils/sanitizePaste.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/selection.d.ts +1 -1
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/video.d.ts +23 -0
- package/dist/components/form/inputs/RichText/utils/video.d.ts.map +1 -0
- package/dist/components/form/inputs/SelectBtn.vue.d.ts +28 -1
- package/dist/components/form/inputs/SelectBtn.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts +2 -0
- package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/Upload/useFileUpload.d.ts +1 -0
- package/dist/components/form/inputs/Upload/useFileUpload.d.ts.map +1 -1
- package/dist/core.css +2 -2
- package/dist/index.cjs +218 -130
- package/dist/index.css +1 -1
- package/dist/index.mjs +18604 -17549
- package/dist/style.css +2 -2
- package/package.json +1 -1
- package/src/components/Dropdown.vue +48 -7
- package/src/components/FilterQuery.vue +1 -1
- package/src/components/Icon/Icon.vue +8 -0
- package/src/components/dataTable/DataTable.vue +6 -3
- package/src/components/form/inputs/RichText/components/EditorToolbar.vue +238 -8
- package/src/components/form/inputs/RichText/composables/useCommands.ts +23 -5
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +28 -0
- package/src/components/form/inputs/RichText/config.ts +17 -14
- package/src/components/form/inputs/RichText/index.vue +354 -463
- package/src/components/form/inputs/RichText/richTextTypes.ts +1 -0
- package/src/components/form/inputs/RichText/utils/commands.ts +437 -24
- package/src/components/form/inputs/RichText/utils/contentMetrics.ts +24 -0
- package/src/components/form/inputs/RichText/utils/image.ts +136 -0
- package/src/components/form/inputs/RichText/utils/sanitizePaste.ts +110 -0
- package/src/components/form/inputs/RichText/utils/selection.ts +32 -0
- package/src/components/form/inputs/RichText/utils/table.ts +77 -8
- package/src/components/form/inputs/RichText/utils/video.ts +270 -0
- package/src/components/form/inputs/SelectBtn.vue +126 -15
- package/src/components/form/inputs/TelInput.vue +1 -1
- package/src/components/form/inputs/Upload/UploadInput.vue +3 -2
- package/src/components/form/inputs/Upload/useFileUpload.ts +2 -0
- package/src/styles/colors.css +5 -2
- package/src/styles/core.css +1 -0
- package/src/styles/rich-content.css +109 -0
package/package.json
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<!-- prettier-ignore-start -->
|
|
2
2
|
<!-- eslint-disable -->
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
// Shared across instances so opening a grouped dropdown closes its siblings.
|
|
5
|
+
const groupRegistry = new Map<string, Set<() => void>>()
|
|
6
|
+
</script>
|
|
7
|
+
|
|
3
8
|
<script setup lang="ts">
|
|
4
9
|
import type { VirtualElement } from '@floating-ui/dom'
|
|
5
10
|
import type { IconType, ThemeType } from '../index'
|
|
@@ -118,9 +123,6 @@ const resolvedDelay = computed((): { show: number; hide: number } => {
|
|
|
118
123
|
return { show: 0, hide: 0 }
|
|
119
124
|
})
|
|
120
125
|
|
|
121
|
-
// ShowGroup registry — shared across all Dropdown instances on the page
|
|
122
|
-
const groupRegistry = new Map<string, Set<() => void>>()
|
|
123
|
-
|
|
124
126
|
function toVirtualElement(pt: AnchorPoint): VirtualElement {
|
|
125
127
|
const width = pt.width ?? 0
|
|
126
128
|
const height = pt.height ?? 0
|
|
@@ -347,6 +349,12 @@ function onDocumentPointerDown(e: PointerEvent) {
|
|
|
347
349
|
internalHide({ restoreFocus: false })
|
|
348
350
|
}
|
|
349
351
|
|
|
352
|
+
function onEmbeddedDocumentPointerDown() {
|
|
353
|
+
if (props.autoHide && isOpen()) {
|
|
354
|
+
internalHide({ restoreFocus: false })
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
350
358
|
// Esc key — LIFO via shared composable (nested layers close first)
|
|
351
359
|
useEscapeKey(() => internalHide(), () => isOpen())
|
|
352
360
|
|
|
@@ -355,13 +363,22 @@ let showTimer: ReturnType<typeof setTimeout> | undefined
|
|
|
355
363
|
let hideTimer: ReturnType<typeof setTimeout> | undefined
|
|
356
364
|
function clearTimers() { clearTimeout(showTimer); clearTimeout(hideTimer) }
|
|
357
365
|
|
|
366
|
+
function hideForGroup() {
|
|
367
|
+
clearTimers()
|
|
368
|
+
internalHide({ restoreFocus: false })
|
|
369
|
+
}
|
|
370
|
+
|
|
358
371
|
let lastToggleAt = 0
|
|
359
372
|
function toggle(viaKeyboard = false) {
|
|
360
373
|
const now = Date.now()
|
|
361
374
|
if (now - lastToggleAt < 50) return
|
|
362
375
|
lastToggleAt = now
|
|
363
376
|
openedViaKeyboard = viaKeyboard
|
|
364
|
-
isOpen()
|
|
377
|
+
if (isOpen()) {
|
|
378
|
+
internalHide()
|
|
379
|
+
} else {
|
|
380
|
+
internalShow()
|
|
381
|
+
}
|
|
365
382
|
}
|
|
366
383
|
|
|
367
384
|
function onTriggerClick(e?: MouseEvent) {
|
|
@@ -425,10 +442,33 @@ function onScroll() {
|
|
|
425
442
|
}
|
|
426
443
|
|
|
427
444
|
let openListenersActive = false
|
|
445
|
+
const embeddedDocuments = new Set<Document>()
|
|
446
|
+
|
|
447
|
+
function addEmbeddedDocumentListeners() {
|
|
448
|
+
document.querySelectorAll('iframe').forEach((iframe) => {
|
|
449
|
+
try {
|
|
450
|
+
const embeddedDocument = iframe.contentDocument
|
|
451
|
+
if (!embeddedDocument || embeddedDocuments.has(embeddedDocument)) return
|
|
452
|
+
embeddedDocument.addEventListener('pointerdown', onEmbeddedDocumentPointerDown, true)
|
|
453
|
+
embeddedDocuments.add(embeddedDocument)
|
|
454
|
+
} catch {
|
|
455
|
+
// Cross-origin iframes are not inspectable; the browser owns their focus boundary.
|
|
456
|
+
}
|
|
457
|
+
})
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function removeEmbeddedDocumentListeners() {
|
|
461
|
+
embeddedDocuments.forEach((embeddedDocument) => {
|
|
462
|
+
embeddedDocument.removeEventListener('pointerdown', onEmbeddedDocumentPointerDown, true)
|
|
463
|
+
})
|
|
464
|
+
embeddedDocuments.clear()
|
|
465
|
+
}
|
|
466
|
+
|
|
428
467
|
function addOpenListeners() {
|
|
429
468
|
if (openListenersActive) return
|
|
430
469
|
document.addEventListener('pointerdown', onDocumentPointerDown, true)
|
|
431
470
|
document.addEventListener('scroll', onScroll, { capture: true, passive: true })
|
|
471
|
+
addEmbeddedDocumentListeners()
|
|
432
472
|
openListenersActive = true
|
|
433
473
|
}
|
|
434
474
|
|
|
@@ -436,13 +476,14 @@ function removeOpenListeners() {
|
|
|
436
476
|
if (!openListenersActive) return
|
|
437
477
|
document.removeEventListener('pointerdown', onDocumentPointerDown, true)
|
|
438
478
|
document.removeEventListener('scroll', onScroll, true)
|
|
479
|
+
removeEmbeddedDocumentListeners()
|
|
439
480
|
openListenersActive = false
|
|
440
481
|
}
|
|
441
482
|
|
|
442
483
|
onMounted(() => {
|
|
443
484
|
if (props.showGroup) {
|
|
444
485
|
if (!groupRegistry.has(props.showGroup)) groupRegistry.set(props.showGroup, new Set())
|
|
445
|
-
groupRegistry.get(props.showGroup)!.add(
|
|
486
|
+
groupRegistry.get(props.showGroup)!.add(hideForGroup)
|
|
446
487
|
}
|
|
447
488
|
if (shown.value) internalShow()
|
|
448
489
|
})
|
|
@@ -450,7 +491,7 @@ onMounted(() => {
|
|
|
450
491
|
onBeforeUnmount(() => {
|
|
451
492
|
removeOpenListeners()
|
|
452
493
|
clearTimers()
|
|
453
|
-
if (props.showGroup) groupRegistry.get(props.showGroup)?.delete(
|
|
494
|
+
if (props.showGroup) groupRegistry.get(props.showGroup)?.delete(hideForGroup)
|
|
454
495
|
if (isOpen()) popoverRef.value?.hidePopover()
|
|
455
496
|
})
|
|
456
497
|
|
|
@@ -482,7 +523,7 @@ defineExpose({ show, hide, toggle, openAt, updatePosition, shown })
|
|
|
482
523
|
</div>
|
|
483
524
|
|
|
484
525
|
<!-- Popup — teleported to closest dialog[open] or body, promoted to top layer via popover -->
|
|
485
|
-
<Teleport :to="teleportTarget">
|
|
526
|
+
<Teleport v-if="hasOpened" :to="teleportTarget">
|
|
486
527
|
<div
|
|
487
528
|
v-if="hasOpened"
|
|
488
529
|
:id="bglId" ref="popoverRef" popover="manual" class="bgl-dropdown m-0 p-0 border-none"
|
|
@@ -489,7 +489,7 @@ function onConnectorChange(id: string, connector: LogicalOperator) {
|
|
|
489
489
|
<TransitionGroup name="condition">
|
|
490
490
|
<div
|
|
491
491
|
v-for="(condition, index) in conditions" :key="condition.id"
|
|
492
|
-
class="grid gap-025 align-items-center
|
|
492
|
+
class="grid gap-025 align-items-center py-025" :class="{
|
|
493
493
|
'': index > 0 && condition.connector === 'or',
|
|
494
494
|
'pt-075 border-top-or': index > 0 && condition.connector === 'or',
|
|
495
495
|
'first-filter-row': index === 0,
|
|
@@ -4,6 +4,8 @@ import { useDevice } from '../../index'
|
|
|
4
4
|
import { computed, ref, onMounted, watchEffect } from 'vue'
|
|
5
5
|
import { MATERIAL_ICONS, FONT_AWESOME_BRANDS_ICONS } from './constants'
|
|
6
6
|
|
|
7
|
+
defineOptions({ inheritAttrs: false })
|
|
8
|
+
|
|
7
9
|
type AnimateType = 'spin' | 'pulse' | 'bounce' | 'fade-in' | 'slide-up' | 'rotate-in'
|
|
8
10
|
type TransitionType = 'slide-up' | 'rotate' | 'scale' | 'fade'
|
|
9
11
|
|
|
@@ -192,6 +194,7 @@ const iconStyle = computed(() => {
|
|
|
192
194
|
<template>
|
|
193
195
|
<Transition :name="transitionName" mode="out-in">
|
|
194
196
|
<span
|
|
197
|
+
v-bind="$attrs"
|
|
195
198
|
v-if="iconRenderType === 'material'" :key="`material-${iconRender}`" class="bgl_icon-font notranslate"
|
|
196
199
|
:class="[animateClass, roundClass, roundPairClass, softClass, { 'color-white keep-white': round && !soft && isRawCssColor }]"
|
|
197
200
|
:style="iconStyle" aria-hidden="true" translate="no"
|
|
@@ -199,6 +202,7 @@ const iconStyle = computed(() => {
|
|
|
199
202
|
{{ iconRender }}
|
|
200
203
|
</span>
|
|
201
204
|
<span
|
|
205
|
+
v-bind="$attrs"
|
|
202
206
|
v-else-if="iconRenderType === 'font-awesome-brand'" :key="`font-awesome-brand-${iconRender}`" class="fa fa-brands bgl_icon-font notranslate" :class="[
|
|
203
207
|
`fa-${iconRender}`,
|
|
204
208
|
animateClass,
|
|
@@ -250,6 +254,10 @@ const iconStyle = computed(() => {
|
|
|
250
254
|
font-family: 'Font Awesome 6 Brands', serif !important;
|
|
251
255
|
}
|
|
252
256
|
|
|
257
|
+
.bgl_icon-font.fa-brands::before {
|
|
258
|
+
content: var(--fa);
|
|
259
|
+
}
|
|
260
|
+
|
|
253
261
|
/* ── Entrance / loop animations (opt-in via `animate`) ─────────────────────── */
|
|
254
262
|
.bgl_icon--spin,
|
|
255
263
|
.bgl_icon--pulse,
|
|
@@ -208,7 +208,10 @@ const headerFontSize = computed(() => {
|
|
|
208
208
|
<tr
|
|
209
209
|
v-for="{ data: row } in list" :key="row?.id || `row-${Math.random()}`"
|
|
210
210
|
class="row row-item position-relative"
|
|
211
|
-
:class="{
|
|
211
|
+
:class="{
|
|
212
|
+
selected: row?.id != null
|
|
213
|
+
&& computedSelectedItems.map(String).includes(String(row.id)),
|
|
214
|
+
}"
|
|
212
215
|
@click="(event) => toggleSelectItem(row, event)"
|
|
213
216
|
>
|
|
214
217
|
<td v-if="isSortable" class="col-drag">
|
|
@@ -295,11 +298,11 @@ min-height: 150px;
|
|
|
295
298
|
overflow-anchor: none;
|
|
296
299
|
}
|
|
297
300
|
|
|
298
|
-
.selected {
|
|
301
|
+
.selected > td {
|
|
299
302
|
background: var(--bgl-primary-tint);
|
|
300
303
|
}
|
|
301
304
|
|
|
302
|
-
tbody tr.selected:hover {
|
|
305
|
+
tbody tr.selected:hover > td {
|
|
303
306
|
background: var(--bgl-primary-light);
|
|
304
307
|
}
|
|
305
308
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import type { ToolbarConfig, ToolbarConfigOption, ToolbarOption } from '../richTextTypes'
|
|
3
|
-
import { Btn, Divider, Dropdown } from '../../../../../index'
|
|
3
|
+
import { Btn, ColorInput, Divider, Dropdown } from '../../../../../index'
|
|
4
4
|
import { basicToolbarConfig, toolbarOptions } from '../config'
|
|
5
5
|
import TableGridSelector from './TableGridSelector.vue'
|
|
6
|
+
import { onBeforeUnmount, ref } from 'vue'
|
|
6
7
|
|
|
7
8
|
const {
|
|
8
9
|
config = basicToolbarConfig,
|
|
@@ -14,7 +15,8 @@ const {
|
|
|
14
15
|
hideAlignment = false,
|
|
15
16
|
hideDirections = false,
|
|
16
17
|
hideH5H6 = false,
|
|
17
|
-
hide = []
|
|
18
|
+
hide = [],
|
|
19
|
+
documentColors = [],
|
|
18
20
|
} = defineProps<{
|
|
19
21
|
config?: ToolbarConfig
|
|
20
22
|
selectedStyles: Set<string>
|
|
@@ -26,8 +28,23 @@ const {
|
|
|
26
28
|
hideDirections?: boolean
|
|
27
29
|
hideH5H6?: boolean
|
|
28
30
|
hide?: string[]
|
|
31
|
+
documentColors?: string[]
|
|
29
32
|
}>()
|
|
30
33
|
const emit = defineEmits(['action'])
|
|
34
|
+
const fontColor = ref('#000000')
|
|
35
|
+
const backgroundColor = ref('#fff59d')
|
|
36
|
+
const basicColors = [
|
|
37
|
+
'#000000',
|
|
38
|
+
'#ffffff',
|
|
39
|
+
'#6b7280',
|
|
40
|
+
'#ef4444',
|
|
41
|
+
'#f97316',
|
|
42
|
+
'#eab308',
|
|
43
|
+
'#22c55e',
|
|
44
|
+
'#3b82f6',
|
|
45
|
+
'#8b5cf6',
|
|
46
|
+
]
|
|
47
|
+
let colorCloseTimer: ReturnType<typeof setTimeout> | null = null
|
|
31
48
|
|
|
32
49
|
function getCurrentAlignmentIcon(): string {
|
|
33
50
|
if (selectedStyles.has('alignLeft')) { return 'format_align_left' }
|
|
@@ -58,8 +75,8 @@ function shouldHideAction(actionName: string): boolean {
|
|
|
58
75
|
ol: ['orderedList'],
|
|
59
76
|
splitView: ['splitView'],
|
|
60
77
|
p: ['p'],
|
|
61
|
-
align: ['alignMenu', 'alignLeft', 'alignCenter', 'alignRight', 'alignJustify'],
|
|
62
|
-
alignment: ['alignMenu', 'alignLeft', 'alignCenter', 'alignRight', 'alignJustify']
|
|
78
|
+
align: ['alignMenu', 'paragraphToolsMenu', 'alignLeft', 'alignCenter', 'alignRight', 'alignJustify'],
|
|
79
|
+
alignment: ['alignMenu', 'paragraphToolsMenu', 'alignLeft', 'alignCenter', 'alignRight', 'alignJustify']
|
|
63
80
|
}
|
|
64
81
|
|
|
65
82
|
for (const [hideKey, actionNames] of Object.entries(alternativeNames)) {
|
|
@@ -77,6 +94,7 @@ function shouldHideAction(actionName: string): boolean {
|
|
|
77
94
|
embed: hideEmbeds,
|
|
78
95
|
insertTable: hideTables,
|
|
79
96
|
alignMenu: hideAlignment,
|
|
97
|
+
paragraphToolsMenu: hideAlignment,
|
|
80
98
|
alignLeft: hideAlignment,
|
|
81
99
|
alignCenter: hideAlignment,
|
|
82
100
|
alignRight: hideAlignment,
|
|
@@ -93,6 +111,41 @@ function shouldHideAction(actionName: string): boolean {
|
|
|
93
111
|
|
|
94
112
|
const configToOption = (action: ToolbarConfigOption) => toolbarOptions.find(option => option.name === action) as ToolbarOption
|
|
95
113
|
|
|
114
|
+
function getActionLabel(action: ToolbarOption): string {
|
|
115
|
+
const state = selectedStyles.has(action.name) ? 'Active' : 'Inactive'
|
|
116
|
+
return [action.label || action.name, action.shortcut, state].filter(Boolean).join(' · ')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getToolbarControls(toolbar: HTMLElement): HTMLElement[] {
|
|
120
|
+
return Array.from(toolbar.querySelectorAll<HTMLElement>(
|
|
121
|
+
'button:not(:disabled), [role="button"]:not([aria-disabled="true"]), input:not(:disabled), select:not(:disabled)'
|
|
122
|
+
)).filter(element => element.offsetParent !== null || element.getClientRects().length > 0)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function handleToolbarKeydown(event: KeyboardEvent) {
|
|
126
|
+
if (!['ArrowRight', 'ArrowLeft', 'Home', 'End'].includes(event.key)) { return }
|
|
127
|
+
const toolbar = event.currentTarget as HTMLElement
|
|
128
|
+
const controls = getToolbarControls(toolbar)
|
|
129
|
+
if (!controls.length) { return }
|
|
130
|
+
|
|
131
|
+
const currentIndex = controls.indexOf(event.target as HTMLElement)
|
|
132
|
+
if (currentIndex < 0) { return }
|
|
133
|
+
|
|
134
|
+
event.preventDefault()
|
|
135
|
+
const isRtl = getComputedStyle(toolbar).direction === 'rtl'
|
|
136
|
+
let nextIndex = currentIndex
|
|
137
|
+
if (event.key === 'Home') {
|
|
138
|
+
nextIndex = 0
|
|
139
|
+
} else if (event.key === 'End') {
|
|
140
|
+
nextIndex = controls.length - 1
|
|
141
|
+
} else {
|
|
142
|
+
const visualDirection = event.key === 'ArrowRight' ? 1 : -1
|
|
143
|
+
const direction = isRtl ? -visualDirection : visualDirection
|
|
144
|
+
nextIndex = (currentIndex + direction + controls.length) % controls.length
|
|
145
|
+
}
|
|
146
|
+
controls[nextIndex]?.focus()
|
|
147
|
+
}
|
|
148
|
+
|
|
96
149
|
function shouldShowSeparator(currentIndex: number): boolean {
|
|
97
150
|
const allActions = config.map(configToOption).filter(Boolean)
|
|
98
151
|
|
|
@@ -139,14 +192,122 @@ function handleQuickTableInsert(rows: number, cols: number) {
|
|
|
139
192
|
function handleOpenAdvanced() {
|
|
140
193
|
emit('action', 'insertTable')
|
|
141
194
|
}
|
|
195
|
+
|
|
196
|
+
function scheduleColorMenuClose(closeMenu: () => void) {
|
|
197
|
+
if (colorCloseTimer) {
|
|
198
|
+
clearTimeout(colorCloseTimer)
|
|
199
|
+
}
|
|
200
|
+
colorCloseTimer = setTimeout(() => {
|
|
201
|
+
closeMenu()
|
|
202
|
+
colorCloseTimer = null
|
|
203
|
+
}, 4000)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function applyColor(command: 'fontColor' | 'bgColor', value: string, closeMenu: () => void) {
|
|
207
|
+
if (value) {
|
|
208
|
+
runAction(command, value)
|
|
209
|
+
}
|
|
210
|
+
scheduleColorMenuClose(closeMenu)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function applyRecentColor(command: string, value: string, closeMenu: () => void) {
|
|
214
|
+
if (command !== 'fontColor' && command !== 'bgColor') { return }
|
|
215
|
+
if (command === 'fontColor') {
|
|
216
|
+
fontColor.value = value
|
|
217
|
+
} else {
|
|
218
|
+
backgroundColor.value = value
|
|
219
|
+
}
|
|
220
|
+
applyColor(command, value, closeMenu)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
onBeforeUnmount(() => {
|
|
224
|
+
if (colorCloseTimer) {
|
|
225
|
+
clearTimeout(colorCloseTimer)
|
|
226
|
+
}
|
|
227
|
+
})
|
|
142
228
|
</script>
|
|
143
229
|
|
|
144
230
|
<template>
|
|
145
|
-
<div
|
|
231
|
+
<div
|
|
232
|
+
class="toolbar flex gap-025 pb-05 flex-wrap m_flex-wrap-nowrap overflow-x"
|
|
233
|
+
role="toolbar"
|
|
234
|
+
aria-label="Rich text formatting"
|
|
235
|
+
style="position: relative;"
|
|
236
|
+
@keydown="handleToolbarKeydown"
|
|
237
|
+
>
|
|
146
238
|
<template v-for="(action, index) in config.map(configToOption).filter(Boolean)" :key="index">
|
|
147
239
|
<!-- Tables -->
|
|
148
240
|
<TableGridSelector v-if="action.name === 'insertTable' && !shouldHideAction('insertTable')" @insert="handleQuickTableInsert" @open-advanced="handleOpenAdvanced" />
|
|
149
241
|
|
|
242
|
+
<!-- Table editing tools -->
|
|
243
|
+
<Dropdown
|
|
244
|
+
v-else-if="action.name === 'tableToolsMenu' && !hideTables"
|
|
245
|
+
placement="bottom-start"
|
|
246
|
+
thin
|
|
247
|
+
flat
|
|
248
|
+
icon="table_edit"
|
|
249
|
+
aria-label="Table tools"
|
|
250
|
+
:disabled="!selectedStyles.has('tableContext')"
|
|
251
|
+
:class="{ 'opacity-4': !selectedStyles.has('tableContext') }"
|
|
252
|
+
:title="selectedStyles.has('tableContext') ? 'Table tools' : 'Select a table cell to use table tools'"
|
|
253
|
+
class="table-tools-menu"
|
|
254
|
+
>
|
|
255
|
+
<template #default="{ hide: closeMenu }">
|
|
256
|
+
<div class="grid min-w-220px p-025">
|
|
257
|
+
<span class="txt12 semi color-gray px-05 py-025">Table tools</span>
|
|
258
|
+
<Btn thin flat full-width align-txt="start" icon="table_chart" value="Merge selected cells" @click="runAction('mergeCells'); closeMenu()" />
|
|
259
|
+
<Btn thin flat full-width align-txt="start" icon="dashboard" value="Split cell" @click="runAction('splitCells'); closeMenu()" />
|
|
260
|
+
<Divider class="my-025" />
|
|
261
|
+
<Btn thin flat full-width align-txt="start" icon="add_row_above" value="Insert row above" @click="runAction('addRowBefore'); closeMenu()" />
|
|
262
|
+
<Btn thin flat full-width align-txt="start" icon="add_row_below" value="Insert row below" @click="runAction('addRowAfter'); closeMenu()" />
|
|
263
|
+
<Btn thin flat full-width align-txt="start" icon="remove" value="Delete row" @click="runAction('deleteRow'); closeMenu()" />
|
|
264
|
+
<Divider class="my-025" />
|
|
265
|
+
<Btn thin flat full-width align-txt="start" icon="add_column_left" value="Insert column left" @click="runAction('insertColumnLeft'); closeMenu()" />
|
|
266
|
+
<Btn thin flat full-width align-txt="start" icon="add_column_right" value="Insert column right" @click="runAction('insertColumnRight'); closeMenu()" />
|
|
267
|
+
<Btn thin flat full-width align-txt="start" icon="delete" value="Delete column" @click="runAction('deleteColumn'); closeMenu()" />
|
|
268
|
+
<Divider class="my-025" />
|
|
269
|
+
<span class="txt12 semi color-gray px-05 py-025">Column alignment</span>
|
|
270
|
+
<div class="flex gap-025 px-025 pb-025">
|
|
271
|
+
<Btn thin flat icon="format_align_left" aria-label="Align table column left" @click="runAction('tableAlignLeft'); closeMenu()" />
|
|
272
|
+
<Btn thin flat icon="format_align_center" aria-label="Align table column center" @click="runAction('tableAlignCenter'); closeMenu()" />
|
|
273
|
+
<Btn thin flat icon="format_align_right" aria-label="Align table column right" @click="runAction('tableAlignRight'); closeMenu()" />
|
|
274
|
+
<Btn thin flat icon="format_align_justify" aria-label="Justify table column" @click="runAction('tableAlignJustify'); closeMenu()" />
|
|
275
|
+
</div>
|
|
276
|
+
<Divider class="my-025" />
|
|
277
|
+
<Btn thin flat full-width align-txt="start" color="red" icon="delete_sweep" value="Delete table" @click="runAction('deleteTable'); closeMenu()" />
|
|
278
|
+
</div>
|
|
279
|
+
</template>
|
|
280
|
+
</Dropdown>
|
|
281
|
+
|
|
282
|
+
<!-- Paragraph alignment and direction -->
|
|
283
|
+
<Dropdown
|
|
284
|
+
v-else-if="action.name === 'paragraphToolsMenu' && !shouldHideAction('paragraphToolsMenu')"
|
|
285
|
+
placement="bottom-start"
|
|
286
|
+
thin
|
|
287
|
+
flat
|
|
288
|
+
:icon="getCurrentAlignmentIcon()"
|
|
289
|
+
:class="{ 'alignment-active': isAlignmentActive() }"
|
|
290
|
+
aria-label="Paragraph tools"
|
|
291
|
+
>
|
|
292
|
+
<template #default="{ hide: closeMenu }">
|
|
293
|
+
<div class="grid min-w-220px p-025">
|
|
294
|
+
<span class="txt12 semi color-gray px-05 py-025">Paragraph alignment</span>
|
|
295
|
+
<div class="flex gap-025 px-025 pb-025">
|
|
296
|
+
<Btn thin flat icon="format_align_left" aria-label="Align paragraph left" :class="{ active: selectedStyles.has('alignLeft') }" @click="runAction('alignLeft'); closeMenu()" />
|
|
297
|
+
<Btn thin flat icon="format_align_center" aria-label="Align paragraph center" :class="{ active: selectedStyles.has('alignCenter') }" @click="runAction('alignCenter'); closeMenu()" />
|
|
298
|
+
<Btn thin flat icon="format_align_right" aria-label="Align paragraph right" :class="{ active: selectedStyles.has('alignRight') }" @click="runAction('alignRight'); closeMenu()" />
|
|
299
|
+
<Btn thin flat icon="format_align_justify" aria-label="Justify paragraph" :class="{ active: selectedStyles.has('alignJustify') }" @click="runAction('alignJustify'); closeMenu()" />
|
|
300
|
+
</div>
|
|
301
|
+
<Divider class="my-025" />
|
|
302
|
+
<span class="txt12 semi color-gray px-05 py-025">Writing direction</span>
|
|
303
|
+
<div class="flex gap-025 px-025 pb-025">
|
|
304
|
+
<Btn thin flat icon="format_textdirection_l_to_r" value="LTR" aria-label="Left to right" :class="{ active: selectedStyles.has('ltrDirection') }" @click="runAction('ltrDirection'); closeMenu()" />
|
|
305
|
+
<Btn thin flat icon="format_textdirection_r_to_l" value="RTL" aria-label="Right to left" :class="{ active: selectedStyles.has('rtlDirection') }" @click="runAction('rtlDirection'); closeMenu()" />
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
</template>
|
|
309
|
+
</Dropdown>
|
|
310
|
+
|
|
150
311
|
<!-- Alignment Menu -->
|
|
151
312
|
<Dropdown
|
|
152
313
|
v-else-if="action.name === 'alignMenu' && !shouldHideAction('alignMenu')" placement="bottom-start" thin flat :icon="getCurrentAlignmentIcon()"
|
|
@@ -169,20 +330,83 @@ function handleOpenAdvanced() {
|
|
|
169
330
|
:class="[action.class, { active: selectedStyles.has('textDirection') }]" tabindex="-1" @click="runAction(action.name)"
|
|
170
331
|
/>
|
|
171
332
|
|
|
333
|
+
<!-- Text / background colors -->
|
|
334
|
+
<Dropdown
|
|
335
|
+
v-else-if="(action.name === 'fontColor' || action.name === 'bgColor') && !shouldHideAction(action.name)"
|
|
336
|
+
placement="bottom-start"
|
|
337
|
+
thin
|
|
338
|
+
flat
|
|
339
|
+
:icon="action.icon"
|
|
340
|
+
>
|
|
341
|
+
<template #default="{ hide: closeMenu }">
|
|
342
|
+
<div class="px-05 min-w-220px" @pointerdown="scheduleColorMenuClose(closeMenu)" @keydown="scheduleColorMenuClose(closeMenu)">
|
|
343
|
+
<ColorInput
|
|
344
|
+
v-if="action.name === 'fontColor'"
|
|
345
|
+
v-model="fontColor"
|
|
346
|
+
label="Text color"
|
|
347
|
+
@update:model-value="applyColor('fontColor', $event, closeMenu)"
|
|
348
|
+
/>
|
|
349
|
+
<ColorInput
|
|
350
|
+
v-else
|
|
351
|
+
v-model="backgroundColor"
|
|
352
|
+
label="Background color"
|
|
353
|
+
@update:model-value="applyColor('bgColor', $event, closeMenu)"
|
|
354
|
+
/>
|
|
355
|
+
<Divider label="Basic colors" class="py-025" />
|
|
356
|
+
<div class="flex flex-wrap gap-025">
|
|
357
|
+
<Btn
|
|
358
|
+
v-for="color in basicColors"
|
|
359
|
+
:key="color"
|
|
360
|
+
round
|
|
361
|
+
flat
|
|
362
|
+
thin
|
|
363
|
+
class="square-24 border"
|
|
364
|
+
:style="{ backgroundColor: color }"
|
|
365
|
+
:aria-label="`Use ${color}`"
|
|
366
|
+
:title="color"
|
|
367
|
+
@click="applyRecentColor(action.name, color, closeMenu)"
|
|
368
|
+
/>
|
|
369
|
+
</div>
|
|
370
|
+
<template v-if="documentColors.length">
|
|
371
|
+
<Divider label="Colors in document" class="py-025" />
|
|
372
|
+
<div class="flex flex-wrap gap-025">
|
|
373
|
+
<Btn
|
|
374
|
+
v-for="color in documentColors"
|
|
375
|
+
:key="color"
|
|
376
|
+
round
|
|
377
|
+
flat
|
|
378
|
+
thin
|
|
379
|
+
class="square-24 border"
|
|
380
|
+
:style="{ backgroundColor: color }"
|
|
381
|
+
:aria-label="`Use ${color}`"
|
|
382
|
+
:title="color"
|
|
383
|
+
@click="applyRecentColor(action.name, color, closeMenu)"
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
386
|
+
</template>
|
|
387
|
+
</div>
|
|
388
|
+
</template>
|
|
389
|
+
</Dropdown>
|
|
390
|
+
|
|
172
391
|
<!-- All other buttons -->
|
|
173
392
|
<Btn
|
|
174
393
|
v-else-if="action.name !== 'separator'
|
|
175
394
|
&& action.name !== 'insertTable'
|
|
395
|
+
&& action.name !== 'tableToolsMenu'
|
|
396
|
+
&& action.name !== 'paragraphToolsMenu'
|
|
176
397
|
&& action.name !== 'alignMenu'
|
|
177
398
|
&& action.name !== 'textDirection'
|
|
399
|
+
&& action.name !== 'fontColor'
|
|
400
|
+
&& action.name !== 'bgColor'
|
|
178
401
|
&& !shouldHideAction(action.name)"
|
|
179
|
-
v-tooltip="action.label"
|
|
180
402
|
:icon="action.icon"
|
|
181
403
|
thin
|
|
182
404
|
flat
|
|
183
|
-
:aria-label="action
|
|
405
|
+
:aria-label="getActionLabel(action)"
|
|
184
406
|
:class="[action.class, { active: selectedStyles.has(action.name) }]"
|
|
185
|
-
|
|
407
|
+
:aria-pressed="selectedStyles.has(action.name)"
|
|
408
|
+
:title="getActionLabel(action)"
|
|
409
|
+
v-tooltip="getActionLabel(action)"
|
|
186
410
|
@click="runAction(action.name)"
|
|
187
411
|
/>
|
|
188
412
|
|
|
@@ -194,6 +418,12 @@ function handleOpenAdvanced() {
|
|
|
194
418
|
</template>
|
|
195
419
|
|
|
196
420
|
<style scoped>
|
|
421
|
+
.toolbar {
|
|
422
|
+
scrollbar-width: thin;
|
|
423
|
+
}
|
|
424
|
+
.toolbar > * {
|
|
425
|
+
flex-shrink: 0;
|
|
426
|
+
}
|
|
197
427
|
.toolbar :deep(.active) {
|
|
198
428
|
background: var(--bgl-primary);
|
|
199
429
|
color: white !important;
|
|
@@ -18,6 +18,10 @@ export function useCommands(state: EditorState, debug?: { logCommand: (command:
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
debug?.logCommand(command, value)
|
|
21
|
+
const activeSelection = state.doc.getSelection()
|
|
22
|
+
const preFocusRange = activeSelection?.rangeCount
|
|
23
|
+
? activeSelection.getRangeAt(0).cloneRange()
|
|
24
|
+
: state.range?.cloneRange()
|
|
21
25
|
|
|
22
26
|
if (['splitView', 'codeView', 'fullScreen'].includes(command)) {
|
|
23
27
|
switch (command) {
|
|
@@ -33,16 +37,30 @@ export function useCommands(state: EditorState, debug?: { logCommand: (command:
|
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
state.doc.body.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
const selectionIsInsideEditor = Boolean(activeSelection?.anchorNode
|
|
41
|
+
&& state.doc.body.contains(activeSelection.anchorNode))
|
|
42
|
+
if (!selectionIsInsideEditor) {
|
|
43
|
+
try {
|
|
44
|
+
state.doc.body.focus({ preventScroll: true })
|
|
45
|
+
} catch {
|
|
46
|
+
// Ignore focus errors
|
|
47
|
+
}
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
// Restore selection if focusing the body cleared or collapsed the user's active selection
|
|
43
51
|
const selection = state.doc.getSelection()
|
|
44
52
|
if (selection) {
|
|
45
|
-
if (
|
|
53
|
+
if (preFocusRange) {
|
|
54
|
+
try {
|
|
55
|
+
selection.removeAllRanges()
|
|
56
|
+
selection.addRange(preFocusRange)
|
|
57
|
+
state.selection = selection
|
|
58
|
+
state.range = preFocusRange.cloneRange()
|
|
59
|
+
state.rangeCount = selection.rangeCount
|
|
60
|
+
} catch {
|
|
61
|
+
// Ignore invalid ranges after external DOM changes
|
|
62
|
+
}
|
|
63
|
+
} else if (selection.rangeCount > 0) {
|
|
46
64
|
const currentRange = selection.getRangeAt(0)
|
|
47
65
|
if (currentRange.collapsed && state.range && !state.range.collapsed) {
|
|
48
66
|
try {
|
|
@@ -43,6 +43,34 @@ export function useEditorKeyboard(doc: Document, executor: CommandExecutor): ()
|
|
|
43
43
|
const range = selection?.rangeCount ? selection.getRangeAt(0) : null
|
|
44
44
|
const activeCell = range ? resolveCellFromNode(range.startContainer) : null
|
|
45
45
|
|
|
46
|
+
const startElement = range?.startContainer.nodeType === Node.TEXT_NODE
|
|
47
|
+
? range.startContainer.parentElement
|
|
48
|
+
: range?.startContainer as Element | undefined
|
|
49
|
+
const activeListItem = (startElement?.matches?.('li')
|
|
50
|
+
? startElement
|
|
51
|
+
: startElement?.closest('li')) as HTMLLIElement | null
|
|
52
|
+
|
|
53
|
+
if (e.key === 'Tab' && activeListItem && !activeCell) {
|
|
54
|
+
e.preventDefault()
|
|
55
|
+
executor.execute(e.shiftKey ? 'outdent' : 'indent')
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const parentList = activeListItem?.parentElement
|
|
60
|
+
const isNestedListItem = Boolean(parentList && ['UL', 'OL'].includes(parentList.tagName)
|
|
61
|
+
&& parentList.closest('li'))
|
|
62
|
+
if (e.key === 'Enter' && activeListItem && !activeCell
|
|
63
|
+
&& !activeListItem.textContent?.trim()
|
|
64
|
+
&& isNestedListItem) {
|
|
65
|
+
e.preventDefault()
|
|
66
|
+
const itemRange = doc.createRange()
|
|
67
|
+
itemRange.selectNodeContents(activeListItem)
|
|
68
|
+
selection?.removeAllRanges()
|
|
69
|
+
selection?.addRange(itemRange)
|
|
70
|
+
executor.execute('outdent')
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
46
74
|
// Table Tab / Shift+Tab navigation
|
|
47
75
|
if (e.key === 'Tab' && activeCell) {
|
|
48
76
|
const table = activeCell.closest('table') as HTMLTableElement | null
|