@konce-pt/react 0.8.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/LICENSE +21 -0
- package/README.md +58 -0
- package/dist/accordion/accordion.d.ts +45 -0
- package/dist/accordion/accordion.js +69 -0
- package/dist/alert/alert.d.ts +25 -0
- package/dist/alert/alert.js +31 -0
- package/dist/app-shell/app-shell.d.ts +94 -0
- package/dist/app-shell/app-shell.js +105 -0
- package/dist/auth/auth-types.d.ts +44 -0
- package/dist/auth/auth-types.js +2 -0
- package/dist/auth/auth.d.ts +76 -0
- package/dist/auth/auth.js +184 -0
- package/dist/autocomplete/autocomplete.d.ts +18 -0
- package/dist/autocomplete/autocomplete.js +72 -0
- package/dist/avatar/avatar.d.ts +33 -0
- package/dist/avatar/avatar.js +30 -0
- package/dist/avatar-group/avatar-group.d.ts +21 -0
- package/dist/avatar-group/avatar-group.js +15 -0
- package/dist/badge/badge.d.ts +25 -0
- package/dist/badge/badge.js +14 -0
- package/dist/bottom-sheet/bottom-sheet.d.ts +21 -0
- package/dist/bottom-sheet/bottom-sheet.js +22 -0
- package/dist/breadcrumb/breadcrumb.d.ts +19 -0
- package/dist/breadcrumb/breadcrumb.js +20 -0
- package/dist/button/button.d.ts +27 -0
- package/dist/button/button.js +49 -0
- package/dist/button-group/button-group.d.ts +25 -0
- package/dist/button-group/button-group.js +30 -0
- package/dist/card/card.d.ts +66 -0
- package/dist/card/card.js +96 -0
- package/dist/carousel/carousel.d.ts +20 -0
- package/dist/carousel/carousel.js +24 -0
- package/dist/charts/chart-legend.d.ts +23 -0
- package/dist/charts/chart-legend.js +9 -0
- package/dist/charts/chart-tooltip.d.ts +21 -0
- package/dist/charts/chart-tooltip.js +13 -0
- package/dist/charts/chart.d.ts +18 -0
- package/dist/charts/chart.js +231 -0
- package/dist/charts-entry/public-api.d.ts +4 -0
- package/dist/charts-entry/public-api.js +7 -0
- package/dist/checkbox/checkbox.d.ts +33 -0
- package/dist/checkbox/checkbox.js +29 -0
- package/dist/chip/chip.d.ts +22 -0
- package/dist/chip/chip.js +16 -0
- package/dist/chips-input/chips-input.d.ts +25 -0
- package/dist/chips-input/chips-input.js +54 -0
- package/dist/clock/clock.d.ts +17 -0
- package/dist/clock/clock.js +57 -0
- package/dist/color-picker/color-picker.d.ts +23 -0
- package/dist/color-picker/color-picker.js +28 -0
- package/dist/confirm/confirm-container.d.ts +13 -0
- package/dist/confirm/confirm-container.js +26 -0
- package/dist/confirm/confirm-store.d.ts +29 -0
- package/dist/confirm/confirm-store.js +52 -0
- package/dist/context-menu/context-menu.d.ts +21 -0
- package/dist/context-menu/context-menu.js +36 -0
- package/dist/data-table/data-table-types.d.ts +67 -0
- package/dist/data-table/data-table-types.js +1 -0
- package/dist/data-table/data-table.d.ts +62 -0
- package/dist/data-table/data-table.js +453 -0
- package/dist/data-table/export-csv.d.ts +7 -0
- package/dist/data-table/export-csv.js +19 -0
- package/dist/data-view/data-view.d.ts +21 -0
- package/dist/data-view/data-view.js +25 -0
- package/dist/date-range/date-range.d.ts +30 -0
- package/dist/date-range/date-range.js +155 -0
- package/dist/datepicker/datepicker.d.ts +39 -0
- package/dist/datepicker/datepicker.js +202 -0
- package/dist/dialog/dialog.d.ts +35 -0
- package/dist/dialog/dialog.js +59 -0
- package/dist/divider/divider.d.ts +16 -0
- package/dist/divider/divider.js +13 -0
- package/dist/drawer/drawer.d.ts +25 -0
- package/dist/drawer/drawer.js +25 -0
- package/dist/empty/empty.d.ts +18 -0
- package/dist/empty/empty.js +14 -0
- package/dist/fab/fab.d.ts +22 -0
- package/dist/fab/fab.js +17 -0
- package/dist/fieldset/fieldset.d.ts +22 -0
- package/dist/fieldset/fieldset.js +24 -0
- package/dist/file-upload/file-upload.d.ts +22 -0
- package/dist/file-upload/file-upload.js +50 -0
- package/dist/form-field/form-field-context.d.ts +23 -0
- package/dist/form-field/form-field-context.js +17 -0
- package/dist/form-field/form-field.d.ts +39 -0
- package/dist/form-field/form-field.js +41 -0
- package/dist/galleria/galleria.d.ts +22 -0
- package/dist/galleria/galleria.js +21 -0
- package/dist/grid/col.d.ts +39 -0
- package/dist/grid/col.js +32 -0
- package/dist/grid/flex.d.ts +45 -0
- package/dist/grid/flex.js +35 -0
- package/dist/grid/grid.d.ts +47 -0
- package/dist/grid/grid.js +38 -0
- package/dist/grid/layout.d.ts +18 -0
- package/dist/grid/layout.js +32 -0
- package/dist/grid-entry/public-api.d.ts +6 -0
- package/dist/grid-entry/public-api.js +8 -0
- package/dist/i18n/i18n-context.d.ts +45 -0
- package/dist/i18n/i18n-context.js +65 -0
- package/dist/icon/icon-registry.d.ts +16 -0
- package/dist/icon/icon-registry.js +43 -0
- package/dist/icon/icon.d.ts +17 -0
- package/dist/icon/icon.js +23 -0
- package/dist/icon-button/icon-button.d.ts +23 -0
- package/dist/icon-button/icon-button.js +14 -0
- package/dist/icons-entry/public-api.d.ts +12 -0
- package/dist/icons-entry/public-api.js +20 -0
- package/dist/image/image.d.ts +17 -0
- package/dist/image/image.js +16 -0
- package/dist/input/input.d.ts +41 -0
- package/dist/input/input.js +36 -0
- package/dist/input-mask/input-mask.d.ts +19 -0
- package/dist/input-mask/input-mask.js +65 -0
- package/dist/input-number/input-number.d.ts +28 -0
- package/dist/input-number/input-number.js +52 -0
- package/dist/input-otp/input-otp.d.ts +21 -0
- package/dist/input-otp/input-otp.js +59 -0
- package/dist/internal/cn.d.ts +9 -0
- package/dist/internal/cn.js +19 -0
- package/dist/internal/compose-refs.d.ts +6 -0
- package/dist/internal/compose-refs.js +16 -0
- package/dist/internal/overlay-container.d.ts +3 -0
- package/dist/internal/overlay-container.js +19 -0
- package/dist/internal/overlay.d.ts +26 -0
- package/dist/internal/overlay.js +64 -0
- package/dist/internal/slot.d.ts +23 -0
- package/dist/internal/slot.js +49 -0
- package/dist/internal/types.d.ts +25 -0
- package/dist/internal/types.js +1 -0
- package/dist/internal/use-breakpoint.d.ts +18 -0
- package/dist/internal/use-breakpoint.js +52 -0
- package/dist/internal/use-controllable-state.d.ts +11 -0
- package/dist/internal/use-controllable-state.js +18 -0
- package/dist/internal/use-id.d.ts +9 -0
- package/dist/internal/use-id.js +13 -0
- package/dist/knob/knob.d.ts +27 -0
- package/dist/knob/knob.js +86 -0
- package/dist/listbox/listbox.d.ts +28 -0
- package/dist/listbox/listbox.js +43 -0
- package/dist/map/map-create-dialog.d.ts +21 -0
- package/dist/map/map-create-dialog.js +196 -0
- package/dist/map/map-hours-field.d.ts +22 -0
- package/dist/map/map-hours-field.js +90 -0
- package/dist/map/map-image-field.d.ts +24 -0
- package/dist/map/map-image-field.js +61 -0
- package/dist/map/map-legend.d.ts +29 -0
- package/dist/map/map-legend.js +41 -0
- package/dist/map/map-poi-card.d.ts +42 -0
- package/dist/map/map-poi-card.js +152 -0
- package/dist/map/map-search.d.ts +37 -0
- package/dist/map/map-search.js +201 -0
- package/dist/map/map.d.ts +113 -0
- package/dist/map/map.js +425 -0
- package/dist/map/ports.d.ts +76 -0
- package/dist/map/ports.js +174 -0
- package/dist/map-entry/public-api.d.ts +20 -0
- package/dist/map-entry/public-api.js +19 -0
- package/dist/megamenu/megamenu.d.ts +17 -0
- package/dist/megamenu/megamenu.js +28 -0
- package/dist/menu/menu-item.d.ts +33 -0
- package/dist/menu/menu-item.js +1 -0
- package/dist/menu/menu-rows.d.ts +9 -0
- package/dist/menu/menu-rows.js +15 -0
- package/dist/menu/menu.d.ts +22 -0
- package/dist/menu/menu.js +31 -0
- package/dist/menubar/menubar.d.ts +16 -0
- package/dist/menubar/menubar.js +39 -0
- package/dist/meter-group/meter-group.d.ts +20 -0
- package/dist/meter-group/meter-group.js +24 -0
- package/dist/order-list/order-list.d.ts +18 -0
- package/dist/order-list/order-list.js +37 -0
- package/dist/paginator/paginator.d.ts +29 -0
- package/dist/paginator/paginator.js +45 -0
- package/dist/panel/panel.d.ts +31 -0
- package/dist/panel/panel.js +36 -0
- package/dist/password/password.d.ts +28 -0
- package/dist/password/password.js +37 -0
- package/dist/pick-list/pick-list.d.ts +28 -0
- package/dist/pick-list/pick-list.js +65 -0
- package/dist/popover/popover.d.ts +23 -0
- package/dist/popover/popover.js +27 -0
- package/dist/progress/progress.d.ts +19 -0
- package/dist/progress/progress.js +21 -0
- package/dist/public-api.d.ts +181 -0
- package/dist/public-api.js +105 -0
- package/dist/radio-group/radio-group.d.ts +24 -0
- package/dist/radio-group/radio-group.js +23 -0
- package/dist/rating/rating.d.ts +22 -0
- package/dist/rating/rating.js +30 -0
- package/dist/rich-text/lists.d.ts +17 -0
- package/dist/rich-text/lists.js +144 -0
- package/dist/rich-text/rich-text.d.ts +28 -0
- package/dist/rich-text/rich-text.js +654 -0
- package/dist/rich-text/sanitize.d.ts +10 -0
- package/dist/rich-text/sanitize.js +85 -0
- package/dist/rich-text/tables.d.ts +44 -0
- package/dist/rich-text/tables.js +268 -0
- package/dist/roadmap/roadmap.d.ts +37 -0
- package/dist/roadmap/roadmap.js +271 -0
- package/dist/roadmap-entry/public-api.d.ts +4 -0
- package/dist/roadmap-entry/public-api.js +7 -0
- package/dist/scroll-top/scroll-top.d.ts +16 -0
- package/dist/scroll-top/scroll-top.js +38 -0
- package/dist/select/select.d.ts +37 -0
- package/dist/select/select.js +117 -0
- package/dist/shared/types.d.ts +8 -0
- package/dist/shared/types.js +1 -0
- package/dist/sidenav/sidenav.d.ts +26 -0
- package/dist/sidenav/sidenav.js +41 -0
- package/dist/skeleton/skeleton.d.ts +20 -0
- package/dist/skeleton/skeleton.js +15 -0
- package/dist/slider/slider.d.ts +28 -0
- package/dist/slider/slider.js +30 -0
- package/dist/speed-dial/speed-dial.d.ts +20 -0
- package/dist/speed-dial/speed-dial.js +23 -0
- package/dist/spinner/spinner.d.ts +15 -0
- package/dist/spinner/spinner.js +14 -0
- package/dist/split-button/split-button.d.ts +28 -0
- package/dist/split-button/split-button.js +34 -0
- package/dist/splitter/splitter.d.ts +39 -0
- package/dist/splitter/splitter.js +80 -0
- package/dist/stepper/stepper.d.ts +51 -0
- package/dist/stepper/stepper.js +71 -0
- package/dist/switch/switch.d.ts +39 -0
- package/dist/switch/switch.js +30 -0
- package/dist/switch-group/switch-group.d.ts +29 -0
- package/dist/switch-group/switch-group.js +47 -0
- package/dist/tabs/tabs.d.ts +38 -0
- package/dist/tabs/tabs.js +74 -0
- package/dist/textarea/textarea.d.ts +28 -0
- package/dist/textarea/textarea.js +44 -0
- package/dist/timeline/timeline.d.ts +19 -0
- package/dist/timeline/timeline.js +12 -0
- package/dist/toast/toast-container.d.ts +12 -0
- package/dist/toast/toast-container.js +23 -0
- package/dist/toast/toast-store.d.ts +26 -0
- package/dist/toast/toast-store.js +51 -0
- package/dist/toolbar/toolbar.d.ts +29 -0
- package/dist/toolbar/toolbar.js +24 -0
- package/dist/tooltip/tooltip.d.ts +26 -0
- package/dist/tooltip/tooltip.js +113 -0
- package/dist/tree/tree.d.ts +31 -0
- package/dist/tree/tree.js +62 -0
- package/package.json +91 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState, } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import { cn } from "../internal/cn.js";
|
|
5
|
+
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
6
|
+
import { useKptOverlayContainer } from "../internal/overlay-container.js";
|
|
7
|
+
import { KptOverlay } from "../internal/overlay.js";
|
|
8
|
+
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
9
|
+
import { KptIcon } from "../icon/icon.js";
|
|
10
|
+
import { sanitizeHtml, sanitizeUrl } from "./sanitize.js";
|
|
11
|
+
import { canIndentItem, canOutdentItem, selectedItems, shiftItems } from "./lists.js";
|
|
12
|
+
import { CELL_SELECTED, deleteColumn, deleteRow, insertColumn, insertRow, mergeCells, regionBetween, splitCell, tableHtml, toggleHeaderColumn, toggleHeaderRow, } from "./tables.js";
|
|
13
|
+
const ICON = {
|
|
14
|
+
copy: 'copy',
|
|
15
|
+
paste: 'clipboard',
|
|
16
|
+
pasteText: 'clipboard-text',
|
|
17
|
+
heading: 'code', // nieużywane — „heading" renderuje select
|
|
18
|
+
bold: 'bold',
|
|
19
|
+
italic: 'italic',
|
|
20
|
+
underline: 'underline',
|
|
21
|
+
strike: 'strikethrough',
|
|
22
|
+
ul: 'list',
|
|
23
|
+
ol: 'list-numbers',
|
|
24
|
+
indent: 'indent-increase',
|
|
25
|
+
outdent: 'indent-decrease',
|
|
26
|
+
foreColor: 'palette',
|
|
27
|
+
backColor: 'highlight',
|
|
28
|
+
alignLeft: 'align-left',
|
|
29
|
+
alignCenter: 'align-center',
|
|
30
|
+
alignRight: 'align-right',
|
|
31
|
+
alignJustify: 'align-justified',
|
|
32
|
+
link: 'link',
|
|
33
|
+
table: 'table',
|
|
34
|
+
clear: 'clear-formatting',
|
|
35
|
+
html: 'code',
|
|
36
|
+
};
|
|
37
|
+
/** Narzędzia zwykłe → komenda `execCommand` (zarazem nazwa dla `queryCommandState`). */
|
|
38
|
+
const COMMAND = {
|
|
39
|
+
bold: 'bold',
|
|
40
|
+
italic: 'italic',
|
|
41
|
+
underline: 'underline',
|
|
42
|
+
strike: 'strikeThrough',
|
|
43
|
+
ul: 'insertUnorderedList',
|
|
44
|
+
ol: 'insertOrderedList',
|
|
45
|
+
alignLeft: 'justifyLeft',
|
|
46
|
+
alignCenter: 'justifyCenter',
|
|
47
|
+
alignRight: 'justifyRight',
|
|
48
|
+
alignJustify: 'justifyFull',
|
|
49
|
+
clear: 'removeFormat',
|
|
50
|
+
};
|
|
51
|
+
const SWATCHES = [
|
|
52
|
+
'#000000', '#434343', '#666666', '#999999', '#b7b7b7', '#cccccc', '#ffffff',
|
|
53
|
+
'#e11d48', '#ea580c', '#ca8a04', '#16a34a', '#0891b2', '#2563eb', '#7c3aed',
|
|
54
|
+
];
|
|
55
|
+
const DEFAULT_TOOLBAR = [
|
|
56
|
+
['copy', 'paste', 'pasteText'],
|
|
57
|
+
['heading', 'bold', 'italic', 'underline', 'strike', 'ul', 'ol', 'indent', 'outdent'],
|
|
58
|
+
['foreColor', 'backColor'],
|
|
59
|
+
['alignLeft', 'alignCenter', 'alignRight', 'alignJustify'],
|
|
60
|
+
['link', 'table', 'clear', 'html'],
|
|
61
|
+
];
|
|
62
|
+
const GRID_ROWS = Array.from({ length: 8 }, (_, i) => i + 1);
|
|
63
|
+
const GRID_COLS = Array.from({ length: 8 }, (_, i) => i + 1);
|
|
64
|
+
/**
|
|
65
|
+
* Jedyne wywołania `document.execCommand` w komponencie. API jest formalnie wycofane, ale nie ma
|
|
66
|
+
* zamiennika: to jedyny sposób formatowania trafiający do natywnego stosu cofania przeglądarki
|
|
67
|
+
* (Ctrl+Z). Własne komendy wymagałyby własnej historii, czyli przepisania edytora.
|
|
68
|
+
*/
|
|
69
|
+
function legacyExec(cmd, value) {
|
|
70
|
+
return document.execCommand(cmd, false, value); // NOSONAR: brak zamiennika, patrz JSDoc
|
|
71
|
+
}
|
|
72
|
+
function legacyState(cmd) {
|
|
73
|
+
try {
|
|
74
|
+
return document.queryCommandState(cmd); // NOSONAR: brak zamiennika
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Lekki edytor tekstu sformatowanego na `contenteditable`, z konfigurowalnym paskiem sekcji
|
|
82
|
+
* i edycją tabel. Wartością jest HTML.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* <KptRichText value={html} onValueChange={setHtml} />
|
|
86
|
+
* <KptRichText value={html} onValueChange={setHtml} toolbar={[['bold', 'italic'], ['link']]} />
|
|
87
|
+
*/
|
|
88
|
+
export function KptRichText({ value, defaultValue = '', onValueChange, toolbar = DEFAULT_TOOLBAR, placeholder = '', maxListLevel = 6, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
|
|
89
|
+
const { t } = useKptMessages({ locale, dictionary });
|
|
90
|
+
const container = useKptOverlayContainer();
|
|
91
|
+
const editorRef = useRef(null);
|
|
92
|
+
const savedRange = useRef(null);
|
|
93
|
+
const [current, setCurrent] = useControllableState({ value, defaultValue, onChange: onValueChange });
|
|
94
|
+
const showInvalid = invalid && (touched ?? true);
|
|
95
|
+
// Kotwice narzędzi z panelem — `KptOverlay` przypina panel do konkretnego przycisku.
|
|
96
|
+
const [styleAnchor, setStyleAnchor] = useState(null);
|
|
97
|
+
const [foreAnchor, setForeAnchor] = useState(null);
|
|
98
|
+
const [backAnchor, setBackAnchor] = useState(null);
|
|
99
|
+
const [tableAnchor, setTableAnchor] = useState(null);
|
|
100
|
+
const [styleOpen, setStyleOpen] = useState(false);
|
|
101
|
+
const [colorOpen, setColorOpen] = useState(false);
|
|
102
|
+
const [colorKind, setColorKind] = useState('foreColor');
|
|
103
|
+
const [tableOpen, setTableOpen] = useState(false);
|
|
104
|
+
const [grid, setGrid] = useState({ r: 0, c: 0 });
|
|
105
|
+
const [sourceView, setSourceView] = useState(false);
|
|
106
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
107
|
+
const [menuPoint, setMenuPoint] = useState({ x: 0, y: 0 });
|
|
108
|
+
const [borderOpen, setBorderOpen] = useState(false);
|
|
109
|
+
const [borderTarget, setBorderTarget] = useState('table');
|
|
110
|
+
const [activeCmds, setActiveCmds] = useState(new Set());
|
|
111
|
+
const [canIndent, setCanIndent] = useState(false);
|
|
112
|
+
const [canOutdent, setCanOutdent] = useState(false);
|
|
113
|
+
const [currentBlock, setCurrentBlock] = useState('p');
|
|
114
|
+
const [selCount, setSelCount] = useState(0);
|
|
115
|
+
const ctxCell = useRef(null);
|
|
116
|
+
const canSplitCtx = useRef(false);
|
|
117
|
+
const selCells = useRef(new Set());
|
|
118
|
+
const selAnchor = useRef(null);
|
|
119
|
+
const selecting = useRef(false);
|
|
120
|
+
const resize = useRef(null);
|
|
121
|
+
const sections = toolbar.length === 0
|
|
122
|
+
? []
|
|
123
|
+
: Array.isArray(toolbar[0])
|
|
124
|
+
? toolbar
|
|
125
|
+
: [toolbar];
|
|
126
|
+
const pushValue = useCallback(() => {
|
|
127
|
+
const el = editorRef.current;
|
|
128
|
+
if (el)
|
|
129
|
+
setCurrent(el.innerHTML);
|
|
130
|
+
}, [setCurrent]);
|
|
131
|
+
/*
|
|
132
|
+
* Zapis z zewnątrz wstawiamy ręcznie, bo `contenteditable` nie może być renderowany przez
|
|
133
|
+
* Reacta — przerysowanie kasowałoby zaznaczenie i pozycję kursora przy każdym znaku.
|
|
134
|
+
* Porównanie z surowym `innerHTML` sprawia, że pisanie w edytorze nie wchodzi w tę gałąź,
|
|
135
|
+
* więc sanityzacja włącza się wyłącznie przy zapisie z aplikacji albo z trybu HTML.
|
|
136
|
+
*/
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
const el = editorRef.current;
|
|
139
|
+
if (el && el.innerHTML !== current)
|
|
140
|
+
el.innerHTML = sanitizeHtml(current);
|
|
141
|
+
}, [current, sourceView]);
|
|
142
|
+
const inEditor = useCallback(() => {
|
|
143
|
+
const el = editorRef.current;
|
|
144
|
+
const selection = window.getSelection();
|
|
145
|
+
return !!el && !!selection && selection.rangeCount > 0 && el.contains(selection.anchorNode);
|
|
146
|
+
}, []);
|
|
147
|
+
/** Tag bloku, w którym stoi kursor — dla selecta stylu; domyślnie paragraf. */
|
|
148
|
+
const blockTag = useCallback(() => {
|
|
149
|
+
let node = window.getSelection()?.anchorNode;
|
|
150
|
+
while (node && node !== editorRef.current) {
|
|
151
|
+
const tag = node.tagName?.toLowerCase();
|
|
152
|
+
if (tag && /^(h[1-6]|p|blockquote)$/.test(tag))
|
|
153
|
+
return tag;
|
|
154
|
+
node = node.parentElement;
|
|
155
|
+
}
|
|
156
|
+
return 'p';
|
|
157
|
+
}, []);
|
|
158
|
+
const refreshState = useCallback(() => {
|
|
159
|
+
if (disabled || !inEditor())
|
|
160
|
+
return;
|
|
161
|
+
const active = new Set();
|
|
162
|
+
for (const cmd of Object.values(COMMAND)) {
|
|
163
|
+
if (cmd && cmd !== 'removeFormat' && legacyState(cmd))
|
|
164
|
+
active.add(cmd);
|
|
165
|
+
}
|
|
166
|
+
setActiveCmds(active);
|
|
167
|
+
const items = selectedItems(editorRef.current);
|
|
168
|
+
setCanIndent(items.some((li) => canIndentItem(editorRef.current, li, maxListLevel)));
|
|
169
|
+
setCanOutdent(items.some((li) => canOutdentItem(li)));
|
|
170
|
+
setCurrentBlock(blockTag());
|
|
171
|
+
}, [blockTag, disabled, inEditor, maxListLevel]);
|
|
172
|
+
const clearCellSelection = useCallback(() => {
|
|
173
|
+
for (const cell of selCells.current)
|
|
174
|
+
cell.classList.remove(CELL_SELECTED);
|
|
175
|
+
selCells.current = new Set();
|
|
176
|
+
setSelCount(0);
|
|
177
|
+
}, []);
|
|
178
|
+
// Stan narzędzi zależy od zaznaczenia, a to zmienia się także poza naszymi zdarzeniami
|
|
179
|
+
// (myszą, klawiaturą, przez inne okno), więc nasłuchujemy `selectionchange` na dokumencie.
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
const onSelectionChange = () => refreshState();
|
|
182
|
+
document.addEventListener('selectionchange', onSelectionChange);
|
|
183
|
+
return () => document.removeEventListener('selectionchange', onSelectionChange);
|
|
184
|
+
}, [refreshState]);
|
|
185
|
+
// Przeciąganie krawędzi kolumny i zaznaczanie komórek biegną na całym dokumencie: kursor
|
|
186
|
+
// wyjeżdża poza tabelę praktycznie zawsze.
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
const onMove = (event) => {
|
|
189
|
+
if (resize.current) {
|
|
190
|
+
const width = Math.max(32, resize.current.startW + (event.clientX - resize.current.startX));
|
|
191
|
+
resize.current.col.style.width = `${width}px`;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (!selAnchor.current || !(event.buttons & 1))
|
|
195
|
+
return;
|
|
196
|
+
const target = event.target;
|
|
197
|
+
if (!(target instanceof Element))
|
|
198
|
+
return;
|
|
199
|
+
const focus = target.closest('td, th');
|
|
200
|
+
if (!focus || focus.closest('table') !== selAnchor.current.closest('table'))
|
|
201
|
+
return;
|
|
202
|
+
if (focus === selAnchor.current && !selecting.current)
|
|
203
|
+
return;
|
|
204
|
+
selecting.current = true;
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
window.getSelection()?.removeAllRanges();
|
|
207
|
+
const region = regionBetween(selAnchor.current, focus);
|
|
208
|
+
clearCellSelection();
|
|
209
|
+
for (const cell of region)
|
|
210
|
+
cell.classList.add(CELL_SELECTED);
|
|
211
|
+
selCells.current = region;
|
|
212
|
+
setSelCount(region.size);
|
|
213
|
+
};
|
|
214
|
+
const onUp = () => {
|
|
215
|
+
if (resize.current) {
|
|
216
|
+
resize.current = null;
|
|
217
|
+
pushValue();
|
|
218
|
+
}
|
|
219
|
+
if (selAnchor.current && !selecting.current)
|
|
220
|
+
clearCellSelection(); // zwykły klik
|
|
221
|
+
selAnchor.current = null;
|
|
222
|
+
selecting.current = false;
|
|
223
|
+
};
|
|
224
|
+
document.addEventListener('mousemove', onMove);
|
|
225
|
+
document.addEventListener('mouseup', onUp);
|
|
226
|
+
return () => {
|
|
227
|
+
document.removeEventListener('mousemove', onMove);
|
|
228
|
+
document.removeEventListener('mouseup', onUp);
|
|
229
|
+
};
|
|
230
|
+
}, [clearCellSelection, pushValue]);
|
|
231
|
+
const focusEditor = () => editorRef.current?.focus();
|
|
232
|
+
const saveSelection = () => {
|
|
233
|
+
const selection = window.getSelection();
|
|
234
|
+
savedRange.current = selection && selection.rangeCount > 0 ? selection.getRangeAt(0).cloneRange() : null;
|
|
235
|
+
};
|
|
236
|
+
const restoreSelection = () => {
|
|
237
|
+
const selection = window.getSelection();
|
|
238
|
+
if (savedRange.current && selection) {
|
|
239
|
+
selection.removeAllRanges();
|
|
240
|
+
selection.addRange(savedRange.current);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
// ── Schowek ──
|
|
244
|
+
const copySelection = async () => {
|
|
245
|
+
const selection = window.getSelection();
|
|
246
|
+
if (!selection || selection.rangeCount === 0 || selection.isCollapsed)
|
|
247
|
+
return;
|
|
248
|
+
const holder = document.createElement('div');
|
|
249
|
+
holder.append(selection.getRangeAt(0).cloneContents());
|
|
250
|
+
try {
|
|
251
|
+
await navigator.clipboard.write([
|
|
252
|
+
new ClipboardItem({
|
|
253
|
+
'text/html': new Blob([holder.innerHTML], { type: 'text/html' }),
|
|
254
|
+
'text/plain': new Blob([selection.toString()], { type: 'text/plain' }),
|
|
255
|
+
}),
|
|
256
|
+
]);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
legacyExec('copy'); // brak uprawnień albo starsza przeglądarka
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
const pasteRich = async () => {
|
|
263
|
+
try {
|
|
264
|
+
const items = await navigator.clipboard.read();
|
|
265
|
+
for (const item of items) {
|
|
266
|
+
if (item.types.includes('text/html')) {
|
|
267
|
+
const html = sanitizeHtml(await (await item.getType('text/html')).text());
|
|
268
|
+
focusEditor();
|
|
269
|
+
legacyExec('insertHTML', html);
|
|
270
|
+
pushValue();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const text = await navigator.clipboard.readText();
|
|
275
|
+
focusEditor();
|
|
276
|
+
legacyExec('insertText', text);
|
|
277
|
+
pushValue();
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
/* brak uprawnień albo API — ignorujemy */
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
const pastePlain = async () => {
|
|
284
|
+
try {
|
|
285
|
+
const text = await navigator.clipboard.readText();
|
|
286
|
+
focusEditor();
|
|
287
|
+
legacyExec('insertText', text);
|
|
288
|
+
pushValue();
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
/* brak uprawnień albo API — ignorujemy */
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
// ── Narzędzia ──
|
|
295
|
+
const exec = (item) => {
|
|
296
|
+
if (disabled)
|
|
297
|
+
return;
|
|
298
|
+
if (item === 'html') {
|
|
299
|
+
setSourceView((v) => !v);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (item === 'table') {
|
|
303
|
+
saveSelection();
|
|
304
|
+
setTableOpen(true);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
focusEditor();
|
|
308
|
+
switch (item) {
|
|
309
|
+
case 'link': {
|
|
310
|
+
const url = window.prompt(t('richText.linkPrompt'));
|
|
311
|
+
if (url) {
|
|
312
|
+
const safe = sanitizeUrl(url);
|
|
313
|
+
if (safe)
|
|
314
|
+
legacyExec('createLink', safe);
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
case 'copy':
|
|
319
|
+
void copySelection();
|
|
320
|
+
return;
|
|
321
|
+
case 'paste':
|
|
322
|
+
void pasteRich();
|
|
323
|
+
return;
|
|
324
|
+
case 'pasteText':
|
|
325
|
+
void pastePlain();
|
|
326
|
+
return;
|
|
327
|
+
case 'indent':
|
|
328
|
+
case 'outdent':
|
|
329
|
+
if (shiftItems(editorRef.current, item, maxListLevel)) {
|
|
330
|
+
pushValue();
|
|
331
|
+
refreshState();
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
default: {
|
|
335
|
+
const cmd = COMMAND[item];
|
|
336
|
+
if (cmd)
|
|
337
|
+
legacyExec(cmd);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
pushValue();
|
|
341
|
+
refreshState();
|
|
342
|
+
};
|
|
343
|
+
/**
|
|
344
|
+
* Tab: wcięcie w liście, przeskok komórki w tabeli. Poza nimi i na krawędziach zostawiamy Tab
|
|
345
|
+
* natywny — inaczej edytor byłby pułapką klawiaturową (WCAG 2.1.2).
|
|
346
|
+
*/
|
|
347
|
+
const onKeyDown = (event) => {
|
|
348
|
+
if (disabled || event.key !== 'Tab' || event.ctrlKey || event.altKey || event.metaKey)
|
|
349
|
+
return;
|
|
350
|
+
if (shiftItems(editorRef.current, event.shiftKey ? 'outdent' : 'indent', maxListLevel)) {
|
|
351
|
+
event.preventDefault();
|
|
352
|
+
pushValue();
|
|
353
|
+
refreshState();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const node = window.getSelection()?.anchorNode ?? null;
|
|
357
|
+
const el = node instanceof Element ? node : (node?.parentElement ?? null);
|
|
358
|
+
const cell = el?.closest('td, th');
|
|
359
|
+
if (!cell || !editorRef.current?.contains(cell))
|
|
360
|
+
return;
|
|
361
|
+
const table = cell.closest('table');
|
|
362
|
+
if (!table)
|
|
363
|
+
return;
|
|
364
|
+
const cells = [...table.querySelectorAll('td, th')];
|
|
365
|
+
const next = cells[cells.indexOf(cell) + (event.shiftKey ? -1 : 1)];
|
|
366
|
+
if (!next)
|
|
367
|
+
return; // koniec tabeli — Tab zostaje natywny
|
|
368
|
+
event.preventDefault();
|
|
369
|
+
const range = document.createRange();
|
|
370
|
+
range.selectNodeContents(next);
|
|
371
|
+
const selection = window.getSelection();
|
|
372
|
+
selection?.removeAllRanges();
|
|
373
|
+
selection?.addRange(range);
|
|
374
|
+
clearCellSelection();
|
|
375
|
+
refreshState();
|
|
376
|
+
};
|
|
377
|
+
/** Natywne wklejanie — czyścimy treść, żeby nie wnosić śmieci z edytorów tekstu i stron. */
|
|
378
|
+
const onPaste = (event) => {
|
|
379
|
+
if (disabled)
|
|
380
|
+
return;
|
|
381
|
+
const html = event.clipboardData?.getData('text/html');
|
|
382
|
+
const text = event.clipboardData?.getData('text/plain') ?? '';
|
|
383
|
+
event.preventDefault();
|
|
384
|
+
if (html)
|
|
385
|
+
legacyExec('insertHTML', sanitizeHtml(html));
|
|
386
|
+
else
|
|
387
|
+
legacyExec('insertText', text);
|
|
388
|
+
pushValue();
|
|
389
|
+
};
|
|
390
|
+
// ── Kolory ──
|
|
391
|
+
const activeCells = () => {
|
|
392
|
+
if (selCells.current.size)
|
|
393
|
+
return [...selCells.current];
|
|
394
|
+
return ctxCell.current ? [ctxCell.current] : [];
|
|
395
|
+
};
|
|
396
|
+
const applyColor = (color) => {
|
|
397
|
+
setColorOpen(false);
|
|
398
|
+
if (colorKind === 'cellBg') {
|
|
399
|
+
for (const cell of activeCells())
|
|
400
|
+
cell.style.backgroundColor = color;
|
|
401
|
+
pushValue();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
focusEditor();
|
|
405
|
+
restoreSelection();
|
|
406
|
+
if (colorKind === 'foreColor')
|
|
407
|
+
legacyExec('foreColor', color);
|
|
408
|
+
else if (!legacyExec('hiliteColor', color))
|
|
409
|
+
legacyExec('backColor', color);
|
|
410
|
+
pushValue();
|
|
411
|
+
};
|
|
412
|
+
// ── Tabele ──
|
|
413
|
+
const insertTableAt = (rows, cols) => {
|
|
414
|
+
setTableOpen(false);
|
|
415
|
+
setGrid({ r: 0, c: 0 });
|
|
416
|
+
focusEditor();
|
|
417
|
+
restoreSelection();
|
|
418
|
+
legacyExec('insertHTML', `${tableHtml(rows, cols)}<p><br></p>`);
|
|
419
|
+
pushValue();
|
|
420
|
+
};
|
|
421
|
+
const afterTableOp = () => {
|
|
422
|
+
setMenuOpen(false);
|
|
423
|
+
pushValue();
|
|
424
|
+
};
|
|
425
|
+
const onContextMenu = (event) => {
|
|
426
|
+
if (disabled)
|
|
427
|
+
return;
|
|
428
|
+
const cell = event.target.closest('td, th');
|
|
429
|
+
if (!cell || !editorRef.current?.contains(cell))
|
|
430
|
+
return; // poza tabelą zostaje natywne menu
|
|
431
|
+
event.preventDefault();
|
|
432
|
+
ctxCell.current = cell;
|
|
433
|
+
canSplitCtx.current = (cell.colSpan || 1) > 1 || (cell.rowSpan || 1) > 1;
|
|
434
|
+
setMenuPoint({ x: event.clientX, y: event.clientY });
|
|
435
|
+
setMenuOpen(true);
|
|
436
|
+
};
|
|
437
|
+
const onEditorMouseDown = (event) => {
|
|
438
|
+
if (disabled || event.button !== 0)
|
|
439
|
+
return;
|
|
440
|
+
const cell = event.target.closest('td, th');
|
|
441
|
+
if (!cell) {
|
|
442
|
+
clearCellSelection();
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const rect = cell.getBoundingClientRect();
|
|
446
|
+
if (event.clientX >= rect.right - 6) {
|
|
447
|
+
// Strefa przy prawej krawędzi komórki = zmiana szerokości kolumny.
|
|
448
|
+
const col = cell.closest('table')?.querySelectorAll('col')[cell.cellIndex];
|
|
449
|
+
if (col) {
|
|
450
|
+
event.preventDefault();
|
|
451
|
+
resize.current = { col, startX: event.clientX, startW: col.getBoundingClientRect().width };
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
clearCellSelection();
|
|
456
|
+
selAnchor.current = cell;
|
|
457
|
+
selecting.current = false;
|
|
458
|
+
};
|
|
459
|
+
const onEditorMouseMove = (event) => {
|
|
460
|
+
const cell = event.target.closest('td, th');
|
|
461
|
+
const editor = editorRef.current;
|
|
462
|
+
if (!editor)
|
|
463
|
+
return;
|
|
464
|
+
const near = !!cell && event.clientX >= cell.getBoundingClientRect().right - 6;
|
|
465
|
+
editor.classList.toggle('is-col-resize', near || !!resize.current);
|
|
466
|
+
};
|
|
467
|
+
const borderCells = () => {
|
|
468
|
+
if (borderTarget === 'cell')
|
|
469
|
+
return activeCells();
|
|
470
|
+
const table = ctxCell.current?.closest('table');
|
|
471
|
+
return table ? Array.from(table.querySelectorAll('td, th')) : [];
|
|
472
|
+
};
|
|
473
|
+
const setBorderWidth = (width) => {
|
|
474
|
+
const px = { thin: '1px', medium: '2px', thick: '3px' };
|
|
475
|
+
for (const cell of borderCells()) {
|
|
476
|
+
if (width === 'none') {
|
|
477
|
+
cell.style.borderStyle = 'none';
|
|
478
|
+
cell.style.borderWidth = '';
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
cell.style.borderStyle = 'solid';
|
|
482
|
+
cell.style.borderWidth = px[width];
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
pushValue();
|
|
486
|
+
};
|
|
487
|
+
const setBorderColor = (color) => {
|
|
488
|
+
for (const cell of borderCells()) {
|
|
489
|
+
cell.style.borderColor = color;
|
|
490
|
+
if (!cell.style.borderStyle || cell.style.borderStyle === 'none')
|
|
491
|
+
cell.style.borderStyle = 'solid';
|
|
492
|
+
if (!cell.style.borderWidth)
|
|
493
|
+
cell.style.borderWidth = '1px';
|
|
494
|
+
}
|
|
495
|
+
pushValue();
|
|
496
|
+
};
|
|
497
|
+
// ── Prezentacja narzędzi ──
|
|
498
|
+
const styleOptions = [
|
|
499
|
+
{ tag: 'p', label: t('richText.styleNormal') },
|
|
500
|
+
...[1, 2, 3, 4, 5, 6].map((level) => ({
|
|
501
|
+
tag: `h${level}`,
|
|
502
|
+
label: t('richText.styleHeading', { level }),
|
|
503
|
+
})),
|
|
504
|
+
];
|
|
505
|
+
const styleLabel = styleOptions.find((o) => o.tag === currentBlock)?.label ?? t('richText.styleNormal');
|
|
506
|
+
const isActive = (item) => {
|
|
507
|
+
if (item === 'html')
|
|
508
|
+
return sourceView;
|
|
509
|
+
const cmd = COMMAND[item];
|
|
510
|
+
return !!cmd && activeCmds.has(cmd);
|
|
511
|
+
};
|
|
512
|
+
/** `aria-pressed` tylko dla przełączników — wcięcia są akcjami, nie stanem. */
|
|
513
|
+
const pressed = (item) => item === 'indent' || item === 'outdent' ? undefined : isActive(item);
|
|
514
|
+
const enabled = (item) => {
|
|
515
|
+
if (item === 'indent')
|
|
516
|
+
return canIndent;
|
|
517
|
+
if (item === 'outdent')
|
|
518
|
+
return canOutdent;
|
|
519
|
+
return true;
|
|
520
|
+
};
|
|
521
|
+
const palette = (onPick, customLabel) => (_jsxs("div", { className: "kpt-rich-text__palette", onMouseDown: (event) => event.preventDefault(), children: [_jsx("div", { className: "kpt-rich-text__swatches", children: SWATCHES.map((color) => (_jsx("button", { type: "button", className: "kpt-rich-text__swatch", style: { background: color }, "aria-label": color, onClick: () => onPick(color) }, color))) }), _jsxs("label", { className: "kpt-rich-text__picker", children: [_jsx("span", { children: customLabel }), _jsx("input", { type: "color", onInput: (event) => onPick(event.target.value) })] })] }));
|
|
522
|
+
/**
|
|
523
|
+
* Panel otwarty przy dolnej albo prawej krawędzi okna wychodziłby poza ekran — CDK robił to
|
|
524
|
+
* w Angularze sam, tutaj przesuwamy go po zamontowaniu, gdy już znać jego rozmiar.
|
|
525
|
+
*/
|
|
526
|
+
const clampToViewport = (el) => {
|
|
527
|
+
if (!el)
|
|
528
|
+
return;
|
|
529
|
+
const rect = el.getBoundingClientRect();
|
|
530
|
+
const left = Math.max(8, Math.min(menuPoint.x, window.innerWidth - rect.width - 8));
|
|
531
|
+
const top = Math.max(8, Math.min(menuPoint.y, window.innerHeight - rect.height - 8));
|
|
532
|
+
el.style.left = `${left}px`;
|
|
533
|
+
el.style.top = `${top}px`;
|
|
534
|
+
};
|
|
535
|
+
/**
|
|
536
|
+
* Panel przypięty do punktu kursora — menu tabeli i jego popovery nie mają wyzwalacza, więc
|
|
537
|
+
* nie przechodzą przez `KptOverlay`; pozycję niosą współrzędne kliknięcia, tak jak
|
|
538
|
+
* w `KptContextMenu`.
|
|
539
|
+
*/
|
|
540
|
+
const pointPanel = (content, onClose) => {
|
|
541
|
+
if (!container)
|
|
542
|
+
return null; // render na serwerze — nie ma czego portalować
|
|
543
|
+
const layer = { zIndex: 'var(--kpt-z-overlay, 1100)' };
|
|
544
|
+
return createPortal(_jsxs(_Fragment, { children: [_jsx("div", { style: { position: 'fixed', inset: 0, ...layer }, onClick: onClose, onContextMenu: (event) => {
|
|
545
|
+
event.preventDefault();
|
|
546
|
+
onClose();
|
|
547
|
+
} }), _jsx("div", { ref: clampToViewport, style: { position: 'fixed', left: menuPoint.x, top: menuPoint.y, ...layer }, children: content })] }), container);
|
|
548
|
+
};
|
|
549
|
+
const toolButton = (item) => (_jsx("button", { type: "button", className: cn('kpt-rich-text__tool', isActive(item) && 'is-active'), "aria-label": t(`richText.${item}`), "aria-pressed": pressed(item), title: t(`richText.${item}`), disabled: disabled || !enabled(item), onMouseDown: (event) => event.preventDefault(), onClick: () => exec(item), children: _jsx(KptIcon, { name: ICON[item] }) }, item));
|
|
550
|
+
const menuItem = (icon, label, onClick, extra) => (_jsxs("button", { type: "button", className: cn('kpt-rt-menu__item', extra), onClick: onClick, children: [_jsx(KptIcon, { name: icon }), " ", label] }));
|
|
551
|
+
return (_jsxs("div", { className: cn('kpt-rich-text-host', className), ...rest, children: [_jsxs("div", { className: cn('kpt-rich-text', showInvalid && 'kpt-rich-text--invalid', disabled && 'kpt-rich-text--disabled'), children: [_jsx("div", { className: "kpt-rich-text__toolbar", role: "toolbar", children: sections.map((section, index) => (_jsxs("span", { style: { display: 'contents' }, children: [index > 0 ? _jsx("span", { className: "kpt-rich-text__divider", "aria-hidden": "true" }) : null, section.map((item) => {
|
|
552
|
+
if (item === 'heading') {
|
|
553
|
+
return (_jsxs("button", { ref: setStyleAnchor, type: "button", className: cn('kpt-rich-text__style', styleOpen && 'is-open'), disabled: disabled, "aria-label": t('richText.heading'), "aria-expanded": styleOpen, title: t('richText.heading'), onMouseDown: (event) => event.preventDefault(), onClick: () => {
|
|
554
|
+
if (disabled)
|
|
555
|
+
return;
|
|
556
|
+
saveSelection();
|
|
557
|
+
setStyleOpen(true);
|
|
558
|
+
}, children: [_jsx("span", { children: styleLabel }), _jsx(KptIcon, { name: "chevron-down", className: "kpt-rich-text__style-caret" })] }, item));
|
|
559
|
+
}
|
|
560
|
+
if (item === 'foreColor' || item === 'backColor') {
|
|
561
|
+
return (_jsx("button", { ref: item === 'foreColor' ? setForeAnchor : setBackAnchor, type: "button", className: "kpt-rich-text__tool kpt-rich-text__tool--color", "aria-label": t(`richText.${item}`), title: t(`richText.${item}`), disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: () => {
|
|
562
|
+
if (disabled)
|
|
563
|
+
return;
|
|
564
|
+
saveSelection();
|
|
565
|
+
setColorKind(item);
|
|
566
|
+
setColorOpen(true);
|
|
567
|
+
}, children: _jsx(KptIcon, { name: ICON[item] }) }, item));
|
|
568
|
+
}
|
|
569
|
+
if (item === 'table') {
|
|
570
|
+
return (_jsx("button", { ref: setTableAnchor, type: "button", className: cn('kpt-rich-text__tool', tableOpen && 'is-active'), "aria-label": t('richText.table'), title: t('richText.table'), disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: () => exec(item), children: _jsx(KptIcon, { name: ICON[item] }) }, item));
|
|
571
|
+
}
|
|
572
|
+
return toolButton(item);
|
|
573
|
+
})] }, index))) }), sourceView ? (_jsx("textarea", { className: "kpt-rich-text__source", value: current, disabled: disabled, spellCheck: false, "aria-label": t('richText.htmlSource'), onChange: (event) => setCurrent(event.target.value), onBlur: () => onTouch?.() })) : (_jsx("div", { ref: editorRef, className: "kpt-rich-text__editor", contentEditable: !disabled, suppressContentEditableWarning: true, "data-placeholder": placeholder, "aria-invalid": showInvalid || undefined, role: "textbox", "aria-multiline": "true", onInput: pushValue, onKeyDown: onKeyDown, onPaste: onPaste, onContextMenu: onContextMenu, onMouseDown: onEditorMouseDown, onMouseMove: onEditorMouseMove, onBlur: () => onTouch?.() }))] }), _jsx(KptOverlay, { open: styleOpen, anchor: styleAnchor, onClose: () => setStyleOpen(false), offset: 4, children: _jsx("ul", { className: "kpt-rich-text__style-panel", role: "listbox", children: styleOptions.map((option) => (_jsx("li", { className: cn(`kpt-rich-text__style-opt kpt-rich-text__style-opt--${option.tag}`, currentBlock === option.tag && 'is-selected'), role: "option", "aria-selected": currentBlock === option.tag, onClick: () => {
|
|
574
|
+
setStyleOpen(false);
|
|
575
|
+
focusEditor();
|
|
576
|
+
restoreSelection();
|
|
577
|
+
legacyExec('formatBlock', `<${option.tag}>`);
|
|
578
|
+
pushValue();
|
|
579
|
+
refreshState();
|
|
580
|
+
}, children: option.label }, option.tag))) }) }), _jsx(KptOverlay, { open: colorOpen && colorKind === 'foreColor', anchor: foreAnchor, onClose: () => setColorOpen(false), offset: 4, children: palette(applyColor, t('richText.paletteCustom')) }), _jsx(KptOverlay, { open: colorOpen && colorKind === 'backColor', anchor: backAnchor, onClose: () => setColorOpen(false), offset: 4, children: palette(applyColor, t('richText.paletteCustom')) }), _jsx(KptOverlay, { open: tableOpen, anchor: tableAnchor, onClose: () => {
|
|
581
|
+
setTableOpen(false);
|
|
582
|
+
setGrid({ r: 0, c: 0 });
|
|
583
|
+
}, offset: 4, children: _jsxs("div", { className: "kpt-rt-grid-pop", onMouseDown: (event) => event.preventDefault(), children: [_jsx("div", { className: "kpt-rt-grid", onMouseLeave: () => setGrid({ r: 0, c: 0 }), children: GRID_ROWS.map((r) => (_jsx("div", { className: "kpt-rt-grid__row", children: GRID_COLS.map((c) => (_jsx("span", { className: cn('kpt-rt-grid__cell', r <= grid.r && c <= grid.c && 'is-on'), onMouseEnter: () => setGrid({ r, c }), onClick: () => insertTableAt(r, c) }, c))) }, r))) }), _jsxs("div", { className: "kpt-rt-grid__label", children: [grid.r, " \u00D7 ", grid.c] })] }) }), menuOpen
|
|
584
|
+
? pointPanel(_jsxs("div", { className: "kpt-rt-menu", onContextMenu: (event) => event.preventDefault(), children: [_jsx("div", { className: "kpt-rt-menu__group", children: t('richText.menu.column') }), menuItem('plus', t('richText.menu.insertLeft'), () => {
|
|
585
|
+
if (ctxCell.current)
|
|
586
|
+
insertColumn(ctxCell.current, 'left');
|
|
587
|
+
afterTableOp();
|
|
588
|
+
}), menuItem('plus', t('richText.menu.insertRight'), () => {
|
|
589
|
+
if (ctxCell.current)
|
|
590
|
+
insertColumn(ctxCell.current, 'right');
|
|
591
|
+
afterTableOp();
|
|
592
|
+
}), menuItem('table', t('richText.menu.headerColumn'), () => {
|
|
593
|
+
if (ctxCell.current)
|
|
594
|
+
toggleHeaderColumn(ctxCell.current);
|
|
595
|
+
ctxCell.current = null; // komórki zostały podmienione
|
|
596
|
+
afterTableOp();
|
|
597
|
+
}), menuItem('trash', t('richText.menu.deleteColumn'), () => {
|
|
598
|
+
if (ctxCell.current)
|
|
599
|
+
deleteColumn(ctxCell.current);
|
|
600
|
+
afterTableOp();
|
|
601
|
+
}), _jsx("div", { className: "kpt-rt-menu__sep" }), _jsx("div", { className: "kpt-rt-menu__group", children: t('richText.menu.row') }), menuItem('plus', t('richText.menu.insertAbove'), () => {
|
|
602
|
+
if (ctxCell.current)
|
|
603
|
+
insertRow(ctxCell.current, 'above');
|
|
604
|
+
afterTableOp();
|
|
605
|
+
}), menuItem('plus', t('richText.menu.insertBelow'), () => {
|
|
606
|
+
if (ctxCell.current)
|
|
607
|
+
insertRow(ctxCell.current, 'below');
|
|
608
|
+
afterTableOp();
|
|
609
|
+
}), menuItem('table', t('richText.menu.headerRow'), () => {
|
|
610
|
+
if (ctxCell.current)
|
|
611
|
+
toggleHeaderRow(ctxCell.current);
|
|
612
|
+
ctxCell.current = null;
|
|
613
|
+
afterTableOp();
|
|
614
|
+
}), menuItem('trash', t('richText.menu.deleteRow'), () => {
|
|
615
|
+
if (ctxCell.current)
|
|
616
|
+
deleteRow(ctxCell.current);
|
|
617
|
+
afterTableOp();
|
|
618
|
+
}), _jsx("div", { className: "kpt-rt-menu__sep" }), _jsx("div", { className: "kpt-rt-menu__group", children: t('richText.menu.cell') }), menuItem('palette', t('richText.menu.cellBackground'), () => {
|
|
619
|
+
setMenuOpen(false);
|
|
620
|
+
setColorKind('cellBg');
|
|
621
|
+
setColorOpen(true);
|
|
622
|
+
}), menuItem('align-left', t('richText.menu.alignLeft'), () => {
|
|
623
|
+
for (const cell of activeCells())
|
|
624
|
+
cell.style.textAlign = 'left';
|
|
625
|
+
afterTableOp();
|
|
626
|
+
}), menuItem('align-center', t('richText.menu.alignCenter'), () => {
|
|
627
|
+
for (const cell of activeCells())
|
|
628
|
+
cell.style.textAlign = 'center';
|
|
629
|
+
afterTableOp();
|
|
630
|
+
}), menuItem('align-right', t('richText.menu.alignRight'), () => {
|
|
631
|
+
for (const cell of activeCells())
|
|
632
|
+
cell.style.textAlign = 'right';
|
|
633
|
+
afterTableOp();
|
|
634
|
+
}), _jsx("div", { className: "kpt-rt-menu__sep" }), menuItem('table', t('richText.menu.mergeCells'), () => {
|
|
635
|
+
mergeCells([...selCells.current]);
|
|
636
|
+
clearCellSelection();
|
|
637
|
+
afterTableOp();
|
|
638
|
+
}, selCount < 2 ? 'is-disabled' : undefined), menuItem('grip-vertical', t('richText.menu.splitCell'), () => {
|
|
639
|
+
if (ctxCell.current)
|
|
640
|
+
splitCell(ctxCell.current);
|
|
641
|
+
afterTableOp();
|
|
642
|
+
}, canSplitCtx.current ? undefined : 'is-disabled'), _jsx("div", { className: "kpt-rt-menu__sep" }), menuItem('table', t('richText.menu.border'), () => {
|
|
643
|
+
setMenuOpen(false);
|
|
644
|
+
setBorderOpen(true);
|
|
645
|
+
}), _jsx("div", { className: "kpt-rt-menu__sep" }), menuItem('trash', t('richText.menu.deleteTable'), () => {
|
|
646
|
+
ctxCell.current?.closest('table')?.remove();
|
|
647
|
+
afterTableOp();
|
|
648
|
+
}, 'kpt-rt-menu__item--danger')] }), () => setMenuOpen(false))
|
|
649
|
+
: null, borderOpen
|
|
650
|
+
? pointPanel(_jsxs("div", { className: "kpt-rt-border", onMouseDown: (event) => event.preventDefault(), children: [_jsxs("div", { className: "kpt-rt-border__seg", children: [_jsx("button", { type: "button", className: cn(borderTarget === 'table' && 'is-active'), onClick: () => setBorderTarget('table'), children: t('richText.border.table') }), _jsx("button", { type: "button", className: cn(borderTarget === 'cell' && 'is-active'), onClick: () => setBorderTarget('cell'), children: t('richText.border.cell') })] }), _jsxs("div", { className: "kpt-rt-border__widths", children: [_jsx("button", { type: "button", onClick: () => setBorderWidth('none'), children: t('richText.border.none') }), _jsx("button", { type: "button", onClick: () => setBorderWidth('thin'), children: t('richText.border.thin') }), _jsx("button", { type: "button", onClick: () => setBorderWidth('medium'), children: t('richText.border.medium') }), _jsx("button", { type: "button", onClick: () => setBorderWidth('thick'), children: t('richText.border.thick') })] }), _jsx("div", { className: "kpt-rich-text__swatches", children: SWATCHES.map((color) => (_jsx("button", { type: "button", className: "kpt-rich-text__swatch", style: { background: color }, "aria-label": color, onClick: () => setBorderColor(color) }, color))) }), _jsxs("label", { className: "kpt-rich-text__picker", children: [_jsx("span", { children: t('richText.border.customColor') }), _jsx("input", { type: "color", onInput: (event) => setBorderColor(event.target.value) })] })] }), () => setBorderOpen(false))
|
|
651
|
+
: null, colorOpen && colorKind === 'cellBg'
|
|
652
|
+
? pointPanel(palette(applyColor, t('richText.paletteCustom')), () => setColorOpen(false))
|
|
653
|
+
: null] }));
|
|
654
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Czyści HTML wchodzący do edytora. Dotyczy **każdego** zapisu do DOM edytora, nie tylko
|
|
3
|
+
* wklejania: `value` z aplikacji też bywa niezaufane (baza, CMS, inny użytkownik).
|
|
4
|
+
*/
|
|
5
|
+
export declare function sanitizeHtml(html: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Waliduje URL z narzędzia „link". Brak schematu → dopisujemy `https://`; `#` i `/` przechodzą
|
|
8
|
+
* bez zmian; schemat spoza allowlisty → `null`, czyli link nie zostaje wstawiony.
|
|
9
|
+
*/
|
|
10
|
+
export declare function sanitizeUrl(url: string): string | null;
|