@svgrid/grid 2.6.22 → 2.7.0
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/CHANGELOG.md +88 -88
- package/README.md +199 -199
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +181 -181
- package/dist/GridMenus.svelte +184 -6
- package/dist/SvAutoComplete.svelte +169 -165
- package/dist/SvAvatar.svelte +75 -75
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +102 -102
- package/dist/SvCircularProgress.svelte +109 -109
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +279 -275
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +163 -163
- package/dist/SvDateRangeInput.svelte +25 -3
- package/dist/SvDrawer.svelte +254 -254
- package/dist/SvDropDownList.svelte +378 -374
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGrid.controller.svelte.d.ts +48 -1
- package/dist/SvGrid.controller.svelte.js +214 -52
- package/dist/SvGrid.css +221 -0
- package/dist/SvGrid.helpers.d.ts +55 -0
- package/dist/SvGrid.helpers.js +83 -0
- package/dist/SvGrid.svelte +145 -0
- package/dist/SvGrid.types.d.ts +105 -0
- package/dist/SvGridCellEditor.svelte +744 -662
- package/dist/SvGridChart.svelte +1716 -1716
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +728 -728
- package/dist/SvGridSelect.svelte +270 -266
- package/dist/SvGridSelect.svelte.d.ts +1 -1
- package/dist/SvGroupCell.svelte +116 -116
- package/dist/SvListBox.svelte +334 -326
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +293 -289
- package/dist/SvMultiSelect.svelte.d.ts +1 -1
- package/dist/SvNumberInput.svelte +172 -168
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +133 -133
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvProgress.svelte +116 -116
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRating.svelte +112 -112
- package/dist/SvResult.svelte +73 -73
- package/dist/SvRichCell.svelte +125 -0
- package/dist/SvRichCell.svelte.d.ts +15 -0
- package/dist/SvRichText.svelte +211 -211
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +203 -203
- package/dist/SvSwitchButton.svelte +108 -108
- package/dist/SvTagsInput.svelte +115 -115
- package/dist/SvTextArea.svelte +5 -1
- package/dist/SvTextInput.svelte +147 -147
- package/dist/SvTimePicker.svelte +245 -245
- package/dist/SvToaster.svelte +159 -159
- package/dist/SvToggleButton.svelte +85 -85
- package/dist/SvTooltip.svelte +161 -161
- package/dist/SvTour.svelte +208 -208
- package/dist/SvTree.svelte +444 -444
- package/dist/SvTreeSelect.svelte +239 -235
- package/dist/SvTreeSelect.svelte.d.ts +1 -1
- package/dist/builtin-editors.d.ts +2 -2
- package/dist/builtin-editors.js +19 -3
- package/dist/cdn/GridMenus-C4oLsJyj.js +600 -0
- package/dist/cdn/GridMenus-yEivjNwl.js +601 -0
- package/dist/cdn/SvDateRangeInput-Bh0A0JkF.js +199 -0
- package/dist/cdn/SvDateRangeInput-DflbiP7N.js +198 -0
- package/dist/cdn/SvDateTimePicker-BWwpfB_o.js +812 -0
- package/dist/cdn/SvDateTimePicker-Bivn8dAP.js +816 -0
- package/dist/cdn/SvGridCellEditor-AXL8cHGO.js +550 -0
- package/dist/cdn/SvGridCellEditor-Ba7rY3eu.js +549 -0
- package/dist/cdn/{SvGridChart-BwVos976.js → SvGridChart-Bs2GIR2Q.js} +1 -1
- package/dist/cdn/{SvGridChart-BEJmNNx9.js → SvGridChart-CwhFz7GV.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-BOknOkrP.js → SvGridChartPanel-1WXuStzM.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-D2PjII4O.js → SvGridChartPanel-e5_nqNP9.js} +1 -1
- package/dist/cdn/{SvGridChartView-00LPUHL1.js → SvGridChartView-SmPW10dI.js} +1 -1
- package/dist/cdn/{SvGridChartView-BhUEJ5ki.js → SvGridChartView-eSPuJE6g.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-B1ZcLpgs.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CtdbHcIS.js} +1 -1
- package/dist/cdn/date-format-CcP1tafP.js +1395 -0
- package/dist/cdn/date-format-DZT7T1wf.js +1393 -0
- package/dist/cdn/{editor-registry-CiI0xlKg.js → editor-registry-BhgE3S84.js} +51 -25
- package/dist/cdn/{src-C9Hihx1W.js → src-COC0vnpg.js} +7233 -6701
- package/dist/cdn/{src-D1lXwq1l.js → src-pYGV2rMR.js} +5747 -5215
- package/dist/cdn/svgrid.js +15 -13
- package/dist/cdn/svgrid.svelte-external.js +15 -13
- package/dist/cell-values.d.ts +16 -0
- package/dist/cell-values.js +28 -0
- package/dist/chart-export.js +8 -8
- package/dist/clipboard.d.ts +22 -0
- package/dist/clipboard.js +405 -27
- package/dist/editing.js +12 -7
- package/dist/editors/cell-editors.d.ts +11 -0
- package/dist/editors/cell-editors.js +18 -1
- package/dist/grid-messages.d.ts +15 -0
- package/dist/grid-messages.js +15 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/list-option.d.ts +6 -0
- package/dist/menus.js +40 -7
- package/dist/sanitize-html.d.ts +37 -0
- package/dist/sanitize-html.js +234 -0
- package/dist/selection-bar-view.svelte.d.ts +25 -0
- package/dist/selection-bar-view.svelte.js +13 -0
- package/dist/selection.d.ts +2 -1
- package/dist/selection.js +129 -13
- package/package.json +11 -11
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +181 -181
- package/src/GridMenus.svelte +184 -6
- package/src/SvAutoComplete.svelte +169 -165
- package/src/SvAvatar.svelte +75 -75
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +102 -102
- package/src/SvCircularProgress.svelte +109 -109
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +279 -275
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +163 -163
- package/src/SvDateRangeInput.svelte +25 -3
- package/src/SvDrawer.svelte +254 -254
- package/src/SvDropDownList.svelte +378 -374
- package/src/SvDurationInput.svelte +126 -126
- package/src/SvField.svelte +293 -293
- package/src/SvForm.svelte +437 -437
- package/src/SvForm.test.ts +411 -411
- package/src/SvGrid.controller.svelte.ts +251 -61
- package/src/SvGrid.css +221 -0
- package/src/SvGrid.helpers.ts +87 -0
- package/src/SvGrid.svelte +145 -0
- package/src/SvGrid.types.ts +2092 -1980
- package/src/SvGridCellEditor.svelte +744 -662
- package/src/SvGridChart.svelte +1716 -1716
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +728 -728
- package/src/SvGridSelect.svelte +270 -266
- package/src/SvGroupCell.svelte +116 -116
- package/src/SvListBox.svelte +334 -326
- package/src/SvMaskedInput.svelte +122 -122
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +146 -146
- package/src/SvMultiSelect.svelte +293 -289
- package/src/SvNumberInput.svelte +172 -168
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -133
- package/src/SvPopover.svelte +197 -197
- package/src/SvProgress.svelte +116 -116
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRating.svelte +112 -112
- package/src/SvResult.svelte +73 -73
- package/src/SvRichCell.svelte +125 -0
- package/src/SvRichText.svelte +211 -211
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +203 -203
- package/src/SvSwitchButton.svelte +108 -108
- package/src/SvTagsInput.svelte +115 -115
- package/src/SvTextArea.svelte +5 -1
- package/src/SvTextInput.svelte +147 -147
- package/src/SvTimePicker.svelte +245 -245
- package/src/SvToaster.svelte +159 -159
- package/src/SvToaster.test.ts +95 -95
- package/src/SvToggleButton.svelte +85 -85
- package/src/SvTooltip.svelte +161 -161
- package/src/SvTour.svelte +208 -208
- package/src/SvTree.svelte +444 -444
- package/src/SvTreeSelect.svelte +239 -235
- package/src/a11y/dismissable.test.ts +119 -119
- package/src/a11y/dismissable.ts +114 -114
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/ai.test.ts +502 -502
- package/src/ai.ts +1419 -1419
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +846 -846
- package/src/builtin-editors.grid.test.ts +83 -83
- package/src/builtin-editors.test.ts +18 -1
- package/src/builtin-editors.ts +19 -3
- package/src/cell-formatting.ts +171 -171
- package/src/cell-render.test.ts +513 -513
- package/src/cell-render.ts +496 -496
- package/src/cell-values.ts +148 -118
- package/src/chart-export.ts +202 -202
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2321 -2321
- package/src/clipboard.test.ts +93 -20
- package/src/clipboard.ts +433 -33
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/column-groups.ts +78 -78
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1865 -1865
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCombobox.svelte.ts +191 -191
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +168 -168
- package/src/createForm.svelte.ts +386 -386
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createListbox.svelte.ts +250 -250
- package/src/createMenu.svelte.ts +224 -224
- package/src/createPopoverSelect.svelte.ts +213 -213
- package/src/createSlider.svelte.ts +191 -191
- package/src/createTooltip.svelte.ts +144 -144
- package/src/createTree.svelte.ts +322 -322
- package/src/date-string-column.test.ts +87 -0
- package/src/datetime/date-core.ts +206 -206
- package/src/datetime/date-restrict.ts +61 -61
- package/src/datetime/timezone.ts +135 -135
- package/src/dock-manager-model.ts +596 -596
- package/src/dock-model.ts +374 -374
- package/src/editing.test.ts +974 -949
- package/src/editing.ts +609 -606
- package/src/editor-block-prop.test.ts +132 -0
- package/src/editor-contract.ts +171 -171
- package/src/editor-registry.grid.test.ts +144 -144
- package/src/editor-registry.ts +122 -122
- package/src/editors/cell-editors.ts +27 -1
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +601 -601
- package/src/filter-operators.ts +160 -160
- package/src/filtering/excel-filters.ts +325 -325
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/grid-messages.ts +34 -0
- package/src/group-display.test.ts +167 -167
- package/src/group-display.ts +200 -200
- package/src/headless.ts +87 -87
- package/src/index.ts +13 -0
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard-handlers.ts +270 -270
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/list-option.test.ts +56 -56
- package/src/list-option.ts +179 -173
- package/src/menus.test.ts +43 -0
- package/src/menus.ts +597 -567
- package/src/merge-objects.ts +48 -48
- package/src/move-cells.test.ts +850 -0
- package/src/number-editor-literal.test.ts +84 -0
- package/src/number-editor.grid.test.ts +160 -0
- package/src/overlays.test.ts +90 -90
- package/src/positioning.ts +268 -268
- package/src/render-component.ts +28 -28
- package/src/row-drag-touch.ts +211 -211
- package/src/row-drag.test.ts +353 -353
- package/src/row-drag.ts +415 -415
- package/src/row-resize.test.ts +524 -524
- package/src/row-resize.ts +228 -228
- package/src/sanitize-html.test.ts +182 -0
- package/src/sanitize-html.ts +235 -0
- package/src/scheduler-ical.ts +181 -181
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +873 -873
- package/src/selection-bar-view.svelte.ts +36 -0
- package/src/selection.test.ts +900 -743
- package/src/selection.ts +131 -13
- package/src/server-data-source.test.ts +383 -383
- package/src/server-data-source.ts +469 -469
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +488 -488
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/summaries.ts +113 -113
- package/src/svgrid-wrapper.types.ts +563 -563
- package/src/svgrid.async-editor-options.test.ts +273 -273
- package/src/svgrid.auto-row-height.test.ts +204 -204
- package/src/svgrid.behavior.test.ts +910 -910
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.comments-autocomplete.test.ts +127 -127
- package/src/svgrid.context-menu.test.ts +147 -147
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-depth.test.ts +163 -163
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +377 -331
- package/src/svgrid.filter-menu-scroll.test.ts +112 -112
- package/src/svgrid.grand-total.test.ts +188 -188
- package/src/svgrid.group-display-mode.test.ts +171 -171
- package/src/svgrid.group-footers.test.ts +121 -121
- package/src/svgrid.group-pagination.test.ts +153 -153
- package/src/svgrid.in-suggest-snapshot.svelte.test.ts +91 -0
- package/src/svgrid.menu-scroll-close.test.ts +204 -0
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.selection-bar-seam.test.ts +185 -0
- package/src/svgrid.tree-data.test.ts +186 -186
- package/src/svgrid.upsell-license.test.ts +117 -0
- package/src/svgrid.wrapper.test.ts +63 -63
- package/src/svgriddropdown.async-panel.svelte.test.ts +195 -195
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +288 -288
- package/src/toast-store.svelte.ts +250 -250
- package/src/toast-store.test.ts +147 -147
- package/src/tree-row-model.test.ts +168 -168
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-localization.test.ts +113 -113
- package/src/ui-range.test.ts +70 -70
- package/src/ui-selection.test.ts +155 -155
- package/src/ui-tier1.test.ts +142 -142
- package/src/virtual.test.ts +88 -88
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +322 -322
- package/dist/cdn/GridMenus-BuoBPqxx.js +0 -493
- package/dist/cdn/GridMenus-n4llxoOI.js +0 -494
- package/dist/cdn/SvDateTimePicker-CHnUkWcH.js +0 -2206
- package/dist/cdn/SvDateTimePicker-CRQH_U7E.js +0 -2201
- package/dist/cdn/SvGridCellEditor-BGUCzuPB.js +0 -523
- package/dist/cdn/SvGridCellEditor-G8IMWIws.js +0 -522
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvRichCell - render rich text inside a grid cell, safely.
|
|
4
|
+
*
|
|
5
|
+
* Pair it with `editorType: 'richtext'` (or feed it Markdown) to get a column
|
|
6
|
+
* whose values carry formatting. The value is always run through
|
|
7
|
+
* `sanitizeHtml` before it is painted, so markup that arrived from a user
|
|
8
|
+
* cannot execute in the host page - see sanitize-html.ts for the allowlist.
|
|
9
|
+
*
|
|
10
|
+
* ```svelte
|
|
11
|
+
* <script>
|
|
12
|
+
* import { SvRichCell } from '@svgrid/grid'
|
|
13
|
+
* <\/script>
|
|
14
|
+
*
|
|
15
|
+
* {#snippet notesCell({ getValue })}
|
|
16
|
+
* <SvRichCell value={getValue()} />
|
|
17
|
+
* {/snippet}
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* A grid row is one line tall by default, so the content is clamped to
|
|
21
|
+
* `clamp` lines with an ellipsis and the full text goes on the `title`
|
|
22
|
+
* attribute. Set `clamp={0}` in a master/detail panel or a tall row where the
|
|
23
|
+
* whole value should show.
|
|
24
|
+
*/
|
|
25
|
+
import { sanitizeHtml, renderMarkdown, htmlToText } from './sanitize-html'
|
|
26
|
+
|
|
27
|
+
type Props = {
|
|
28
|
+
/** The cell value: an HTML string, or Markdown when `format="markdown"`. */
|
|
29
|
+
value?: unknown
|
|
30
|
+
/** How to read `value`. Markdown is converted first, then sanitized. */
|
|
31
|
+
format?: 'html' | 'markdown'
|
|
32
|
+
/** Lines to clamp to. 0 removes the clamp and lets the content wrap. */
|
|
33
|
+
clamp?: number
|
|
34
|
+
/** Native tooltip carrying the flattened text. Off when the grid already
|
|
35
|
+
* supplies its own cell tooltip. */
|
|
36
|
+
title?: boolean
|
|
37
|
+
class?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let { value = '', format = 'html', clamp = 1, title = true, class: className = '' }: Props = $props()
|
|
41
|
+
|
|
42
|
+
const source = $derived(value == null ? '' : String(value))
|
|
43
|
+
const html = $derived(format === 'markdown' ? renderMarkdown(source) : sanitizeHtml(source))
|
|
44
|
+
// The flattened text doubles as the tooltip and as the accessible name when
|
|
45
|
+
// the markup collapses to something a screen reader would read as noise.
|
|
46
|
+
const text = $derived(title ? htmlToText(html) : '')
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<div
|
|
50
|
+
class="sv-rich-cell {className}"
|
|
51
|
+
class:is-clamped={clamp > 0}
|
|
52
|
+
style={clamp > 0 ? `--sv-rich-cell-lines: ${clamp}` : undefined}
|
|
53
|
+
title={text || undefined}
|
|
54
|
+
>
|
|
55
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
56
|
+
{@html html}
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<style>
|
|
60
|
+
.sv-rich-cell {
|
|
61
|
+
min-width: 0;
|
|
62
|
+
color: var(--sg-fg, #0f172a);
|
|
63
|
+
font: inherit;
|
|
64
|
+
}
|
|
65
|
+
/* Clamp to N lines. The grid row is a single line tall by default, so
|
|
66
|
+
unclamped rich text would push the row open and break virtualization's
|
|
67
|
+
fixed-height assumption. */
|
|
68
|
+
.sv-rich-cell.is-clamped {
|
|
69
|
+
display: -webkit-box;
|
|
70
|
+
-webkit-box-orient: vertical;
|
|
71
|
+
-webkit-line-clamp: var(--sv-rich-cell-lines, 1);
|
|
72
|
+
line-clamp: var(--sv-rich-cell-lines, 1);
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
}
|
|
75
|
+
/* Block elements inside a cell must not carry their document margins, or a
|
|
76
|
+
single <p> pushes the text out of the row box. */
|
|
77
|
+
.sv-rich-cell :global(p),
|
|
78
|
+
.sv-rich-cell :global(h1),
|
|
79
|
+
.sv-rich-cell :global(h2),
|
|
80
|
+
.sv-rich-cell :global(h3),
|
|
81
|
+
.sv-rich-cell :global(h4),
|
|
82
|
+
.sv-rich-cell :global(h5),
|
|
83
|
+
.sv-rich-cell :global(h6),
|
|
84
|
+
.sv-rich-cell :global(ul),
|
|
85
|
+
.sv-rich-cell :global(ol),
|
|
86
|
+
.sv-rich-cell :global(blockquote),
|
|
87
|
+
.sv-rich-cell :global(pre) {
|
|
88
|
+
margin: 0;
|
|
89
|
+
padding: 0;
|
|
90
|
+
}
|
|
91
|
+
.sv-rich-cell.is-clamped :global(ul),
|
|
92
|
+
.sv-rich-cell.is-clamped :global(ol) {
|
|
93
|
+
list-style-position: inside;
|
|
94
|
+
}
|
|
95
|
+
/* Headings keep their weight but drop to cell size: a real <h1> inside a row
|
|
96
|
+
would dwarf every neighbouring cell. */
|
|
97
|
+
.sv-rich-cell :global(h1),
|
|
98
|
+
.sv-rich-cell :global(h2),
|
|
99
|
+
.sv-rich-cell :global(h3),
|
|
100
|
+
.sv-rich-cell :global(h4),
|
|
101
|
+
.sv-rich-cell :global(h5),
|
|
102
|
+
.sv-rich-cell :global(h6) {
|
|
103
|
+
font-size: inherit;
|
|
104
|
+
font-weight: 650;
|
|
105
|
+
}
|
|
106
|
+
.sv-rich-cell :global(a) {
|
|
107
|
+
color: var(--sg-accent, #2563eb);
|
|
108
|
+
}
|
|
109
|
+
.sv-rich-cell :global(code) {
|
|
110
|
+
font-family: var(--sg-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
111
|
+
font-size: 0.92em;
|
|
112
|
+
background: var(--sg-code-bg, color-mix(in srgb, var(--sg-fg, #0f172a) 8%, transparent));
|
|
113
|
+
border-radius: var(--sg-radius-sm, 4px);
|
|
114
|
+
padding: 0 4px;
|
|
115
|
+
}
|
|
116
|
+
.sv-rich-cell :global(blockquote) {
|
|
117
|
+
border-inline-start: 2px solid var(--sg-border, #cbd5e1);
|
|
118
|
+
padding-inline-start: 8px;
|
|
119
|
+
color: var(--sg-muted, #64748b);
|
|
120
|
+
}
|
|
121
|
+
.sv-rich-cell :global(img) {
|
|
122
|
+
max-height: 1.4em;
|
|
123
|
+
vertical-align: text-bottom;
|
|
124
|
+
}
|
|
125
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
/** The cell value: an HTML string, or Markdown when `format="markdown"`. */
|
|
3
|
+
value?: unknown;
|
|
4
|
+
/** How to read `value`. Markdown is converted first, then sanitized. */
|
|
5
|
+
format?: 'html' | 'markdown';
|
|
6
|
+
/** Lines to clamp to. 0 removes the clamp and lets the content wrap. */
|
|
7
|
+
clamp?: number;
|
|
8
|
+
/** Native tooltip carrying the flattened text. Off when the grid already
|
|
9
|
+
* supplies its own cell tooltip. */
|
|
10
|
+
title?: boolean;
|
|
11
|
+
class?: string;
|
|
12
|
+
};
|
|
13
|
+
declare const SvRichCell: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type SvRichCell = ReturnType<typeof SvRichCell>;
|
|
15
|
+
export default SvRichCell;
|
package/dist/SvRichText.svelte
CHANGED
|
@@ -1,211 +1,211 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
export type RichTextTool =
|
|
3
|
-
| 'bold' | 'italic' | 'underline' | 'strike'
|
|
4
|
-
| 'h1' | 'h2' | 'h3' | 'p'
|
|
5
|
-
| 'ul' | 'ol' | 'quote' | 'code'
|
|
6
|
-
| 'alignLeft' | 'alignCenter' | 'alignRight'
|
|
7
|
-
| 'link' | 'clear' | 'undo' | 'redo'
|
|
8
|
-
| '|'
|
|
9
|
-
</script>
|
|
10
|
-
|
|
11
|
-
<script lang="ts">
|
|
12
|
-
/**
|
|
13
|
-
* SvRichText - a lightweight WYSIWYG editor: a formatting toolbar over a
|
|
14
|
-
* `contentEditable` region, emitting HTML. Parity: Smart `smart-editor` (the
|
|
15
|
-
* common commands). Uses `document.execCommand` - the pragmatic, widely-working
|
|
16
|
-
* approach for a component-kit editor. Bindable `value` (HTML string).
|
|
17
|
-
*
|
|
18
|
-
* ```svelte
|
|
19
|
-
* <SvRichText bind:value={html} placeholder="Write something…" />
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
import { nextEditorId, resolveMessages } from './editor-contract'
|
|
23
|
-
|
|
24
|
-
const DEFAULT_TOOLS: RichTextTool[] = [
|
|
25
|
-
'bold', 'italic', 'underline', 'strike', '|',
|
|
26
|
-
'h1', 'h2', 'h3', 'p', '|',
|
|
27
|
-
'ul', 'ol', 'quote', 'code', '|',
|
|
28
|
-
'alignLeft', 'alignCenter', 'alignRight', '|',
|
|
29
|
-
'link', 'clear', '|', 'undo', 'redo',
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
/** Toolbar glyphs. Mode-independent and not language-dependent, so they stay
|
|
33
|
-
* fixed; the LABELS beside them are localizable via `messages`. */
|
|
34
|
-
const GLYPH: Record<Exclude<RichTextTool, '|'>, string> = {
|
|
35
|
-
bold: 'B', italic: 'I', underline: 'U', strike: 'S',
|
|
36
|
-
h1: 'H1', h2: 'H2', h3: 'H3', p: '¶',
|
|
37
|
-
ul: '•', ol: '1.', quote: '❝', code: '</>',
|
|
38
|
-
alignLeft: '≡', alignCenter: '≣', alignRight: '≡', link: '🔗',
|
|
39
|
-
clear: '⌫', undo: '↶', redo: '↷',
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** User-facing strings (localizable via `messages`). Every one is a tooltip +
|
|
43
|
-
* accessible name on a toolbar button, so leaving them hard-coded made the
|
|
44
|
-
* whole toolbar English-only for screen-reader users. */
|
|
45
|
-
type RichTextMessages = Record<Exclude<RichTextTool, '|'>, string> & { linkPrompt: string; toolbar: string }
|
|
46
|
-
const DEFAULT_MESSAGES: RichTextMessages = {
|
|
47
|
-
bold: 'Bold', italic: 'Italic', underline: 'Underline', strike: 'Strikethrough',
|
|
48
|
-
h1: 'Heading 1', h2: 'Heading 2', h3: 'Heading 3', p: 'Paragraph',
|
|
49
|
-
ul: 'Bullet list', ol: 'Numbered list', quote: 'Quote', code: 'Code block',
|
|
50
|
-
alignLeft: 'Align left', alignCenter: 'Align center', alignRight: 'Align right',
|
|
51
|
-
link: 'Link', clear: 'Clear formatting', undo: 'Undo', redo: 'Redo',
|
|
52
|
-
linkPrompt: 'Link URL', toolbar: 'Formatting',
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
type Props = {
|
|
56
|
-
/** HTML string (bindable). */
|
|
57
|
-
value?: string
|
|
58
|
-
onChange?: (html: string) => void
|
|
59
|
-
placeholder?: string
|
|
60
|
-
disabled?: boolean
|
|
61
|
-
readonly?: boolean
|
|
62
|
-
minHeight?: string
|
|
63
|
-
/** Toolbar tools + order; use '|' for a separator. */
|
|
64
|
-
tools?: RichTextTool[]
|
|
65
|
-
ariaLabel?: string
|
|
66
|
-
/** Override any toolbar label / prompt (localization). */
|
|
67
|
-
messages?: Partial<RichTextMessages>
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
let {
|
|
71
|
-
value = $bindable(''),
|
|
72
|
-
onChange,
|
|
73
|
-
placeholder = '',
|
|
74
|
-
disabled = false,
|
|
75
|
-
readonly = false,
|
|
76
|
-
minHeight = '160px',
|
|
77
|
-
tools = DEFAULT_TOOLS,
|
|
78
|
-
ariaLabel = 'Rich text editor',
|
|
79
|
-
messages,
|
|
80
|
-
}: Props = $props()
|
|
81
|
-
|
|
82
|
-
const M = $derived(resolveMessages(DEFAULT_MESSAGES, messages))
|
|
83
|
-
const uid = nextEditorId('sv-rt')
|
|
84
|
-
let editorEl = $state<HTMLDivElement | null>(null)
|
|
85
|
-
let focused = $state(false)
|
|
86
|
-
let empty = $state(true)
|
|
87
|
-
let activeState = $state<Record<string, boolean>>({})
|
|
88
|
-
|
|
89
|
-
// Push external value into the DOM only when it differs and we're not typing
|
|
90
|
-
// (writing innerHTML while focused would collapse the caret).
|
|
91
|
-
$effect(() => {
|
|
92
|
-
const v = value ?? ''
|
|
93
|
-
// A contenteditable owns its own DOM: letting Svelte re-render the body
|
|
94
|
-
// would wipe the caret mid-typing, so the value is pushed in by hand and
|
|
95
|
-
// only while the editor is unfocused.
|
|
96
|
-
// eslint-disable-next-line svelte/no-dom-manipulating
|
|
97
|
-
if (editorEl && !focused && editorEl.innerHTML !== v) editorEl.innerHTML = v
|
|
98
|
-
if (editorEl) empty = editorEl.textContent!.trim().length === 0
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
function sync() {
|
|
102
|
-
if (!editorEl) return
|
|
103
|
-
const html = editorEl.innerHTML
|
|
104
|
-
empty = editorEl.textContent!.trim().length === 0
|
|
105
|
-
value = html
|
|
106
|
-
onChange?.(html)
|
|
107
|
-
}
|
|
108
|
-
function q(cmd: string): boolean {
|
|
109
|
-
try { return document.queryCommandState(cmd) } catch { return false }
|
|
110
|
-
}
|
|
111
|
-
function refreshState() {
|
|
112
|
-
activeState = {
|
|
113
|
-
bold: q('bold'), italic: q('italic'), underline: q('underline'), strike: q('strikeThrough'),
|
|
114
|
-
ul: q('insertUnorderedList'), ol: q('insertOrderedList'),
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
function exec(cmd: string, arg?: string) {
|
|
118
|
-
if (disabled || readonly) return
|
|
119
|
-
editorEl?.focus()
|
|
120
|
-
try { document.execCommand(cmd, false, arg) } catch { /* jsdom / unsupported */ }
|
|
121
|
-
sync()
|
|
122
|
-
refreshState()
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const run: Record<Exclude<RichTextTool, '|'>, () => void> = {
|
|
126
|
-
bold: () => exec('bold'), italic: () => exec('italic'), underline: () => exec('underline'), strike: () => exec('strikeThrough'),
|
|
127
|
-
h1: () => exec('formatBlock', 'H1'), h2: () => exec('formatBlock', 'H2'), h3: () => exec('formatBlock', 'H3'), p: () => exec('formatBlock', 'P'),
|
|
128
|
-
ul: () => exec('insertUnorderedList'), ol: () => exec('insertOrderedList'),
|
|
129
|
-
quote: () => exec('formatBlock', 'BLOCKQUOTE'), code: () => exec('formatBlock', 'PRE'),
|
|
130
|
-
alignLeft: () => exec('justifyLeft'), alignCenter: () => exec('justifyCenter'), alignRight: () => exec('justifyRight'),
|
|
131
|
-
link: () => { const url = typeof prompt === 'function' ? prompt(M.linkPrompt, 'https://') : null; if (url) exec('createLink', url) },
|
|
132
|
-
clear: () => exec('removeFormat'), undo: () => exec('undo'), redo: () => exec('redo'),
|
|
133
|
-
}
|
|
134
|
-
</script>
|
|
135
|
-
|
|
136
|
-
<div class="sv-rt" class:is-disabled={disabled} class:is-readonly={readonly}>
|
|
137
|
-
{#if !readonly}
|
|
138
|
-
<div class="sv-rt__toolbar" role="toolbar" aria-label={M.toolbar} aria-controls={uid}>
|
|
139
|
-
{#each tools as t, i (i)}
|
|
140
|
-
{#if t === '|'}
|
|
141
|
-
<span class="sv-rt__sep" aria-hidden="true"></span>
|
|
142
|
-
{:else}
|
|
143
|
-
<button
|
|
144
|
-
type="button"
|
|
145
|
-
class="sv-rt__btn"
|
|
146
|
-
class:is-on={activeState[t]}
|
|
147
|
-
data-tool={t}
|
|
148
|
-
title={M[t]}
|
|
149
|
-
aria-label={M[t]}
|
|
150
|
-
aria-pressed={activeState[t] ? true : undefined}
|
|
151
|
-
{disabled}
|
|
152
|
-
onmousedown={(e) => e.preventDefault()}
|
|
153
|
-
onclick={run[t]}
|
|
154
|
-
>{GLYPH[t]}</button>
|
|
155
|
-
{/if}
|
|
156
|
-
{/each}
|
|
157
|
-
</div>
|
|
158
|
-
{/if}
|
|
159
|
-
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
160
|
-
<div
|
|
161
|
-
bind:this={editorEl}
|
|
162
|
-
id={uid}
|
|
163
|
-
class="sv-rt__body"
|
|
164
|
-
class:is-empty={empty}
|
|
165
|
-
style:min-height={minHeight}
|
|
166
|
-
contenteditable={!disabled && !readonly}
|
|
167
|
-
role="textbox"
|
|
168
|
-
aria-multiline="true"
|
|
169
|
-
aria-label={ariaLabel}
|
|
170
|
-
data-placeholder={placeholder}
|
|
171
|
-
tabindex={disabled ? -1 : 0}
|
|
172
|
-
oninput={sync}
|
|
173
|
-
onfocus={() => (focused = true)}
|
|
174
|
-
onblur={() => { focused = false; sync() }}
|
|
175
|
-
onkeyup={refreshState}
|
|
176
|
-
onmouseup={refreshState}
|
|
177
|
-
></div>
|
|
178
|
-
</div>
|
|
179
|
-
|
|
180
|
-
<style>
|
|
181
|
-
.sv-rt {
|
|
182
|
-
--_accent: var(--sg-accent, #2563eb);
|
|
183
|
-
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
|
|
184
|
-
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); overflow: hidden;
|
|
185
|
-
display: flex; flex-direction: column;
|
|
186
|
-
}
|
|
187
|
-
.sv-rt:focus-within { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 20%, transparent); }
|
|
188
|
-
.sv-rt.is-disabled { opacity: 0.6; }
|
|
189
|
-
.sv-rt__toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 1px; padding: 5px 6px; border-bottom: 1px solid var(--sg-border, #e2e8f0); background: var(--sg-header-bg, #f8fafc); }
|
|
190
|
-
.sv-rt__btn {
|
|
191
|
-
min-width: 28px; height: 28px; padding: 0 6px; display: grid; place-items: center;
|
|
192
|
-
background: none; border: 0; border-radius: 6px; color: var(--sg-fg, #334155); cursor: pointer;
|
|
193
|
-
font-size: 12.5px; font-weight: 600;
|
|
194
|
-
}
|
|
195
|
-
.sv-rt__btn:hover:not(:disabled) { background: var(--sg-row-hover-bg, #eef2f7); }
|
|
196
|
-
.sv-rt__btn.is-on { background: color-mix(in srgb, var(--_accent) 16%, transparent); color: var(--_accent); }
|
|
197
|
-
.sv-rt__btn[data-tool='italic'] { font-style: italic; }
|
|
198
|
-
.sv-rt__btn[data-tool='underline'] { text-decoration: underline; }
|
|
199
|
-
.sv-rt__btn[data-tool='strike'] { text-decoration: line-through; }
|
|
200
|
-
.sv-rt__btn[data-tool='alignRight'] { transform: scaleX(-1); }
|
|
201
|
-
.sv-rt__sep { width: 1px; height: 18px; background: var(--sg-border, #e2e8f0); margin: 0 4px; }
|
|
202
|
-
.sv-rt__body { padding: 10px 12px; outline: none; overflow: auto; font-size: 14px; line-height: 1.6; }
|
|
203
|
-
.sv-rt__body.is-empty::before { content: attr(data-placeholder); color: var(--sg-muted, #94a3b8); pointer-events: none; }
|
|
204
|
-
.sv-rt__body :global(h1) { font-size: 1.5em; font-weight: 700; margin: 0.4em 0; }
|
|
205
|
-
.sv-rt__body :global(h2) { font-size: 1.25em; font-weight: 700; margin: 0.4em 0; }
|
|
206
|
-
.sv-rt__body :global(h3) { font-size: 1.1em; font-weight: 650; margin: 0.4em 0; }
|
|
207
|
-
.sv-rt__body :global(blockquote) { margin: 0.4em 0; padding-inline-start: 12px; border-inline-start: 3px solid var(--sg-border, #cbd5e1); color: var(--sg-muted, #64748b); }
|
|
208
|
-
.sv-rt__body :global(pre) { margin: 0.4em 0; padding: 10px 12px; border-radius: 8px; background: var(--sg-row-hover-bg, #f1f5f9); font: 12.5px/1.6 ui-monospace, monospace; overflow-x: auto; }
|
|
209
|
-
.sv-rt__body :global(a) { color: var(--_accent); }
|
|
210
|
-
.sv-rt__body :global(ul), .sv-rt__body :global(ol) { margin: 0.4em 0; padding-inline-start: 24px; }
|
|
211
|
-
</style>
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type RichTextTool =
|
|
3
|
+
| 'bold' | 'italic' | 'underline' | 'strike'
|
|
4
|
+
| 'h1' | 'h2' | 'h3' | 'p'
|
|
5
|
+
| 'ul' | 'ol' | 'quote' | 'code'
|
|
6
|
+
| 'alignLeft' | 'alignCenter' | 'alignRight'
|
|
7
|
+
| 'link' | 'clear' | 'undo' | 'redo'
|
|
8
|
+
| '|'
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
/**
|
|
13
|
+
* SvRichText - a lightweight WYSIWYG editor: a formatting toolbar over a
|
|
14
|
+
* `contentEditable` region, emitting HTML. Parity: Smart `smart-editor` (the
|
|
15
|
+
* common commands). Uses `document.execCommand` - the pragmatic, widely-working
|
|
16
|
+
* approach for a component-kit editor. Bindable `value` (HTML string).
|
|
17
|
+
*
|
|
18
|
+
* ```svelte
|
|
19
|
+
* <SvRichText bind:value={html} placeholder="Write something…" />
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import { nextEditorId, resolveMessages } from './editor-contract'
|
|
23
|
+
|
|
24
|
+
const DEFAULT_TOOLS: RichTextTool[] = [
|
|
25
|
+
'bold', 'italic', 'underline', 'strike', '|',
|
|
26
|
+
'h1', 'h2', 'h3', 'p', '|',
|
|
27
|
+
'ul', 'ol', 'quote', 'code', '|',
|
|
28
|
+
'alignLeft', 'alignCenter', 'alignRight', '|',
|
|
29
|
+
'link', 'clear', '|', 'undo', 'redo',
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
/** Toolbar glyphs. Mode-independent and not language-dependent, so they stay
|
|
33
|
+
* fixed; the LABELS beside them are localizable via `messages`. */
|
|
34
|
+
const GLYPH: Record<Exclude<RichTextTool, '|'>, string> = {
|
|
35
|
+
bold: 'B', italic: 'I', underline: 'U', strike: 'S',
|
|
36
|
+
h1: 'H1', h2: 'H2', h3: 'H3', p: '¶',
|
|
37
|
+
ul: '•', ol: '1.', quote: '❝', code: '</>',
|
|
38
|
+
alignLeft: '≡', alignCenter: '≣', alignRight: '≡', link: '🔗',
|
|
39
|
+
clear: '⌫', undo: '↶', redo: '↷',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** User-facing strings (localizable via `messages`). Every one is a tooltip +
|
|
43
|
+
* accessible name on a toolbar button, so leaving them hard-coded made the
|
|
44
|
+
* whole toolbar English-only for screen-reader users. */
|
|
45
|
+
type RichTextMessages = Record<Exclude<RichTextTool, '|'>, string> & { linkPrompt: string; toolbar: string }
|
|
46
|
+
const DEFAULT_MESSAGES: RichTextMessages = {
|
|
47
|
+
bold: 'Bold', italic: 'Italic', underline: 'Underline', strike: 'Strikethrough',
|
|
48
|
+
h1: 'Heading 1', h2: 'Heading 2', h3: 'Heading 3', p: 'Paragraph',
|
|
49
|
+
ul: 'Bullet list', ol: 'Numbered list', quote: 'Quote', code: 'Code block',
|
|
50
|
+
alignLeft: 'Align left', alignCenter: 'Align center', alignRight: 'Align right',
|
|
51
|
+
link: 'Link', clear: 'Clear formatting', undo: 'Undo', redo: 'Redo',
|
|
52
|
+
linkPrompt: 'Link URL', toolbar: 'Formatting',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type Props = {
|
|
56
|
+
/** HTML string (bindable). */
|
|
57
|
+
value?: string
|
|
58
|
+
onChange?: (html: string) => void
|
|
59
|
+
placeholder?: string
|
|
60
|
+
disabled?: boolean
|
|
61
|
+
readonly?: boolean
|
|
62
|
+
minHeight?: string
|
|
63
|
+
/** Toolbar tools + order; use '|' for a separator. */
|
|
64
|
+
tools?: RichTextTool[]
|
|
65
|
+
ariaLabel?: string
|
|
66
|
+
/** Override any toolbar label / prompt (localization). */
|
|
67
|
+
messages?: Partial<RichTextMessages>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let {
|
|
71
|
+
value = $bindable(''),
|
|
72
|
+
onChange,
|
|
73
|
+
placeholder = '',
|
|
74
|
+
disabled = false,
|
|
75
|
+
readonly = false,
|
|
76
|
+
minHeight = '160px',
|
|
77
|
+
tools = DEFAULT_TOOLS,
|
|
78
|
+
ariaLabel = 'Rich text editor',
|
|
79
|
+
messages,
|
|
80
|
+
}: Props = $props()
|
|
81
|
+
|
|
82
|
+
const M = $derived(resolveMessages(DEFAULT_MESSAGES, messages))
|
|
83
|
+
const uid = nextEditorId('sv-rt')
|
|
84
|
+
let editorEl = $state<HTMLDivElement | null>(null)
|
|
85
|
+
let focused = $state(false)
|
|
86
|
+
let empty = $state(true)
|
|
87
|
+
let activeState = $state<Record<string, boolean>>({})
|
|
88
|
+
|
|
89
|
+
// Push external value into the DOM only when it differs and we're not typing
|
|
90
|
+
// (writing innerHTML while focused would collapse the caret).
|
|
91
|
+
$effect(() => {
|
|
92
|
+
const v = value ?? ''
|
|
93
|
+
// A contenteditable owns its own DOM: letting Svelte re-render the body
|
|
94
|
+
// would wipe the caret mid-typing, so the value is pushed in by hand and
|
|
95
|
+
// only while the editor is unfocused.
|
|
96
|
+
// eslint-disable-next-line svelte/no-dom-manipulating
|
|
97
|
+
if (editorEl && !focused && editorEl.innerHTML !== v) editorEl.innerHTML = v
|
|
98
|
+
if (editorEl) empty = editorEl.textContent!.trim().length === 0
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
function sync() {
|
|
102
|
+
if (!editorEl) return
|
|
103
|
+
const html = editorEl.innerHTML
|
|
104
|
+
empty = editorEl.textContent!.trim().length === 0
|
|
105
|
+
value = html
|
|
106
|
+
onChange?.(html)
|
|
107
|
+
}
|
|
108
|
+
function q(cmd: string): boolean {
|
|
109
|
+
try { return document.queryCommandState(cmd) } catch { return false }
|
|
110
|
+
}
|
|
111
|
+
function refreshState() {
|
|
112
|
+
activeState = {
|
|
113
|
+
bold: q('bold'), italic: q('italic'), underline: q('underline'), strike: q('strikeThrough'),
|
|
114
|
+
ul: q('insertUnorderedList'), ol: q('insertOrderedList'),
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function exec(cmd: string, arg?: string) {
|
|
118
|
+
if (disabled || readonly) return
|
|
119
|
+
editorEl?.focus()
|
|
120
|
+
try { document.execCommand(cmd, false, arg) } catch { /* jsdom / unsupported */ }
|
|
121
|
+
sync()
|
|
122
|
+
refreshState()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const run: Record<Exclude<RichTextTool, '|'>, () => void> = {
|
|
126
|
+
bold: () => exec('bold'), italic: () => exec('italic'), underline: () => exec('underline'), strike: () => exec('strikeThrough'),
|
|
127
|
+
h1: () => exec('formatBlock', 'H1'), h2: () => exec('formatBlock', 'H2'), h3: () => exec('formatBlock', 'H3'), p: () => exec('formatBlock', 'P'),
|
|
128
|
+
ul: () => exec('insertUnorderedList'), ol: () => exec('insertOrderedList'),
|
|
129
|
+
quote: () => exec('formatBlock', 'BLOCKQUOTE'), code: () => exec('formatBlock', 'PRE'),
|
|
130
|
+
alignLeft: () => exec('justifyLeft'), alignCenter: () => exec('justifyCenter'), alignRight: () => exec('justifyRight'),
|
|
131
|
+
link: () => { const url = typeof prompt === 'function' ? prompt(M.linkPrompt, 'https://') : null; if (url) exec('createLink', url) },
|
|
132
|
+
clear: () => exec('removeFormat'), undo: () => exec('undo'), redo: () => exec('redo'),
|
|
133
|
+
}
|
|
134
|
+
</script>
|
|
135
|
+
|
|
136
|
+
<div class="sv-rt" class:is-disabled={disabled} class:is-readonly={readonly}>
|
|
137
|
+
{#if !readonly}
|
|
138
|
+
<div class="sv-rt__toolbar" role="toolbar" aria-label={M.toolbar} aria-controls={uid}>
|
|
139
|
+
{#each tools as t, i (i)}
|
|
140
|
+
{#if t === '|'}
|
|
141
|
+
<span class="sv-rt__sep" aria-hidden="true"></span>
|
|
142
|
+
{:else}
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
class="sv-rt__btn"
|
|
146
|
+
class:is-on={activeState[t]}
|
|
147
|
+
data-tool={t}
|
|
148
|
+
title={M[t]}
|
|
149
|
+
aria-label={M[t]}
|
|
150
|
+
aria-pressed={activeState[t] ? true : undefined}
|
|
151
|
+
{disabled}
|
|
152
|
+
onmousedown={(e) => e.preventDefault()}
|
|
153
|
+
onclick={run[t]}
|
|
154
|
+
>{GLYPH[t]}</button>
|
|
155
|
+
{/if}
|
|
156
|
+
{/each}
|
|
157
|
+
</div>
|
|
158
|
+
{/if}
|
|
159
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
160
|
+
<div
|
|
161
|
+
bind:this={editorEl}
|
|
162
|
+
id={uid}
|
|
163
|
+
class="sv-rt__body"
|
|
164
|
+
class:is-empty={empty}
|
|
165
|
+
style:min-height={minHeight}
|
|
166
|
+
contenteditable={!disabled && !readonly}
|
|
167
|
+
role="textbox"
|
|
168
|
+
aria-multiline="true"
|
|
169
|
+
aria-label={ariaLabel}
|
|
170
|
+
data-placeholder={placeholder}
|
|
171
|
+
tabindex={disabled ? -1 : 0}
|
|
172
|
+
oninput={sync}
|
|
173
|
+
onfocus={() => (focused = true)}
|
|
174
|
+
onblur={() => { focused = false; sync() }}
|
|
175
|
+
onkeyup={refreshState}
|
|
176
|
+
onmouseup={refreshState}
|
|
177
|
+
></div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<style>
|
|
181
|
+
.sv-rt {
|
|
182
|
+
--_accent: var(--sg-accent, #2563eb);
|
|
183
|
+
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
|
|
184
|
+
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); overflow: hidden;
|
|
185
|
+
display: flex; flex-direction: column;
|
|
186
|
+
}
|
|
187
|
+
.sv-rt:focus-within { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 20%, transparent); }
|
|
188
|
+
.sv-rt.is-disabled { opacity: 0.6; }
|
|
189
|
+
.sv-rt__toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 1px; padding: 5px 6px; border-bottom: 1px solid var(--sg-border, #e2e8f0); background: var(--sg-header-bg, #f8fafc); }
|
|
190
|
+
.sv-rt__btn {
|
|
191
|
+
min-width: 28px; height: 28px; padding: 0 6px; display: grid; place-items: center;
|
|
192
|
+
background: none; border: 0; border-radius: 6px; color: var(--sg-fg, #334155); cursor: pointer;
|
|
193
|
+
font-size: 12.5px; font-weight: 600;
|
|
194
|
+
}
|
|
195
|
+
.sv-rt__btn:hover:not(:disabled) { background: var(--sg-row-hover-bg, #eef2f7); }
|
|
196
|
+
.sv-rt__btn.is-on { background: color-mix(in srgb, var(--_accent) 16%, transparent); color: var(--_accent); }
|
|
197
|
+
.sv-rt__btn[data-tool='italic'] { font-style: italic; }
|
|
198
|
+
.sv-rt__btn[data-tool='underline'] { text-decoration: underline; }
|
|
199
|
+
.sv-rt__btn[data-tool='strike'] { text-decoration: line-through; }
|
|
200
|
+
.sv-rt__btn[data-tool='alignRight'] { transform: scaleX(-1); }
|
|
201
|
+
.sv-rt__sep { width: 1px; height: 18px; background: var(--sg-border, #e2e8f0); margin: 0 4px; }
|
|
202
|
+
.sv-rt__body { padding: 10px 12px; outline: none; overflow: auto; font-size: 14px; line-height: 1.6; }
|
|
203
|
+
.sv-rt__body.is-empty::before { content: attr(data-placeholder); color: var(--sg-muted, #94a3b8); pointer-events: none; }
|
|
204
|
+
.sv-rt__body :global(h1) { font-size: 1.5em; font-weight: 700; margin: 0.4em 0; }
|
|
205
|
+
.sv-rt__body :global(h2) { font-size: 1.25em; font-weight: 700; margin: 0.4em 0; }
|
|
206
|
+
.sv-rt__body :global(h3) { font-size: 1.1em; font-weight: 650; margin: 0.4em 0; }
|
|
207
|
+
.sv-rt__body :global(blockquote) { margin: 0.4em 0; padding-inline-start: 12px; border-inline-start: 3px solid var(--sg-border, #cbd5e1); color: var(--sg-muted, #64748b); }
|
|
208
|
+
.sv-rt__body :global(pre) { margin: 0.4em 0; padding: 10px 12px; border-radius: 8px; background: var(--sg-row-hover-bg, #f1f5f9); font: 12.5px/1.6 ui-monospace, monospace; overflow-x: auto; }
|
|
209
|
+
.sv-rt__body :global(a) { color: var(--_accent); }
|
|
210
|
+
.sv-rt__body :global(ul), .sv-rt__body :global(ol) { margin: 0.4em 0; padding-inline-start: 24px; }
|
|
211
|
+
</style>
|