@bagelink/vue 1.19.1 → 1.19.3
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.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 +18654 -17606
- package/dist/style.css +2 -2
- package/package.json +1 -1
- package/src/components/Dropdown.vue +30 -0
- package/src/components/FilterQuery.vue +1 -1
- package/src/components/Icon/Icon.vue +4 -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
|
@@ -347,6 +347,12 @@ function onDocumentPointerDown(e: PointerEvent) {
|
|
|
347
347
|
internalHide({ restoreFocus: false })
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
function onEmbeddedDocumentPointerDown() {
|
|
351
|
+
if (props.autoHide && isOpen()) {
|
|
352
|
+
internalHide({ restoreFocus: false })
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
350
356
|
// Esc key — LIFO via shared composable (nested layers close first)
|
|
351
357
|
useEscapeKey(() => internalHide(), () => isOpen())
|
|
352
358
|
|
|
@@ -425,10 +431,33 @@ function onScroll() {
|
|
|
425
431
|
}
|
|
426
432
|
|
|
427
433
|
let openListenersActive = false
|
|
434
|
+
const embeddedDocuments = new Set<Document>()
|
|
435
|
+
|
|
436
|
+
function addEmbeddedDocumentListeners() {
|
|
437
|
+
document.querySelectorAll('iframe').forEach((iframe) => {
|
|
438
|
+
try {
|
|
439
|
+
const embeddedDocument = iframe.contentDocument
|
|
440
|
+
if (!embeddedDocument || embeddedDocuments.has(embeddedDocument)) return
|
|
441
|
+
embeddedDocument.addEventListener('pointerdown', onEmbeddedDocumentPointerDown, true)
|
|
442
|
+
embeddedDocuments.add(embeddedDocument)
|
|
443
|
+
} catch {
|
|
444
|
+
// Cross-origin iframes are not inspectable; the browser owns their focus boundary.
|
|
445
|
+
}
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function removeEmbeddedDocumentListeners() {
|
|
450
|
+
embeddedDocuments.forEach((embeddedDocument) => {
|
|
451
|
+
embeddedDocument.removeEventListener('pointerdown', onEmbeddedDocumentPointerDown, true)
|
|
452
|
+
})
|
|
453
|
+
embeddedDocuments.clear()
|
|
454
|
+
}
|
|
455
|
+
|
|
428
456
|
function addOpenListeners() {
|
|
429
457
|
if (openListenersActive) return
|
|
430
458
|
document.addEventListener('pointerdown', onDocumentPointerDown, true)
|
|
431
459
|
document.addEventListener('scroll', onScroll, { capture: true, passive: true })
|
|
460
|
+
addEmbeddedDocumentListeners()
|
|
432
461
|
openListenersActive = true
|
|
433
462
|
}
|
|
434
463
|
|
|
@@ -436,6 +465,7 @@ function removeOpenListeners() {
|
|
|
436
465
|
if (!openListenersActive) return
|
|
437
466
|
document.removeEventListener('pointerdown', onDocumentPointerDown, true)
|
|
438
467
|
document.removeEventListener('scroll', onScroll, true)
|
|
468
|
+
removeEmbeddedDocumentListeners()
|
|
439
469
|
openListenersActive = false
|
|
440
470
|
}
|
|
441
471
|
|
|
@@ -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,
|
|
@@ -250,6 +250,10 @@ const iconStyle = computed(() => {
|
|
|
250
250
|
font-family: 'Font Awesome 6 Brands', serif !important;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
.bgl_icon-font.fa-brands::before {
|
|
254
|
+
content: var(--fa);
|
|
255
|
+
}
|
|
256
|
+
|
|
253
257
|
/* ── Entrance / loop animations (opt-in via `animate`) ─────────────────────── */
|
|
254
258
|
.bgl_icon--spin,
|
|
255
259
|
.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
|
|
@@ -33,9 +33,7 @@ export const basicToolbarConfig: ToolbarConfig = [
|
|
|
33
33
|
'italic',
|
|
34
34
|
'underline',
|
|
35
35
|
'separator',
|
|
36
|
-
'
|
|
37
|
-
'ltrDirection',
|
|
38
|
-
'rtlDirection',
|
|
36
|
+
'paragraphToolsMenu',
|
|
39
37
|
'separator',
|
|
40
38
|
'insertTable',
|
|
41
39
|
'separator',
|
|
@@ -67,6 +65,8 @@ export const fullToolbarConfig: ToolbarConfig = [
|
|
|
67
65
|
'fontColor',
|
|
68
66
|
'bgColor',
|
|
69
67
|
'separator',
|
|
68
|
+
'paragraphToolsMenu',
|
|
69
|
+
'separator',
|
|
70
70
|
'p',
|
|
71
71
|
'blockquote',
|
|
72
72
|
'orderedList',
|
|
@@ -81,8 +81,9 @@ export const fullToolbarConfig: ToolbarConfig = [
|
|
|
81
81
|
'separator',
|
|
82
82
|
'splitView',
|
|
83
83
|
'clear',
|
|
84
|
+
'separator',
|
|
84
85
|
'insertTable',
|
|
85
|
-
|
|
86
|
+
'tableToolsMenu',
|
|
86
87
|
'separator',
|
|
87
88
|
'undo',
|
|
88
89
|
'redo',
|
|
@@ -93,6 +94,8 @@ export const toolbarOptions: ToolbarOption[] = [
|
|
|
93
94
|
{ name: 'headingsMenu', label: 'Headings', icon: 'title' },
|
|
94
95
|
{ name: 'mediaMenu', label: 'Media', icon: 'perm_media' },
|
|
95
96
|
{ name: 'alignMenu', label: 'Alignment', icon: 'format_align_left' },
|
|
97
|
+
{ name: 'paragraphToolsMenu', label: 'Paragraph tools', icon: 'format_align_right' },
|
|
98
|
+
{ name: 'tableToolsMenu', label: 'Table tools', icon: 'table_edit' },
|
|
96
99
|
{ name: 'h1', label: 'Heading 1', icon: 'format_h1' },
|
|
97
100
|
{ name: 'h2', label: 'Heading 2', icon: 'format_h2' },
|
|
98
101
|
{ name: 'h3', label: 'Heading 3', icon: 'format_h3' },
|
|
@@ -101,12 +104,12 @@ export const toolbarOptions: ToolbarOption[] = [
|
|
|
101
104
|
{ name: 'h6', label: 'Heading 6', icon: 'format_h6' },
|
|
102
105
|
{ name: 'p', label: 'Paragraph', icon: 'format_paragraph' },
|
|
103
106
|
{ name: 'blockquote', label: 'Blockquote', icon: 'format_quote' },
|
|
104
|
-
{ name: 'bold', label: 'Bold', icon: 'format_bold' },
|
|
105
|
-
{ name: 'italic', label: 'Italic', icon: 'format_italic' },
|
|
106
|
-
{ name: 'underline', label: 'Underline', icon: 'format_underlined' },
|
|
107
|
-
{ name: 'orderedList', label: 'Ordered List', icon: 'format_list_numbered' },
|
|
108
|
-
{ name: 'unorderedList', label: 'Unordered List', icon: 'format_list_bulleted' },
|
|
109
|
-
{ name: 'link', label: 'Link', icon: 'add_link' },
|
|
107
|
+
{ name: 'bold', label: 'Bold', icon: 'format_bold', shortcut: '⌘/Ctrl+B' },
|
|
108
|
+
{ name: 'italic', label: 'Italic', icon: 'format_italic', shortcut: '⌘/Ctrl+I' },
|
|
109
|
+
{ name: 'underline', label: 'Underline', icon: 'format_underlined', shortcut: '⌘/Ctrl+U' },
|
|
110
|
+
{ name: 'orderedList', label: 'Ordered List', icon: 'format_list_numbered', shortcut: '⌘/Ctrl+Shift+.' },
|
|
111
|
+
{ name: 'unorderedList', label: 'Unordered List', icon: 'format_list_bulleted', shortcut: '⌘/Ctrl+Shift+/' },
|
|
112
|
+
{ name: 'link', label: 'Link', icon: 'add_link', shortcut: '⌘/Ctrl+K' },
|
|
110
113
|
{ name: 'image', label: 'Image', icon: 'add_photo_alternate' },
|
|
111
114
|
{ name: 'video', label: 'Video', icon: 'play_circle' },
|
|
112
115
|
{ name: 'embed', label: 'Embed', icon: 'media_link' },
|
|
@@ -120,8 +123,8 @@ export const toolbarOptions: ToolbarOption[] = [
|
|
|
120
123
|
{ name: 'tableAlignCenter', label: 'Table Align Center', icon: 'format_align_center' },
|
|
121
124
|
{ name: 'tableAlignRight', label: 'Table Align Right', icon: 'format_align_right' },
|
|
122
125
|
{ name: 'tableAlignJustify', label: 'Table Align Justify', icon: 'format_align_justify' },
|
|
123
|
-
{ name: 'indent', label: 'Indent', icon: 'format_indent_increase' },
|
|
124
|
-
{ name: 'outdent', label: 'Outdent', icon: 'format_indent_decrease' },
|
|
126
|
+
{ name: 'indent', label: 'Indent', icon: 'format_indent_increase', shortcut: 'Tab' },
|
|
127
|
+
{ name: 'outdent', label: 'Outdent', icon: 'format_indent_decrease', shortcut: 'Shift+Tab' },
|
|
125
128
|
{ name: 'fontColor', label: 'Font Color', icon: 'format_color_text' },
|
|
126
129
|
{ name: 'bgColor', label: 'Background Color', icon: 'format_color_fill' },
|
|
127
130
|
{ name: 'insertTable', label: 'Insert Table', icon: 'table' },
|
|
@@ -135,8 +138,8 @@ export const toolbarOptions: ToolbarOption[] = [
|
|
|
135
138
|
{ name: 'insertColumnRight', label: 'Insert Column Right', icon: 'format_indent_increase' },
|
|
136
139
|
{ name: 'deleteColumn', label: 'Delete Column', icon: 'view_week' },
|
|
137
140
|
{ name: 'separator' },
|
|
138
|
-
{ name: 'undo', label: 'Undo', icon: 'undo' },
|
|
139
|
-
{ name: 'redo', label: 'Redo', icon: 'redo' },
|
|
141
|
+
{ name: 'undo', label: 'Undo', icon: 'undo', shortcut: '⌘/Ctrl+Z' },
|
|
142
|
+
{ name: 'redo', label: 'Redo', icon: 'redo', shortcut: '⌘/Ctrl+Shift+Z' },
|
|
140
143
|
{ name: 'separator' },
|
|
141
144
|
{ name: 'fullScreen', label: 'Full Screen', icon: 'fullscreen', class: 'ms-auto' },
|
|
142
145
|
{ name: 'textDirection', label: 'Text Direction (RTL/LTR)', icon: 'format_textdirection_r_to_l' },
|