@sikka/hawa 0.0.233 → 0.0.235

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 (49) hide show
  1. package/_config.yml +1 -0
  2. package/dist/styles.css +3 -3
  3. package/docs/CNAME +1 -0
  4. package/docs/README.md +60 -0
  5. package/docs/_layouts/default.html +47 -0
  6. package/docs/assets/css/style.css +366 -0
  7. package/docs/documentation/229.1b36ccba973b04d66b4c.manager.bundle.js +1 -0
  8. package/docs/documentation/229.f7a1c971.iframe.bundle.js +1 -0
  9. package/docs/documentation/295.67c251ec00675ab59b60.manager.bundle.js +1 -0
  10. package/docs/documentation/51.07b491d3.iframe.bundle.js +2 -0
  11. package/docs/documentation/51.07b491d3.iframe.bundle.js.LICENSE.txt +8 -0
  12. package/docs/documentation/51.e44cb3212565a342a42d.manager.bundle.js +2 -0
  13. package/docs/documentation/51.e44cb3212565a342a42d.manager.bundle.js.LICENSE.txt +8 -0
  14. package/docs/documentation/551.67cd309b0648b0a52636.manager.bundle.js +1 -0
  15. package/docs/documentation/551.c82ea8f1.iframe.bundle.js +1 -0
  16. package/docs/documentation/701.07623c34.iframe.bundle.js +1 -0
  17. package/docs/documentation/767.e90d0d608aa8557f855d.manager.bundle.js +2 -0
  18. package/docs/documentation/767.e90d0d608aa8557f855d.manager.bundle.js.LICENSE.txt +94 -0
  19. package/docs/documentation/807.1424ceed.iframe.bundle.js +2 -0
  20. package/docs/documentation/807.1424ceed.iframe.bundle.js.LICENSE.txt +31 -0
  21. package/docs/documentation/807.4e87168c6f719304d458.manager.bundle.js +2 -0
  22. package/docs/documentation/807.4e87168c6f719304d458.manager.bundle.js.LICENSE.txt +31 -0
  23. package/docs/documentation/897.386c170cbd1467abc7ca.manager.bundle.js +2 -0
  24. package/docs/documentation/897.386c170cbd1467abc7ca.manager.bundle.js.LICENSE.txt +12 -0
  25. package/docs/documentation/897.d9a35fd0.iframe.bundle.js +2 -0
  26. package/docs/documentation/897.d9a35fd0.iframe.bundle.js.LICENSE.txt +12 -0
  27. package/docs/documentation/901.ae81c179.iframe.bundle.js +2 -0
  28. package/docs/documentation/901.ae81c179.iframe.bundle.js.LICENSE.txt +105 -0
  29. package/docs/documentation/935.3a33e233.iframe.bundle.js +1 -0
  30. package/docs/documentation/935.df0c782bef63c348b9da.manager.bundle.js +1 -0
  31. package/docs/documentation/favicon.ico +0 -0
  32. package/docs/documentation/iframe.html +364 -0
  33. package/docs/documentation/index.html +59 -0
  34. package/docs/documentation/main.051275cac7b0dc69501c.manager.bundle.js +1 -0
  35. package/docs/documentation/main.fb64f936.iframe.bundle.js +1 -0
  36. package/docs/documentation/project.json +1 -0
  37. package/docs/documentation/runtime~main.a832da8f0c3235fa0d36.manager.bundle.js +1 -0
  38. package/docs/documentation/runtime~main.d6831407.iframe.bundle.js +1 -0
  39. package/es/elements/index.d.ts +0 -1
  40. package/es/index.es.js +2 -2
  41. package/lib/elements/index.d.ts +0 -1
  42. package/lib/index.js +2 -2
  43. package/package.json +2 -3
  44. package/src/elements/FloatingComment.tsx +711 -92
  45. package/src/elements/index.ts +0 -1
  46. package/src/styles.css +3 -3
  47. package/es/elements/FloatingCommentCE.d.ts +0 -6
  48. package/lib/elements/FloatingCommentCE.d.ts +0 -6
  49. package/src/elements/FloatingCommentCE.tsx +0 -466
@@ -1,12 +1,11 @@
1
1
  import React, { useRef, useState, useEffect } from "react"
2
- import { RichTextarea } from "rich-textarea"
3
2
  import clsx from "clsx"
