@startlibs/form 1.0.12 → 1.1.1

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 (92) hide show
  1. package/lib/AutoComplete.mjs +550 -0
  2. package/lib/AutoComplete.mjs.map +1 -0
  3. package/lib/Checkbox.mjs +76 -0
  4. package/lib/Checkbox.mjs.map +1 -0
  5. package/lib/Combobox.mjs +544 -0
  6. package/lib/Combobox.mjs.map +1 -0
  7. package/lib/ComboboxWithCustom.mjs +77 -0
  8. package/lib/ComboboxWithCustom.mjs.map +1 -0
  9. package/lib/DatePicker.mjs +625 -0
  10. package/lib/DatePicker.mjs.map +1 -0
  11. package/lib/EditableBox.mjs +496 -0
  12. package/lib/EditableBox.mjs.map +1 -0
  13. package/lib/Errors.mjs +176 -0
  14. package/lib/Errors.mjs.map +1 -0
  15. package/lib/ExpandableBox.mjs +209 -0
  16. package/lib/ExpandableBox.mjs.map +1 -0
  17. package/lib/Field.mjs +65 -0
  18. package/lib/Field.mjs.map +1 -0
  19. package/lib/FieldBox.mjs +80 -0
  20. package/lib/FieldBox.mjs.map +1 -0
  21. package/lib/Form.mjs +322 -0
  22. package/lib/Form.mjs.map +1 -0
  23. package/lib/FormValue.mjs +111 -0
  24. package/lib/FormValue.mjs.map +1 -0
  25. package/lib/InputboxGroup.mjs +104 -0
  26. package/lib/InputboxGroup.mjs.map +1 -0
  27. package/lib/Radiobox.mjs +67 -0
  28. package/lib/Radiobox.mjs.map +1 -0
  29. package/lib/RichText.mjs +634 -0
  30. package/lib/RichText.mjs.map +1 -0
  31. package/lib/TextInput.mjs +359 -0
  32. package/lib/TextInput.mjs.map +1 -0
  33. package/lib/alt/checkbox/SimpleCheckbox.mjs +82 -0
  34. package/lib/alt/checkbox/SimpleCheckbox.mjs.map +1 -0
  35. package/lib/alt/checkbox/ToggleCheckbox.mjs +68 -0
  36. package/lib/alt/checkbox/ToggleCheckbox.mjs.map +1 -0
  37. package/lib/alt/radiobox/CleanTabRadiobox.mjs +44 -0
  38. package/lib/alt/radiobox/CleanTabRadiobox.mjs.map +1 -0
  39. package/lib/alt/radiobox/IconRadioBox.mjs +52 -0
  40. package/lib/alt/radiobox/IconRadioBox.mjs.map +1 -0
  41. package/lib/alt/radiobox/SimpleRadiobox.mjs +61 -0
  42. package/lib/alt/radiobox/SimpleRadiobox.mjs.map +1 -0
  43. package/lib/alt/radiobox/TabRadiobox.mjs +54 -0
  44. package/lib/alt/radiobox/TabRadiobox.mjs.map +1 -0
  45. package/lib/enhanceInput.mjs +131 -0
  46. package/lib/enhanceInput.mjs.map +1 -0
  47. package/lib/index.cjs +5345 -0
  48. package/lib/index.cjs.map +1 -0
  49. package/lib/index.mjs +28 -0
  50. package/lib/index.mjs.map +1 -0
  51. package/lib/useConfirmDialog.mjs +117 -0
  52. package/lib/useConfirmDialog.mjs.map +1 -0
  53. package/lib/useDraftConfirmation.mjs +56 -0
  54. package/lib/useDraftConfirmation.mjs.map +1 -0
  55. package/lib/validation.mjs +91 -0
  56. package/lib/validation.mjs.map +1 -0
  57. package/lib/withFormImageInput.mjs +281 -0
  58. package/lib/withFormImageInput.mjs.map +1 -0
  59. package/package.json +46 -26
  60. package/src/AutoComplete.jsx +642 -0
  61. package/src/Checkbox.jsx +45 -0
  62. package/src/Combobox.jsx +606 -0
  63. package/src/ComboboxWithCustom.jsx +68 -0
  64. package/src/DatePicker.jsx +733 -0
  65. package/src/EditableBox.jsx +502 -0
  66. package/src/Errors.jsx +174 -0
  67. package/src/ExpandableBox.jsx +207 -0
  68. package/src/Field.jsx +76 -0
  69. package/src/FieldBox.jsx +136 -0
  70. package/src/Form.jsx +291 -0
  71. package/src/FormValue.jsx +67 -0
  72. package/src/IconRadioBoxOldHubt.jsx +93 -0
  73. package/src/InputboxGroup.jsx +121 -0
  74. package/src/Radiobox.jsx +33 -0
  75. package/src/RichText.jsx +812 -0
  76. package/src/TextInput.jsx +519 -0
  77. package/src/alt/checkbox/SimpleCheckbox.jsx +162 -0
  78. package/src/alt/checkbox/ToggleCheckbox.jsx +143 -0
  79. package/src/alt/radiobox/CleanTabRadiobox.jsx +46 -0
  80. package/src/alt/radiobox/IconRadioBox.jsx +93 -0
  81. package/src/alt/radiobox/SimpleRadiobox.jsx +153 -0
  82. package/src/alt/radiobox/TabRadiobox.jsx +73 -0
  83. package/src/enhanceInput.jsx +107 -0
  84. package/src/index.js +27 -0
  85. package/src/useConfirmDialog.jsx +105 -0
  86. package/src/useDraftConfirmation.jsx +60 -0
  87. package/src/validation.js +92 -0
  88. package/src/withFormImageInput.jsx +297 -0
  89. package/.babelrc +0 -28
  90. package/lib/index.js +0 -6258
  91. package/rollup.config.js +0 -43
  92. package/yarn-error.log +0 -1607
