goblin-magic 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.editorconfig +9 -0
  2. package/.zou-flow +2 -0
  3. package/eslint.config.js +65 -0
  4. package/magicNavigation.js +7 -0
  5. package/package.json +45 -0
  6. package/widgets/dialog/widget.js +78 -0
  7. package/widgets/element-helpers/element-has-direct-text.js +9 -0
  8. package/widgets/element-helpers/is-empty-area-element.js +17 -0
  9. package/widgets/element-helpers/is-flat-element.js +52 -0
  10. package/widgets/get-modifiers/get-modifiers.js +34 -0
  11. package/widgets/input-group/styles.js +74 -0
  12. package/widgets/input-group/widget.js +24 -0
  13. package/widgets/magic-action/styles.js +45 -0
  14. package/widgets/magic-action/widget.js +44 -0
  15. package/widgets/magic-background/bg-alps.jpg +0 -0
  16. package/widgets/magic-background/bg-fur.png +0 -0
  17. package/widgets/magic-background/bg-milkyway.png +0 -0
  18. package/widgets/magic-background/bg-space.jpg +0 -0
  19. package/widgets/magic-background/bg-synth.jpg +0 -0
  20. package/widgets/magic-background/bg-white.png +0 -0
  21. package/widgets/magic-background/styles.js +81 -0
  22. package/widgets/magic-background/widget.js +20 -0
  23. package/widgets/magic-box/styles.js +10 -0
  24. package/widgets/magic-box/widget.js +28 -0
  25. package/widgets/magic-box-old/styles.js +111 -0
  26. package/widgets/magic-box-old/widget.js +30 -0
  27. package/widgets/magic-button/styles.js +156 -0
  28. package/widgets/magic-button/widget.js +89 -0
  29. package/widgets/magic-checkbox/styles.js +116 -0
  30. package/widgets/magic-checkbox/widget.js +68 -0
  31. package/widgets/magic-color-field/styles.js +22 -0
  32. package/widgets/magic-color-field/widget.js +68 -0
  33. package/widgets/magic-date-field/styles.js +9 -0
  34. package/widgets/magic-date-field/widget.js +145 -0
  35. package/widgets/magic-datetime-field/styles.js +11 -0
  36. package/widgets/magic-datetime-field/widget.js +95 -0
  37. package/widgets/magic-dialog/styles.js +39 -0
  38. package/widgets/magic-dialog/widget.js +116 -0
  39. package/widgets/magic-div/styles.js +22 -0
  40. package/widgets/magic-div/widget.js +20 -0
  41. package/widgets/magic-emoji/styles.js +14 -0
  42. package/widgets/magic-emoji/widget.js +33 -0
  43. package/widgets/magic-emoji-picker/styles.js +21 -0
  44. package/widgets/magic-emoji-picker/widget.js +44 -0
  45. package/widgets/magic-inplace-input/styles.js +55 -0
  46. package/widgets/magic-inplace-input/widget.js +26 -0
  47. package/widgets/magic-input/styles.js +50 -0
  48. package/widgets/magic-input/widget.js +397 -0
  49. package/widgets/magic-label/styles.js +20 -0
  50. package/widgets/magic-label/widget.js +24 -0
  51. package/widgets/magic-navigation/service.js +1306 -0
  52. package/widgets/magic-navigation/styles.js +103 -0
  53. package/widgets/magic-navigation/view-context.js +15 -0
  54. package/widgets/magic-navigation/widget.js +540 -0
  55. package/widgets/magic-number-field/styles.js +10 -0
  56. package/widgets/magic-number-field/widget.js +103 -0
  57. package/widgets/magic-panel/styles.js +61 -0
  58. package/widgets/magic-panel/widget.js +63 -0
  59. package/widgets/magic-radio/styles.js +93 -0
  60. package/widgets/magic-radio/widget.js +74 -0
  61. package/widgets/magic-scroll/styles.js +22 -0
  62. package/widgets/magic-scroll/widget.js +20 -0
  63. package/widgets/magic-select/styles.js +16 -0
  64. package/widgets/magic-select/widget.js +134 -0
  65. package/widgets/magic-table/reducer.js +63 -0
  66. package/widgets/magic-table/styles.js +170 -0
  67. package/widgets/magic-table/widget.js +627 -0
  68. package/widgets/magic-tag/styles.js +32 -0
  69. package/widgets/magic-tag/widget.js +32 -0
  70. package/widgets/magic-text-field/styles.js +58 -0
  71. package/widgets/magic-text-field/widget.js +66 -0
  72. package/widgets/magic-time-field/styles.js +8 -0
  73. package/widgets/magic-time-field/widget.js +142 -0
  74. package/widgets/magic-timer/styles.js +30 -0
  75. package/widgets/magic-timer/widget.js +162 -0
  76. package/widgets/magic-zen/styles.js +61 -0
  77. package/widgets/magic-zen/widget.js +42 -0
  78. package/widgets/main-tabs/styles.js +106 -0
  79. package/widgets/main-tabs/widget.js +23 -0
  80. package/widgets/menu/styles.js +156 -0
  81. package/widgets/menu/test-menu.html +154 -0
  82. package/widgets/menu/widget.js +575 -0
  83. package/widgets/movable/widget.js +80 -0
  84. package/widgets/splitter/styles.js +57 -0
  85. package/widgets/splitter/widget.js +40 -0
  86. package/widgets/tab-layout/styles.js +31 -0
  87. package/widgets/tab-layout/widget.js +59 -0
  88. package/widgets/with-computed-size/widget.js +52 -0