4
3
 
5
4
  const Property = (props) => {
6
5
  return (
7
6
  <div
8
7
  className="border-box mr-[5px] flex h-[32px] w-[32px] items-center justify-center rounded bg-gray-400 p-2"
9
- onClick={props.onClick}
8
+ onMouseDown={props.onMouseDown}
10
9
  >
11
10
  {props.name}
12
11
  </div>
@@ -17,8 +16,6 @@ type ComponentTypes = {
17
16
  foo?: string
18
17
  }
19
18
 
20
- // TODO: Handle copy pasting by assigning to local storage ?
21
-
22
19
  const styleClasses = {
23
20
  bold: "font-bold",
24
21
  italic: "italic",
@@ -26,15 +23,118 @@ const styleClasses = {
26
23
  strike: "line-through",
27
24
  }
28
25
 
26
+ // FIXME: Highlighting a part of styled text with a bit on the left with an overall length not equal to clipboard copied text will result in paste issues
27
+
28
+ // FIXME: Highlighting the beginning characters of styled text and then pasting text sometimes doesn't register as right intersecting
29
+ // This expecially happens when the selection is for example, [0, 2] and the styling is [0, 3], this might be failure of addition which doesn't offset the styling
30
+
29
31
  export const FloatingComment: React.FunctionComponent<ComponentTypes> = (
30
32
  props
31
33
  ) => {
32
- const [text, setText] = useState({
34
+ const [text, _setText] = useState({
33
35
  content: "",
34
36
  stylings: [], // A styling is an object with 2 indices specifying a substring of text, and the applied effect
37
+ revert: [0, 0],
38
+ lastCopy: [],
39
+ pasted: { status: false, length: 0 },
35
40
  })
36
41
 
37
42
  const field = useRef(null)
43
+ const _text = useRef(text)
44
+ const setText = (data) => {
45
+ _text.current = data
46
+ _setText(data)
47
+ }
48
+
49
+ const getChildIndex = (child) => {
50
+ for (var i = 0; (child = child.previousSibling); i++);
51
+ return i
52
+ }
53
+
54
+ // Full reversion achieved !
55
+ const getFieldSelection = () => {
56
+ if (document.activeElement != field.current) return [0, 0]
57
+
58
+ let selection = window.getSelection()
59
+ let nodes = Array.from(field.current.childNodes)
60
+
61
+ nodes = nodes.filter(
62
+ (item: any) => !["#text", "BR"].includes(item.nodeName)
63
+ )
64
+
65
+ let startParent: any = selection.anchorNode.parentNode
66
+
67
+ let startNodeIndex =
68
+ startParent == field.current
69
+ ? nodes.length
70
+ : // : parseInt(startParent.dataset.childIndex)
71
+ getChildIndex(startParent)
72
+
73
+ let startPrecedingSum = nodes
74
+ .slice(0, startNodeIndex)
75
+ .map((span: any) => span.textContent.length)
76
+ .reduce((a, b) => a + b, 0)
77
+
78
+ let endParent: any = selection.focusNode.parentNode
79
+ let endNodeIndex =
80
+ endParent == field.current
81
+ ? nodes.length
82
+ : // : parseInt(endParent.dataset.childIndex)
83
+ getChildIndex(endParent)
84
+
85
+ let endPrecedingSum = nodes
86
+ .slice(0, endNodeIndex)
87
+ .map((span: any) => span.textContent.length)
88
+ .reduce((a, b) => a + b, 0)
89
+
90
+ let result = [
91
+ startPrecedingSum + selection.anchorOffset,
92
+ endPrecedingSum + selection.focusOffset,
93
+ ]
94
+
95
+ // Sort to make the minimum selection the start selection
96
+ return result.sort((a, b) => a - b)
97
+ }
98
+
99
+ useEffect(() => {
100
+ setTimeout(function () {
101
+ let [start, end] = _text.current.revert
102
+
103
+ if (start == 0 && end == 0) return
104
+
105
+ let startNode = null
106
+ let endNode = null
107
+
108
+ let total = 0
109
+ let nodes = Array.from(field.current.childNodes)
110
+
111
+ for (let i = 0; i < nodes.length; i++) {
112
+ let node: any = nodes[i]
113
+ let sum = node.textContent.length + total
114
+
115
+ if (startNode == null && start >= total && start <= sum) {
116
+ startNode = nodes[i]
117
+ start -= total
118
+ }
119
+
120
+ if (endNode == null && end > total && end <= sum) {
121
+ endNode = nodes[i]
122
+ end -= total
123
+ }
124
+
125
+ total += node.textContent.length
126
+ }
127
+
128
+ var range = document.createRange()
129
+ var sel = window.getSelection()
130
+
131
+ range.setStart(startNode.firstChild, start)
132
+ range.setEnd(endNode.firstChild, end)
133
+
134
+ sel.removeAllRanges()
135
+ sel.addRange(range)
136
+ }, 1)
137
+ }, [text.revert])
38
138
 
39
139
  // utility functions
40
140
  const getRange = (start, end) => {
@@ -156,9 +256,8 @@ export const FloatingComment: React.FunctionComponent<ComponentTypes> = (
156
256
  }
157
257
 
158
258
  const perform = (id) => {
159
- let stylings = text.stylings.slice()
160
- var selectionStart = field.current.selectionStart
161
- var selectionEnd = field.current.selectionEnd
259
+ let stylings = _text.current.stylings.slice()
260
+ let [selectionStart, selectionEnd] = getFieldSelection()
162
261
 
163
262
  if (selectionStart == selectionEnd) return
164
263
 
@@ -194,9 +293,40 @@ export const FloatingComment: React.FunctionComponent<ComponentTypes> = (
194
293
  setText({
195
294
  ...text,
196
295
  stylings: result,
296
+ revert: [selectionStart, selectionEnd],
197
297
  })
198
298
  }
199
299
 
300
+ const getRangeIntersectStylings = (start, end, startOffset, finishOffset) => {
301
+ // Get all stylings intersecting
302
+ let stylings = []
303
+ for (let i = start; i < end; i++) {
304
+ stylings.push(getIntersectStylings(i, startOffset, finishOffset))
305
+ }
306
+
307
+ // Remove duplicates
308
+ stylings = stylings.flat().filter((item) => item)
309
+ stylings = stylings.filter(
310
+ (item, index) =>
311
+ stylings.findIndex(
312
+ (_item) =>
313
+ _item.start == item.start &&
314
+ _item.finish == item.finish &&
315
+ _item.type == item.type
316
+ ) == index
317
+ )
318
+
319
+ return stylings
320
+ }
321
+
322
+ const compareStylings = (styling1, styling2) => {
323
+ return (
324
+ styling1.type == styling2.type &&
325
+ styling1.start == styling2.start &&
326
+ styling1.finish == styling2.finish
327
+ )
328
+ }
329
+
200
330
  // Get stylings encompassing an index within it's range
201
331
  const getIntersectStylings = (index, startOffset = 0, finishOffset = 0) => {
202
332
  // Find all stylings with encompassing range
@@ -225,140 +355,629 @@ export const FloatingComment: React.FunctionComponent<ComponentTypes> = (
225
355
  )
226
356
  }
227
357
 
228
- const onChange = (value) => {
229
- let difference = value.length - text.content.length
358
+ const handleScenario = (stylings, predicate) => {
359
+ let matches = stylings.filter(({ start: _start, finish: _finish }) =>
360
+ predicate(_start, _finish)
361
+ )
230
362
 
231
- let selection = field.current.selectionStart - difference
363
+ let indices = []
364
+
365
+ stylings.map((styling, index) => {
366
+ let result = matches.find((item) => compareStylings(styling, item))
367
+ if (!result) return
368
+ indices.push({
369
+ index: index,
370
+ type: styling.type,
371
+ start: styling.start,
372
+ finish: styling.finish,
373
+ })
374
+ })
375
+
376
+ return indices
377
+ }
232
378
 
233
- let stylings = text.stylings.slice()
234
- let succeeding = getSucceedStylings(selection)
379
+ const handleDeletion = (length, start, end) => {
380
+ let stylings = _text.current.stylings.slice()
235
381
  let changes = []
236
382
 
237
- for (let succeed of succeeding) {
238
- let index = getStylingIndex(succeed)
239
- let styling = stylings[index]
383
+ // TODO: Refactor
384
+
385
+ // Offset all succeeding stylings by length
386
+ // changes.push(
387
+ // handleScenario(
388
+ // stylings,
389
+ // (_start, _finish) => start < _start && end <= start
390
+ // ).map((styling) => {
391
+ // return {
392
+ // ...styling,
393
+ // start: styling.start - length,
394
+ // finish: styling.finish - length,
395
+ // }
396
+ // })
397
+ // )
398
+
399
+ let succeeding = stylings.filter(
400
+ ({ start: _start }) => start < _start && end <= _start
401
+ )
402
+ stylings.map((styling, index) => {
403
+ let result = succeeding.find((item) => compareStylings(styling, item))
404
+ if (!result) return
405
+
406
+ changes.push({
407
+ index: index,
408
+ start: styling.start - length,
409
+ finish: styling.finish - length,
410
+ })
411
+ })
240
412
 
241
- changes.push([
242
- index,
243
- styling.start + difference,
244
- styling.finish + difference,
245
- ])
246
- }
413
+ // Handle complete encapsulation over styling
414
+ let encapsulating = stylings.filter(
415
+ ({ start: _start, finish: _finish }) => start <= _start && end >= _finish
416
+ )
417
+ stylings.map((styling, index) => {
418
+ let result = encapsulating.find((item) => compareStylings(styling, item))
419
+ if (!result) return
420
+
421
+ // This will effectively remove the styling, since collapsed ranges are automatically removed
422
+ changes.push({
423
+ index: index,
424
+ start: styling.start,
425
+ finish: styling.start,
426
+ })
427
+ })
428
+
429
+ // Handle deletion being encapsulated by styling
430
+ let encapsulated = stylings.filter(
431
+ ({ start: _start, finish: _finish }) =>
432
+ (start > _start && end <= _finish) || (start >= _start && end < _finish)
433
+ )
434
+ stylings.map((styling, index) => {
435
+ let result = encapsulated.find((item) => compareStylings(styling, item))
436
+ if (!result) return
437
+
438
+ changes.push({
439
+ index: index,
440
+ start: styling.start,
441
+ finish: styling.finish - length,
442
+ })
443
+ })
444
+
445
+ // Handle deletion being encapsulated by styling with left resumption
446
+ let leftResumption = stylings.filter(
447
+ ({ start: _start, finish: _finish }) =>
448
+ end < _finish && end > _start && start < _start
449
+ )
450
+ stylings.map((styling, index) => {
451
+ let result = leftResumption.find((item) => compareStylings(styling, item))
452
+ if (!result) return
453
+
454
+ changes.push({
455
+ index: index,
456
+ start: getMaximum([end, styling.start]) - length,
457
+ finish: styling.finish - length,
458
+ })
459
+ })
460
+
461
+ let rightResumption = stylings.filter(
462
+ ({ start: _start, finish: _finish }) =>
463
+ start > _start && start < _finish && end > _finish
464
+ )
465
+ stylings.map((styling, index) => {
466
+ let result = rightResumption.find((item) =>
467
+ compareStylings(styling, item)
468
+ )
469
+ if (!result) return
470
+
471
+ changes.push({
472
+ index: index,
473
+ start: styling.start,
474
+ finish: getMinimum([start, styling.finish]),
475
+ })
476
+ })
477
+
478
+ // Apply changes
479
+ changes.map(({ index, start, finish }) => {
480
+ stylings[index] = {
481
+ ...stylings[index],
482
+ start: start,
483
+ finish: finish,
484
+ }
485
+ })
486
+
487
+ // Handle complete encapsulation by styling
488
+ // stylings = stylings.map((styling) => {
489
+ // if (start >= styling.start && end <= styling.finish) {
490
+ // console.log("hi")
491
+ // }
247
492
 
248
- let intersecting = getIntersectStylings(selection, 1, 1)
493
+ // return styling
494
+ // })
249
495
 
250
- for (let intersect of intersecting) {
251
- let index = getStylingIndex(intersect)
252
- let styling = stylings[index]
496
+ // If deletion is surrounded by styling, decrease finish by length (this might not include first character, but try first)
497
+ // If deletion is intersecting with a styling behind it, get maximum between deletion finish and styling start, and set the start to it, then offset styling by length
498
+ // If deletion is intersecting wit ha styling after it, get minimum between deletion start and styling finish, and set the finish to it
499
+
500
+ // console.log(succeeding)
501
+
502
+ // If the start and finish of any styling is greater than the new length, remove it
503
+
504
+ return stylings
505
+ }
253
506
 
254
- changes.push([index, styling.start, styling.finish + difference])
507
+ const handleAddition = (length, start, end) => {
508
+ start -= length + 1
509
+ end -= length + 1
510
+
511
+ let stylings = _text.current.stylings.slice()
512
+
513
+ let changes = []
514
+
515
+ let succeeding = handleScenario(
516
+ stylings,
517
+ (_start, _finish) => _start > start && _start + 1 >= end
518
+ ).map((styling) => {
519
+ return {
520
+ ...styling,
521
+ start: styling.start + length,
522
+ finish: styling.finish + length,
523
+ }
524
+ })
525
+ changes.push(succeeding)
526
+
527
+ console.log(`Addition (${length})`)
528
+ console.log([start, end])
529
+ if (stylings.length > 0) {
530
+ console.log([stylings[0].start, stylings[0].finish])
255
531
  }
256
532
 
257
- let spliced = []
258
- for (let [index, start, finish] of changes) {
533
+ // console.log(succeeding)
534
+
535
+ let preceding = handleScenario(
536
+ stylings,
537
+ // (_start, _finish) => start < _finish && start >= _start + 2 && length == 1 // This is to not style pasted text
538
+ (_start, _finish) => start + 1 == _finish && length == 1 // This is to not style pasted text
539
+ ).map((styling) => {
540
+ return {
541
+ ...styling,
542
+ start: styling.start,
543
+ finish: styling.finish + length,
544
+ }
545
+ })
546
+ changes.push(preceding)
547
+
548
+ let encapsulated = handleScenario(
549
+ stylings,
550
+ (_start, _finish) => start >= _start && end + 1 < _finish
551
+ ).map((styling) => {
552
+ return {
553
+ ...styling,
554
+ finish: styling.finish + length,
555
+ }
556
+ })
557
+
558
+ changes.push(encapsulated)
559
+
560
+ console.log(length)
561
+
562
+ console.log(`Encapsulated: ${encapsulated.length}`)
563
+
564
+ console.log(`Succeeding: ${succeeding.length}`)
565
+ console.log(`Preceding: ${preceding.length}`)
566
+ // console.log(preceding)
567
+
568
+ changes.flat().map(({ index, start, finish }) => {
569
+ stylings[index] = {
570
+ ...stylings[index],
571
+ start: start,
572
+ finish: finish,
573
+ }
574
+ })
575
+
576
+ return stylings
577
+ }
578
+
579
+ const handlePaste = (stylings, difference, start, end) => {
580
+ console.log(`Paste:`)
581
+ console.log([start, end])
582
+ console.log(stylings)
583
+ let changes = []
584
+
585
+ // Get stylings being encapsulated by pasting range
586
+
587
+ let encapsulating = handleScenario(
588
+ stylings,
589
+ (_start, _end) => start <= _start && end >= _end // This is needed because of conflict with addition/deletion
590
+ ).map((styling) => {
591
+ // Effective removal
592
+ return {
593
+ ...styling,
594
+ start: styling.start,
595
+ finish: styling.start,
596
+ }
597
+ })
598
+ changes.push(encapsulating)
599
+
600
+ // Get encapsulated stylings
601
+ let encapsulated = handleScenario(
602
+ stylings,
603
+ (_start, _end) => _start < start && _end > end
604
+ ).map((styling) => {
605
+ // Right splice
606
+ changes.push({
607
+ ...styling,
608
+ index: -1,
609
+ start: end,
610
+ finish: styling.finish,
611
+ })
612
+
613
+ // Left splice
614
+ changes.push({
615
+ ...styling,
616
+ index: -1,
617
+ start: styling.start,
618
+ finish: start,
619
+ })
620
+
621
+ // Effective removal
622
+ return {
623
+ ...styling,
624
+ start: styling.start,
625
+ finish: styling.start,
626
+ }
627
+ })
628
+ changes.push(encapsulated)
629
+
630
+ // Get intersecting stylings from the left
631
+ let left = handleScenario(
632
+ stylings,
633
+ (_start, _end) =>
634
+ _end > start && _end <= end - difference + 1 && _start < start
635
+ ).map((styling) => {
636
+ return {
637
+ ...styling,
638
+ finish: start,
639
+ }
640
+ })
641
+
642
+ changes.push(left)
643
+
644
+ // Get intersecting stylings from the right
645
+ let right = handleScenario(
646
+ stylings,
647
+ (_start, _end) =>
648
+ _end > end - difference && _start >= start && _start < end - difference
649
+ ).map((styling) => {
650
+ return {
651
+ ...styling,
652
+ start: end,
653
+ }
654
+ })
655
+
656
+ changes.push(right)
657
+
658
+ console.log(`Encapsulating: ${encapsulating.length}`)
659
+ console.log(`Encapsulated: ${encapsulated.length}`)
660
+ console.log(`Left Intersecting: ${left.length}`)
661
+ console.log(`Right Intersecting: ${right.length}`)
662
+
663
+ changes = changes.flat()
664
+ changes.map((styling) => {
665
+ let { index, start, finish } = styling
666
+ if (index == -1) {
667
+ stylings.push({
668
+ type: styling.type,
669
+ start: start,
670
+ finish: finish,
671
+ })
672
+ return
673
+ }
674
+
259
675
  stylings[index] = {
260
676
  ...stylings[index],
261
677
  start: start,
262
678
  finish: finish,
263
679
  }
680
+ })
681
+
682
+ console.log(stylings)
683
+
684
+ return stylings
685
+ }
686
+
687
+ const onChange = (value) => {
688
+ setTimeout(function () {
689
+ let [selectionStart, selectionEnd] = getFieldSelection()
690
+
691
+ let difference = value.length - _text.current.content.length
692
+
693
+ let start = selectionStart - difference
694
+ let end = selectionEnd - difference
695
+
696
+ let stylings = _text.current.stylings.slice()
697
+
698
+ let succeeding = getSucceedStylings(start)
699
+ let changes = []
700
+
701
+ for (let succeed of succeeding) {
702
+ let index = getStylingIndex(succeed)
703
+ let styling = stylings[index]
704
+
705
+ changes.push([
706
+ index,
707
+ styling.start + difference,
708
+ styling.finish + difference,
709
+ ])
710
+ }
711
+
712
+ if (difference < 0) {
713
+ stylings = handleDeletion(
714
+ Math.abs(difference),
715
+ selectionStart,
716
+ selectionEnd + Math.abs(difference)
717
+ )
718
+ }
719
+
720
+ if (difference > 0) {
721
+ stylings = handleAddition(
722
+ Math.abs(difference),
723
+ selectionStart,
724
+ selectionEnd
725
+ )
726
+ }
727
+
728
+ // Remove empty stylings and invisible stylings
729
+ stylings = stylings.filter(
730
+ (styling) =>
731
+ !(
732
+ styling.start == styling.finish ||
733
+ (styling.start >= value.length && styling.finish >= value.length)
734
+ )
735
+ )
736
+
737
+ if (_text.current.pasted.status) {
738
+ stylings = handlePaste(
739
+ stylings,
740
+ difference,
741
+ selectionStart - _text.current.pasted.length,
742
+ selectionStart
743
+ )
744
+
745
+ for (let styling of _text.current.lastCopy) {
746
+ styling = {
747
+ ...styling,
748
+ start: styling.start + selectionStart - _text.current.pasted.length,
749
+ finish:
750
+ styling.finish + selectionStart - _text.current.pasted.length,
751
+ }
752
+
753
+ stylings.push(styling)
754
+ }
755
+ }
756
+
757
+ // Remove empty stylings and invisible stylings
758
+ stylings = stylings.filter(
759
+ (styling) =>
760
+ !(
761
+ styling.start == styling.finish ||
762
+ (styling.start >= value.length && styling.finish >= value.length)
763
+ )
764
+ )
765
+
766
+ setText({
767
+ ..._text.current,
768
+ content: value,
769
+ stylings: stylings,
770
+ revert: [selectionStart, selectionEnd],
771
+ pasted: { status: false, length: 0 },
772
+ })
773
+ }, 0)
774
+ }
775
+
776
+ const getContent = () => {
777
+ let content = _text.current.content
778
+
779
+ // Get all styling indices
780
+ let indices = _text.current.stylings
781
+ .map(({ start, finish }) => [start, finish])
782
+ .flat()
783
+
784
+ // Sort ascendingly
785
+ indices = indices.sort((a, b) => a - b)
786
+
787
+ // Remove duplicates
788
+ indices = indices.filter(
789
+ (element, index) => indices.indexOf(element) == index
790
+ )
791
+
792
+ // Add first index if not present
793
+ if (indices[0] != 0) indices.unshift(0)
264
794
 
265
- if (start >= finish) spliced.push(index)
795
+ // Add last index if not present
796
+ let last = content.length
797
+ if (indices[indices.length - 1] != last) indices.push(last)
798
+
799
+ let result = []
800
+
801
+ for (let i = 0; i < indices.length - 1; i++) {
802
+ result.push([indices[i], content.substring(indices[i], indices[i + 1])])
266
803
  }
267
804
 
268
- stylings = stylings.filter((_, index) => !spliced.includes(index))
805
+ return result
806
+ }
269
807
 
270
- setText({ ...text, content: value, stylings: stylings })
808
+ const serializeStyleInRange = (start, end) => {
809
+ // Get all stylings intersecting
810
+ let stylings = []
811
+ for (let i = start; i < end; i++) {
812
+ stylings.push(getIntersectStylings(i))
813
+ }
814
+
815
+ // Remove duplicates
816
+ stylings = stylings.flat().filter((item) => item)
817
+ stylings = stylings.filter(
818
+ (item, index) =>
819
+ stylings.findIndex(
820
+ (_item) =>
821
+ _item.start == item.start &&
822
+ _item.finish == item.finish &&
823
+ _item.type == item.type
824
+ ) == index
825
+ )
826
+
827
+ // Clamp start and finish values and offset by start index
828
+ stylings = stylings.map((styling) => {
829
+ return {
830
+ ...styling,
831
+ start: getMaximum([styling.start, start]) - start,
832
+ finish: getMinimum([styling.finish, end]) - start,
833
+ }
834
+ })
835
+
836
+ return stylings
271
837
  }
272
838
 
839
+ // dangerouslySetInnerHTML incorrectly renders when the entire text is highlighted, copied, and then pasted in succession
840
+ useEffect(() => {
841
+ let html = getContent()
842
+ .map((_data, index) => {
843
+ let [start, data] = _data
844
+
845
+ // Get stylings encompassing an index within it's range
846
+ let stylings = getIntersectStylings(start)
847
+ // console.log(data)
848
+ // console.log(stylings)
849
+ return `<span class="${stylings
850
+ .map((styling) => styleClasses[styling.type])
851
+ .join(" ")}" data-child-index="${index}">${data}</span>`
852
+ })
853
+ .join("")
854
+
855
+ field.current.innerHTML = html
856
+ }, [text.content, text.stylings, text.revert])
857
+
273
858
  return (
274
- <div className="align-center box-border flex h-min w-[400px] flex-col items-center justify-center rounded bg-gray-300 shadow-md">
859
+ <div className="align-center box-border flex h-min w-[400px] flex-col items-center justify-center rounded bg-blue-300 shadow-md">
275
860
  <div className={clsx("flex w-full flex-row justify-start p-2")}>
276
861
  <Property
277
862
  name="B"
278
- onClick={() => {
863
+ onMouseDown={(event) => {
864
+ event.preventDefault() // This does not take focus away from field which allows the function to retrieve the current selection data
279
865
  perform("bold")
280
866
  }}
281
867
  />
282
868
  <Property
283
869
  name="I"
284
- onClick={() => {
870
+ onMouseDown={(event) => {
871
+ event.preventDefault()
285
872
  perform("italic")
286
873
  }}
287
874
  />
288
875
  <Property
289
876
  name="U"
290
- onClick={() => {
877
+ onMouseDown={(event) => {
878
+ event.preventDefault()
291
879
  perform("under")
292
880
  }}
293
881
  />
294
882
  <Property
295
883
  name="S"
296
- onClick={() => {
884
+ onMouseDown={(event) => {
885
+ event.preventDefault()
297
886
  perform("strike")
298
887
  }}
299
888
  />
300
889
  </div>
301
890
  <div className="h-[1px] w-full bg-slate-600">&nbsp;</div>
302
891
  <div className="w-full">
303
- <RichTextarea
892
+ <div
304
893
  ref={field}
305
- value={text.content}
306
- style={{ width: "100%" }} // tailwind w-full does not work
307
- className="order-none h-[150px] resize-none p-2 outline-none"
308
- onChange={(e) => {
309
- onChange(e.target.value)
894
+ contentEditable="true"
895
+ className="h-[150px] w-full resize-none overflow-auto overflow-x-hidden border-none p-2 outline-none"
896
+ onPaste={(event) => {
897
+ // pastes all copied text from the content editable as plain text
898
+ event.preventDefault()
899
+ const data = event.clipboardData.getData("text/plain")
900
+ document.execCommand("insertHTML", false, data)
901
+
902
+ console.log(data)
903
+
904
+ setText({
905
+ ..._text.current,
906
+ pasted: { status: true, length: data.length },
907
+ })
908
+
909
+ // let [start, end] = getFieldSelection()
910
+
911
+ // console.log(index)
912
+ // console.log(start)
913
+
914
+ // let index = start - data.length
915
+ // let stylings = text.stylings.slice()
916
+
917
+ // FIXME:
918
+ // stylings.push({
919
+ // type: "bold",
920
+ // start: 5,
921
+ // finish: 7,
922
+ // })
923
+
924
+ // let copy = text.lastCopy
925
+ // if (copy.length != 0) {
926
+ // for (let styling of copy) {
927
+ // stylings.push({
928
+ // type: styling.type,
929
+ // start: styling.start + start,
930
+ // finish: styling.finish + start,
931
+ // })
932
+ // }
933
+ // }
934
+
935
+ // let content: any = text.content
936
+ // let original = content.length
937
+
938
+ // // If not collapsed, insert text
939
+ // if (start == end) {
940
+ // content = content.split("")
941
+ // content.splice(start, 0, data)
942
+ // content = content.join("")
943
+ // } else {
944
+ // console.log(content)
945
+ // // Otherwise, replace substring
946
+ // content =
947
+ // content.substring(0, start) +
948
+ // data +
949
+ // content.substring(end, content.length)
950
+ // }
951
+
952
+ // let difference = content.length - original
953
+
954
+ // stylings = handleDeletion()
955
+
956
+ // setText({
957
+ // ...text,
958
+ // content: content,
959
+ // stylings: stylings,
960
+ // revert: [start + data.length, start + data.length],
961
+ // })
962
+ }}
963
+ onInput={(event: any) => {
964
+ onChange(event.target.textContent)
310
965
  }}
311
- >
312
- {(value) => {
313
- // Get all styling indices
314
- let indices = text.stylings
315
- .map(({ start, finish }) => [start, finish])
316
- .flat()
317
-
318
- // Sort ascendingly
319
- indices = indices.sort((a, b) => a - b)
320
-
321
- // Remove duplicates
322
- indices = indices.filter(
323
- (element, index) => indices.indexOf(element) == index
324
- )
325
-
326
- // Add first index if not present
327
- if (indices[0] != 0) indices.unshift(0)
328
-
329
- // Add last index if not present
330
- let last = text.content.length
331
- if (indices[indices.length - 1] != last) indices.push(last)
332
-
333
- let result = []
334
-
335
- for (let i = 0; i < indices.length - 1; i++) {
336
- result.push([
337
- indices[i],
338
- value.substring(indices[i], indices[i + 1]),
339
- ])
340
- }
341
-
342
- return result.map((_data, index) => {
343
- let [start, data] = _data
344
-
345
- // Get stylings encompassing an index within it's range
346
- let stylings = getIntersectStylings(start)
347
-
348
- return (
349
- <span
350
- key={index}
351
- className={`${stylings
352
- .map((styling) => styleClasses[styling.type])
353
- .join(" ")}
354
- `}
355
- >
356
- {data}
357
- </span>
358
- )
966
+ onCopy={(event) => {
967
+ let [start, end] = getFieldSelection()
968
+
969
+ let data = serializeStyleInRange(start, end)
970
+
971
+ setText({
972
+ ...text,
973
+ lastCopy: data,
359
974
  })
360
975
  }}
361
- </RichTextarea>
976
+ // onKeyDown={(event: any) => {
977
+ // event.preventDefault()
978
+ // console.log(event)
979
+ // }}
980
+ ></div>
362
981
  </div>
363
982
  <div className="h-[1px] w-full bg-slate-600">&nbsp;</div>
364
983
  <button className="my-1 rounded bg-cyan-800 p-2 py-1 text-white">