@festo-ui/react-extra 9.0.0-dev.725 → 9.0.0-dev.727

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.
@@ -0,0 +1,28 @@
1
+ import './TextEditor.scss';
2
+ export interface TextEditorConfiguration {
3
+ toolbar?: {
4
+ bold?: boolean;
5
+ italic?: boolean;
6
+ underline?: boolean;
7
+ alignCenter?: boolean;
8
+ alignRight?: boolean;
9
+ bulletList?: boolean;
10
+ orderedList?: boolean;
11
+ image?: boolean;
12
+ link?: boolean;
13
+ };
14
+ }
15
+ export interface TextEditorProps {
16
+ readonly disabled?: boolean;
17
+ readonly label: string;
18
+ readonly maxLength?: number;
19
+ readonly value?: string;
20
+ readonly defaultValue?: string;
21
+ readonly hint?: string;
22
+ readonly error?: string;
23
+ readonly readOnly?: boolean;
24
+ readonly onChange?: (value: string | null) => void;
25
+ readonly className?: string;
26
+ readonly config?: TextEditorConfiguration;
27
+ }
28
+ export declare function TextEditor({ disabled, defaultValue, label, maxLength, value, hint, error, readOnly, onChange, className, config: configProps, }: TextEditorProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,272 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import classnames from "classnames";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import "./TextEditor.css";
5
+ import { IconAlignCenter, IconAlignRight, IconEnumeration, IconImage, IconLink, IconListView } from "@festo-ui/react-icons";
6
+ import quill from "quill";
7
+ import { filterXSS, whiteList as external_xss_whiteList } from "xss";
8
+ import { useId } from "../../utils/useId.js";
9
+ import { TextEditorButton } from "./TextEditorButton.js";
10
+ const defaultConfig = {
11
+ toolbar: {
12
+ bold: true,
13
+ italic: true,
14
+ underline: true,
15
+ alignCenter: false,
16
+ alignRight: false,
17
+ bulletList: true,
18
+ orderedList: true,
19
+ image: true,
20
+ link: true
21
+ }
22
+ };
23
+ function postpone(fn) {
24
+ Promise.resolve().then(fn);
25
+ }
26
+ function TextEditor({ disabled = false, defaultValue, label, maxLength, value, hint, error, readOnly = false, onChange, className, config: configProps }) {
27
+ const editorRef = useRef(null);
28
+ const [editor, setEditor] = useState(null);
29
+ const id = useId();
30
+ const [innerValue, setInnerValue] = useState(null);
31
+ const config = {
32
+ toolbar: {
33
+ ...defaultConfig.toolbar,
34
+ ...configProps?.toolbar
35
+ }
36
+ };
37
+ const setEditorContents = useCallback((e, v)=>{
38
+ if (e) {
39
+ const whiteList = {
40
+ ...external_xss_whiteList,
41
+ p: [
42
+ ...external_xss_whiteList?.p ?? [],
43
+ 'class'
44
+ ],
45
+ li: [
46
+ ...external_xss_whiteList?.li ?? [],
47
+ 'class'
48
+ ],
49
+ a: [
50
+ ...external_xss_whiteList?.a ?? [],
51
+ 'rel',
52
+ 'class'
53
+ ]
54
+ };
55
+ const sanitizedValue = filterXSS(v, {
56
+ whiteList
57
+ });
58
+ const content = e.clipboard.convert({
59
+ html: sanitizedValue
60
+ });
61
+ const selection = e.getSelection();
62
+ e.setContents(content, 'silent');
63
+ setInnerValue(sanitizedValue);
64
+ postpone(()=>e.setSelection(selection));
65
+ }
66
+ }, []);
67
+ useEffect(()=>{
68
+ if (editorRef.current && null === editor) {
69
+ const newEditor = new quill(editorRef.current, {
70
+ modules: {
71
+ toolbar: `#editor-toolbar-${CSS.escape(id ?? '')}`
72
+ },
73
+ theme: 'snow'
74
+ });
75
+ newEditor.root.setAttribute('role', 'textbox');
76
+ newEditor.root.setAttribute('aria-labelledby', `editor-label-${id}`);
77
+ newEditor.root.setAttribute('aria-multiline', 'true');
78
+ newEditor.enable(!readOnly);
79
+ if (disabled) newEditor.disable();
80
+ else if (!readOnly) newEditor.enable();
81
+ setEditor(newEditor);
82
+ if (defaultValue) setEditorContents(newEditor, defaultValue);
83
+ }
84
+ }, [
85
+ editor,
86
+ disabled,
87
+ readOnly,
88
+ id,
89
+ setEditorContents,
90
+ defaultValue
91
+ ]);
92
+ useEffect(()=>{
93
+ if (editor) editor.on('text-change', ()=>{
94
+ let html = editor.getSemanticHTML();
95
+ if ('<p><br></p>' === html || '<div><br></div>' === html || void 0 === html) html = null;
96
+ if (null !== html) {
97
+ const relativeLinkRegex = /href="(?!https?:\/\/|mailto:|tel:)([^"]+)"/g;
98
+ html = html.replace(relativeLinkRegex, 'href="https://$1"');
99
+ }
100
+ if (onChange) onChange(html);
101
+ setInnerValue(html);
102
+ });
103
+ }, [
104
+ editor,
105
+ onChange
106
+ ]);
107
+ useEffect(()=>{
108
+ if (value !== innerValue && null != value) setEditorContents(editor, value);
109
+ }, [
110
+ editor,
111
+ innerValue,
112
+ setEditorContents,
113
+ value
114
+ ]);
115
+ function currentLength() {
116
+ return innerValue?.length || 0;
117
+ }
118
+ function hideDivider(name) {
119
+ const linkOrImage = config.toolbar?.image || config.toolbar?.link;
120
+ const lists = config.toolbar?.bulletList || config.toolbar?.orderedList;
121
+ const typos = config.toolbar?.bold || config.toolbar?.italic || config.toolbar?.underline;
122
+ const textAlign = config.toolbar?.alignCenter || config.toolbar?.alignRight;
123
+ switch(name){
124
+ case 'typo':
125
+ return !typos || !textAlign && !linkOrImage && !lists;
126
+ case 'text-align':
127
+ return !textAlign || !linkOrImage && !lists;
128
+ case 'lists':
129
+ return !lists || !linkOrImage;
130
+ case 'image':
131
+ return !config.toolbar?.image || !config.toolbar.link;
132
+ default:
133
+ break;
134
+ }
135
+ return true;
136
+ }
137
+ return /*#__PURE__*/ jsxs("label", {
138
+ className: classnames('fwe-input-text', {
139
+ disabled
140
+ }),
141
+ htmlFor: `editor-label-${id}`,
142
+ children: [
143
+ /*#__PURE__*/ jsx("div", {
144
+ className: classnames('fwe-editor-toolbar', {
145
+ [`fwe-editor-toolbar-${className}`]: className
146
+ }),
147
+ id: `editor-toolbar-${id}`,
148
+ children: /*#__PURE__*/ jsxs("span", {
149
+ className: "ql-formats fwe-mr-3",
150
+ children: [
151
+ config?.toolbar?.bold && /*#__PURE__*/ jsx(TextEditorButton, {
152
+ disabled: disabled,
153
+ type: "bold",
154
+ className: "fwe-mr-3",
155
+ label: "B"
156
+ }),
157
+ config?.toolbar?.italic && /*#__PURE__*/ jsx(TextEditorButton, {
158
+ disabled: disabled,
159
+ type: "italic",
160
+ className: "fwe-mr-3",
161
+ label: "I"
162
+ }),
163
+ config?.toolbar?.underline && /*#__PURE__*/ jsx(TextEditorButton, {
164
+ disabled: disabled,
165
+ type: "underline",
166
+ label: "U"
167
+ }),
168
+ !hideDivider('typo') && /*#__PURE__*/ jsx("div", {
169
+ className: "fwe-divider-y fwe-d-inline-flex fwe-mx-4"
170
+ }),
171
+ config?.toolbar?.alignCenter && /*#__PURE__*/ jsx(TextEditorButton, {
172
+ disabled: disabled,
173
+ category: "align",
174
+ type: "align-center",
175
+ icon: /*#__PURE__*/ jsx(IconAlignCenter, {
176
+ className: classnames({
177
+ 'fwe-gray': disabled
178
+ })
179
+ }),
180
+ value: "center",
181
+ className: classnames({
182
+ 'fwe-mr-3': config?.toolbar?.alignRight
183
+ })
184
+ }),
185
+ config?.toolbar?.alignRight && /*#__PURE__*/ jsx(TextEditorButton, {
186
+ disabled: disabled,
187
+ category: "align",
188
+ type: "align-right",
189
+ icon: /*#__PURE__*/ jsx(IconAlignRight, {
190
+ className: classnames({
191
+ 'fwe-gray': disabled
192
+ })
193
+ }),
194
+ value: "right"
195
+ }),
196
+ !hideDivider('text-align') && /*#__PURE__*/ jsx("div", {
197
+ className: "fwe-divider-y fwe-d-inline-flex fwe-mx-4"
198
+ }),
199
+ config?.toolbar?.bulletList && /*#__PURE__*/ jsx(TextEditorButton, {
200
+ disabled: disabled,
201
+ className: "fwe-mr-3",
202
+ type: "ul",
203
+ list: true,
204
+ icon: /*#__PURE__*/ jsx(IconListView, {}),
205
+ value: "bullet"
206
+ }),
207
+ config?.toolbar?.orderedList && /*#__PURE__*/ jsx(TextEditorButton, {
208
+ disabled: disabled,
209
+ type: "ol",
210
+ list: true,
211
+ icon: /*#__PURE__*/ jsx(IconEnumeration, {}),
212
+ value: "ordered"
213
+ }),
214
+ config?.toolbar?.image && /*#__PURE__*/ jsxs(Fragment, {
215
+ children: [
216
+ /*#__PURE__*/ jsx("div", {
217
+ className: "fwe-divider-y fwe-d-inline-flex fwe-mx-4"
218
+ }),
219
+ /*#__PURE__*/ jsx(TextEditorButton, {
220
+ disabled: disabled,
221
+ type: "image",
222
+ icon: /*#__PURE__*/ jsx(IconImage, {}),
223
+ noAction: true
224
+ })
225
+ ]
226
+ }),
227
+ config?.toolbar?.link && /*#__PURE__*/ jsxs(Fragment, {
228
+ children: [
229
+ /*#__PURE__*/ jsx("div", {
230
+ className: "fwe-divider-y fwe-d-inline-flex fwe-mx-4"
231
+ }),
232
+ /*#__PURE__*/ jsx(TextEditorButton, {
233
+ disabled: disabled,
234
+ type: "link",
235
+ icon: /*#__PURE__*/ jsx(IconLink, {}),
236
+ noAction: true
237
+ })
238
+ ]
239
+ })
240
+ ]
241
+ })
242
+ }),
243
+ /*#__PURE__*/ jsx("div", {
244
+ className: classnames('fwe-editor-container', {
245
+ 'fwe-editor-container--error': error
246
+ }),
247
+ id: `editor-container-${id}`,
248
+ children: /*#__PURE__*/ jsx("div", {
249
+ className: "fwe-editor",
250
+ ref: editorRef
251
+ })
252
+ }),
253
+ /*#__PURE__*/ jsx("span", {
254
+ className: "fwe-input-text-label",
255
+ children: label
256
+ }),
257
+ error && /*#__PURE__*/ jsx("span", {
258
+ className: "fwe-text-editor-invalid",
259
+ children: error
260
+ }),
261
+ hint && /*#__PURE__*/ jsx("span", {
262
+ className: "fwe-text-editor-info",
263
+ children: hint
264
+ }),
265
+ maxLength && maxLength > 0 && null != value && /*#__PURE__*/ jsx("span", {
266
+ className: "fwe-input-text-count",
267
+ children: `${currentLength()} / ${maxLength}`
268
+ })
269
+ ]
270
+ });
271
+ }
272
+ export { TextEditor };
@@ -0,0 +1,13 @@
1
+ import { type ReactElement } from 'react';
2
+ export interface TextEditorButtonProps {
3
+ type: string;
4
+ label?: string;
5
+ icon?: ReactElement;
6
+ disabled: boolean;
7
+ className?: string;
8
+ list?: boolean;
9
+ value?: string;
10
+ noAction?: boolean;
11
+ category?: string;
12
+ }
13
+ export declare function TextEditorButton({ disabled, label, type, className, icon, list, value, noAction, category, }: Readonly<TextEditorButtonProps>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,72 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import classnames from "classnames";
3
+ import { useEffect, useRef, useState } from "react";
4
+ function TextEditorButton({ disabled, label, type, className, icon, list, value, noAction, category }) {
5
+ const [active, setActive] = useState(false);
6
+ const btnRef = useRef(null);
7
+ function handleClick() {
8
+ const btn = btnRef.current;
9
+ setActive((prevActive)=>!prevActive);
10
+ btn?.click();
11
+ }
12
+ useEffect(()=>{
13
+ function callback(mutationRecords) {
14
+ for (const mutationRecord of mutationRecords){
15
+ const { classList } = mutationRecord.target;
16
+ const { oldValue } = mutationRecord;
17
+ if (classList.contains('ql-active')) setActive(true);
18
+ if (!classList.contains('ql-active') && oldValue?.includes('ql-active')) setActive(false);
19
+ }
20
+ }
21
+ if (btnRef.current && !noAction) {
22
+ const observer = new MutationObserver(callback);
23
+ observer.observe(btnRef.current, {
24
+ attributes: true,
25
+ attributeFilter: [
26
+ 'class'
27
+ ],
28
+ attributeOldValue: true
29
+ });
30
+ }
31
+ }, [
32
+ noAction
33
+ ]);
34
+ return /*#__PURE__*/ jsxs(Fragment, {
35
+ children: [
36
+ /*#__PURE__*/ jsx("button", {
37
+ ref: btnRef,
38
+ type: "button",
39
+ className: classnames('fwe-d-none', {
40
+ 'ql-list': list
41
+ }, {
42
+ [`ql-${category || type}`]: !list
43
+ }, {
44
+ [`action-${type}`]: !noAction
45
+ }),
46
+ "aria-hidden": "true",
47
+ tabIndex: -1,
48
+ value: value
49
+ }),
50
+ /*#__PURE__*/ jsxs("button", {
51
+ type: "button",
52
+ className: classnames({
53
+ 'fr-icon-button': !!icon
54
+ }, 'fr-button', className, {
55
+ 'fwe-active': active && !noAction
56
+ }),
57
+ onClick: ()=>handleClick(),
58
+ disabled: disabled,
59
+ children: [
60
+ label && /*#__PURE__*/ jsx("div", {
61
+ className: `fr-button-text fwe-text-${type}`,
62
+ children: label
63
+ }),
64
+ /*#__PURE__*/ jsx("div", {
65
+ children: icon
66
+ })
67
+ ]
68
+ })
69
+ ]
70
+ });
71
+ }
72
+ export { TextEditorButton };
@@ -0,0 +1,6 @@
1
+ export { ColorIndicator, type ColorIndicatorProps, } from './components/color-indicator/ColorIndicator';
2
+ export { ScrollArea, type ScrollAreaProps, } from './components/scroll-area/ScrollArea';
3
+ export { ColorPicker, type ColorPickerProps, } from './forms/color-picker/ColorPicker';
4
+ export { DatePicker, type DatePickerOptions, type DatePickerProps, } from './forms/date-picker/DatePicker';
5
+ export { DateRangePicker, type DateRangePickerOptions, type DateRangePickerProps, } from './forms/date-range-picker/DateRangePicker';
6
+ export { TextEditor, type TextEditorConfiguration, type TextEditorProps, } from './forms/text-editor/TextEditor';
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import { ColorIndicator } from "./components/color-indicator/ColorIndicator.js";
2
+ import { ScrollArea } from "./components/scroll-area/ScrollArea.js";
3
+ import { ColorPicker } from "./forms/color-picker/ColorPicker.js";
4
+ import { DatePicker } from "./forms/date-picker/DatePicker.js";
5
+ import { DateRangePicker } from "./forms/date-range-picker/DateRangePicker.js";
6
+ import { TextEditor } from "./forms/text-editor/TextEditor.js";
7
+ export { ColorIndicator, ColorPicker, DatePicker, DateRangePicker, ScrollArea, TextEditor };
@@ -0,0 +1 @@
1
+ export declare function useId(idInput?: string): string | undefined;
@@ -0,0 +1,20 @@
1
+ import react from "react";
2
+ const maybeReactUseId = react["useId"];
3
+ let nextId = 0;
4
+ function useLegacyId() {
5
+ const [id, setId] = react.useState(void 0);
6
+ react.useEffect(()=>{
7
+ if (null == id) {
8
+ nextId += 1;
9
+ setId(`fr-${nextId}`);
10
+ }
11
+ }, [
12
+ id
13
+ ]);
14
+ return id;
15
+ }
16
+ function useId(idInput) {
17
+ if (null != idInput) return idInput;
18
+ return maybeReactUseId ? maybeReactUseId() : useLegacyId();
19
+ }
20
+ export { useId };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@festo-ui/react-extra",
3
- "version": "9.0.0-dev.725",
3
+ "version": "9.0.0-dev.727",
4
4
  "license": "apache-2.0",
5
5
  "author": "Festo UI (styleguide@festo.com)",
6
6
  "type": "module",