@@ -0,0 +1,642 @@
1
+ import React, {useEffect, useRef, useState} from 'react'
2
+ import ContentEditable from 'react-contenteditable'
3
+ import {useRefState, useToggle, useEnsureRef} from '@startlibs/core'
4
+ import {ContextMenu, Li, Loading, Icon} from '@startlibs/components'
5
+ import {Errors, useProvideErrors} from './Errors'
6
+ import {Field} from './Field'
7
+ import {useInput} from './enhanceInput'
8
+ import _ from 'lodash/fp'
9
+ import {callIfFunction, stopPropagation, constrainEvent, getColor, media, customTheme, _s} from '@startlibs/utils'
10
+ import styled, {css} from 'styled-components'
11
+ import {darken} from 'polished'
12
+
13
+ const ua = navigator.userAgent.toLowerCase();
14
+ const isSafari = ua.indexOf('safari') >= 0 && ua.indexOf('chrome') < 0
15
+
16
+ const InputLoading = styled(Loading)`
17
+ bottom: 8px;
18
+ right: 0.6rem;
19
+ left: auto;
20
+ top: 50%;
21
+ transform: translate(-50%);
22
+ `
23
+ const AutoCompleteInput = styled.div`
24
+ width: 100%;
25
+ font-size: 14px;
26
+ border: 1px solid ${getColor('gray210')};
27
+ border-radius: 8px;
28
+ color: ${getColor('gray60')};
29
+ cursor: text;
30
+ background: white;
31
+ position: relative;
32
+ outline: 0;
33
+ outline: none;
34
+ min-height: 3rem;
35
+ line-height: 28px;
36
+ padding: 0.25rem 0.7rem;
37
+ display: flex;
38
+ align-items: center;
39
+ flex-wrap: wrap;
40
+ ${props => props.focused && `
41
+ border-color: ${getColor('gray180')};
42
+ box-shadow: 0px 0px 0px 2px rgba(0,0,0,0.075);
43
+ `}
44
+ ${props => props.hasErrors && css`
45
+ border-color: ${getColor('alert')} !important;
46
+ `}
47
+ ${media.mobile`
48
+ padding: 0.25rem 1rem;
49
+ min-height: 42px;
50
+ `}
51
+ ${props => props.withDropdown && css`
52
+ padding-right: 3rem;
53
+ ${media.mobile`
54
+ padding-right: 4rem;
55
+ `}
56
+ `}
57
+ ${customTheme('TextInput')}
58
+ `
59
+
60
+ const AutoCompletePlaceholder = styled.span`
61
+ position: absolute;
62
+ top: 50%;
63
+ transform: translateY(-50%);
64
+ pointer-events: none;
65
+ color: rgba(0,0,0,0.3);
66
+ left: 0.7rem;
67
+ right: 0.7rem;
68
+ ${media.mobile`
69
+ left: 1rem;
70
+ right: 1rem;
71
+ `}
72
+ `
73
+
74
+ const AutoCompleteTextboxDiv = styled(ContentEditable)`
75
+ display: inline-block;
76
+ outline: 0;
77
+ outline: none;
78
+ padding: 0 1px;
79
+ line-height: 130%;
80
+ `
81
+ const AddedBox = styled.span`
82
+ background-color: rgb(230,230,230);
83
+ padding: 5px 2rem 5px 0.75rem;
84
+ border-radius: 12px;
85
+ font-size: 13px;
86
+ position: relative;
87
+ margin-left: -3px;
88
+ margin-right: 9px;
89
+ white-space: nowrap;
90
+ cursor: default;
91
+ line-height: normal;
92
+ max-width: 90%;
93
+ white-space: nowrap;
94
+ overflow: hidden;
95
+ text-overflow: ellipsis;
96
+ ${isSafari ? `
97
+ vertical-align: 2px;
98
+ `:`
99
+ vertical-align: -7px;
100
+ `}
101
+ `
102
+ const RemoveIcon = styled(Icon)`
103
+ width: 20px;
104
+ height: 20px;
105
+ border-radius: 50%;
106
+ display: inline-block;
107
+ position: absolute;
108
+ top: 50%;
109
+ line-height: 20px;
110
+ transform: translateY(-50%);
111
+ right: 2px;
112
+ font-size: 14px;
113
+ text-align: center;
114
+ cursor: pointer;
115
+ color: ${getColor('gray120')};
116
+ :hover {
117
+ background-color: ${getColor('gray210')};
118
+ color: ${props => darken(0.05, getColor('alert')(props))};
119
+ }
120
+ `
121
+
122
+ const AutoCompleteMenu = styled(ContextMenu)`
123
+ box-shadow: 0 2px 3px 1px rgba(0,0,0,0.2),0 0px 0px 1px rgba(0,0,0,0.05);
124
+ max-height: 22rem;
125
+ overflow: auto;
126
+ ${ContextMenu.ListItem} {
127
+ > * {
128
+ padding: 11px;
129
+ }
130
+ }
131
+ `
132
+
133
+ const AutoCompleteError = styled(Errors)`
134
+ ${Errors.Item} {
135
+ background: transparent;
136
+ border: none;
137
+ color: ${getColor('alert')};
138
+ padding: 0;
139
+ margin-top: 0.5rem;
140
+ font-weight: 600;
141
+ ${Errors.CloseIcon} {
142
+ display: none;
143
+ }
144
+ }
145
+ `
146
+
147
+ const DropdownIconContainer = styled(Icon)`
148
+ position: absolute;
149
+ right: 0;
150
+ bottom: 0;
151
+ width: 3rem;
152
+ height: 3rem;
153
+ bottom: -1px;
154
+ ${media.mobile`
155
+ height: 42px;
156
+ `}
157
+ ${Icon} {
158
+ position: absolute;
159
+ width: 2rem;
160
+ height: 2rem;
161
+ line-height: 2rem;
162
+ top: 50%;
163
+ left: 50%;
164
+ transform: translate(-50%,-50%);
165
+ background: rgba(0,0,0,0.1);
166
+ border-radius: 50%;
167
+ text-align: center;
168
+ cursor: pointer;
169
+ color: ${getColor('gray90')};
170
+ :hover, :active {
171
+ background: rgba(0,0,0,0.15);
172
+ }
173
+ }
174
+ `
175
+
176
+ const focusBeforeLast = (e) => {
177
+ if (e.target && e.target !== e.currentTarget) return
178
+ const el = [].slice.call(e.currentTarget.querySelectorAll('div[contenteditable]')).slice(-1)[0]
179
+ if (!el) return
180
+ el.focus()
181
+ if (!el.firstChild) {
182
+ const node = document.createTextNode('')
183
+ el.appendChild(node)
184
+ }
185
+ setSelectionRange(el.firstChild, el.textContent.length + 1)
186
+ }
187
+
188
+ const setSelectionRange = (input, selectionStart, selectionEnd = 0) => {
189
+ let range = document.createRange()
190
+ let sel = window.getSelection()
191
+ range.setStart(input, Math.max(selectionStart - 1, 0))
192
+ sel.removeAllRanges()
193
+ sel.addRange(range)
194
+ }
195
+
196
+ export const AutoComplete =
197
+ React.forwardRef(({
198
+ path,
199
+ AutoCompleteLi = Li,
200
+ CompletedBox = ({label, remove, value}) => <AddedBox title={label}>
201
+ {label}
202
+ <RemoveIcon icon="x" onMouseUp={constrainEvent(remove)}/>
203
+ </AddedBox>,
204
+ EditableInput = (props) => <AutoCompleteTextboxDiv {...props} />,
205
+ getLabel = (v) => v,
206
+ minQueryLength = 3,
207
+ ignoreCase = true,
208
+ autoSelect,
209
+ autoSelectEquals,
210
+ fetchLimit = 6,
211
+ deleteOnBackspace = true,
212
+ fetchSuggestions: rawFetchSuggestions,
213
+ validation = (value) => ({success: value.trim()}),
214
+ transform,
215
+ withDropdown,
216
+ autoFocus,
217
+ clickOpensWhenEmpty,
218
+ tabIndex=1,
219
+ confirmIfNewKeys = [],
220
+ confirmKeys = ['Enter', ','],
221
+ confirmIfValueKeys = ['Tab'],
222
+ placeholder,
223
+ className,
224
+ getListItem = getLabel,
225
+ onAddValue,
226
+ getUniqueKey,
227
+ translatable = true,
228
+ withoutField, fieldClassName, bellowDescText, mandatory, descText, helpText, label,
229
+ value,setValue,raw
230
+ },outerRef) => {
231
+ const autoCompleteBox = useToggle()
232
+ const hoverIndex = useToggle()
233
+ const isLoading = useToggle()
234
+ const cancelEditable = useRef()
235
+
236
+ const [formValue, _setFormValue,{hasErrors}] = useInput({path,transform, value, setValue}, raw)
237
+ const formValueRef = useRef(formValue)
238
+ formValueRef.current = formValue
239
+ const [localFormValue,setLocalFormValue] = useState(_.last(formValue) === "" ? formValue : formValue.concat(""))
240
+ const setFormValue = (updater) => {
241
+ _setFormValue(updater)
242
+ setLocalFormValue(updater)
243
+ formValueRef.current = typeof updater === 'function' ? updater(formValueRef.current) : updater
244
+ }
245
+ const lastIsEditable = _.isEqual(localFormValue,formValue)
246
+
247
+ const [ErrorProvider, errorUtils] = useProvideErrors()
248
+ const hooksRef = useEnsureRef(outerRef)
249
+ const ref = useRef()
250
+ const queries = useRefState([])
251
+
252
+ const suggestionsFetchCache = useRefState({})
253
+ const suggestionsLabelMapCache = useRefState({})
254
+ const lastSuggestionQuery = useRefState('')
255
+
256
+ const [autoCompleteList, setAutoCompleteList] = useState([])
257
+
258
+ const getValue = () => formValueRef.current || []
259
+
260
+ const shouldFocusInput = useToggle()
261
+ const recentlyAdded = useRefState(false)
262
+
263
+ useEffect(() => {
264
+ if (autoFocus) {
265
+ focusInput()
266
+ }
267
+ },[])
268
+
269
+ const focusInput = () => shouldFocusInput.openWith(i => i+1)
270
+ useEffect(() => {
271
+ if (shouldFocusInput.isOpen)
272
+ focusBeforeLast({currentTarget: ref.current,target:ref.current})
273
+ },[shouldFocusInput.isOpen])
274
+
275
+ const editableIsEmpty = () => !lastIsEditable || (lastIsEditable && !formValue[formValue.length - 1])
276
+
277
+ const ignoreCaseIfNeeded = (s) => ignoreCase ? (s || '').toLowerCase() : s
278
+
279
+ const refreshSuggestions = (value = '', justFetch, dropdownFetch) => {
280
+ if ((value.length >= minQueryLength || dropdownFetch) && rawFetchSuggestions) {
281
+ queries.set([value])
282
+ const removeEditable = (list) => lastIsEditable ? list.slice(0, -1) : list
283
+ return isLoading.wait(Promise.resolve(fetchSuggestions(ignoreCaseIfNeeded(value))).then(list => {
284
+ const noRepeatedList = list.filter(v => !removeEditable(getValue()).find(_ => getLabel(v) === getLabel(_)))
285
+ if (justFetch) {
286
+ return noRepeatedList
287
+ }
288
+ const index = queries.get().indexOf(value)
289
+ if (index >= 0) {
290
+ queries.set(q => q.slice(index + 1))
291
+ if (!autoSelect && !(autoSelectEquals && noRepeatedList.slice(0,1).find(v => ignoreCaseIfNeeded(getLabel(v)) === ignoreCaseIfNeeded(value)))) {
292
+ hoverIndex.openWith(-1)
293
+ } else {
294
+ hoverIndex.openWith(0)
295
+ }
296
+ autoCompleteBox.open()
297
+ setAutoCompleteList(noRepeatedList)
298
+ }
299
+ }))
300
+ } else {
301
+ hoverIndex.openWith(-1)
302
+ setAutoCompleteList([])
303
+ return Promise.resolve([])
304
+ }
305
+ }
306
+
307
+ const fetchSuggestions = (s) => {
308
+ const prevQuery = lastSuggestionQuery.get()
309
+ lastSuggestionQuery.set(s)
310
+ if (suggestionsFetchCache.get()[s]) {
311
+ return suggestionsFetchCache.get()[s]
312
+ } else {
313
+ const lastFetch = suggestionsFetchCache.get()[ignoreCaseIfNeeded(prevQuery)]
314
+ if (s.indexOf(ignoreCaseIfNeeded(prevQuery)) >= 0 && lastFetch && lastFetch.length < fetchLimit) {
315
+ return suggestionsFetchCache.get()[s] = lastFetch.map(getLabel).filter(_ => ignoreCaseIfNeeded(_).indexOf(ignoreCaseIfNeeded(s)) >= 0).map(_ => suggestionsLabelMapCache.get()()[ignoreCaseIfNeeded(_)])
316
+ } else {
317
+ const fetch = Promise.resolve(rawFetchSuggestions(s))
318
+ fetch.then(list => {
319
+ suggestionsFetchCache[ignoreCaseIfNeeded(s)] = list
320
+ list.forEach(suggestion =>
321
+ suggestionsLabelMapCache.get()[ignoreCaseIfNeeded(getLabel(suggestion))] = suggestionsLabelMapCache.get()[ignoreCaseIfNeeded(getLabel(suggestion))] || suggestion
322
+ )
323
+ })
324
+ return fetch
325
+ }
326
+ }
327
+ }
328
+ useEffect(() => {
329
+ if (!lastIsEditable) {
330
+ if (recentlyAdded.get() !== cancelEditable.current) {
331
+ setFormValue(_.without([cancelEditable.current], formValue).concat(""))
332
+ } else {
333
+ setLocalFormValue(_.last(formValue) === "" ? formValue : formValue.concat(""))
334
+ }
335
+ cancelEditable.current = null
336
+ }
337
+ }, [lastIsEditable]);
338
+
339
+ useEffect(() => {
340
+ const newValue = (formValue || [])
341
+ refreshSuggestions(lastIsEditable ? getValue().slice(-1)[0] : '')
342
+ if (recentlyAdded.get()) {
343
+ recentlyAdded.set(false)
344
+ callIfFunction(onAddValue,formValue,setFormValue)
345
+ }
346
+ }, [formValue])
347
+
348
+
349
+ const confirmIfNew = (index, newValue) => {
350
+ refreshSuggestions(newValue, true).then(list => {
351
+ if (!list.length) {
352
+ confirm(index, newValue)
353
+ } else {
354
+ onChange(index, newValue)
355
+ }
356
+ })
357
+ }
358
+
359
+ const confirm = (index, newValue) => {
360
+ if (autoCompleteList.length && hoverIndex.isOpen >= 0) {
361
+ addValue(autoCompleteList[hoverIndex.isOpen])
362
+ } else {
363
+ confirmValue(index, newValue)
364
+ }
365
+ }
366
+
367
+ const confirmValue = (index, newValue, fromBlur) => {
368
+ if (!newValue) { return }
369
+ const suggestedValue = suggestionsLabelMapCache[ignoreCaseIfNeeded(newValue || '').trim()]
370
+ if (suggestedValue) {
371
+ addValue(suggestedValue, fromBlur)
372
+ return
373
+ }
374
+ const {success, errors} = validation(newValue)
375
+ if (errors) {
376
+ errorUtils.setErrors({errors})
377
+ } else {
378
+ addValue(success, fromBlur)
379
+ }
380
+ }
381
+
382
+ const onChange = (index, value) => {
383
+ errorUtils.clearErrors()
384
+ setFormValue(_s.set([index], value))
385
+ }
386
+
387
+
388
+ const addValue = (value, fromBlur) => {
389
+ const index = Math.max(lastIsEditable ? getValue().length - 1 : getValue().length,0)
390
+ recentlyAdded.set(formValue?.[index] || "")
391
+ setFormValue(_.flow(_.set([index], value),_.concat(_,"")))
392
+ if (!fromBlur) {
393
+ focusInput()
394
+ }
395
+ }
396
+
397
+ const remove = (index) => {
398
+ setFormValue(_.flow(_s.unset(index),_.filter(_=>_!==undefined)))
399
+ focusInput()
400
+ }
401
+
402
+ const onBlur = (e) => {
403
+ autoCompleteBox.close()
404
+ const v = getValue().slice(-1)[0]
405
+ if (lastIsEditable) {
406
+ const {errors} = validation(v)
407
+ const firstIsEqual = autoCompleteList.slice(0,1).find((item) => ignoreCaseIfNeeded(getLabel(item)) === ignoreCaseIfNeeded(v))
408
+ if (v && autoSelect && (autoCompleteList.length === 1 || firstIsEqual) && errors) {
409
+ addValue(autoCompleteList[0], true)
410
+ } else if (autoSelectEquals && firstIsEqual) {
411
+ addValue(autoCompleteList[0], true)
412
+ } else {
413
+ confirmValue(getValue().length - 1, v, true)
414
+ }
415
+ } else if (autoCompleteList.length) {
416
+ setAutoCompleteList([])
417
+ }
418
+ }
419
+
420
+ React.useImperativeHandle(hooksRef, () => ({addValue,confirm,remove}), [formValue])
421
+
422
+ const validValue = formValue || ['']
423
+ const uniqueKeyLabel = {}
424
+ const _getUniqueKey = (value) => {
425
+ if (getUniqueKey) {
426
+ return getUniqueKey(value)
427
+ }
428
+ const label = getLabel(value)
429
+ const uid = uniqueKeyLabel[label] = (uniqueKeyLabel[label] || 0) + 1
430
+ return label + uid
431
+ }
432
+
433
+ const tryOpenSuggestions = (e) => {
434
+ if (autoCompleteBox.get()) {
435
+ stopPropagation(onBlur)(e)
436
+ } else {
437
+ errorUtils.clearErrors()
438
+ refreshSuggestions(lastIsEditable ? getValue().slice(-1)[0] : '',false,true)
439
+ focusInput()
440
+ }
441
+ }
442
+ const input = <>
443
+ <AutoCompleteInput
444
+ onKeyDown={(e) => e.key === ' ' && e.stopPropagation()}
445
+ className={className}
446
+ ref={ref}
447
+ onFocus={() => {
448
+ if (autoCompleteList.length) autoCompleteBox.open()
449
+ else if (withDropdown && editableIsEmpty()) refreshSuggestions(undefined, undefined, true)
450
+ }}
451
+ hasErrors={errorUtils.errors.length>0 || hasErrors}
452
+ onBlur={onBlur}
453
+ onClick={(e) => { focusBeforeLast(e); if (editableIsEmpty() && !isLoading.isOpen) {refreshSuggestions(undefined,undefined,true)}}}
454
+ focused={autoCompleteBox.isOpen}
455
+ withDropdown={withDropdown}
456
+ >
457
+ <span>{'\u200b'}</span>
458
+ {validValue.map((v, i) =>
459
+ <ValueItem
460
+ value={v}
461
+ index={i}
462
+ cancelEditable={(v) => {cancelEditable.current = v }}
463
+ isEditable={lastIsEditable && validValue.length===i+1}
464
+ confirmKeys={confirmKeys}
465
+ confirmIfValueKeys={confirmIfValueKeys}
466
+ deleteOnBackspace={deleteOnBackspace}
467
+ EditableInput={EditableInput}
468
+ key={lastIsEditable && validValue.length===i+1 ? '$$editable$$' : _getUniqueKey(v)}
469
+ onChange={onChange}
470
+ remove={remove}
471
+ confirm={confirm}
472
+ getLabel={getLabel}
473
+ tabIndex={tabIndex}
474
+ confirmIfNew={confirmIfNew}
475
+ confirmIfNewKeys={confirmIfNewKeys}
476
+ CompletedBox={CompletedBox}
477
+ />
478
+ )}
479
+ <span>{'\u200b'}</span>
480
+ {(!formValue || !formValue.join('')) && placeholder &&
481
+ <AutoCompletePlaceholder onClick={(e) => ref.current.click()}>{placeholder}</AutoCompletePlaceholder>}
482
+ {
483
+ withDropdown && !isLoading.isOpen &&
484
+ <DropdownIconContainer onClick={tryOpenSuggestions}>
485
+ <Icon icon="arrow-down"/>
486
+ </DropdownIconContainer>
487
+ }
488
+ </AutoCompleteInput>
489
+ {
490
+ autoCompleteBox.isOpen !== false && !errorUtils.errors.length && autoCompleteList.length > 0 &&
491
+ <AutoCompleteMenu
492
+ hoverIndex={hoverIndex.isOpen}
493
+ autoHover={autoSelect}
494
+ onClick={constrainEvent()}
495
+ onHover={hoverIndex.openWith}
496
+ my="top left"
497
+ at="bottom left"
498
+ fill
499
+ >
500
+ {autoCompleteList.map(value =>
501
+ <AutoCompleteLi
502
+ className={ !translatable ? 'notranslate' : null }
503
+ key={_getUniqueKey(value)}
504
+ label={getListItem(value)}
505
+ onMouseDown={e => e.preventDefault()}
506
+ onClick={() => addValue(value)}
507
+ />
508
+ )}
509
+ </AutoCompleteMenu>
510
+ }
511
+ <ErrorProvider value={errorUtils}>
512
+ <AutoCompleteError/>
513
+ </ErrorProvider>
514
+ </>
515
+ return withoutField
516
+ ? input
517
+ : <Field bellowDescText={bellowDescText} className={fieldClassName} value={formValue} mandatory={mandatory}
518
+ descText={descText} helpText={helpText} label={label} labelClick={focusInput}>
519
+ {isLoading.isOpen && <InputLoading absolute size={24}/>}
520
+ {input}
521
+ </Field>
522
+ }
523
+ )
524
+
525
+ AutoComplete.AddedBox = AddedBox
526
+ AutoComplete.RemoveIcon = RemoveIcon
527
+ AutoComplete.AutoCompleteTextboxDiv = AutoCompleteTextboxDiv
528
+
529
+ const cleanText = text => {
530
+ const clean = text.replace('\u200b', '').replace(/\n/g, ' ').replace(/\s+/g, ' ')
531
+ return clean.trim() ? clean : ''
532
+ }
533
+
534
+ const ValueItem = ({CompletedBox,isEditable,getLabel,cancelEditable,value,remove,index,...props}) => {
535
+ const isEditing = useToggle(isEditable)
536
+ const valueRef = useRef(value)
537
+ valueRef.current = value
538
+ useEffect(() => () => {
539
+ if (isEditing.isOpen) {
540
+ cancelEditable(valueRef.current)
541
+ }
542
+ },[])
543
+ return isEditing.isOpen
544
+ ? <AutoCompleteTextbox
545
+ {...props}
546
+ value={value}
547
+ index={index}
548
+ remove={remove}
549
+ />
550
+ : <CompletedBox
551
+ label={getLabel(value)}
552
+ value={value}
553
+ remove={() => remove(index)}
554
+ />
555
+ }
556
+
557
+ export class AutoCompleteTextbox extends React.Component {
558
+
559
+ state = {value:this.props.value}
560
+
561
+ deletePrev = () => this.props.index > 0 && this.props.remove(this.props.index - 1)
562
+
563
+ onKeyDown = (e) => {
564
+ const range = window.getSelection().getRangeAt(0)
565
+ if (this.props.confirmIfNewKeys.indexOf(e.key) >= 0) {
566
+ e.preventDefault()
567
+ const text = range.startContainer.textContent
568
+ this.props.confirmIfNew(
569
+ this.props.index,
570
+ cleanText((text.slice(0, range.startOffset) + '\uFFFD' + text.slice(range.startOffset)).replace('\uFFFD', e.key))
571
+ )
572
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
573
+ e.stopPropagation()
574
+ } else if (this.props.confirmKeys.indexOf(e.key) >= 0 || this.props.confirmIfValueKeys.indexOf(e.key) >= 0) {
575
+ if (this.props.confirmKeys.indexOf(e.key) >= 0) {
576
+ e.preventDefault()
577
+ }
578
+ if (this.props.value) {
579
+ e.preventDefault()
580
+ e.stopPropagation()
581
+ this.props.confirm(this.props.index, this.props.value)
582
+ }
583
+ } else if (e.key === 'Backspace' && this.props.deleteOnBackspace) {
584
+ if (e.target.textContent !== '\u200b' || !range.collapsed) {
585
+ if (range.startContainer.previousSibling) {
586
+ if (range.startContainer.previousSibling.textContent.length) {
587
+ return
588
+ }
589
+ } else if (range.startOffset > 0) {
590
+ return
591
+ }
592
+ }
593
+ e.preventDefault()
594
+ this.deletePrev()
595
+ }
596
+ }
597
+
598
+ onChange = (evt) => {
599
+ this.props.onChange(this.props.index, cleanText(evt.currentTarget.isContentEditable ? evt.currentTarget.innerText : evt.target.value))
600
+ }
601
+
602
+ shouldComponentUpdate(nextProps) {
603
+ const event = new CustomEvent('portalReposition')
604
+ setTimeout(() => window.dispatchEvent(event), 1)
605
+ if (this.el.innerHTML === '<br>' || !this.el.innerHTML) {
606
+ this.props.onChange(this.props.index, '')
607
+ }
608
+ return nextProps.value !== cleanText(this.el.innerText) || !!Object.keys(nextProps).find((k) => nextProps[k] !== this.props[k])
609
+ }
610
+
611
+ componentDidUpdate(oldProps) {
612
+ if (this.props.value.slice(-1) === ' ' && /\S/.test(this.el.textContent.slice(-1))) { // does not have blank space as last
613
+ this.el.innerHTML = this.props.value.replace(/\s/, '&nbsp;')
614
+ }
615
+ if (this.props.value !== oldProps.value && document.activeElement === this.el) {
616
+ setSelectionRange(this.el.firstChild || this.el, this.el.textContent.length + 1)
617
+ }
618
+ }
619
+
620
+ onPaste = (ev) => {
621
+ ev.preventDefault()
622
+ const text = ev.clipboardData.getData('text')
623
+ document.execCommand('insertText', false, text)
624
+ }
625
+
626
+ render() {
627
+ const {value, confirmKeys, index, EditableInput, confirmIfNewKeys, confirmIfNew, confirm, remove, ...rest} = this.props
628
+ const valueOrBlank = /*android fix, not needed? value || */ _.isString(value) && value ? value : '\u200b'
629
+
630
+ const props = {
631
+ innerRef: ref => this.el = ref,
632
+ onKeyDown: this.onKeyDown,
633
+ onPaste: this.onPaste,
634
+ value: value,
635
+ index: index,
636
+ html: valueOrBlank.replace(/\s$/, '&nbsp;'),
637
+ ...rest,
638
+ onChange: this.onChange,
639
+ }
640
+ return typeof EditableInput === 'function' ? EditableInput(props) : <EditableInput {...props} />
641
+ }
642
+ }
@@ -0,0 +1,45 @@
1
+ import React from 'react'
2
+ import _ from 'lodash/fp'
3
+ import styled from 'styled-components'
4
+ import {useInput} from './enhanceInput'
5
+ import {useToggle} from '@startlibs/core'
6
+ import {callIfFunction, isTouchDevice} from '@startlibs/utils'
7
+
8
+ const HiddenInput = styled.input`
9
+ opacity: 0 !important;
10
+ position: absolute;
11
+ pointer-events: none;
12
+ `
13
+
14
+ export const Checkbox = ({className, children, isChecked = _.identity, isList, onClick, disabled, inverted, path, tabIndex = 1, raw, ...rest}) => {
15
+ const focused = useToggle()
16
+ const [v, ,{name, toggle, toggleItem, hasErrors}] = useInput({ path, isChecked,...rest}, raw)
17
+ const testInvert = (v) => inverted ? !v : v
18
+ const value = isChecked(testInvert(isList ? v?.indexOf(rest.fieldValue) >= 0 : v))
19
+ const _onClick = (e) => {
20
+ callIfFunction(onClick, e)
21
+ if (!e.isDefaultPrevented()) {
22
+ if (isList) {
23
+ toggleItem()
24
+ } else {
25
+ toggle();
26
+ }
27
+ e.preventDefault();
28
+ }
29
+ }
30
+ return <label onClick={_onClick} data-drag="false" className={className}>
31
+ {
32
+ !isTouchDevice() && <HiddenInput
33
+ onFocus={focused.open}
34
+ onBlur={focused.close}
35
+ disabled={disabled}
36
+ type="checkbox"
37
+ name={name}
38
+ readOnly
39
+ checked={!!value}
40
+ tabIndex={tabIndex}
41
+ />
42
+ }
43
+ {children && children({disabled, checked: !!value, hasErrors, focused: focused.isOpen})}
44
+ </label>
45
+ }