react-input-material 0.0.433 → 0.0.435

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.
@@ -1,2875 +0,0 @@
1
- // #!/usr/bin/env babel-node
2
- // -*- coding: utf-8 -*-
3
- /** @module GenericInput */
4
- 'use strict'
5
- /* !
6
- region header
7
- [Project page](https://torben.website/react-material-input)
8
-
9
- Copyright Torben Sickert (info["~at~"]torben.website) 16.12.2012
10
-
11
- License
12
- -------
13
-
14
- This library written by Torben Sickert stand under a creative commons
15
- naming 3.0 unported license.
16
- See https://creativecommons.org/licenses/by/3.0/deed.de
17
- endregion
18
- */
19
- // region imports
20
- import {Ace as CodeEditorNamespace} from 'ace-builds'
21
-
22
- import Tools, {optionalRequire} from 'clientnode'
23
- import {EvaluationResult, Mapping} from 'clientnode/type'
24
-
25
- import {
26
- FocusEvent as ReactFocusEvent,
27
- forwardRef,
28
- ForwardedRef,
29
- KeyboardEvent as ReactKeyboardEvent,
30
- lazy,
31
- memo as memorize,
32
- MouseEvent as ReactMouseEvent,
33
- MutableRefObject,
34
- ReactElement,
35
- ReactNode,
36
- Suspense,
37
- useEffect,
38
- useImperativeHandle,
39
- useRef,
40
- useState
41
- } from 'react'
42
- import CodeEditorType, {IAceEditorProps as CodeEditorProps} from 'react-ace'
43
- import {TransitionProps} from 'react-transition-group/Transition'
44
- import UseAnimationsType from 'react-useanimations'
45
- import LockAnimation from 'react-useanimations/lib/lock'
46
- import PlusToXAnimation from 'react-useanimations/lib/plusToX'
47
-
48
- import {Editor as RichTextEditor} from 'tinymce'
49
-
50
- import {MDCMenuFoundation} from '@material/menu'
51
- import {MDCSelectFoundation} from '@material/select'
52
- import {MDCTextFieldFoundation} from '@material/textfield'
53
-
54
- import {CircularProgress} from '@rmwc/circular-progress'
55
- import {FormField} from '@rmwc/formfield'
56
- import {Icon} from '@rmwc/icon'
57
- import {IconButton} from '@rmwc/icon-button'
58
- import {
59
- Menu, MenuApi, MenuSurface, MenuSurfaceAnchor, MenuItem, MenuOnSelectEventT
60
- } from '@rmwc/menu'
61
- import {Select, SelectProps} from '@rmwc/select'
62
- import {TextField, TextFieldProps} from '@rmwc/textfield'
63
- import {Theme} from '@rmwc/theme'
64
- import {IconOptions} from '@rmwc/types'
65
-
66
- import {
67
- Editor as RichTextEditorComponent, IAllProps as RichTextEditorProps
68
- } from '@tinymce/tinymce-react'
69
- import {
70
- EventHandler as RichTextEventHandler
71
- } from '@tinymce/tinymce-react/lib/cjs/main/ts/Events'
72
-
73
- import Dummy from './Dummy'
74
- import GenericAnimate from './GenericAnimate'
75
- /*
76
- "namedExport" version of css-loader:
77
-
78
- import {
79
- genericInputSuggestionsSuggestionClassName,
80
- genericInputSuggestionsSuggestionMarkClassName,
81
- genericInputClassName,
82
- genericInputCustomClassName,
83
- genericInputEditorLabelClassName,
84
- genericInputSuggestionsClassName,
85
- genericInputSuggestionsPendingClassName
86
- } from './GenericInput.module'
87
- */
88
- import cssClassNames from './GenericInput.module'
89
- import WrapConfigurations from './WrapConfigurations'
90
- import WrapTooltip from './WrapTooltip'
91
- import {
92
- deriveMissingPropertiesFromState as deriveMissingBasePropertiesFromState,
93
- determineInitialValue,
94
- determineInitialRepresentation,
95
- determineValidationState as determineBaseValidationState,
96
- formatValue,
97
- getConsolidatedProperties as getBaseConsolidatedProperties,
98
- getLabelAndValues,
99
- getValueFromSelection,
100
- mapPropertiesIntoModel,
101
- normalizeSelection,
102
- parseValue,
103
- translateKnownSymbols,
104
- triggerCallbackIfExists,
105
- useMemorizedValue,
106
- wrapStateSetter
107
- } from '../helper'
108
- import {
109
- CursorState,
110
- DataTransformSpecification,
111
- defaultInputModelState as defaultModelState,
112
- DefaultInputProperties as DefaultProperties,
113
- defaultInputProperties as defaultProperties,
114
- EditorState,
115
- GenericEvent,
116
- InputAdapter as Adapter,
117
- InputAdapterWithReferences as AdapterWithReferences,
118
- InputDataTransformation,
119
- InputModelState as ModelState,
120
- InputProperties as Properties,
121
- inputPropertyTypes as propertyTypes,
122
- InputProps as Props,
123
- inputRenderProperties as renderProperties,
124
- InputState as State,
125
- InputModel as Model,
126
- NativeInputType,
127
- NormalizedSelection,
128
- Renderable,
129
- GenericInputComponent,
130
- InputTablePosition as TablePosition,
131
- InputValueState as ValueState,
132
- TinyMCEOptions
133
- } from '../type'
134
-
135
- declare const TARGET_TECHNOLOGY:string
136
- const isBrowser =
137
- !(TARGET_TECHNOLOGY === 'node' || typeof window === undefined)
138
- const UseAnimations:null|typeof Dummy|typeof UseAnimationsType =
139
- isBrowser ? optionalRequire('react-useanimations') : null
140
- const lockAnimation:null|typeof LockAnimation = isBrowser ?
141
- optionalRequire('react-useanimations/lib/lock') :
142
- null
143
- const plusToXAnimation:null|typeof PlusToXAnimation = isBrowser ?
144
- optionalRequire('react-useanimations/lib/plusToX') :
145
- null
146
- // endregion
147
- const CSS_CLASS_NAMES:Mapping = cssClassNames as Mapping
148
- // region code editor configuration
149
- export const ACEEditorOptions = {
150
- basePath: '/node_modules/ace-builds/src-noconflict/',
151
- useWorker: false
152
- }
153
- const CodeEditor = lazy<typeof CodeEditorType>(
154
- async ():Promise<{default:typeof CodeEditorType}> => {
155
- const {config} = await import('ace-builds')
156
- for (const [name, value] of Object.entries(ACEEditorOptions))
157
- config.set(name, value)
158
-
159
- return await import('react-ace')
160
- }
161
- )
162
- // endregion
163
- // region rich text editor configuration
164
- declare const UTC_BUILD_TIMESTAMP:number|undefined
165
- // NOTE: Could be set via module bundler environment variables.
166
- const CURRENT_UTC_BUILD_TIMESTAMP =
167
- typeof UTC_BUILD_TIMESTAMP === 'undefined' ? 1 : UTC_BUILD_TIMESTAMP
168
- let richTextEditorLoadedOnce = false
169
- const tinymceBasePath = '/node_modules/tinymce/'
170
- export const TINYMCE_DEFAULT_OPTIONS:Partial<TinyMCEOptions> = {
171
- /* eslint-disable camelcase */
172
- // region paths
173
- base_url: tinymceBasePath,
174
- skin_url: `${tinymceBasePath}skins/ui/oxide`,
175
- theme_url: `${tinymceBasePath}themes/silver/theme.min.js`,
176
- // endregion
177
- allow_conditional_comments: false,
178
- allow_script_urls: false,
179
- body_class: 'mdc-text-field__input',
180
- branding: false,
181
- cache_suffix: `?version=${CURRENT_UTC_BUILD_TIMESTAMP}`,
182
- contextmenu: [],
183
- document_base_url: '/',
184
- element_format: 'xhtml',
185
- entity_encoding: 'raw',
186
- fix_list_elements: true,
187
- hidden_input: false,
188
- icon: 'material',
189
- invalid_elements: 'em',
190
- invalid_styles: 'color font-size line-height',
191
- keep_styles: false,
192
- menubar: false,
193
- /* eslint-disable max-len */
194
- plugins: [
195
- 'fullscreen',
196
- 'link',
197
- 'code',
198
- 'nonbreaking',
199
- 'searchreplace',
200
- 'visualblocks'
201
- ],
202
- /* eslint-enable max-len */
203
- relative_urls: false,
204
- remove_script_host: false,
205
- remove_trailing_brs: true,
206
- schema: 'html5',
207
- toolbar1: `
208
- cut copy paste |
209
- undo redo removeformat |
210
- styleselect formatselect fontselect fontsizeselect |
211
- searchreplace visualblocks fullscreen code
212
- `.trim(),
213
- toolbar2: `
214
- alignleft aligncenter alignright alignjustify outdent indent |
215
- link nonbreaking bullist numlist bold italic underline strikethrough
216
- `.trim(),
217
- trim: true
218
- /* eslint-enable camelcase */
219
- }
220
- // endregion
221
- // region static helper
222
- /**
223
- * Derives validation state from provided properties and state.
224
- * @param properties - Current component properties.
225
- * @param currentState - Current component state.
226
- *
227
- * @returns Whether component is in an aggregated valid or invalid state.
228
- */
229
- export function determineValidationState<T>(
230
- properties:DefaultProperties<T>, currentState:Partial<ModelState>
231
- ):boolean {
232
- return determineBaseValidationState<
233
- DefaultProperties<T>, Partial<ModelState>
234
- >(
235
- properties,
236
- currentState,
237
- {
238
- invalidMaximum: ():boolean => (
239
- typeof properties.model.maximum === 'number' &&
240
- typeof properties.model.value === 'number' &&
241
- !isNaN(properties.model.value) &&
242
- properties.model.maximum >= 0 &&
243
- properties.model.maximum < properties.model.value
244
- ),
245
- invalidMinimum: ():boolean => (
246
- typeof properties.model.minimum === 'number' &&
247
- typeof properties.model.value === 'number' &&
248
- !isNaN(properties.model.value) &&
249
- properties.model.value < properties.model.minimum
250
- ),
251
-
252
- invalidMaximumLength: ():boolean => (
253
- typeof properties.model.maximumLength === 'number' &&
254
- typeof properties.model.value === 'string' &&
255
- properties.model.maximumLength >= 0 &&
256
- properties.model.maximumLength < properties.model.value.length
257
- ),
258
- invalidMinimumLength: ():boolean => (
259
- typeof properties.model.minimumLength === 'number' &&
260
- typeof properties.model.value === 'string' &&
261
- properties.model.value.length < properties.model.minimumLength
262
- ),
263
-
264
- invalidInvertedPattern: ():boolean => (
265
- typeof properties.model.value === 'string' &&
266
- ([] as Array<null|RegExp|string>)
267
- .concat(properties.model.invertedRegularExpressionPattern)
268
- .some((expression:null|RegExp|string):boolean =>
269
- typeof expression === 'string' &&
270
- (new RegExp(expression)).test(
271
- properties.model.value as unknown as string
272
- ) ||
273
- expression !== null &&
274
- typeof expression === 'object' &&
275
- expression
276
- .test(properties.model.value as unknown as string)
277
- )
278
- ),
279
- invalidPattern: ():boolean => (
280
- typeof properties.model.value === 'string' &&
281
- ([] as Array<null|RegExp|string>)
282
- .concat(properties.model.regularExpressionPattern)
283
- .some((expression:null|RegExp|string):boolean =>
284
- typeof expression === 'string' &&
285
- !(new RegExp(expression)).test(
286
- properties.model.value as unknown as string
287
- ) ||
288
- expression !== null &&
289
- typeof expression === 'object' &&
290
- !expression
291
- .test(properties.model.value as unknown as string)
292
- )
293
- )
294
- }
295
- )
296
- }
297
- /**
298
- * Avoid propagating the enter key event since this usually sends a form which
299
- * is not intended when working in a text field.
300
- * @param event - Keyboard event.
301
- *
302
- * @returns Nothing.
303
- */
304
- export function preventEnterKeyPropagation(event:ReactKeyboardEvent):void {
305
- if (Tools.keyCode.ENTER === event.keyCode)
306
- event.stopPropagation()
307
- }
308
- /**
309
- * Indicates whether a provided query is matching currently provided
310
- * suggestion.
311
- * @param suggestion - Candidate to match again.
312
- * @param query - Search query to check for matching.
313
- *
314
- * @returns Boolean result whether provided suggestion matches given query or
315
- * not.
316
- */
317
- export function suggestionMatches(
318
- suggestion:string, query?:null|string
319
- ):boolean {
320
- if (query) {
321
- suggestion = suggestion.toLowerCase()
322
-
323
- return query
324
- .replace(/ +/g, ' ')
325
- .toLowerCase()
326
- .split(' ')
327
- .every((part:string):boolean => suggestion.includes(part))
328
- }
329
-
330
- return false
331
- }
332
- // endregion
333
- /* eslint-disable jsdoc/require-description-complete-sentence */
334
- /**
335
- * Generic input wrapper component which automatically determines a useful
336
- * input field depending on given model specification.
337
- *
338
- * Dataflow:
339
- *
340
- * 1. On-Render all states are merged with given properties into a normalized
341
- * property object.
342
- * 2. Properties, corresponding state values and sub node instances are saved
343
- * into a "ref" object (to make them accessible from the outside e.g. for
344
- * wrapper like web-components).
345
- * 3. Event handler saves corresponding data modifications into state and
346
- * normalized properties object.
347
- * 4. All state changes except selection changes trigger an "onChange" event
348
- * which delivers the consolidated properties object (with latest
349
- * modifications included).
350
- * @property static:displayName - Descriptive name for component to show in web
351
- * developer tools.
352
- *
353
- * @param props - Given components properties.
354
- * @param reference - Reference object to forward internal state.
355
- *
356
- * @returns React elements.
357
- */
358
- export const GenericInputInner = function<Type = unknown>(
359
- props:Props<Type>, reference?:ForwardedRef<Adapter<Type>>
360
- ):ReactElement {
361
- /* eslint-enable jsdoc/require-description-complete-sentence */
362
- // region live-cycle
363
- /**
364
- * Is triggered immediate after a re-rendering. Re-stores cursor selection
365
- * state if editor has been switched.
366
- * @returns Nothing.
367
- */
368
- useEffect(():void => {
369
- // region text-editor selection synchronisation
370
- if (selectionIsUnstable || editorState.selectionIsUnstable)
371
- if (properties.editorIsActive) {
372
- /*
373
- NOTE: If the corresponding editor are not loaded yet they
374
- will set the selection state on initialisation as long as
375
- "editorState.selectionIsUnstable" is set to "true".
376
- */
377
- if (codeEditorReference.current?.editor?.selection) {
378
- (codeEditorReference.current.editor.textInput as
379
- unknown as
380
- HTMLInputElement
381
- ).focus()
382
- setCodeEditorSelectionState(codeEditorReference.current)
383
-
384
- if (editorState.selectionIsUnstable)
385
- setEditorState(
386
- {...editorState, selectionIsUnstable: false}
387
- )
388
- } else if (richTextEditorInstance.current?.selection) {
389
- richTextEditorInstance.current.focus(false)
390
- setRichTextEditorSelectionState(
391
- richTextEditorInstance.current
392
- )
393
-
394
- if (editorState.selectionIsUnstable)
395
- setEditorState(
396
- {...editorState, selectionIsUnstable: false}
397
- )
398
- }
399
- } else if (inputReference.current) {
400
- // eslint-disable-next-line @typescript-eslint/no-extra-semi
401
- ;(
402
- inputReference.current as
403
- HTMLInputElement|HTMLTextAreaElement
404
- ).setSelectionRange(
405
- properties.cursor.start, properties.cursor.end
406
- )
407
-
408
- if (editorState.selectionIsUnstable)
409
- setEditorState(
410
- {...editorState, selectionIsUnstable: false}
411
- )
412
- }
413
- // endregion
414
- })
415
- // endregion
416
- // region context helper
417
- /// region render helper
418
- /**
419
- * Applies icon preset configurations.
420
- * @param options - Icon options to extend of known preset identified.
421
- *
422
- * @returns Given potential extended icon configuration.
423
- */
424
- const applyIconPreset = (
425
- options?:Properties['icon']
426
- ):IconOptions|string|undefined => {
427
- if (options === 'clear_preset')
428
- return {
429
- icon: <GenericAnimate
430
- in={!Tools.equals(properties.value, properties.default)}
431
- >
432
- {(
433
- UseAnimations &&
434
- !(UseAnimations as typeof Dummy).isDummy &&
435
- plusToXAnimation
436
- ) ?
437
- <UseAnimations
438
- animation={plusToXAnimation} reverse={true}
439
- /> :
440
- <IconButton icon="clear"/>
441
- }
442
- </GenericAnimate>,
443
- onClick: (event:ReactMouseEvent):void => {
444
- event.preventDefault()
445
- event.stopPropagation()
446
-
447
- onChangeValue(parseValue<Type>(
448
- properties,
449
- properties.default as Type,
450
- GenericInput.transformer
451
- ))
452
- },
453
- strategy: 'component',
454
- tooltip: 'Clear input'
455
- }
456
- if (options === 'password_preset')
457
- return useMemorizedValue(
458
- {
459
- icon: (
460
- UseAnimations &&
461
- !(UseAnimations as typeof Dummy).isDummy &&
462
- lockAnimation
463
- ) ?
464
- <UseAnimations
465
- animation={lockAnimation}
466
- reverse={!properties.hidden}
467
- /> :
468
- <IconButton
469
- icon={properties.hidden ? 'lock_open' : 'lock'}
470
- />,
471
- onClick: (event:ReactMouseEvent):void => {
472
- event.preventDefault()
473
- event.stopPropagation()
474
- setHidden((value:boolean|undefined):boolean => {
475
- if (value === undefined)
476
- value = properties.hidden
477
- properties.hidden = !value
478
-
479
- onChange(event)
480
-
481
- return properties.hidden
482
- })
483
- },
484
- strategy: 'component',
485
- tooltip:
486
- `${(properties.hidden ? 'Show' : 'Hide')} password`
487
- },
488
- properties.hidden
489
- )
490
- return options
491
- }
492
- /**
493
- * Derives native input type from given input property configuration.
494
- * @param properties - Input configuration to derive native input type
495
- * from.
496
- *
497
- * @returns Determined input type.
498
- */
499
- const determineNativeType = (
500
- properties:Properties<Type>
501
- ):NativeInputType =>
502
- (
503
- properties.type === 'string' ?
504
- properties.hidden ?
505
- 'password' :
506
- 'text' :
507
- transformer[
508
- properties.type as keyof InputDataTransformation
509
- ]?.type ?? properties.type
510
- ) as NativeInputType
511
- /**
512
- * Render help or error texts with current validation state color.
513
- * @returns Determined renderable markup specification.
514
- */
515
- const renderHelpText = ():ReactElement => <>
516
- <GenericAnimate
517
- in={
518
- properties.selectableEditor &&
519
- properties.type === 'string' &&
520
- properties.editor !== 'plain'
521
- }
522
- >
523
- <IconButton
524
- icon={{
525
- icon: properties.editorIsActive ?
526
- 'subject' :
527
- properties.editor.startsWith('code') ?
528
- 'code' :
529
- 'text_format',
530
- onClick: onChangeEditorIsActive
531
- }}
532
- />
533
- </GenericAnimate>
534
- <GenericAnimate in={Boolean(properties.declaration)}>
535
- <IconButton
536
- icon={{
537
- icon:
538
- 'more_' +
539
- (properties.showDeclaration ? 'vert' : 'horiz'),
540
- onClick: onChangeShowDeclaration
541
- }}
542
- />
543
- </GenericAnimate>
544
- <GenericAnimate in={properties.showDeclaration}>
545
- {properties.declaration}
546
- </GenericAnimate>
547
- <GenericAnimate in={
548
- !properties.showDeclaration &&
549
- properties.invalid &&
550
- (
551
- properties.showInitialValidationState ||
552
- /*
553
- Material inputs show their validation state at
554
- least after a blur event so we synchronize error
555
- message appearances.
556
- */
557
- properties.visited
558
- )
559
- }>
560
- <Theme use="error">{renderMessage(
561
- properties.invalidMaximum &&
562
- properties.maximumText ||
563
- properties.invalidMaximumLength &&
564
- properties.maximumLengthText ||
565
- properties.invalidMinimum &&
566
- properties.minimumText ||
567
- properties.invalidMinimumLength &&
568
- properties.minimumLengthText ||
569
- properties.invalidInvertedPattern &&
570
- properties.invertedPatternText ||
571
- properties.invalidPattern &&
572
- properties.patternText ||
573
- properties.invalidRequired &&
574
- properties.requiredText
575
- )}</Theme>
576
- </GenericAnimate>
577
- </>
578
- /**
579
- * Renders given template string against all properties in current
580
- * instance.
581
- * @param template - Template to render.
582
- *
583
- * @returns Evaluated template or an empty string if something goes wrong.
584
- */
585
- const renderMessage = (template?:unknown):string => {
586
- if (typeof template === 'string') {
587
- const evaluated:EvaluationResult = Tools.stringEvaluate(
588
- `\`${template}\``,
589
- {
590
- formatValue: (value:Type):string =>
591
- formatValue<Type>(properties, value, transformer),
592
- ...properties
593
- }
594
- )
595
-
596
- if (evaluated.error) {
597
- console.warn(
598
- 'Given message template could not be proceed:',
599
- evaluated.error
600
- )
601
-
602
- return ''
603
- }
604
-
605
- return evaluated.result
606
- }
607
-
608
- return ''
609
- }
610
- /**
611
- * Wraps given component with animation component if given condition holds.
612
- * @param content - Component or string to wrap.
613
- * @param propertiesOrInCondition - Animation properties or in condition
614
- * only.
615
- * @param condition - Show condition.
616
- *
617
- * @returns Wrapped component.
618
- */
619
- const wrapAnimationConditionally = (
620
- content:Renderable,
621
- propertiesOrInCondition:(
622
- boolean|Partial<TransitionProps<HTMLElement|undefined>>
623
- ) = {},
624
- condition = true
625
- ):Renderable => {
626
- if (typeof propertiesOrInCondition === 'boolean')
627
- return condition ?
628
- <GenericAnimate in={propertiesOrInCondition}>
629
- {content}
630
- </GenericAnimate> :
631
- propertiesOrInCondition ? content : ''
632
-
633
- return condition ?
634
- <GenericAnimate {...propertiesOrInCondition}>
635
- {content}
636
- </GenericAnimate> :
637
- propertiesOrInCondition.in ? content : ''
638
- }
639
- /**
640
- * If given icon options has an additional tooltip configuration integrate
641
- * a wrapping tooltip component into given configuration and remove initial
642
- * tooltip configuration.
643
- * @param options - Icon configuration potential extended a tooltip
644
- * configuration.
645
- *
646
- * @returns Resolved icon configuration.
647
- */
648
- const wrapIconWithTooltip = (
649
- options?:Properties['icon']
650
- ):IconOptions|undefined => {
651
- if (typeof options === 'object' && options?.tooltip) {
652
- const tooltip:Properties['tooltip'] = options.tooltip
653
- options = {...options}
654
- delete options.tooltip
655
- const nestedOptions:IconOptions = {...options}
656
- options.strategy = 'component'
657
- options.icon = <WrapTooltip options={tooltip}>
658
- <Icon icon={nestedOptions} />
659
- </WrapTooltip>
660
- }
661
- return options as IconOptions|undefined
662
- }
663
- /// endregion
664
- /// region handle cursor selection state
665
- //// region rich-text editor
666
- /**
667
- * Determines absolute offset in given markup.
668
- * @param contentDomNode - Wrapping dom node where all content is
669
- * contained.
670
- * @param domNode - Dom node which contains given position.
671
- * @param offset - Relative position within given node.
672
- *
673
- * @returns Determine absolute offset.
674
- */
675
- const determineAbsoluteSymbolOffsetFromHTML = (
676
- contentDomNode:Element, domNode:Element, offset:number
677
- ):number => {
678
- if (!properties.value)
679
- return 0
680
-
681
- const indicatorKey = 'generic-input-selection-indicator'
682
- const indicatorValue = '###'
683
- const indicator = ` ${indicatorKey}="${indicatorValue}"`
684
-
685
- domNode.setAttribute(indicatorKey, indicatorValue)
686
- // NOTE: TinyMCE seems to add a newline after each paragraph.
687
- const content:string = contentDomNode.innerHTML.replace(
688
- /(<\/p>)/gi, '$1\n'
689
- )
690
- domNode.removeAttribute(indicatorKey)
691
-
692
- const domNodeOffset:number = content.indexOf(indicator)
693
- const startIndex:number = domNodeOffset + indicator.length
694
-
695
- return (
696
- offset +
697
- content.indexOf('>', startIndex) +
698
- 1 -
699
- indicator.length
700
- )
701
- }
702
- //// endregion
703
- //// region code editor
704
- /**
705
- * Determines absolute range from table oriented position.
706
- * @param column - Symbol offset in given row.
707
- * @param row - Offset row.
708
- *
709
- * @returns Determined offset.
710
- */
711
- const determineAbsoluteSymbolOffsetFromTable = (
712
- column:number, row:number
713
- ):number => {
714
- if (typeof properties.value !== 'string' && !properties.value)
715
- return 0
716
-
717
- if (row > 0)
718
- return column + (properties.value as unknown as string)
719
- .split('\n')
720
- .slice(0, row)
721
- .map((line:string):number => 1 + line.length)
722
- .reduce((sum:number, value:number):number => sum + value)
723
- return column
724
- }
725
- /**
726
- * Converts absolute range into table oriented position.
727
- * @param offset - Absolute position.
728
- *
729
- * @returns Position.
730
- */
731
- const determineTablePosition = (offset:number):TablePosition => {
732
- const result:TablePosition = {column: 0, row: 0}
733
-
734
- if (typeof properties.value === 'string')
735
- for (const line of properties.value.split('\n')) {
736
- if (line.length < offset)
737
- offset -= 1 + line.length
738
- else {
739
- result.column = offset
740
- break
741
- }
742
-
743
- result.row += 1
744
- }
745
-
746
- return result
747
- }
748
- /**
749
- * Sets current cursor selection range in given code editor instance.
750
- * @param instance - Code editor instance.
751
- *
752
- * @returns Nothing.
753
- */
754
- const setCodeEditorSelectionState = (instance:CodeEditorType):void => {
755
- const range:CodeEditorNamespace.Range =
756
- instance.editor.selection.getRange()
757
- const endPosition:TablePosition =
758
- determineTablePosition(properties.cursor.end)
759
- range.setEnd(endPosition.row, endPosition.column)
760
- const startPosition:TablePosition =
761
- determineTablePosition(properties.cursor.start)
762
- range.setStart(startPosition.row, startPosition.column)
763
- instance.editor.selection.setRange(range)
764
- }
765
- /**
766
- * Sets current cursor selection range in given rich text editor instance.
767
- * @param instance - Code editor instance.
768
- *
769
- * @returns Nothing.
770
- */
771
- const setRichTextEditorSelectionState = (instance:RichTextEditor):void => {
772
- const indicator:{
773
- end:string
774
- start:string
775
- } = {
776
- end: '###generic-input-selection-indicator-end###',
777
- start: '###generic-input-selection-indicator-start###'
778
- }
779
- const cursor:CursorState = {
780
- end: properties.cursor.end + indicator.start.length,
781
- start: properties.cursor.start
782
- }
783
- const keysSorted:Array<keyof typeof indicator> =
784
- ['start', 'end']
785
-
786
- let value:string = properties.representation as string
787
- for (const type of keysSorted)
788
- value = (
789
- value.substring(0, cursor[type]) +
790
- indicator[type] +
791
- value.substring(cursor[type])
792
- )
793
- instance.getBody().innerHTML = value
794
-
795
- const walker:TreeWalker = document.createTreeWalker(
796
- instance.getBody(), NodeFilter.SHOW_TEXT, null
797
- )
798
-
799
- const range:Range = instance.dom.createRng()
800
- const result:{
801
- end?:[Node, number]
802
- start?:[Node, number]
803
- } = {}
804
-
805
- let node:Node|null
806
- while (node = walker.nextNode())
807
- for (const type of keysSorted)
808
- if (node.nodeValue) {
809
- const index:number =
810
- node.nodeValue.indexOf(indicator[type])
811
- if (index > -1) {
812
- node.nodeValue = node.nodeValue.replace(
813
- indicator[type], ''
814
- )
815
-
816
- result[type] = [node, index]
817
- }
818
- }
819
-
820
- for (const type of keysSorted)
821
- if (result[type])
822
- range[
823
- `set${Tools.stringCapitalize(type)}` as 'setEnd'|'setStart'
824
- ](...(result[type] as [Node, number]))
825
-
826
- if (result.end && result.start)
827
- instance.selection.setRng(range)
828
- }
829
- //// endregion
830
- /**
831
- * Saves current selection/cursor state in components state.
832
- * @param event - Event which triggered selection change.
833
- *
834
- * @returns Nothing.
835
- */
836
- const saveSelectionState = (event:GenericEvent):void => {
837
- /*
838
- NOTE: Known issues is that we do not get the absolute positions but
839
- the one in current selected node.
840
- */
841
- const codeEditorRange =
842
- codeEditorReference.current?.editor?.selection?.getRange()
843
- const richTextEditorRange =
844
- richTextEditorInstance.current?.selection?.getRng()
845
- const selectionEnd:null|number = (
846
- inputReference.current as HTMLInputElement|HTMLTextAreaElement
847
- )?.selectionEnd
848
- const selectionStart:null|number = (
849
- inputReference.current as HTMLInputElement|HTMLTextAreaElement
850
- )?.selectionStart
851
- if (codeEditorRange)
852
- setCursor({
853
- end: determineAbsoluteSymbolOffsetFromTable(
854
- codeEditorRange.end.column,
855
- typeof codeEditorRange.end.row === 'number' ?
856
- codeEditorRange.end.row :
857
- typeof properties.value === 'string' ?
858
- properties.value.split('\n').length - 1 :
859
- 0
860
- ),
861
- start: determineAbsoluteSymbolOffsetFromTable(
862
- codeEditorRange.start.column,
863
- typeof codeEditorRange.start.row === 'number' ?
864
- codeEditorRange.start.row :
865
- typeof properties.value === 'string' ?
866
- properties.value.split('\n').length - 1 :
867
- 0
868
- )
869
- })
870
- else if (richTextEditorRange)
871
- setCursor({
872
- end: determineAbsoluteSymbolOffsetFromHTML(
873
- richTextEditorInstance.current!.getBody(),
874
- richTextEditorInstance.current!.selection.getEnd(),
875
- richTextEditorRange.endOffset
876
- ),
877
- start: determineAbsoluteSymbolOffsetFromHTML(
878
- richTextEditorInstance.current!.getBody(),
879
- richTextEditorInstance.current!.selection.getStart(),
880
- richTextEditorRange.startOffset
881
- )
882
- })
883
- else if (
884
- typeof selectionEnd === 'number' &&
885
- typeof selectionStart === 'number'
886
- ) {
887
- const add:0|1|-1 =
888
- (event as unknown as KeyboardEvent)?.key?.length === 1 ?
889
- 1 :
890
- (event as unknown as KeyboardEvent)?.key === 'Backspace' &&
891
- (
892
- (properties.representation as string).length >
893
- selectionStart
894
- ) ?
895
- -1 :
896
- 0
897
-
898
- setCursor({end: selectionEnd + add, start: selectionStart + add})
899
- }
900
- }
901
- /// endregion
902
- /// region property aggregation
903
- const deriveMissingPropertiesFromState = () => {
904
- if (
905
- givenProperties.cursor === null ||
906
- typeof givenProperties.cursor !== 'object'
907
- )
908
- givenProperties.cursor = {} as CursorState
909
- if (givenProperties.cursor.end === undefined)
910
- givenProperties.cursor.end = cursor.end
911
- if (givenProperties.cursor.start === undefined)
912
- givenProperties.cursor.start = cursor.start
913
-
914
- if (givenProperties.editorIsActive === undefined)
915
- givenProperties.editorIsActive = editorState.editorIsActive
916
-
917
- if (givenProperties.hidden === undefined)
918
- givenProperties.hidden = hidden
919
-
920
- /*
921
- NOTE: This logic is important to re-determine representation when a
922
- new value is provided via properties.
923
- */
924
- if (givenProperties.representation === undefined)
925
- givenProperties.representation = valueState.representation
926
-
927
- if (givenProperties.showDeclaration === undefined)
928
- givenProperties.showDeclaration = showDeclaration
929
-
930
- deriveMissingBasePropertiesFromState<Props<Type>, ValueState<Type>>(
931
- givenProperties, valueState
932
- )
933
-
934
- if (givenProperties.value === undefined) {
935
- if (
936
- givenProperties.representation === undefined &&
937
- givenProperties.model!.value === undefined
938
- )
939
- givenProperties.representation = valueState.representation
940
- } else if (
941
- !representationControlled &&
942
- givenProperties.value !== valueState.value
943
- )
944
- /*
945
- NOTE: Set representation to "undefined" to trigger to derive
946
- current representation from current value.
947
- */
948
- givenProperties.representation = undefined
949
- }
950
- /**
951
- * Synchronizes property, state and model configuration:
952
- * Properties overwrites default properties which overwrites default model
953
- * properties.
954
- * @param properties - Properties to merge.
955
- *
956
- * @returns Nothing.
957
- */
958
- const mapPropertiesAndValidationStateIntoModel = (
959
- properties:Props<Type>
960
- ):DefaultProperties<Type> => {
961
- const result:DefaultProperties<Type> =
962
- mapPropertiesIntoModel<Props<Type>, DefaultProperties<Type>>(
963
- properties,
964
- GenericInput.defaultProperties.model as unknown as Model<Type>
965
- )
966
-
967
- result.model.value = parseValue<Type>(
968
- result, result.model.value as null|Type, transformer
969
- )
970
-
971
- determineValidationState<Type>(result, result.model.state)
972
-
973
- return result
974
- }
975
- /**
976
- * Calculate external properties (a set of all configurable properties).
977
- * @param properties - Properties to merge.
978
- *
979
- * @returns External properties object.
980
- */
981
- const getConsolidatedProperties = (
982
- properties:Props<Type>
983
- ):Properties<Type> => {
984
- const result:Properties<Type> =
985
- getBaseConsolidatedProperties<Props<Type>, Properties<Type>>(
986
- mapPropertiesAndValidationStateIntoModel(properties) as
987
- Props<Type>
988
- )
989
-
990
- if (!result.selection && result.type === 'boolean')
991
- // NOTE: Select-Fields restricts values to strings.
992
- result.selection = [
993
- {label: 'No', value: false as unknown as string},
994
- {label: 'Yes', value: true as unknown as string}
995
- ]
996
-
997
- // NOTE: If only an editor is specified it should be displayed.
998
- if (!(result.selectableEditor || result.editor === 'plain'))
999
- result.editorIsActive = true
1000
-
1001
- if (typeof result.representation !== 'string') {
1002
- result.representation = formatValue<Type>(
1003
- result,
1004
- result.value as null|Type,
1005
- transformer,
1006
- /*
1007
- NOTE: Handle two cases:
1008
-
1009
- 1. Representation has to be determine initially
1010
- (-> usually no focus).
1011
- 2. Representation was set from the outside
1012
- (-> usually no focus).
1013
- */
1014
- !result.focused
1015
- )
1016
- /*
1017
- NOTE: We will try to restore last known selection state if
1018
- representation has been modified.
1019
- */
1020
- if (
1021
- result.focused &&
1022
- result.representation !== result.value as unknown as string &&
1023
- ['password', 'text'].includes(determineNativeType(result))
1024
- )
1025
- selectionIsUnstable = true
1026
- }
1027
-
1028
- return result
1029
- }
1030
- /// endregion
1031
- /// region reference setter
1032
- /**
1033
- * Set code editor references.
1034
- * @param instance - Code editor instance.
1035
- *
1036
- * @returns Nothing.
1037
- */
1038
- const setCodeEditorReference = (instance:CodeEditorType|null):void => {
1039
- codeEditorReference.current = instance
1040
-
1041
- if (codeEditorReference.current?.editor?.container?.querySelector(
1042
- 'textarea'
1043
- ))
1044
- codeEditorInputReference.current =
1045
- codeEditorReference.current.editor.container
1046
- .querySelector('textarea')
1047
-
1048
- if (
1049
- codeEditorReference.current &&
1050
- properties.editorIsActive &&
1051
- editorState.selectionIsUnstable
1052
- ) {
1053
- (codeEditorReference.current.editor.textInput as
1054
- unknown as
1055
- HTMLInputElement
1056
- ).focus()
1057
- setCodeEditorSelectionState(codeEditorReference.current)
1058
- setEditorState({...editorState, selectionIsUnstable: false})
1059
- }
1060
- }
1061
- /**
1062
- * Set rich text editor references.
1063
- * @param instance - Editor instance.
1064
- *
1065
- * @returns Nothing.
1066
- */
1067
- const setRichTextEditorReference = (
1068
- instance:null|RichTextEditorComponent
1069
- ):void => {
1070
- richTextEditorReference.current = instance
1071
-
1072
- /*
1073
- Refer inner element here is possible but marked as private.
1074
-
1075
- if (richTextEditorReference.current?.elementRef)
1076
- richTextEditorInputReference.current =
1077
- richTextEditorReference.current.elementRef
1078
- */
1079
- }
1080
- /// endregion
1081
- // endregion
1082
- // region event handler
1083
- /**
1084
- * Triggered on blur events.
1085
- * @param event - Event object.
1086
- *
1087
- * @returns Nothing.
1088
- */
1089
- const onBlur = (event:GenericEvent):void => setValueState((
1090
- oldValueState:ValueState<Type, ModelState>
1091
- ):ValueState<Type, ModelState> => {
1092
- setIsSuggestionOpen(false)
1093
-
1094
- let changed = false
1095
- let stateChanged = false
1096
-
1097
- if (oldValueState.modelState.focused) {
1098
- properties.focused = false
1099
- changed = true
1100
- stateChanged = true
1101
- }
1102
-
1103
- if (!oldValueState.modelState.visited) {
1104
- properties.visited = true
1105
- changed = true
1106
- stateChanged = true
1107
- }
1108
-
1109
- if (!useSuggestions || properties.suggestSelection) {
1110
- const candidate:null|Type = getValueFromSelection<Type>(
1111
- properties.representation, normalizedSelection!
1112
- )
1113
- if (candidate === null) {
1114
- properties.value = parseValue<Type>(
1115
- properties, properties.value as null|Type, transformer
1116
- )
1117
- properties.representation = formatValue<Type>(
1118
- properties, properties.value, transformer
1119
- )
1120
- } else
1121
- properties.value = candidate
1122
- }
1123
-
1124
- if (
1125
- !Tools.equals(oldValueState.value, properties.value) ||
1126
- oldValueState.representation !== properties.representation
1127
- )
1128
- changed = true
1129
-
1130
- if (changed)
1131
- onChange(event)
1132
-
1133
- if (!Tools.equals(oldValueState.value, properties.value))
1134
- triggerCallbackIfExists<Properties<Type>>(
1135
- properties,
1136
- 'changeValue',
1137
- controlled,
1138
- properties.value,
1139
- event,
1140
- properties
1141
- )
1142
-
1143
- if (stateChanged)
1144
- triggerCallbackIfExists<Properties<Type>>(
1145
- properties,
1146
- 'changeState',
1147
- controlled,
1148
- properties.model.state,
1149
- event,
1150
- properties
1151
- )
1152
-
1153
- triggerCallbackIfExists<Properties<Type>>(
1154
- properties, 'blur', controlled, event, properties
1155
- )
1156
-
1157
- return changed ?
1158
- {
1159
- modelState: properties.model.state,
1160
- representation: properties.representation,
1161
- value: properties.value as null|Type
1162
- } :
1163
- oldValueState
1164
- })
1165
- /**
1166
- * Triggered on any change events. Consolidates properties object and
1167
- * triggers given on change callbacks.
1168
- * @param event - Potential event object.
1169
- *
1170
- * @returns Nothing.
1171
- */
1172
- const onChange = (event?:GenericEvent):void => {
1173
- Tools.extend(
1174
- true,
1175
- properties,
1176
- getConsolidatedProperties(
1177
- /*
1178
- Workaround since "Type" isn't identified as subset of
1179
- "RecursivePartial<Type>" yet.
1180
- */
1181
- properties as unknown as Props<Type>
1182
- )
1183
- )
1184
-
1185
- triggerCallbackIfExists<Properties<Type>>(
1186
- properties, 'change', controlled, properties, event
1187
- )
1188
- }
1189
- /**
1190
- * Triggered when editor is active indicator should be changed.
1191
- * @param event - Mouse event object.
1192
- *
1193
- * @returns Nothing.
1194
- */
1195
- const onChangeEditorIsActive = (event?:ReactMouseEvent):void => {
1196
- if (event) {
1197
- event.preventDefault()
1198
- event.stopPropagation()
1199
- }
1200
-
1201
- setEditorState(({editorIsActive}):EditorState => {
1202
- properties.editorIsActive = !editorIsActive
1203
-
1204
- onChange(event)
1205
-
1206
- triggerCallbackIfExists<Properties<Type>>(
1207
- properties,
1208
- 'changeEditorIsActive',
1209
- controlled,
1210
- properties.editorIsActive,
1211
- event,
1212
- properties
1213
- )
1214
-
1215
- return {
1216
- editorIsActive: properties.editorIsActive,
1217
- selectionIsUnstable: true
1218
- }
1219
- })
1220
- }
1221
- /**
1222
- * Triggered when show declaration indicator should be changed.
1223
- * @param event - Potential event object.
1224
- *
1225
- * @returns Nothing.
1226
- */
1227
- const onChangeShowDeclaration = (event?:ReactMouseEvent):void => {
1228
- if (event) {
1229
- event.preventDefault()
1230
- event.stopPropagation()
1231
- }
1232
- setShowDeclaration((value:boolean):boolean => {
1233
- properties.showDeclaration = !value
1234
-
1235
- onChange(event)
1236
-
1237
- triggerCallbackIfExists<Properties<Type>>(
1238
- properties,
1239
- 'changeShowDeclaration',
1240
- controlled,
1241
- properties.showDeclaration,
1242
- event,
1243
- properties
1244
- )
1245
-
1246
- return properties.showDeclaration
1247
- })
1248
- }
1249
- /**
1250
- * Triggered when ever the value changes.
1251
- * Takes a given value or determines it from given event object and
1252
- * generates new value state (internal value, representation and validation
1253
- * states). Derived event handler will be triggered when internal state
1254
- * has been consolidated.
1255
- * @param eventOrValue - Event object or new value.
1256
- * @param editorInstance - Potential editor instance if triggered from a
1257
- * rich text or code editor.
1258
- * @param selectedIndex - Indicates whether given event was triggered by a
1259
- * selection.
1260
- *
1261
- * @returns Nothing.
1262
- */
1263
- const onChangeValue = (
1264
- eventOrValue:GenericEvent|null|Type,
1265
- editorInstance?:RichTextEditor,
1266
- selectedIndex = -1
1267
- ):void => {
1268
- if (properties.disabled)
1269
- return
1270
-
1271
- setIsSuggestionOpen(true)
1272
-
1273
- let event:GenericEvent|undefined
1274
- if (eventOrValue !== null && typeof eventOrValue === 'object') {
1275
- const target:HTMLInputElement|null|undefined =
1276
- (eventOrValue as GenericEvent).target as HTMLInputElement ||
1277
- (eventOrValue as GenericEvent).detail as HTMLInputElement
1278
- if (target)
1279
- /*
1280
- NOTE: Enhanced select fields (menus) do not provide the
1281
- selected value but index.
1282
- */
1283
- if (typeof (
1284
- target as unknown as {index:number}
1285
- ).index === 'number') {
1286
- const index:number = (
1287
- target as unknown as {index:number}
1288
- ).index - (properties.placeholder ? 1 : 0)
1289
- properties.value =
1290
- index >= 0 &&
1291
- index < suggestionValues.length ?
1292
- suggestionValues[index] as Type :
1293
- null
1294
- } else
1295
- properties.value = typeof target.value === 'undefined' ?
1296
- null :
1297
- target.value as unknown as Type
1298
- else
1299
- properties.value = eventOrValue as null|Type
1300
- } else
1301
- properties.value = eventOrValue
1302
-
1303
- const setHelper = ():void => setValueState((
1304
- oldValueState:ValueState<Type, ModelState>
1305
- ):ValueState<Type, ModelState> => {
1306
- if (
1307
- !representationControlled &&
1308
- oldValueState.representation === properties.representation &&
1309
- /*
1310
- NOTE: Unstable intermediate states have to be synced of a
1311
- suggestion creator was pending.
1312
- */
1313
- !properties.suggestionCreator &&
1314
- selectedIndex === -1
1315
- )
1316
- /*
1317
- NOTE: No representation update and no controlled value or
1318
- representation:
1319
-
1320
- -> No value update
1321
- -> No state update
1322
- -> Nothing to trigger
1323
- */
1324
- return oldValueState
1325
-
1326
- const valueState:ValueState<Type, ModelState> = {
1327
- ...oldValueState, representation: properties.representation
1328
- }
1329
-
1330
- if (
1331
- !controlled &&
1332
- Tools.equals(oldValueState.value, properties.value)
1333
- )
1334
- /*
1335
- NOTE: No value update and no controlled value:
1336
-
1337
- -> No state update
1338
- -> Nothing to trigger
1339
- */
1340
- return valueState
1341
-
1342
- valueState.value = properties.value as null|Type
1343
-
1344
- let stateChanged = false
1345
-
1346
- if (oldValueState.modelState.pristine) {
1347
- properties.dirty = true
1348
- properties.pristine = false
1349
- stateChanged = true
1350
- }
1351
-
1352
- onChange(event)
1353
-
1354
- if (determineValidationState<Type>(
1355
- properties as DefaultProperties<Type>, oldValueState.modelState
1356
- ))
1357
- stateChanged = true
1358
-
1359
- triggerCallbackIfExists<Properties<Type>>(
1360
- properties,
1361
- 'changeValue',
1362
- controlled,
1363
- properties.value,
1364
- event,
1365
- properties
1366
- )
1367
-
1368
- if (stateChanged) {
1369
- valueState.modelState = properties.model.state
1370
-
1371
- triggerCallbackIfExists<Properties<Type>>(
1372
- properties,
1373
- 'changeState',
1374
- controlled,
1375
- properties.model.state,
1376
- event,
1377
- properties
1378
- )
1379
- }
1380
-
1381
- if (useSelection || selectedIndex !== -1)
1382
- triggerCallbackIfExists<Properties<Type>>(
1383
- properties,
1384
- 'select',
1385
- controlled,
1386
- event,
1387
- properties
1388
- )
1389
-
1390
- return valueState
1391
- })
1392
-
1393
- properties.representation = selectedIndex !== -1 ?
1394
- currentSuggestionLabels[selectedIndex] :
1395
- typeof properties.value === 'string' ?
1396
- properties.value :
1397
- formatValue<Type>(properties, properties.value, transformer)
1398
-
1399
- if (!useSuggestions) {
1400
- properties.value =
1401
- parseValue<Type>(properties, properties.value, transformer)
1402
-
1403
- setHelper()
1404
- } else if (properties.suggestionCreator) {
1405
- const abortController:AbortController = new AbortController()
1406
-
1407
- const onResultsRetrieved = (
1408
- results:Properties['selection']
1409
- ):void => {
1410
- if (abortController.signal.aborted)
1411
- return
1412
-
1413
- /*
1414
- NOTE: A synchronous retrieved selection may has to stop a
1415
- pending (slower) asynchronous request.
1416
- */
1417
- setSelection((
1418
- oldSelection:AbortController|Properties['selection']
1419
- ):Properties['selection'] => {
1420
- if (
1421
- oldSelection instanceof AbortController &&
1422
- !oldSelection.signal.aborted
1423
- )
1424
- oldSelection.abort()
1425
-
1426
- return results
1427
- })
1428
-
1429
- if (selectedIndex === -1) {
1430
- const result:null|Type = getValueFromSelection<Type>(
1431
- properties.representation, normalizeSelection(results)!
1432
- )
1433
-
1434
- if (result !== null || properties.searchSelection)
1435
- properties.value = result
1436
- else
1437
- properties.value = parseValue<Type>(
1438
- properties,
1439
- properties.representation as unknown as null|Type,
1440
- transformer
1441
- )
1442
- }
1443
-
1444
- setHelper()
1445
- }
1446
-
1447
- /*
1448
- Trigger asynchronous suggestions retrieving and delayed state
1449
- consolidation.
1450
- */
1451
- const result:(
1452
- Properties['selection']|Promise<Properties['selection']>
1453
- ) = properties.suggestionCreator({
1454
- abortController,
1455
- properties,
1456
- query: properties.representation as string
1457
- })
1458
-
1459
- if ((result as Promise<Properties['selection']>)?.then) {
1460
- setSelection((
1461
- oldSelection:AbortController|Properties['selection']
1462
- ):AbortController => {
1463
- if (
1464
- oldSelection instanceof AbortController &&
1465
- !oldSelection.signal.aborted
1466
- )
1467
- oldSelection.abort()
1468
-
1469
- return abortController
1470
- })
1471
- /*
1472
- NOTE: Immediate sync current representation to maintain
1473
- cursor state.
1474
- */
1475
- setValueState((
1476
- oldValueState:ValueState<Type, ModelState>
1477
- ):ValueState<Type, ModelState> => ({
1478
- ...oldValueState, representation: properties.representation
1479
- }))
1480
-
1481
- ;(result as Promise<Properties['selection']>).then(
1482
- onResultsRetrieved,
1483
- /*
1484
- NOTE: Avoid to through an exception when aborting the
1485
- request intentionally.
1486
- */
1487
- Tools.noop
1488
- )
1489
- } else
1490
- onResultsRetrieved(result as Properties['selection'])
1491
- } else {
1492
- if (selectedIndex === -1) {
1493
- /*
1494
- Map value from given selections and trigger state
1495
- consolidation.
1496
- */
1497
- const result:null|Type = getValueFromSelection<Type>(
1498
- properties.representation, normalizedSelection!
1499
- )
1500
-
1501
- if (result !== null || properties.searchSelection)
1502
- properties.value = result
1503
- else
1504
- properties.value = parseValue<Type>(
1505
- properties,
1506
- properties.representation as unknown as null|Type,
1507
- transformer
1508
- )
1509
- }
1510
-
1511
- setHelper()
1512
- }
1513
- }
1514
- /**
1515
- * Triggered on click events.
1516
- * @param event - Mouse event object.
1517
- *
1518
- * @returns Nothing.
1519
- */
1520
- const onClick = (event:ReactMouseEvent):void => {
1521
- onSelectionChange(event)
1522
-
1523
- triggerCallbackIfExists<Properties<Type>>(
1524
- properties, 'click', controlled, event, properties
1525
- )
1526
-
1527
- onTouch(event)
1528
- }
1529
- /**
1530
- * Triggered on focus events and opens suggestions.
1531
- * @param event - Focus event object.
1532
- *
1533
- * @returns Nothing.
1534
- */
1535
- const triggerOnFocusAndOpenSuggestions = (event:ReactFocusEvent):void => {
1536
- setIsSuggestionOpen(true)
1537
-
1538
- onFocus(event)
1539
- }
1540
- /**
1541
- * Triggered on focus events.
1542
- * @param event - Focus event object.
1543
- *
1544
- * @returns Nothing.
1545
- */
1546
- const onFocus = (event:ReactFocusEvent):void => {
1547
- triggerCallbackIfExists<Properties<Type>>(
1548
- properties, 'focus', controlled, event, properties
1549
- )
1550
-
1551
- onTouch(event)
1552
- }
1553
- /**
1554
- * Triggered on down up events.
1555
- * @param event - Key up event object.
1556
- *
1557
- * @returns Nothing.
1558
- */
1559
- const onKeyDown = (event:ReactKeyboardEvent):void => {
1560
- if (useSuggestions && Tools.keyCode.DOWN === event.keyCode)
1561
- suggestionMenuAPIReference.current?.focusItemAtIndex(0)
1562
-
1563
- /*
1564
- NOTE: We do not want to forward keydown enter events coming from
1565
- textareas.
1566
- */
1567
- if (properties.type === 'string' && properties.editor !== 'plain')
1568
- preventEnterKeyPropagation(event)
1569
-
1570
- triggerCallbackIfExists<Properties<Type>>(
1571
- properties, 'keyDown', controlled, event, properties
1572
- )
1573
- }
1574
- /**
1575
- * Triggered on key up events.
1576
- * @param event - Key up event object.
1577
- *
1578
- * @returns Nothing.
1579
- */
1580
- const onKeyUp = (event:ReactKeyboardEvent):void => {
1581
- // NOTE: Avoid breaking password-filler on non textarea fields!
1582
- if (event.keyCode) {
1583
- onSelectionChange(event)
1584
-
1585
- triggerCallbackIfExists<Properties<Type>>(
1586
- properties, 'keyUp', controlled, event, properties
1587
- )
1588
- }
1589
- }
1590
- /**
1591
- * Triggered on selection change events.
1592
- * @param event - Event which triggered selection change.
1593
- *
1594
- * @returns Nothing.
1595
- */
1596
- const onSelectionChange = (event:GenericEvent):void => {
1597
- saveSelectionState(event)
1598
-
1599
- triggerCallbackIfExists<Properties<Type>>(
1600
- properties, 'selectionChange', controlled, event, properties
1601
- )
1602
- }
1603
- /**
1604
- * Triggers on start interacting with the input.
1605
- * @param event - Event object which triggered interaction.
1606
- *
1607
- * @returns Nothing.
1608
- */
1609
- const onTouch = (event:ReactFocusEvent|ReactMouseEvent):void =>
1610
- setValueState((
1611
- oldValueState:ValueState<Type, ModelState>
1612
- ):ValueState<Type, ModelState> => {
1613
- let changedState = false
1614
-
1615
- if (!oldValueState.modelState.focused) {
1616
- properties.focused = true
1617
- changedState = true
1618
- }
1619
-
1620
- if (oldValueState.modelState.untouched) {
1621
- properties.touched = true
1622
- properties.untouched = false
1623
- changedState = true
1624
- }
1625
-
1626
- let result:ValueState<Type, ModelState> = oldValueState
1627
-
1628
- if (changedState) {
1629
- onChange(event)
1630
-
1631
- result = {...oldValueState, modelState: properties.model.state}
1632
-
1633
- triggerCallbackIfExists<Properties<Type>>(
1634
- properties,
1635
- 'changeState',
1636
- controlled,
1637
- properties.model.state,
1638
- event,
1639
- properties
1640
- )
1641
- }
1642
-
1643
- triggerCallbackIfExists<Properties<Type>>(
1644
- properties, 'touch', controlled, event, properties
1645
- )
1646
-
1647
- return result
1648
- })
1649
- // endregion
1650
- // region properties
1651
- /// region references
1652
- const codeEditorReference:MutableRefObject<CodeEditorType|null> =
1653
- useRef<CodeEditorType>(null)
1654
- const codeEditorInputReference:MutableRefObject<HTMLTextAreaElement|null> =
1655
- useRef<HTMLTextAreaElement>(null)
1656
- const foundationReference:MutableRefObject<
1657
- MDCSelectFoundation|MDCTextFieldFoundation|null
1658
- > = useRef<MDCSelectFoundation|MDCTextFieldFoundation>(null)
1659
- const inputReference:MutableRefObject<
1660
- HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|null
1661
- > = useRef<HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement>(null)
1662
- const richTextEditorInputReference:MutableRefObject<
1663
- HTMLTextAreaElement|null
1664
- > = useRef<HTMLTextAreaElement>(null)
1665
- const richTextEditorInstance:MutableRefObject<null|RichTextEditor> =
1666
- useRef<RichTextEditor>(null)
1667
- const richTextEditorReference:MutableRefObject<
1668
- null|RichTextEditorComponent
1669
- > = useRef<RichTextEditorComponent>(null)
1670
- const suggestionMenuAPIReference:MutableRefObject<MenuApi|null> =
1671
- useRef<MenuApi>(null)
1672
- const suggestionMenuFoundationReference:MutableRefObject<
1673
- MDCMenuFoundation|null
1674
- > = useRef<MDCMenuFoundation>(null)
1675
- /// endregion
1676
- const givenProps:Props<Type> = translateKnownSymbols(props)
1677
-
1678
- const [cursor, setCursor] = useState<CursorState>({end: 0, start: 0})
1679
- const [editorState, setEditorState] = useState<EditorState>({
1680
- editorIsActive: false, selectionIsUnstable: false
1681
- })
1682
- const [hidden, setHidden] = useState<boolean|undefined>()
1683
- const [isSuggestionOpen, setIsSuggestionOpen] = useState<boolean>(false)
1684
- const [showDeclaration, setShowDeclaration] = useState<boolean>(false)
1685
-
1686
- let initialValue:null|Type = determineInitialValue<Type>(
1687
- givenProps,
1688
- GenericInput.defaultProperties.model?.default as unknown as null|Type
1689
- )
1690
- if (initialValue instanceof Date)
1691
- initialValue = (initialValue.getTime() / 1000) as unknown as Type
1692
- /*
1693
- NOTE: Extend default properties with given properties while letting
1694
- default property object untouched for unchanged usage in other
1695
- instances.
1696
- */
1697
- const givenProperties:Props<Type> = Tools.extend<Props<Type>>(
1698
- true,
1699
- Tools.copy<Props<Type>>(GenericInput.defaultProperties as Props<Type>),
1700
- givenProps
1701
- )
1702
-
1703
- const type:keyof InputDataTransformation =
1704
- givenProperties.type as keyof InputDataTransformation ||
1705
- givenProperties.model?.type ||
1706
- 'string'
1707
- const transformer:InputDataTransformation =
1708
- givenProperties.transformer ?
1709
- {
1710
- ...GenericInput.transformer,
1711
- [type]: Tools.extend<DataTransformSpecification<Type>>(
1712
- true,
1713
- Tools.copy<DataTransformSpecification<Type>>(
1714
- GenericInput.transformer[type] as
1715
- DataTransformSpecification<Type>
1716
- ) ||
1717
- {},
1718
- givenProperties.transformer as
1719
- DataTransformSpecification<Type>
1720
- )
1721
- } :
1722
- GenericInput.transformer
1723
-
1724
- let [selection, setSelection] =
1725
- useState<AbortController|Properties['selection']>()
1726
- if (givenProperties.selection || givenProperties.model?.selection)
1727
- selection =
1728
- givenProperties.selection || givenProperties.model?.selection
1729
-
1730
- const normalizedSelection:NormalizedSelection|undefined =
1731
- selection instanceof AbortController ?
1732
- [] :
1733
- normalizeSelection(selection, givenProperties.labels)
1734
- const [suggestionLabels, suggestionValues] =
1735
- selection instanceof AbortController ?
1736
- [[], []] :
1737
- getLabelAndValues(normalizedSelection)
1738
-
1739
- /*
1740
- NOTE: This values have to share the same state item since they have to
1741
- be updated in one event loop (set state callback).
1742
- */
1743
- let [valueState, setValueState] = useState<ValueState<Type, ModelState>>(
1744
- () => ({
1745
- modelState: {...GenericInput.defaultModelState},
1746
- representation: determineInitialRepresentation<Type>(
1747
- givenProperties as DefaultProperties<Type>,
1748
- GenericInput.defaultProperties as
1749
- unknown as
1750
- DefaultProperties<Type>,
1751
- initialValue,
1752
- transformer,
1753
- normalizedSelection
1754
- ),
1755
- value: initialValue
1756
- })
1757
- )
1758
-
1759
- /*
1760
- NOTE: Sometimes we need real given properties or derived (default
1761
- extended) "given" properties.
1762
- */
1763
- const controlled:boolean =
1764
- !givenProperties.enforceUncontrolled &&
1765
- (
1766
- givenProps.model?.value !== undefined ||
1767
- givenProps.value !== undefined
1768
- ) &&
1769
- Boolean(givenProps.onChange || givenProps.onChangeValue)
1770
- const representationControlled:boolean =
1771
- controlled && givenProps.representation !== undefined
1772
- let selectionIsUnstable = false
1773
-
1774
- deriveMissingPropertiesFromState()
1775
-
1776
- const properties:Properties<Type> =
1777
- getConsolidatedProperties(givenProperties)
1778
-
1779
- if (properties.hidden === undefined)
1780
- properties.hidden = properties.name?.startsWith('password')
1781
- // region synchronize properties into state where values are not controlled
1782
- if (!Tools.equals(properties.cursor, cursor))
1783
- setCursor(properties.cursor)
1784
- if (properties.editorIsActive !== editorState.editorIsActive)
1785
- setEditorState({
1786
- ...editorState, editorIsActive: properties.editorIsActive
1787
- })
1788
- if (properties.hidden !== hidden)
1789
- setHidden(properties.hidden)
1790
- if (properties.showDeclaration !== showDeclaration)
1791
- setShowDeclaration(properties.showDeclaration)
1792
-
1793
- const currentValueState:ValueState<Type, ModelState> = {
1794
- modelState: properties.model.state,
1795
- representation: properties.representation,
1796
- value: properties.value!
1797
- }
1798
- /*
1799
- NOTE: If value is controlled only trigger/save state changes when model
1800
- state has changed.
1801
- */
1802
- if (
1803
- !controlled &&
1804
- (
1805
- !Tools.equals(properties.value, valueState.value) ||
1806
- properties.representation !== valueState.representation
1807
- ) ||
1808
- !Tools.equals(properties.model.state, valueState.modelState)
1809
- )
1810
- setValueState(currentValueState)
1811
- if (controlled)
1812
- setValueState = wrapStateSetter<ValueState<Type, ModelState>>(
1813
- setValueState, currentValueState
1814
- )
1815
- // endregion
1816
- // endregion
1817
- // region export references
1818
- useImperativeHandle(
1819
- reference,
1820
- ():AdapterWithReferences<Type> => {
1821
- const state:State<Type> =
1822
- {modelState: properties.model.state} as State<Type>
1823
-
1824
- for (const name of [
1825
- 'cursor', 'editorIsActive', 'hidden', 'showDeclaration'
1826
- ] as const)
1827
- if (!Object.prototype.hasOwnProperty.call(givenProps, name))
1828
- (state[name] as boolean|CursorState) = properties[name]
1829
-
1830
- if (!representationControlled)
1831
- state.representation = properties.representation
1832
- if (!controlled)
1833
- state.value = properties.value as null|Type
1834
-
1835
- return {
1836
- properties,
1837
- references: {
1838
- codeEditorReference,
1839
- codeEditorInputReference,
1840
- foundationReference,
1841
- inputReference,
1842
- richTextEditorInputReference,
1843
- richTextEditorInstance,
1844
- richTextEditorReference,
1845
- suggestionMenuAPIReference,
1846
- suggestionMenuFoundationReference
1847
- },
1848
- state
1849
- }
1850
- }
1851
- )
1852
- // endregion
1853
- // region render
1854
- /// region intermediate render properties
1855
- const genericProperties:Partial<
1856
- CodeEditorProps|RichTextEditorProps|SelectProps|TextFieldProps
1857
- > = {
1858
- onBlur,
1859
- onFocus: triggerOnFocusAndOpenSuggestions,
1860
- placeholder: properties.placeholder
1861
- }
1862
- const materialProperties:SelectProps|TextFieldProps = {
1863
- disabled: properties.disabled,
1864
- helpText: {
1865
- children: renderHelpText(),
1866
- persistent: Boolean(properties.declaration)
1867
- },
1868
- invalid: properties.showInitialValidationState && properties.invalid,
1869
- label: properties.description || properties.name,
1870
- outlined: properties.outlined,
1871
- required: properties.required
1872
- }
1873
- if (properties.icon)
1874
- materialProperties.icon = wrapIconWithTooltip(
1875
- applyIconPreset(properties.icon) as IconOptions
1876
- ) as IconOptions
1877
-
1878
- const tinyMCEOptions:Partial<TinyMCEOptions> = {
1879
- ...TINYMCE_DEFAULT_OPTIONS,
1880
- // eslint-disable-next-line camelcase
1881
- content_style: properties.disabled ? 'body {opacity: .38}' : '',
1882
- placeholder: properties.placeholder,
1883
- readonly: Boolean(properties.disabled),
1884
- setup: (instance:RichTextEditor):void => {
1885
- richTextEditorInstance.current = instance
1886
- richTextEditorInstance.current.on('init', ():void => {
1887
- if (!richTextEditorInstance.current)
1888
- return
1889
-
1890
- richTextEditorLoadedOnce = true
1891
-
1892
- if (
1893
- properties.editorIsActive &&
1894
- editorState.selectionIsUnstable
1895
- ) {
1896
- richTextEditorInstance.current.focus(false)
1897
- setRichTextEditorSelectionState(
1898
- richTextEditorInstance.current
1899
- )
1900
- setEditorState(
1901
- {...editorState, selectionIsUnstable: false}
1902
- )
1903
- }
1904
- })
1905
- }
1906
- }
1907
- if (properties.editor.endsWith('raw)')) {
1908
- tinyMCEOptions.toolbar1 =
1909
- 'cut copy paste | undo redo removeformat | code | fullscreen'
1910
- delete tinyMCEOptions.toolbar2
1911
- } else if (properties.editor.endsWith('simple)')) {
1912
- tinyMCEOptions.toolbar1 =
1913
- 'cut copy paste | undo redo removeformat | bold italic ' +
1914
- 'underline strikethrough subscript superscript | fullscreen'
1915
- delete tinyMCEOptions.toolbar2
1916
- } else if (properties.editor.endsWith('normal)'))
1917
- tinyMCEOptions.toolbar1 =
1918
- 'cut copy paste | undo redo removeformat | styleselect ' +
1919
- 'formatselect | searchreplace visualblocks fullscreen code'
1920
-
1921
- const isAdvancedEditor:boolean = (
1922
- !properties.selection &&
1923
- properties.type === 'string' &&
1924
- properties.editorIsActive &&
1925
- (
1926
- properties.editor.startsWith('code') ||
1927
- properties.editor.startsWith('richtext(')
1928
- )
1929
- )
1930
-
1931
- const currentRenderableSuggestions:Array<ReactElement> = []
1932
- const currentSuggestionLabels:Array<ReactNode|string> = []
1933
- const currentSuggestionValues:Array<unknown> = []
1934
- const useSuggestions = Boolean(
1935
- properties.suggestionCreator ||
1936
- suggestionLabels.length &&
1937
- (properties.searchSelection || properties.suggestSelection)
1938
- )
1939
- if (useSuggestions && suggestionLabels.length) {
1940
- // NOTE: Create consistent property configuration.
1941
- properties.suggestSelection = !properties.searchSelection
1942
-
1943
- let index = 0
1944
- for (const suggestion of suggestionLabels) {
1945
- if (Tools.isFunction(properties.children)) {
1946
- const result:null|ReactElement = properties.children({
1947
- index,
1948
- normalizedSelection: normalizedSelection!,
1949
- properties,
1950
- query: properties.representation as string,
1951
- suggestion,
1952
- value: suggestionValues[index] as Type
1953
- })
1954
-
1955
- if (result) {
1956
- currentRenderableSuggestions.push(
1957
- <MenuItem
1958
- className={CSS_CLASS_NAMES[
1959
- 'generic-input__suggestions__suggestion'
1960
- ]}
1961
- key={index}
1962
- >
1963
- {result}
1964
- </MenuItem>
1965
- )
1966
- currentSuggestionLabels.push(suggestion)
1967
- currentSuggestionValues.push(suggestionValues[index])
1968
- }
1969
- } else if (
1970
- !properties.representation ||
1971
- properties.suggestionCreator ||
1972
- suggestionMatches(
1973
- suggestion as string, properties.representation as string
1974
- )
1975
- ) {
1976
- currentRenderableSuggestions.push(
1977
- <MenuItem
1978
- className={CSS_CLASS_NAMES[
1979
- 'generic-input__suggestions__suggestion'
1980
- ]}
1981
- key={index}
1982
- >
1983
- {(Tools.stringMark(
1984
- suggestion,
1985
- (
1986
- properties.representation as string
1987
- )?.split(' ') || '',
1988
- (value:unknown):string =>
1989
- `${value as string}`.toLowerCase(),
1990
- (foundWord:string):ReactElement =>
1991
- <span className={CSS_CLASS_NAMES[
1992
- 'generic-input__suggestions__suggestion' +
1993
- '__mark'
1994
- ]}>
1995
- {foundWord}
1996
- </span>,
1997
- null
1998
- ) as Array<ReactElement|string>).map((
1999
- item:ReactElement|string, index:number
2000
- ):ReactElement =>
2001
- <span key={index}>{item}</span>
2002
- )}
2003
- </MenuItem>
2004
- )
2005
- currentSuggestionLabels.push(suggestion)
2006
- currentSuggestionValues.push(suggestionValues[index])
2007
- }
2008
-
2009
- index += 1
2010
- }
2011
- }
2012
- const useSelection:boolean =
2013
- (Boolean(normalizedSelection) || Boolean(properties.labels)) &&
2014
- !useSuggestions
2015
- /// endregion
2016
- /// region determine type specific constraints
2017
- const constraints:Mapping<unknown> = {}
2018
- if (properties.type === 'number') {
2019
- constraints.step = properties.step
2020
-
2021
- if (properties.maximum !== Infinity)
2022
- constraints.max = properties.maximum
2023
- if (properties.minimum !== -Infinity)
2024
- constraints.min = properties.minimum
2025
- } else if (properties.type === 'string') {
2026
- if (
2027
- properties.maximumLength >= 0 &&
2028
- properties.maximumLength !== Infinity
2029
- )
2030
- constraints.maxLength = properties.maximumLength
2031
- if (properties.minimumLength > 0)
2032
- constraints.minLength = properties.minimumLength
2033
-
2034
- if (properties.editor !== 'plain')
2035
- constraints.rows = properties.rows
2036
- } else if ([
2037
- 'date', 'datetime-local', 'time', 'time-local'
2038
- ].includes(properties.type)) {
2039
- constraints.step = properties.step
2040
-
2041
- if (properties.maximum !== Infinity)
2042
- constraints.max = formatValue<Type>(
2043
- properties,
2044
- properties.maximum as
2045
- unknown as
2046
- Type,
2047
- transformer
2048
- )
2049
- if (properties.minimum !== -Infinity)
2050
- constraints.min = formatValue<Type>(
2051
- properties,
2052
- properties.minimum as
2053
- unknown as
2054
- Type,
2055
- transformer
2056
- )
2057
- }
2058
- /// endregion
2059
- /// region main markup
2060
- return <WrapConfigurations
2061
- strict={GenericInput.strict}
2062
- themeConfiguration={properties.themeConfiguration}
2063
- tooltip={properties.tooltip}
2064
- >
2065
- <div
2066
- className={[CSS_CLASS_NAMES['generic-input']]
2067
- .concat(
2068
- isAdvancedEditor ?
2069
- CSS_CLASS_NAMES['generic-input--custom'] :
2070
- [],
2071
- properties.className ?? []
2072
- )
2073
- .join(' ')
2074
- }
2075
- style={properties.styles}
2076
- >
2077
- {wrapAnimationConditionally(
2078
- <Select
2079
- {...genericProperties as SelectProps}
2080
- {...materialProperties as SelectProps}
2081
- enhanced
2082
- foundationRef={foundationReference as
2083
- MutableRefObject<MDCSelectFoundation|null>
2084
- }
2085
- inputRef={inputReference as
2086
- unknown as
2087
- (_reference:HTMLSelectElement|null) => void
2088
- }
2089
- onChange={onChangeValue}
2090
- options={normalizedSelection as SelectProps['options']}
2091
- rootProps={{
2092
- name: properties.name,
2093
- onClick: onClick,
2094
- ...properties.rootProps
2095
- }}
2096
- value={`${properties.value as unknown as string}`}
2097
- {...properties.inputProperties as SelectProps}
2098
- />,
2099
- useSelection
2100
- )}
2101
- {wrapAnimationConditionally(
2102
- [
2103
- <FormField
2104
- className={['mdc-text-field']
2105
- .concat(
2106
- properties.disabled ?
2107
- 'mdc-text-field--disabled' :
2108
- [],
2109
- 'mdc-text-field--textarea'
2110
- )
2111
- .join(' ')
2112
- }
2113
- onKeyDown={preventEnterKeyPropagation}
2114
- key="advanced-editor-form-field"
2115
- >
2116
- <label>
2117
- <span className={
2118
- [CSS_CLASS_NAMES[
2119
- 'generic-input__editor__label'
2120
- ]]
2121
- .concat(
2122
- 'mdc-floating-label',
2123
- 'mdc-floating-label--float-above'
2124
- )
2125
- .join(' ')
2126
- }>
2127
- <Theme use={
2128
- properties.invalid &&
2129
- (
2130
- properties.showInitialValidationState ||
2131
- properties.visited
2132
- ) ? 'error' : undefined
2133
- }>
2134
- {
2135
- (
2136
- properties.description ||
2137
- properties.name
2138
- ) +
2139
- (properties.required ? '*' : '')
2140
- }
2141
- </Theme>
2142
- </span>
2143
- {
2144
- properties.editor.startsWith('code') ?
2145
- <Suspense fallback={
2146
- <CircularProgress size="large" />
2147
- }>
2148
- <CodeEditor
2149
- {...genericProperties as
2150
- CodeEditorProps
2151
- }
2152
- className="mdc-text-field__input"
2153
- mode={(
2154
- properties.editor.startsWith(
2155
- 'code('
2156
- ) &&
2157
- properties.editor.endsWith(')')
2158
- ) ?
2159
- properties.editor.substring(
2160
- 'code('.length,
2161
- properties.editor.length -
2162
- 1
2163
- ) :
2164
- 'javascript'
2165
- }
2166
- name={properties.name}
2167
- onChange={onChangeValue as
2168
- unknown as
2169
- (
2170
- _value:string,
2171
- _event?:unknown
2172
- ) => void
2173
- }
2174
- onCursorChange={onSelectionChange}
2175
- onSelectionChange={
2176
- onSelectionChange
2177
- }
2178
- ref={setCodeEditorReference}
2179
- setOptions={{
2180
- maxLines: properties.rows,
2181
- minLines: properties.rows,
2182
- readOnly: properties.disabled,
2183
- tabSize: 4,
2184
- useWorker: false
2185
- }}
2186
- theme="github"
2187
- value={
2188
- properties.representation as
2189
- string
2190
- }
2191
- {...properties.inputProperties as
2192
- CodeEditorProps
2193
- }
2194
- />
2195
- </Suspense> :
2196
- <RichTextEditorComponent
2197
- {...genericProperties as
2198
- RichTextEditorProps
2199
- }
2200
- disabled={properties.disabled}
2201
- init={tinyMCEOptions}
2202
- onClick={onClick as
2203
- unknown as
2204
- RichTextEventHandler<MouseEvent>
2205
- }
2206
- onEditorChange={onChangeValue as
2207
- unknown as
2208
- RichTextEditorProps[
2209
- 'onEditorChange'
2210
- ]
2211
- }
2212
- onKeyUp={onKeyUp as
2213
- unknown as
2214
- RichTextEventHandler<KeyboardEvent>
2215
- }
2216
- ref={setRichTextEditorReference}
2217
- textareaName={properties.name}
2218
- tinymceScriptSrc={
2219
- (
2220
- TINYMCE_DEFAULT_OPTIONS
2221
- .base_url as
2222
- string
2223
- ) +
2224
- 'tinymce.min.js'
2225
- }
2226
- value={
2227
- properties.representation as string
2228
- }
2229
- {...properties.inputProperties as
2230
- RichTextEditorProps
2231
- }
2232
- />
2233
- }
2234
- </label>
2235
- </FormField>,
2236
- <div
2237
- className="mdc-text-field-helper-line"
2238
- key="advanced-editor-helper-line"
2239
- >
2240
- <p className={
2241
- 'mdc-text-field-helper-text ' +
2242
- 'mdc-text-field-helper-text--persistent'
2243
- }>
2244
- {(
2245
- materialProperties.helpText as
2246
- {children:ReactElement}
2247
- ).children}
2248
- </p>
2249
- </div>
2250
- ],
2251
- isAdvancedEditor,
2252
- richTextEditorLoadedOnce ||
2253
- properties.editor.startsWith('code')
2254
- )}
2255
- {wrapAnimationConditionally(
2256
- <div>
2257
- {useSuggestions ?
2258
- <MenuSurfaceAnchor
2259
- onKeyDown={preventEnterKeyPropagation}
2260
- >
2261
- {selection instanceof AbortController ?
2262
- <MenuSurface
2263
- anchorCorner="bottomLeft"
2264
- className={
2265
- CSS_CLASS_NAMES[
2266
- 'generic-input__suggestions'
2267
- ] +
2268
- ' ' +
2269
- CSS_CLASS_NAMES[
2270
- 'generic-input__suggestions' +
2271
- '--pending'
2272
- ]
2273
- }
2274
- open={true}
2275
- >
2276
- <CircularProgress size="large" />
2277
- </MenuSurface> :
2278
- <Menu
2279
- anchorCorner="bottomLeft"
2280
- apiRef={(instance:MenuApi|null):void => {
2281
- suggestionMenuAPIReference.current =
2282
- instance
2283
- }}
2284
- className={CSS_CLASS_NAMES[
2285
- 'generic-input__suggestions'
2286
- ]}
2287
- focusOnOpen={false}
2288
- foundationRef={
2289
- suggestionMenuFoundationReference
2290
- }
2291
- onFocus={onFocus}
2292
- onSelect={(
2293
- event:MenuOnSelectEventT
2294
- ):void => {
2295
- onChangeValue(
2296
- currentSuggestionValues[
2297
- event.detail.index
2298
- ] as Type,
2299
- undefined,
2300
- event.detail.index
2301
- )
2302
- setIsSuggestionOpen(false)
2303
- }}
2304
- open={
2305
- Boolean(
2306
- currentSuggestionLabels.length
2307
- ) &&
2308
- isSuggestionOpen &&
2309
- /*
2310
- NOTE: If single possibility is
2311
- already selected avoid showing this
2312
- suggestion.
2313
- */
2314
- !(
2315
- currentSuggestionLabels.length ===
2316
- 1 &&
2317
- currentSuggestionLabels[0] ===
2318
- properties.representation
2319
- )
2320
- }
2321
- >
2322
- {currentRenderableSuggestions}
2323
- </Menu>
2324
- }
2325
- </MenuSurfaceAnchor> :
2326
- ''
2327
- }
2328
- <TextField
2329
- {...genericProperties as TextFieldProps}
2330
- {...materialProperties as TextFieldProps}
2331
- {...constraints}
2332
- align={properties.align}
2333
- characterCount={
2334
- typeof properties.maximumLength === 'number' &&
2335
- !isNaN(properties.maximumLength) &&
2336
- properties.maximumLength >= 0
2337
- }
2338
- foundationRef={foundationReference as
2339
- MutableRefObject<MDCTextFieldFoundation|null>
2340
- }
2341
- inputRef={inputReference as
2342
- MutableRefObject<HTMLInputElement|null>
2343
- }
2344
- onChange={onChangeValue}
2345
- ripple={properties.ripple}
2346
- rootProps={{
2347
- name: properties.name,
2348
- onClick,
2349
- onKeyDown,
2350
- onKeyUp,
2351
- ...properties.rootProps
2352
- }}
2353
- textarea={
2354
- properties.type === 'string' &&
2355
- properties.editor !== 'plain'
2356
- }
2357
- trailingIcon={wrapIconWithTooltip(applyIconPreset(
2358
- properties.trailingIcon
2359
- ))}
2360
- type={determineNativeType(properties)}
2361
- value={properties.representation as string}
2362
- {...properties.inputProperties as TextFieldProps}
2363
- />
2364
- </div>,
2365
- !(isAdvancedEditor || useSelection),
2366
- richTextEditorLoadedOnce ||
2367
- properties.editor.startsWith('code')
2368
- )}
2369
- </div>
2370
- </WrapConfigurations>
2371
- /// endregion
2372
- // endregion
2373
- }
2374
- // NOTE: This is useful in react dev tools.
2375
- GenericInputInner.displayName = 'GenericInput'
2376
- /**
2377
- * Wrapping web component compatible react component.
2378
- * @property static:defaultModelState - Initial model state.
2379
- * @property static:defaultProperties - Initial property configuration.
2380
- * @property static:locales - Defines input formatting locales.
2381
- * @property static:propTypes - Triggers reacts runtime property value checks.
2382
- * @property static:strict - Indicates whether we should wrap render output in
2383
- * reacts strict component.
2384
- * @property static:transformer - Generic input data transformation
2385
- * specifications.
2386
- * @property static:wrapped - Wrapped component.
2387
- *
2388
- * @param props - Given components properties.
2389
- * @param reference - Reference object to forward internal state.
2390
- *
2391
- * @returns React elements.
2392
- */
2393
- export const GenericInput:GenericInputComponent =
2394
- memorize(forwardRef(GenericInputInner)) as unknown as GenericInputComponent
2395
- // region static properties
2396
- /// region web-component hints
2397
- GenericInput.wrapped = GenericInputInner
2398
- GenericInput.webComponentAdapterWrapped = 'react'
2399
- /// endregion
2400
- GenericInput.defaultModelState = defaultModelState
2401
- /*
2402
- NOTE: We set values to "undefined" to identify whether these values where
2403
- provided via "props" and should shadow a state saved valued.
2404
- */
2405
- GenericInput.defaultProperties = {
2406
- ...defaultProperties,
2407
- cursor: undefined,
2408
- model: {
2409
- ...defaultProperties.model,
2410
- // Trigger initial determination.
2411
- state: undefined as unknown as ModelState,
2412
- value: undefined
2413
- },
2414
- representation: undefined,
2415
- value: undefined
2416
- }
2417
- GenericInput.locales = Tools.locales
2418
- GenericInput.propTypes = propertyTypes
2419
- GenericInput.renderProperties = renderProperties
2420
- GenericInput.strict = false
2421
- GenericInput.transformer = {
2422
- boolean: {
2423
- parse: (value:number|string):boolean =>
2424
- typeof value === 'boolean' ?
2425
- value :
2426
- new Map<number|string, boolean>([
2427
- ['false', false],
2428
- ['true', true],
2429
- [0, false],
2430
- [1, true]
2431
- ]).get(value) ??
2432
- true,
2433
- type: 'text'
2434
- },
2435
- currency: {
2436
- format: {final: {
2437
- options: {currency: 'USD'},
2438
- transform: (
2439
- value:number,
2440
- configuration:DefaultProperties<number>,
2441
- transformer:InputDataTransformation
2442
- ):string => {
2443
- const currency:string =
2444
- transformer.currency.format?.final.options?.currency as
2445
- string ??
2446
- 'USD'
2447
-
2448
- if (value === Infinity)
2449
- return `Infinity ${currency}`
2450
-
2451
- if (value === -Infinity)
2452
- return `- Infinity ${currency}`
2453
-
2454
- if (isNaN(value))
2455
- return 'unknown'
2456
-
2457
- return (new Intl.NumberFormat(
2458
- GenericInput.locales,
2459
- {
2460
- style: 'currency',
2461
- ...transformer.currency.format?.final.options ?? {}
2462
- }
2463
- )).format(value)
2464
- }
2465
- }},
2466
- parse: (
2467
- value:string,
2468
- configuration:DefaultProperties<number>,
2469
- transformer:InputDataTransformation
2470
- ):number =>
2471
- transformer.float.parse ?
2472
- transformer.float.parse(value, configuration, transformer) :
2473
- NaN,
2474
- type: 'text'
2475
- },
2476
-
2477
- date: {
2478
- format: {final: {transform: (
2479
- value:Date|number|string,
2480
- configuration:DefaultProperties<number>,
2481
- transformer:InputDataTransformation
2482
- ):string => {
2483
- if (typeof value !== 'number')
2484
- if (transformer.date.parse)
2485
- value = transformer.date.parse(
2486
- value, configuration, transformer
2487
- )
2488
- else {
2489
- const parsedDate:number = value instanceof Date ?
2490
- value.getTime() / 1000 :
2491
- Date.parse(value)
2492
- if (isNaN(parsedDate)) {
2493
- const parsedFloat:number = parseFloat(value as string)
2494
- value = isNaN(parsedFloat) ? 0 : parsedFloat
2495
- } else
2496
- value = parsedDate / 1000
2497
- }
2498
-
2499
- if (value === Infinity)
2500
- return 'Infinitely far in the future'
2501
- if (value === -Infinity)
2502
- return 'Infinitely early in the past'
2503
- if (!isFinite(value))
2504
- return ''
2505
-
2506
- const formattedValue:string =
2507
- (new Date(Math.round(value * 1000))).toISOString()
2508
-
2509
- return formattedValue.substring(0, formattedValue.indexOf('T'))
2510
- }}},
2511
- parse: (value:Date|number|string):number => {
2512
- if (typeof value === 'number')
2513
- return value
2514
-
2515
- if (value instanceof Date)
2516
- return value.getTime() / 1000
2517
-
2518
- const parsedDate:number = Date.parse(value)
2519
- if (isNaN(parsedDate)) {
2520
- const parsedFloat:number = parseFloat(value)
2521
- if (isNaN(parsedFloat))
2522
- return 0
2523
-
2524
- return parsedFloat
2525
- }
2526
-
2527
- return parsedDate / 1000
2528
- }
2529
- },
2530
- // TODO respect local to utc conversion.
2531
- 'datetime-local': {
2532
- format: {final: {transform: (
2533
- value:Date|number|string,
2534
- configuration:DefaultProperties<number>,
2535
- transformer:InputDataTransformation
2536
- ):string => {
2537
- if (typeof value !== 'number')
2538
- if (transformer['datetime-local'].parse)
2539
- value = transformer['datetime-local'].parse(
2540
- value, configuration, transformer
2541
- )
2542
- else {
2543
- const parsedDate:number = value instanceof Date ?
2544
- value.getTime() / 1000 :
2545
- Date.parse(value)
2546
- if (isNaN(parsedDate)) {
2547
- const parsedFloat:number = parseFloat(value as string)
2548
- value = isNaN(parsedFloat) ? 0 : parsedFloat
2549
- } else
2550
- value = parsedDate / 1000
2551
- }
2552
-
2553
- if (value === Infinity)
2554
- return 'Infinitely far in the future'
2555
- if (value === -Infinity)
2556
- return 'Infinitely early in the past'
2557
- if (!isFinite(value))
2558
- return ''
2559
-
2560
- const formattedValue:string =
2561
- (new Date(Math.round(value * 1000))).toISOString()
2562
-
2563
- return formattedValue.substring(0, formattedValue.length - 1)
2564
- }}},
2565
- parse: (
2566
- value:Date|number|string,
2567
- configuration:DefaultProperties<number>,
2568
- transformer:InputDataTransformation
2569
- ):number => {
2570
- if (transformer.date.parse)
2571
- return transformer.date.parse(
2572
- value, configuration, transformer
2573
- )
2574
-
2575
- if (value instanceof Date)
2576
- return value.getTime() / 1000
2577
-
2578
- const parsedDate:number = Date.parse(value as string)
2579
- if (isNaN(parsedDate)) {
2580
- const parsedFloat:number = parseFloat(value as string)
2581
- if (isNaN(parsedFloat))
2582
- return 0
2583
-
2584
- return parsedFloat
2585
- }
2586
-
2587
- return parsedDate / 1000
2588
- }
2589
- },
2590
- time: {
2591
- format: {
2592
- final: {transform: (
2593
- value:Date|number|string,
2594
- configuration:DefaultProperties<number>,
2595
- transformer:InputDataTransformation
2596
- ):string => {
2597
- if (typeof value !== 'number')
2598
- if (transformer.time.parse)
2599
- value = transformer.time.parse(
2600
- value, configuration, transformer
2601
- )
2602
- else {
2603
- const parsedDate:number = value instanceof Date ?
2604
- value.getTime() / 1000 :
2605
- Date.parse(value)
2606
- if (isNaN(parsedDate)) {
2607
- const parsedFloat:number =
2608
- parseFloat(value as string)
2609
- value = isNaN(parsedFloat) ? 0 : parsedFloat
2610
- } else
2611
- value = parsedDate / 1000
2612
- }
2613
-
2614
- if (value === Infinity)
2615
- return 'Infinitely far in the future'
2616
- if (value === -Infinity)
2617
- return 'Infinitely early in the past'
2618
- if (!isFinite(value))
2619
- return ''
2620
-
2621
- let formattedValue:string =
2622
- (new Date(Math.round(value * 1000))).toISOString()
2623
-
2624
- formattedValue = formattedValue.substring(
2625
- formattedValue.indexOf('T') + 1, formattedValue.length - 1
2626
- )
2627
-
2628
- if (
2629
- configuration.step &&
2630
- configuration.step >= 60 &&
2631
- (configuration.step % 60) === 0
2632
- )
2633
- return formattedValue.substring(
2634
- 0, formattedValue.lastIndexOf(':')
2635
- )
2636
-
2637
- return formattedValue
2638
- }}
2639
- },
2640
- parse: (value:Date|number|string):number => {
2641
- if (typeof value === 'number')
2642
- return value
2643
-
2644
- if (value instanceof Date)
2645
- return value.getTime() / 1000
2646
-
2647
- const parsedDate:number = Date.parse(value)
2648
- if (isNaN(parsedDate)) {
2649
- const parsedFloat:number = parseFloat(value.replace(
2650
- /^([0-9]{2}):([0-9]{2})(:([0-9]{2}(\.[0-9]+)?))?$/,
2651
- (
2652
- _match:string,
2653
- hours:string,
2654
- minutes:string,
2655
- secondsSuffix?:string,
2656
- seconds?:string,
2657
- _millisecondsSuffix?:string
2658
- ):string =>
2659
- String(
2660
- parseInt(hours) *
2661
- 60 ** 2 +
2662
- parseInt(minutes) *
2663
- 60 +
2664
- (secondsSuffix ? parseFloat(seconds!) : 0)
2665
- )
2666
- ))
2667
-
2668
- if (isNaN(parsedFloat))
2669
- return 0
2670
-
2671
- return parsedFloat
2672
- }
2673
-
2674
- return parsedDate / 1000
2675
- }
2676
- },
2677
- /*
2678
- NOTE: Daylight saving time should not make a difference since times
2679
- will always be saved on zero unix timestamp where no daylight saving
2680
- time rules existing.
2681
- */
2682
- 'time-local': {
2683
- format: {
2684
- final: {transform: (
2685
- value:Date|number|string,
2686
- configuration:DefaultProperties<number>,
2687
- transformer:InputDataTransformation
2688
- ):string => {
2689
- if (typeof value !== 'number')
2690
- if (transformer['time-local'].parse)
2691
- value = transformer['time-local'].parse(
2692
- value, configuration, transformer
2693
- )
2694
- else {
2695
- const parsedDate:number = value instanceof Date ?
2696
- value.getTime() / 1000 :
2697
- Date.parse(value)
2698
- if (isNaN(parsedDate)) {
2699
- const parsedFloat:number =
2700
- parseFloat(value as string)
2701
- value = isNaN(parsedFloat) ? 0 : parsedFloat
2702
- } else
2703
- value = parsedDate / 1000
2704
- }
2705
-
2706
- if (value === Infinity)
2707
- return 'Infinitely far in the future'
2708
- if (value === -Infinity)
2709
- return 'Infinitely early in the past'
2710
- if (!isFinite(value))
2711
- return ''
2712
-
2713
- const dateTime = new Date(Math.round(value * 1000))
2714
- const hours:number = dateTime.getHours()
2715
- const minutes:number = dateTime.getMinutes()
2716
-
2717
- const formattedValue:string = (
2718
- `${hours < 10 ? '0' : ''}${String(hours)}:` +
2719
- `${minutes < 10 ? '0' : ''}${String(minutes)}`
2720
- )
2721
-
2722
- if (!(
2723
- configuration.step &&
2724
- configuration.step >= 60 &&
2725
- (configuration.step % 60) === 0
2726
- )) {
2727
- const seconds:number = dateTime.getSeconds()
2728
-
2729
- return (
2730
- `${formattedValue}:${(seconds < 10) ? '0' : ''}` +
2731
- String(seconds)
2732
- )
2733
- }
2734
-
2735
- return formattedValue
2736
- }}
2737
- },
2738
- parse: (value:Date|number|string):number => {
2739
- if (typeof value === 'number')
2740
- return value
2741
-
2742
- if (value instanceof Date)
2743
- return value.getTime() / 1000
2744
-
2745
- const parsedDate:number = Date.parse(value)
2746
- if (isNaN(parsedDate)) {
2747
- const parsedFloat:number = parseFloat(value.replace(
2748
- /^([0-9]{2}):([0-9]{2})(:([0-9]{2}(\.[0-9]+)?))?$/,
2749
- (
2750
- _match:string,
2751
- hours:string,
2752
- minutes:string,
2753
- secondsSuffix?:string,
2754
- seconds?:string,
2755
- _millisecondsSuffix?:string
2756
- ):string => {
2757
- const zeroDateTime = new Date(0)
2758
-
2759
- zeroDateTime.setHours(parseInt(hours))
2760
- zeroDateTime.setMinutes(parseInt(minutes))
2761
- if (secondsSuffix)
2762
- zeroDateTime.setSeconds(parseInt(seconds!))
2763
-
2764
- return String(zeroDateTime.getTime() / 1000)
2765
- }
2766
- ))
2767
-
2768
- if (isNaN(parsedFloat))
2769
- return 0
2770
-
2771
- return parsedFloat
2772
- }
2773
-
2774
- return parsedDate / 1000
2775
- },
2776
- type: 'time'
2777
- },
2778
-
2779
- float: {
2780
- format: {final: {transform: (
2781
- value:number,
2782
- configuration:DefaultProperties<number>,
2783
- transformer:InputDataTransformation
2784
- ):string =>
2785
- transformer.float.format ?
2786
- value === Infinity ?
2787
- 'Infinity' :
2788
- value === -Infinity ?
2789
- '- Infinity' :
2790
- (new Intl.NumberFormat(
2791
- GenericInput.locales,
2792
- transformer.float.format.final.options || {}
2793
- )).format(value) :
2794
- `${value}`
2795
- }},
2796
- parse: (
2797
- value:number|string, configuration:DefaultProperties<number>
2798
- ):number => {
2799
- if (typeof value === 'string')
2800
- value = parseFloat(
2801
- GenericInput.locales[0] === 'de-DE' ?
2802
- value.replace(/\./g, '').replace(/,/g, '.') :
2803
- value
2804
- )
2805
-
2806
- // Fix sign if possible.
2807
- if (
2808
- typeof value === 'number' &&
2809
- (
2810
- typeof configuration.minimum === 'number' &&
2811
- configuration.minimum >= 0 &&
2812
- value < 0 ||
2813
- typeof configuration.maximum === 'number' &&
2814
- configuration.maximum <= 0 &&
2815
- value > 0
2816
- )
2817
- )
2818
- value *= -1
2819
-
2820
- return value
2821
- },
2822
- type: 'text'
2823
- },
2824
- integer: {
2825
- format: {final: {transform: (
2826
- value:number,
2827
- configuration:DefaultProperties<number>,
2828
- transformer:InputDataTransformation
2829
- ):string => (
2830
- new Intl.NumberFormat(
2831
- GenericInput.locales,
2832
- {
2833
- maximumFractionDigits: 0,
2834
- ...(transformer.integer.format?.final.options ?? {})
2835
- }
2836
- )).format(value)
2837
- }},
2838
- parse: (
2839
- value:number|string, configuration:DefaultProperties<number>
2840
- ):number => {
2841
- if (typeof value === 'string')
2842
- value = parseInt(
2843
- GenericInput.locales[0] === 'de-DE' ?
2844
- value.replace(/[,.]/g, '') :
2845
- value
2846
- )
2847
-
2848
- // Fix sign if possible.
2849
- if (
2850
- typeof value === 'number' &&
2851
- (
2852
- typeof configuration.minimum === 'number' &&
2853
- configuration.minimum >= 0 &&
2854
- value < 0 ||
2855
- typeof configuration.maximum === 'number' &&
2856
- configuration.maximum <= 0 &&
2857
- value > 0
2858
- )
2859
- )
2860
- value *= -1
2861
-
2862
- return value
2863
- },
2864
- type: 'text'
2865
- },
2866
- number: {parse: (value:number|string):number =>
2867
- typeof value === 'number' ? value : parseInt(value)
2868
- }
2869
- }
2870
- // endregion
2871
- export default GenericInput
2872
- // region vim modline
2873
- // vim: set tabstop=4 shiftwidth=4 expandtab:
2874
- // vim: foldmethod=marker foldmarker=region,endregion:
2875
- // endregion