@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,812 @@
1
+ import React, {useContext, useEffect, useRef, useState} from 'react'
2
+ import styled, {css} from 'styled-components'
3
+ import ContentEditable from 'react-contenteditable'
4
+ import {darken} from 'polished'
5
+ import _ from 'lodash/fp'
6
+ import {getColor, linkRegex, stopPropagation} from '@startlibs/utils'
7
+ import {StartlibsContext, useEnsureRef, useLazyConstant, useToggle, usePopupToggle, useRefState} from '@startlibs/core'
8
+ import {Errors} from './Errors'
9
+ import {Field} from './Field'
10
+ import {WithForm} from './Form'
11
+ import {TextInput} from './TextInput'
12
+ import {useInput} from './enhanceInput'
13
+ import {Button, Icon} from '@startlibs/components'
14
+
15
+ const InputIcon = styled(Icon)`
16
+ position: absolute;
17
+ outline: none;
18
+ bottom: 0;
19
+ font-size: 21px;
20
+ line-height: 3rem;
21
+ right: 0;
22
+ width: 2rem;
23
+ pointer-events: none;
24
+ color: ${getColor('gray150')};
25
+ `
26
+
27
+ const FORMAT_TOOLS = {
28
+ 'bold': ['b', 'strong'],
29
+ 'italic': 'i',
30
+ 'underline': 'u',
31
+ 'strikethrough': 'strike',
32
+ 'link': 'a',
33
+ 'insertUnorderedList': ['ul', 'li'],
34
+ 'insertOrderedList': ['ol', 'li'],
35
+ 'outdent': '',
36
+ 'indent': 'blockquote',
37
+ 'fontSizeUp': 'font',
38
+ 'fontSizeDown': 'font'
39
+ }
40
+
41
+ const TAG_TO_STYLE_PROPERTY = {
42
+ 'b': 'fontWeight',
43
+ 'strong': 'fontWeight',
44
+ 'u': 'textDecoration',
45
+ 'strike': 'textDecoration',
46
+ 'font': 'fontSize',
47
+ 'i': 'fontStyle'
48
+ }
49
+
50
+ let GET_LIMITS_FONT_SIZE_PX = () => {
51
+ const s = document.createElement('span')
52
+ s.style.fontSize = 'medium'
53
+ document.body.appendChild(s)
54
+ const fontSize = parseInt(getComputedStyle(s).fontSize)
55
+ document.body.removeChild(s)
56
+ const limits = [fontSize * 3 / 5, fontSize * 3]
57
+ GET_LIMITS_FONT_SIZE_PX = () => limits
58
+ return limits
59
+ }
60
+
61
+ const wrapWith = (el,wrapper) => {
62
+ el.parentNode.insertBefore(wrapper, el);
63
+ wrapper.appendChild(el)
64
+ }
65
+
66
+ const FIX_STYLE = {
67
+ 'fontWeight': (v, el) => {
68
+ const intFontWeight = parseInt(window.getComputedStyle(el).fontWeight)
69
+ return intFontWeight <= 400 ? 400 : 600
70
+ },
71
+ 'fontSize': (v, el) => {
72
+ const [min, max] = GET_LIMITS_FONT_SIZE_PX()
73
+ const intFontSize = parseInt(window.getComputedStyle(el).fontSize)
74
+ return Math.min(max, Math.max(min, intFontSize)) + 'px'
75
+ },
76
+ 'textDecoration': (v,el) => {
77
+ const values = v.split(' ').filter(v => v === 'underline' || v === 'line-through')
78
+ if (values.indexOf('underline')>=0) {
79
+ wrapWith(el,document.createElement('u'))
80
+ }
81
+ if (values.indexOf('line-through')>=0) {
82
+ wrapWith(el,document.createElement('strike'))
83
+ }
84
+ return ''
85
+ }
86
+ }
87
+
88
+ export const RichTextContainer = styled.div`
89
+ position:relative;
90
+ width: 100%;
91
+ border: 1px solid ${getColor('gray210')};
92
+ border-radius: 8px;
93
+ background: white;
94
+ ${props => props.hasErrors && `border-color: ${getColor(props)('alert')}`};
95
+ ${props => props.showToolbar && `
96
+ ${ToolsContainer} {
97
+ transition: 0.5s;
98
+ max-height: 7.5rem;
99
+ transition: 0.6s;
100
+ pointer-events: auto;
101
+ }
102
+ `};
103
+ ${props => props.focus && `
104
+ border-color: ${getColor('gray180')};
105
+ box-shadow: 0px 0px 0px 2px rgba(0,0,0,0.1);
106
+ `}
107
+ ${props => props.locked && css`
108
+ padding-right: 2.25rem;
109
+ background: rgba(0,0,0,0.075);
110
+ `}
111
+ ${props => props.linkColor && css`
112
+ a, a:visited, a:focus, a:active {
113
+ color: ${props.linkColor};
114
+ }
115
+ a:hover {
116
+ color: ${darken(0.05, props.linkColor)};
117
+ }
118
+ `}
119
+ ul {
120
+ list-style-type: disc;
121
+ }
122
+ ul, ol {
123
+ padding-inline-start: 25px;
124
+ margin-block-start: 0.5em;
125
+ margin-block-end: 0.5em;
126
+ }
127
+ `
128
+
129
+ export const RichTextInput = styled.div`
130
+ font-size: 14px;
131
+ color: ${getColor('gray60')};
132
+ background: white;
133
+ border-radius: 7px;
134
+ > div {
135
+ overflow: auto;
136
+ width: 100%;
137
+ outline: none;
138
+ min-height: ${props=>props.minHeight || 60}px;
139
+ padding: 0.7rem;
140
+ }
141
+ b, strong {
142
+ font-weight: 600;
143
+ }
144
+ a {
145
+ text-decoration: underline;
146
+ }
147
+ > div[contenteditable=true]:empty:before {
148
+ content: attr(placeholder);
149
+ color: rgba(0,0,0,0.3);
150
+ }
151
+ ${props => props.disabled && `
152
+ &[disabled] {
153
+ background: rgba(0, 0, 0, 0.075) !important;
154
+ opacity: 0.75 !important;
155
+ }
156
+ `}
157
+ `
158
+
159
+ const ToolsContainer = styled.div`
160
+ background: ${getColor('lightBluishGray')};
161
+ padding: 0 0.125rem;
162
+ border-top-left-radius: 7px;
163
+ border-top-right-radius: 7px;
164
+ transition: 0.225s;
165
+ max-height: 0;
166
+ pointer-events: none;
167
+ overflow: hidden;
168
+ display: flex;
169
+ align-items: center;
170
+ `
171
+
172
+ const ToolButton = styled(Button)`
173
+ min-height: 2rem;
174
+ min-width: 2rem;
175
+ background: transparent;
176
+ box-shadow: none;
177
+ margin: 0.25rem 0.125rem;
178
+ & ~ & {
179
+ margin-left: 0.125rem;
180
+ }
181
+ .icon {
182
+ font-size: 23px;
183
+ color: ${getColor('gray90')};
184
+ position: absolute;
185
+ top: 50%;
186
+ left: 50%;
187
+ -webkit-transform: translate(-50%,-50%);
188
+ transform: translate(-50%,-50%);
189
+ }
190
+ &:hover, &.activated:hover {
191
+ background: rgba(0,0,0,0.11);
192
+ }
193
+ &.activated {
194
+ background: rgba(0,0,0,0.09);
195
+ }
196
+ &:focus:after {
197
+ content: none;
198
+ }
199
+ `
200
+
201
+ const ToolSeparator = styled.span`
202
+ border-left: 1px solid rgba(0,0,0,0.1);
203
+ margin-left: 0.75rem;
204
+ margin-right: 0.5rem;
205
+ height: 1.5rem;
206
+ `
207
+
208
+ const JS_CLASS = "EDITOR--JS"
209
+
210
+ const getNextRegularCharIndex = (text, offset, direction) => {
211
+ const regex = /[A-Za-z0-9ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏàáâãäåæçèéêëìíîïÐÑÒÓÔÕÖØÙÚÛÜÝÞßðñòóôõöøùúûüýþÿ]/g
212
+ let cursor = offset
213
+ while ((text[cursor + direction] || '').match(regex)) {
214
+ cursor += direction
215
+ }
216
+ return cursor
217
+ }
218
+
219
+ const addHttp = (s) => {
220
+ s = s.replace(/^\s/, '')
221
+ if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) {
222
+ return s
223
+ } else {
224
+ return 'http://' + s
225
+ }
226
+ }
227
+
228
+ const getRightMostElement = (node) => {
229
+ if (node.childNodes.length) {
230
+ return getRightMostElement(node.childNodes[node.childNodes.length - 1])
231
+ } else {
232
+ return node
233
+ }
234
+ }
235
+
236
+ const setSelectionRange = (input, selectionStart, selectionEnd = 0) => {
237
+ if (!input) return
238
+ let range = document.createRange()
239
+ let sel = window.getSelection()
240
+ range.setStart(input, selectionStart)
241
+ if (selectionEnd) {
242
+ range.setEnd(input, selectionEnd)
243
+ } else {
244
+ range.collapse(true)
245
+ }
246
+ sel.removeAllRanges()
247
+ sel.addRange(range)
248
+ }
249
+
250
+ const cleanEditor = (editor, acceptedTags) => {
251
+ Array.prototype.slice.call(editor.querySelectorAll('.Apple-interchange-newline')).forEach(el => el.remove())
252
+ Array.prototype.slice.call(editor.querySelectorAll('iframe')).forEach(el => el.remove())
253
+
254
+ const acceptedStyles = acceptedTags.map(t => TAG_TO_STYLE_PROPERTY[t]).filter(_.identity)
255
+
256
+ const newSpace = document.createTextNode('\u00A0')
257
+ if (window.getSelection().rangeCount) {
258
+ const container = window.getSelection().getRangeAt(0).startContainer
259
+ if (container.nodeType === 3) {
260
+ container.parentNode.insertBefore(newSpace, container.nextSibling)
261
+ } else {
262
+ container.appendChild(newSpace)
263
+ }
264
+ if (newSpace.previousSibling && newSpace.previousSibling.textContent === '\u00A0') {
265
+ newSpace.previousSibling.remove()
266
+ }
267
+ setSelectionRange(newSpace, 0)
268
+ }
269
+
270
+ const clean = (children) => {
271
+ Array.prototype.slice.call(children).forEach(el => {
272
+ try {
273
+ if (el.nodeType === 3)
274
+ return
275
+ if (!el.tagName) {
276
+ el.remove()
277
+ return
278
+ }
279
+ let tagName = el.tagName.toLowerCase()
280
+ const style = getComputedStyle(el)
281
+ const rect = el.getBoundingClientRect()
282
+ const isHidden = style.display === 'none' || rect.width === 0 || rect.height === 0
283
+ if (isHidden && (el.children && !el.children.length) && tagName !== 'br') {
284
+ el.remove()
285
+ } else if (acceptedTags.indexOf(tagName) >= 0) {
286
+ clean(el.childNodes)
287
+ el.removeAttribute('class')
288
+ if (tagName === 'a') {
289
+ el.setAttribute('target', '_blank')
290
+ }
291
+ if (el.getAttribute('style')) {
292
+ const usedStyles = _.toPairs(_.pick(acceptedStyles, el.style)).filter(([k, v]) => v !== "")
293
+ const fixedStyle = _.fromPairs(usedStyles.map(([k, v]) => [k, (FIX_STYLE[k] || _.identity)(v, el)]))
294
+ el.removeAttribute('style')
295
+ Object.assign(el.style, fixedStyle)
296
+ }
297
+ } else if (tagName === 'a') {
298
+ clean(el.childNodes)
299
+ if (el.textContent !== el.href || el.textContent.length < 5)
300
+ el.outerHTML = el.innerHTML
301
+ } else {
302
+ clean(el.childNodes)
303
+ if (el.hasChildNodes()) {
304
+ const newTagName = style.display === 'inline' ? 'span' : 'div'
305
+ const newEl = document.createElement(newTagName)
306
+ Array.prototype.slice.call(el.childNodes).forEach(child => newEl.appendChild(child))
307
+ el.parentNode.replaceChild(newEl, el)
308
+ } else {
309
+ el.remove()
310
+ }
311
+ }
312
+ } catch (e) {
313
+ if (el && el.remove)
314
+ el.remove()
315
+ }
316
+ })
317
+ }
318
+ clean(editor.childNodes)
319
+ }
320
+
321
+ const insertToken = (node, offset) => {
322
+ const parent = node.parentNode
323
+ const token = document.createElement("span")
324
+ if (node.nodeType === Node.TEXT_NODE) {
325
+ const splitted = node.splitText(offset)
326
+ parent.insertBefore(token, splitted)
327
+ } else {
328
+ parent.insertBefore(token, node)
329
+ }
330
+ return token
331
+ }
332
+
333
+ /*
334
+ * pagely reformat
335
+ .replace(/^[a-z]/, function (e) {
336
+ return e.toUpperCase()
337
+ })
338
+ * */
339
+
340
+ const parseAndExecCommand = (c) => {
341
+ if (c === "strikethrough" || c === "underline") {
342
+ document.execCommand('styleWithCSS', false, false)
343
+ } else {
344
+ document.execCommand('styleWithCSS', false, true)
345
+ }
346
+ if (c.indexOf('fontSize') === 0) {
347
+ const direction = c.indexOf('Up') > 0 ? 1 : -1
348
+ const newValue = Math.min(7, Math.max(1, parseInt(document.queryCommandValue('fontSize') || 3) + direction))
349
+ document.execCommand('fontSize', false, newValue)
350
+ } else {
351
+ const sanitizedCommand = (c.indexOf("List") >= 0 && document.queryCommandState(c)) ? 'outdent' : c // FIX CHROME HANGS when list is called and already active
352
+
353
+ document.execCommand(sanitizedCommand)
354
+ }
355
+ }
356
+
357
+ export const RichText = ({path, inputRef, label, autoHideToolbar, autoFocus, disabled, minHeight, placeholder, className, formatTools = ['bold', 'italic', 'underline', 'strikethrough', 'separator', 'link'], reformatText = _.identity, mandatory, descText, helpText, linkColor, tabIndex = 1, value, setValue,raw, locked}) => {
358
+
359
+ const linkPopup = usePopupToggle()
360
+
361
+ const input = useEnsureRef(inputRef)
362
+ const activeLinkNode = useRef()
363
+ const container = useRef()
364
+ const linkJustCreated = useRefState()
365
+
366
+ // auto-hide needs
367
+ const startSelectionToken = useRefState()
368
+ const endSelectionToken = useRefState()
369
+ const commands = useRefState([])
370
+ const containerFocused = useToggle()
371
+
372
+ const needsNormalize = useRefState()
373
+ const alreadyCancelled = useRefState()
374
+
375
+ // const [formValue, setFormValue] = useInput({path})
376
+ const [formValue, _setFormValue, {hasErrors}] = useInput({path,value,setValue},raw)
377
+
378
+ // const tempValue = useConstant(formValue)
379
+ const tempValue = useRef(formValue)
380
+ const setFormValue = (v) => {
381
+ tempValue.current = v
382
+ _setFormValue(v)
383
+ }
384
+
385
+ useEffect(() => {
386
+ if((value === formValue) && (formValue !== tempValue)){
387
+ tempValue.current = formValue
388
+ }
389
+ },[path])
390
+
391
+ const [activatedTools, setActivatedTools] = useState({})
392
+
393
+ const validElements = useLazyConstant(() => ['br', 'div', 'p', 'span'].concat(_.flatten(formatTools.map(tool => FORMAT_TOOLS[tool] || []))))
394
+
395
+ useEffect(() => {
396
+ cleanEditor(input.current, validElements)
397
+ }, [])
398
+
399
+ const focus = () => input.current && input.current.focus()
400
+
401
+ // Foco inicial opcional (ex.: abrir a dialog já no campo de descrição). Em
402
+ // rAF pra rodar depois que a dialog termina de montar e não perder o foco.
403
+ useEffect(() => {
404
+ if (autoFocus) {
405
+ requestAnimationFrame(focus)
406
+ }
407
+ }, [])
408
+
409
+ useEffect(() => {
410
+ if (needsNormalize.get()) {
411
+ input.current.normalize()
412
+ needsNormalize.set(false)
413
+ }
414
+ })
415
+
416
+ const triggerScrollForPanel = (d) => {
417
+ requestAnimationFrame((time) => {
418
+ if (time > d + 250) return
419
+ window.dispatchEvent(new Event('scroll'))
420
+ triggerScrollForPanel(d)
421
+ }
422
+ )
423
+ }
424
+
425
+ const containerFocus = () => {
426
+ containerFocused.openWith(2)
427
+ triggerScrollForPanel(Date.now())
428
+ }
429
+
430
+ const containerBlur = (e) => {
431
+ if (containerFocused.isOpen !== 1) {
432
+ containerFocused.close()
433
+ triggerScrollForPanel(Date.now())
434
+ }
435
+ }
436
+
437
+
438
+ const onBlur = (e) => {
439
+ const newValue = input.current.textContent.length === 0 ? '' : input.current.innerHTML
440
+ // Só reescreve se o conteúdo mudou de fato: focar/desfocar sem editar não pode
441
+ // marcar o form como alterado (senão dispara a confirmação de descarte à toa).
442
+ // textContent vazio equivale a valor ausente/'' do form.
443
+ if (newValue !== (formValue || '')) {
444
+ setFormValue(newValue)
445
+ }
446
+ }
447
+
448
+ const keydownHandler = (e) => {
449
+ if (containerFocused.isOpen === 2){
450
+ if (formatTools.indexOf('link') < 0) {
451
+ return
452
+ }
453
+ if((e.key === "k" && e.ctrlKey) || (e.key === "k" && e.metaKey)){
454
+ e.preventDefault();
455
+ execCommand(null,'link')
456
+ }
457
+ }
458
+ }
459
+
460
+ useEffect(() => {
461
+ document.addEventListener('keydown',keydownHandler);
462
+ return () => document.removeEventListener('keydown',keydownHandler);
463
+ }, [containerFocused.isOpen])
464
+
465
+ const execCommand = (e, command) => {
466
+ if (command === 'link') {
467
+ let nodeEl
468
+ if (!isSelectionInsideRichTextEditor()) {
469
+ let anchor = document.createElement('a')
470
+ anchor.innerHTML = '&#8203;'
471
+ input.current.appendChild(anchor)
472
+ nodeEl = anchor
473
+ } else {
474
+ const range = window.getSelection().getRangeAt(0)
475
+ const selectedNode = range.startContainer
476
+ if (range.collapsed) {
477
+ const start = getNextRegularCharIndex(selectedNode.textContent, range.startOffset, -1)
478
+ const end = getNextRegularCharIndex(selectedNode.textContent, range.startOffset - 1, 1)
479
+ if (start < range.startOffset && end > range.startOffset - 1) {
480
+ range.setStart(selectedNode, start)
481
+ range.setEnd(selectedNode, end + 1)
482
+ }
483
+ }
484
+ const selectedEl = selectedNode.nodeType === Node.TEXT_NODE ? selectedNode.parentNode : selectedNode
485
+ const parent = selectedEl.closest('a,.'+JS_CLASS)
486
+ if (parent && parent.matches('a')) {
487
+ nodeEl = parent
488
+ } else {
489
+ let anchor = document.createElement('a')
490
+ anchor.innerHTML = '&#8203;'
491
+ if (range.collapsed) {
492
+ range.insertNode(anchor)
493
+ } else {
494
+ if (range.commonAncestorContainer === input.current) {
495
+ range.setStart(range.endContainer,0)
496
+ }
497
+ document.execCommand('createLink', false, '_')
498
+ anchor = window.getSelection().anchorNode
499
+ }
500
+ const parent = range.commonAncestorContainer.nodeType === 3 ? range.commonAncestorContainer.parentNode : range.commonAncestorContainer
501
+ const links = parent.querySelectorAll("a[href='_']")
502
+ Array.from(links).slice(0,-1).forEach(removeLinkFromNode)
503
+ nodeEl = _.last(links)
504
+ nodeEl.setAttribute('href', ' ')
505
+ }
506
+ }
507
+ tempValue.current = input.current.innerHTML
508
+ openDialog(nodeEl)
509
+ } else {
510
+ if (!isSelectionInsideRichTextEditor()) {
511
+ input.current.focus()
512
+ const last = document.createTreeWalker(input.current, NodeFilter.SHOW_TEXT).lastChild()
513
+ const range = document.createRange()
514
+ range.selectNodeContents(last || input.current)
515
+ range.collapse(false)
516
+ const sel = window.getSelection()
517
+ sel.removeAllRanges()
518
+ sel.addRange(range)
519
+ }
520
+ document.execCommand(command)
521
+ setFormValue(input.current.innerHTML)
522
+ toggleButtonsState()
523
+ focus()
524
+ }
525
+ }
526
+
527
+ const isSelectionInsideRichTextEditor = () => {
528
+ const range = window.getSelection().getRangeAt(0)
529
+ if (!range) return false
530
+ const el = range.startContainer
531
+ if (!el) return false
532
+ const node = el.closest ? el : (el.parentNode || document.body)
533
+ return node.closest('.'+JS_CLASS) === input.current
534
+ }
535
+
536
+
537
+ const saveRichTextLink = ({linkUrl, linkText}) => {
538
+ const currentActiveLinkNode = activeLinkNode.current
539
+ currentActiveLinkNode.setAttribute('href', addHttp(linkUrl))
540
+ currentActiveLinkNode.setAttribute('target', '_blank')
541
+ if (currentActiveLinkNode.textContent !== linkText) {
542
+ currentActiveLinkNode.innerText = linkText
543
+ }
544
+ linkPopup.close()
545
+ setSelectionRange(currentActiveLinkNode.lastChild, currentActiveLinkNode.textContent.length, 0)
546
+ focus()
547
+ setFormValue(input.current.innerHTML)
548
+ }
549
+
550
+ const cancelRichTextLink = (e) => {
551
+ if (alreadyCancelled.get()) return
552
+ const currentActiveLinkNode = activeLinkNode.current
553
+ const span = document.createElement('span')
554
+ if (currentActiveLinkNode.nextSibling)
555
+ (currentActiveLinkNode.parentNode).insertBefore(span, currentActiveLinkNode.nextSibling)
556
+ else
557
+ (currentActiveLinkNode.parentNode).append(span)
558
+ setSelectionRange(span, 0, 0)
559
+ focus()
560
+ if (currentActiveLinkNode.getAttribute('href') === ' ') {
561
+ removeLink(e)
562
+ }
563
+ alreadyCancelled.set(true)
564
+ span.remove()
565
+ if (input.current) {
566
+ input.current.normalize()
567
+ } else {
568
+ needsNormalize.set(true)
569
+ }
570
+ linkPopup.close()
571
+ }
572
+
573
+ const handleKeyUp = (e) => {
574
+ toggleButtonsState()
575
+ setFormValue(reformatText(input.current.innerHTML))
576
+ }
577
+
578
+ const handleKeyDown = (e) => {
579
+ /*if (linkJustCreated.current && e.which === 8) {
580
+ linkJustCreated.current.outerHTML = linkJustCreated.current.innerHTML
581
+ input.current.normalize()
582
+ return
583
+ }
584
+ commands.set([])
585
+ linkJustCreated.set(null)
586
+ const selectedRange = window.getSelection().getRangeAt(0)
587
+ if (!selectedRange) return
588
+ const possibleTextNode = selectedRange.startContainer
589
+ const node = possibleTextNode.nodeType === Node.TEXT_NODE ? possibleTextNode.parentNode : possibleTextNode
590
+ const parent = node.closest('a,.'+JS_CLASS)
591
+ if ((e.key === 'Enter' || e.which === 32) && parent && parent.matches('.'+JS_CLASS)) {
592
+ const range = document.createRange()
593
+ range.setEnd(selectedRange.startContainer, selectedRange.startOffset)
594
+ range.setStart(selectedRange.startContainer, 0)
595
+ traverseAndAddLink(range)
596
+ }*/
597
+ }
598
+
599
+ const traverseAndAddLink = (range) => {
600
+ const match = range.toString().match(/[\s\t\n\r]([^\s\t\n\r]*)$/)
601
+ if (match) {
602
+ if ((/^(https?:\/\/|www\.)/).test(match[1]) && linkRegex.test(match[1])) {
603
+ range.setStart(range.startContainer, (match).index + 1);
604
+ (range.endContainer).splitText(range.endContainer.textContent.length - 1)
605
+ const newNode = document.createElement('a')
606
+ newNode.href = match[1]
607
+ newNode.appendChild(range.extractContents())
608
+ range.insertNode(newNode)
609
+ window.getSelection().getRangeAt(0).setEndAfter(newNode)
610
+ window.getSelection().getRangeAt(0).setStartAfter(newNode)
611
+ linkJustCreated.set(newNode)
612
+ }
613
+ } else {
614
+ const previous = getPreviousElement(range.startContainer)
615
+ if (previous) {
616
+ range.setStart(previous, 0)
617
+ traverseAndAddLink(range)
618
+ }
619
+ }
620
+ }
621
+
622
+ const getPreviousElement = (node) => {
623
+ if (node.previousSibling) {
624
+ const rightMost = getRightMostElement(node.previousSibling)
625
+ if (rightMost.nodeType === Node.TEXT_NODE) {
626
+ return rightMost
627
+ } else {
628
+ return getPreviousElement(rightMost)
629
+ }
630
+ } else if (node.parentNode && node !== input.current) {
631
+ return getPreviousElement(node.parentNode)
632
+ } else {
633
+ return null
634
+ }
635
+ }
636
+
637
+ const handlePasteDrop = (e) => setTimeout(() => pasted(e), 1)
638
+
639
+ const handleClick = (e) => {
640
+ toggleButtonsState()
641
+ const el = e.target.closest('a,.'+JS_CLASS)
642
+ if (el && el.matches('a') && el.getAttribute('href')) {
643
+ openDialog(el)
644
+ }
645
+ }
646
+
647
+ const openDialog = (el) => {
648
+ activeLinkNode.current = el
649
+ alreadyCancelled.set(false)
650
+ linkPopup.openWith({linkText: el.textContent, linkUrl: (el.getAttribute('href') || '').trim()})
651
+ }
652
+
653
+
654
+ const removeLinkFromNode = (node) => {
655
+ node.outerHTML = node.innerHTML === '\u200b' ? '' : node.innerHTML
656
+ }
657
+
658
+ const removeLink = (e) => {
659
+ if (e) e.preventDefault()
660
+ alreadyCancelled.set(true)
661
+ removeLinkFromNode(activeLinkNode.current)
662
+ linkPopup.close()
663
+ if (input.current) {
664
+ setFormValue(input.current.innerHTML)
665
+ input.current.normalize()
666
+ }
667
+ }
668
+
669
+ const pasted = (e) => {
670
+ cleanEditor(input.current, validElements)
671
+ }
672
+
673
+ const toggleButtonsState = () => {
674
+ return setActivatedTools(_.zipObject(formatTools,formatTools.map((t)=>window.document.queryCommandState(t))))
675
+ }
676
+
677
+ const toolButton = (tool) => <ToolButton
678
+ key={tool}
679
+ tabIndex={-1}
680
+ onClick={(e) => execCommand(e, tool)}
681
+ className={'button exec-command rich-text-button only-icon ' + (activatedTools[tool] ? 'activated' : '')}
682
+ type="button"
683
+ >
684
+ <span className={'icon icon-' + tool}/>
685
+ </ToolButton>
686
+
687
+ const richTextElement = <RichTextContainer
688
+ linkColor={linkColor}
689
+ ref={container}
690
+ onFocus={containerFocus}
691
+ onBlur={containerBlur}
692
+ focus={containerFocused.isOpen}
693
+ showToolbar={!disabled & !autoHideToolbar || containerFocused.isOpen}
694
+ className={className}
695
+ >
696
+ <ToolsContainer className="rich-text-tools">
697
+ {formatTools.map((t, i) => t === 'separator' ? <ToolSeparator className="separator"
698
+ key={"separator-" + i}/> : toolButton(t))}
699
+ </ToolsContainer>
700
+ <RichTextInput>
701
+ <ContentEditable
702
+ className={JS_CLASS}
703
+ innerRef={input}
704
+ html={tempValue.current || ""}
705
+ onBlur={onBlur}
706
+ onClick={handleClick}
707
+ onKeyUp={handleKeyUp}
708
+ onKeyDown={handleKeyDown}
709
+ onPaste={handlePasteDrop}
710
+ onDrop={handlePasteDrop}
711
+ tabIndex={tabIndex}
712
+ disabled={disabled}
713
+ minHeight={minHeight}
714
+ placeholder={placeholder}
715
+ />
716
+ </RichTextInput>
717
+ {locked && <InputIcon icon="lock"/>}
718
+ {
719
+ linkPopup.isOpen &&
720
+ <EditLinkPopup
721
+ input={input}
722
+ activeLinkNode={activeLinkNode}
723
+ cancel={cancelRichTextLink}
724
+ values={linkPopup.isOpen}
725
+ removeLink={removeLink}
726
+ saveLink={saveRichTextLink}
727
+ />
728
+ }
729
+ </RichTextContainer>
730
+ return (label
731
+ ? <Field labelClick={focus} descText={descText} helpText={helpText} label={label} mandatory={mandatory}>
732
+ {richTextElement}
733
+ </Field>
734
+ : richTextElement
735
+ )
736
+ }
737
+
738
+ const EditLinkPopup = ({cancel, removeLink, activeLinkNode, input, saveLink, values}) => {
739
+ const linkPopup = useRef()
740
+ const formRef = useRef()
741
+ const linkTextRef = useRef()
742
+ const linkUrlRef = useRef()
743
+ const LABELS = useContext(StartlibsContext).labels.RichText
744
+
745
+
746
+ const preValidation = (formValues) => {
747
+ let errors
748
+ if (!formValues.linkText) {
749
+ errors = {'linkText': [LABELS.linkTextError]}
750
+ }
751
+ if (!formValues.linkUrl || formValues.linkUrl === ' ' || !linkRegex.test(formValues.linkUrl.trim())) {
752
+ errors = {...errors, 'linkUrl': [LABELS.linkUrlError]}
753
+ }
754
+ return errors
755
+ }
756
+
757
+ const checkEscOrEnter = (e) => {
758
+ if (e.keyCode === 27) {
759
+ e.stopPropagation()
760
+ cancel(e)
761
+ } else if (e.keyCode === 13) {
762
+ e.preventDefault()
763
+ e.stopPropagation()
764
+ formRef.current.submitForm()
765
+ }
766
+ }
767
+
768
+ useEffect(() => {
769
+ const currentActiveLinkNode = activeLinkNode.current
770
+ const currentLinkPopup = linkPopup.current
771
+ const offsetLeft = currentActiveLinkNode.getClientRects().length > 1 ? input.offsetLeft : currentActiveLinkNode.offsetLeft
772
+ const left = ((currentActiveLinkNode.offsetWidth) / 2) - (currentLinkPopup.offsetWidth / 2) + offsetLeft
773
+ const top = (currentActiveLinkNode.offsetHeight) + currentActiveLinkNode.offsetTop
774
+ Object.assign(currentLinkPopup.style, {
775
+ 'position': 'absolute',
776
+ 'left': left + 'px',
777
+ 'top': top + 'px'
778
+ })
779
+ if (values.linkText) {
780
+ linkUrlRef.current.focus()
781
+ } else {
782
+ linkTextRef.current.focus()
783
+ }
784
+ return cancel
785
+ }, [])
786
+
787
+ return <WithForm skipDom ref={formRef} action={saveLink} values={values} preValidation={preValidation}>{form =>
788
+ <div onClick={stopPropagation()} ref={linkPopup} onKeyDown={checkEscOrEnter}
789
+ className="popup rich-text-link-editor ignore-link" contentEditable={false}>
790
+ <TextInput
791
+ ref={linkTextRef}
792
+ mandatory
793
+ path="linkText"
794
+ label={LABELS.popupLinkText}
795
+ />
796
+ <TextInput
797
+ ref={linkUrlRef}
798
+ mandatory
799
+ path="linkUrl"
800
+ label={LABELS.popupLinkUrl}
801
+ />
802
+ <div className="popup-footer">
803
+ <Button className="remove-link" icon="remove" onClick={removeLink}>
804
+ {LABELS.popupRemove}
805
+ </Button>
806
+ <Button onClick={cancel} tabIndex={0}>{LABELS.popupCancel}</Button>
807
+ <Button highlight onClick={form.willSubmitForm}>{LABELS.popupConfirm}</Button>
808
+ </div>
809
+ <Errors/>
810
+ </div>
811
+ }</WithForm>
812
+ }