@@ -0,0 +1,50 @@
1
+ /******************************************************************************/
2
+
3
+ export default function styles() {
4
+ const withEmoji = {
5
+ '&[class]' /* increase specificity */: {
6
+ 'display': 'flex',
7
+ 'flexDirection': 'row',
8
+ 'alignItems': 'baseline',
9
+ ':focus-within': {
10
+ borderColor: 'color-mix(in srgb, var(--text-color), transparent 60%)',
11
+ },
12
+ ':hover': {
13
+ '& .pickerButton': {
14
+ 'display': 'block',
15
+ 'opacity': '20%',
16
+ ':hover': {
17
+ color: 'orange',
18
+ opacity: '100%',
19
+ cursor: 'pointer',
20
+ filter: 'drop-shadow(0 0 2px rgb(0 0 0))',
21
+ },
22
+ },
23
+ },
24
+ '& .pickerButton': {
25
+ 'display': 'none',
26
+ 'marginLeft': '-24px',
27
+ '& > svg': {
28
+ margin: '-5px',
29
+ },
30
+ },
31
+ '& > .input': {
32
+ 'width': 'inherit',
33
+ 'backgroundColor': 'transparent',
34
+ 'border': 'none',
35
+ 'color': 'inherit',
36
+ 'fontSize': 'inherit',
37
+ '&:focus, &:focus-visible': {
38
+ outline: 'none',
39
+ borderColor: 'transparent',
40
+ },
41
+ },
42
+ },
43
+ };
44
+
45
+ return {
46
+ withEmoji,
47
+ };
48
+ }
49
+
50
+ /******************************************************************************/
@@ -0,0 +1,397 @@
1
+ import React from 'react';
2
+ import Widget from 'goblin-laboratory/widgets/widget';
3
+ import * as styles from './styles.js';
4
+ import wrapRawInput from 'goblin-gadgets/widgets/input-wrapper/widget.js';
5
+ import MagicEmojiPickerNC from '../magic-emoji-picker/widget.js';
6
+ import MagicDialog from '../magic-dialog/widget.js';
7
+ import Icon from '@mdi/react';
8
+ import {mdiCat} from '@mdi/js';
9
+ import Mousetrap from 'mousetrap';
10
+
11
+ function focusInput(element) {
12
+ if (element.contentEditable === 'plaintext-only') {
13
+ const range = document.createRange();
14
+ range.selectNodeContents(element);
15
+ range.collapse(false);
16
+ const selection = window.getSelection();
17
+ selection.removeAllRanges();
18
+ selection.addRange(range);
19
+ } else {
20
+ element.focus();
21
+ }
22
+ }
23
+
24
+ /**
25
+ * @param {HTMLElement} element
26
+ * @returns {HTMLElement}
27
+ */
28
+ function findParentContainer(element) {
29
+ for (let e = element.parentElement; e; e = e.parentElement) {
30
+ if (e.tagName === 'DIALOG') {
31
+ return e;
32
+ }
33
+ }
34
+ if (element.offsetParent) {
35
+ return element.offsetParent;
36
+ }
37
+ return document;
38
+ }
39
+
40
+ /**
41
+ * @param {HTMLElement} element
42
+ * @returns {HTMLElement | null}
43
+ */
44
+ function findNextInput(element) {
45
+ const parent = findParentContainer(element);
46
+ const inputs = parent.querySelectorAll('input, div[contenteditable].input');
47
+ const inputsList = [...inputs];
48
+ const index = inputsList.indexOf(element);
49
+ const nextIndex = index + 1;
50
+
51
+ if (nextIndex < inputsList.length) {
52
+ return inputs[nextIndex];
53
+ }
54
+ return null;
55
+ }
56
+
57
+ function getEventValue(event) {
58
+ return event?.target ? event.target.value : event;
59
+ }
60
+
61
+ /**
62
+ * @param {Range} range
63
+ * @returns {number}
64
+ */
65
+ function computeRangeLength(range) {
66
+ const div = document.createElement('div');
67
+ div.appendChild(range.cloneContents());
68
+ let length = 0;
69
+ for (const node of div.childNodes) {
70
+ if (node instanceof Text) {
71
+ length += node.textContent.length;
72
+ } else if (node.tagName === 'BR') {
73
+ length += 1;
74
+ }
75
+ }
76
+ return length;
77
+ }
78
+
79
+ /**
80
+ * @param {HTMLElement} element
81
+ * @param {number} position
82
+ * @returns {{node: Node, offset: number}}
83
+ */
84
+ function computeNodeAndOffset(element, position) {
85
+ for (const node of element.childNodes) {
86
+ if (node instanceof Text) {
87
+ const nodeLength = node.textContent.length;
88
+ if (position - nodeLength < 1) {
89
+ return {node, offset: position};
90
+ }
91
+ position -= nodeLength;
92
+ } else if (node.tagName === 'BR') {
93
+ if (position < 1) {
94
+ break;
95
+ }
96
+ position -= 1;
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+
102
+ class EditableDiv extends Widget {
103
+ constructor() {
104
+ super(...arguments);
105
+ this.element = React.createRef();
106
+ }
107
+
108
+ get selectionStart() {
109
+ const element = this.element.current;
110
+ const range = window.getSelection().getRangeAt(0).cloneRange();
111
+ const {startContainer, startOffset} = range;
112
+ range.setStart(element, 0);
113
+ range.setEnd(startContainer, startOffset);
114
+ const position = computeRangeLength(range);
115
+ return position;
116
+ }
117
+
118
+ get selectionEnd() {
119
+ const element = this.element.current;
120
+ const range = window.getSelection().getRangeAt(0).cloneRange();
121
+ range.setStart(element, 0);
122
+ const position = computeRangeLength(range);
123
+ return position;
124
+ }
125
+
126
+ setSelectionRange(start, end) {
127
+ const element = this.element.current;
128
+ const range = document.createRange();
129
+ range.selectNode(this.element.current);
130
+ const startPos = computeNodeAndOffset(element, start);
131
+ if (startPos) {
132
+ range.setStart(startPos.node, startPos.offset);
133
+ }
134
+ const endPos = computeNodeAndOffset(element, end);
135
+ if (endPos) {
136
+ range.setEnd(endPos.node, endPos.offset);
137
+ }
138
+ const selection = window.getSelection();
139
+ selection.removeAllRanges();
140
+ selection.addRange(range);
141
+ }
142
+
143
+ focus() {
144
+ this.element.current.focus();
145
+ }
146
+
147
+ blur() {
148
+ this.element.current.blur();
149
+ }
150
+
151
+ handleBlur = (e) => {
152
+ if (this.props.onBlur) {
153
+ const value = e.target.innerText;
154
+ this.props.onBlur(value);
155
+ }
156
+ };
157
+
158
+ handleInput = (e) => {
159
+ if (this.props.onChange) {
160
+ this.props.onChange(e.target.innerText);
161
+ }
162
+ };
163
+
164
+ currentValue() {
165
+ const {value} = this.props;
166
+ if (value === undefined || value === null) {
167
+ return '';
168
+ }
169
+ return value;
170
+ }
171
+
172
+ componentDidUpdate() {
173
+ const div = this.element.current;
174
+ const value = this.currentValue();
175
+ if (div.innerText !== value) {
176
+ div.innerText = value;
177
+ }
178
+ }
179
+
180
+ componentDidMount() {
181
+ this.element.current.innerText = this.currentValue();
182
+ }
183
+
184
+ render() {
185
+ const {autoRows, value, onChange, disabled, ...props} = this.props;
186
+ const otherProps = {};
187
+ if (!disabled) {
188
+ otherProps.contentEditable = 'plaintext-only';
189
+ } else {
190
+ otherProps['data-disabled'] = true;
191
+ }
192
+ return (
193
+ <div
194
+ {...props}
195
+ ref={this.element}
196
+ {...otherProps}
197
+ suppressContentEditableWarning={true}
198
+ onInput={this.handleInput}
199
+ onBlur={this.handleBlur}
200
+ />
201
+ );
202
+ }
203
+ }
204
+
205
+ class MagicInputNC extends Widget {
206
+ constructor() {
207
+ super(...arguments);
208
+ this.styles = styles;
209
+ this.htmlInputRef = React.createRef();
210
+ this.state = {
211
+ showPicker: false,
212
+ };
213
+ }
214
+
215
+ get htmlInput() {
216
+ return this.htmlInputRef.current;
217
+ }
218
+
219
+ changeAndSelect(value, selectionStart, selectionEnd) {
220
+ this.props.onChange(value, () => {
221
+ // After state changed.
222
+ this.htmlInput?.setSelectionRange(selectionStart, selectionEnd);
223
+ });
224
+ }
225
+
226
+ onFocus = (event) => {
227
+ if (this.props.onFocus) {
228
+ const value = this.props.autoRows ? event : getEventValue(event);
229
+ this.props.onFocus(value);
230
+ }
231
+ if (this.props.selectAllOnFocus) {
232
+ this.htmlInput?.select();
233
+ }
234
+ Mousetrap.bind('ctrl+space', this.pick);
235
+ };
236
+
237
+ onBlur = (event) => {
238
+ if (this.props.onBlur) {
239
+ const value = this.props.autoRows ? event : getEventValue(event);
240
+ this.props.onBlur(value);
241
+ }
242
+ Mousetrap.unbind('ctrl+space');
243
+ };
244
+
245
+ onChange = (event) => {
246
+ if (this.props.onChange) {
247
+ const value = this.props.autoRows ? event : getEventValue(event);
248
+ this.props.onChange(value);
249
+ }
250
+ };
251
+
252
+ handleKeyDown = (event) => {
253
+ if (!event.shiftKey && event.key === 'Enter') {
254
+ if (this.props.onEnterKey) {
255
+ const value = this.props.autoRows
256
+ ? event.target.innerText
257
+ : event.target.value;
258
+ this.props.onEnterKey(value);
259
+ }
260
+ if (this.props.onValidate) {
261
+ const value = this.props.autoRows
262
+ ? event.target.innerText
263
+ : event.target.value;
264
+ this.props.onValidate(value);
265
+ }
266
+ }
267
+ this.props.onKeyDown?.(event);
268
+ if (event.defaultPrevented) {
269
+ return;
270
+ }
271
+ if (!event.shiftKey && event.key === 'Enter') {
272
+ if (!this.props.autoRows && !this.props.rows) {
273
+ this.htmlInput.blur();
274
+ }
275
+
276
+ const nextInput = findNextInput(this.htmlInput);
277
+ if (nextInput) {
278
+ if (!this.props.autoRows && !this.props.rows) {
279
+ event.preventDefault();
280
+ focusInput(nextInput);
281
+ }
282
+ event.stopPropagation();
283
+ }
284
+ }
285
+ };
286
+
287
+ componentDidMount() {
288
+ if (this.props.autoFocus) {
289
+ this.htmlInput?.setAttribute?.('autofocus', true);
290
+ }
291
+ }
292
+
293
+ insertEmoji = (emoji) => {
294
+ const value = this.props.value;
295
+ return (
296
+ value.slice(0, this.currentSelection.start) +
297
+ emoji +
298
+ value.slice(this.currentSelection.end)
299
+ );
300
+ };
301
+
302
+ pick = () => {
303
+ this.currentSelection = {
304
+ start: this.htmlInput.selectionStart,
305
+ end: this.htmlInput.selectionEnd,
306
+ };
307
+ this.setState({showPicker: true});
308
+ };
309
+
310
+ hide = () => {
311
+ this.setState({showPicker: false});
312
+ };
313
+
314
+ onSelectEmoji = (emoji) => {
315
+ this.hide();
316
+
317
+ this.htmlInput.focus();
318
+
319
+ const newValue = this.insertEmoji(emoji);
320
+ this.props.onChange?.(newValue);
321
+
322
+ const newPosition = this.currentSelection.start + emoji.length;
323
+ this.htmlInput.setSelectionRange(newPosition, newPosition);
324
+ };
325
+
326
+ render() {
327
+ const {
328
+ autoRows,
329
+ onEnterKey,
330
+ onValidate,
331
+ className = '',
332
+ emojiPicker,
333
+ selectAllOnFocus,
334
+ ...props
335
+ } = this.props;
336
+ let Component = 'input';
337
+ if (this.props.rows) {
338
+ Component = 'textarea';
339
+ }
340
+ if (this.props.autoRows) {
341
+ Component = EditableDiv;
342
+ }
343
+
344
+ if (this.props.emojiPicker) {
345
+ return (
346
+ <>
347
+ <div className={this.styles.classNames.withEmoji + ' ' + className}>
348
+ <Component
349
+ {...props}
350
+ className={'input mousetrap'}
351
+ ref={this.htmlInputRef}
352
+ onFocus={this.onFocus}
353
+ onBlur={this.onBlur}
354
+ onChange={this.onChange}
355
+ onKeyDown={this.handleKeyDown}
356
+ />
357
+ <div
358
+ className="pickerButton"
359
+ onClick={this.pick}
360
+ title="Insérer un émoji (Ctrl+Espace)"
361
+ >
362
+ <Icon path={mdiCat} size={1} />
363
+ </div>
364
+ </div>
365
+ {this.state.showPicker ? (
366
+ <MagicDialog modal={true} open onClose={this.hide}>
367
+ <MagicEmojiPickerNC
368
+ onEmojiSelect={(emoji) => this.onSelectEmoji(emoji.native)}
369
+ />
370
+ </MagicDialog>
371
+ ) : null}
372
+ </>
373
+ );
374
+ }
375
+
376
+ return (
377
+ <Component
378
+ {...props}
379
+ className={'input' + ' ' + className}
380
+ ref={this.htmlInputRef}
381
+ onFocus={this.onFocus}
382
+ onBlur={this.onBlur}
383
+ onChange={this.onChange}
384
+ onKeyDown={this.handleKeyDown}
385
+ onKeyUp={this.handleKeyUp}
386
+ />
387
+ );
388
+ }
389
+ }
390
+
391
+ const MagicInput = wrapRawInput(
392
+ ({forwardedRef, ...props}) => <MagicInputNC ref={forwardedRef} {...props} />,
393
+ 'value',
394
+ true
395
+ );
396
+
397
+ export default MagicInput;
@@ -0,0 +1,20 @@
1
+ export default function styles() {
2
+ const label = {
3
+ 'display': 'flex',
4
+ 'flexDirection': 'row',
5
+ 'alignItems': 'baseline',
6
+ 'marginTop': '8px',
7
+
8
+ '& > .input': {
9
+ '&:not(.no-grow)': {
10
+ flexGrow: 1,
11
+ width: 'unset',
12
+ },
13
+ 'margin': '0 0 0 6px',
14
+ },
15
+ };
16
+
17
+ return {
18
+ label,
19
+ };
20
+ }
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import Widget from 'goblin-laboratory/widgets/widget';
3
+ import * as styles from './styles.js';
4
+
5
+ class MagicLabel extends Widget {
6
+ constructor() {
7
+ super(...arguments);
8
+ this.styles = styles;
9
+ }
10
+
11
+ render() {
12
+ const className = this.props.className || '';
13
+ return (
14
+ <label
15
+ {...this.props}
16
+ className={this.styles.classNames.label + ' ' + className}
17
+ >
18
+ {this.props.children}
19
+ </label>
20
+ );
21
+ }
22
+ }
23
+
24
+ export default MagicLabel;