@sikka/hawa 0.0.230 → 0.0.231
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.
- package/dist/styles.css +45 -1
- package/es/elements/FloatingComment - ContentEditable.d.ts +6 -0
- package/es/elements/FloatingComment.d.ts +6 -0
- package/es/elements/index.d.ts +1 -0
- package/es/index.es.js +1 -1
- package/lib/elements/FloatingComment - ContentEditable.d.ts +6 -0
- package/lib/elements/FloatingComment.d.ts +6 -0
- package/lib/elements/index.d.ts +1 -0
- package/lib/index.js +1 -1
- package/package.json +3 -2
- package/src/elements/FloatingComment - ContentEditable.tsx +514 -0
- package/src/elements/FloatingComment.tsx +369 -0
- package/src/elements/index.ts +1 -0
- package/src/styles.css +45 -1
- package/src/tailwind.css +1 -1
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import React, { useRef, useState, useEffect } from "react"
|
|
2
|
+
import { RichTextarea } from "rich-textarea"
|
|
3
|
+
import clsx from "clsx"
|
|
4
|
+
|
|
5
|
+
const Property = (props) => {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
className="border-box mr-[5px] flex h-[32px] w-[32px] items-center justify-center rounded bg-gray-400 p-2"
|
|
9
|
+
onClick={props.onClick}
|
|
10
|
+
>
|
|
11
|
+
{props.name}
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type ComponentTypes = {
|
|
17
|
+
foo?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// TODO: Handle copy pasting by assigning to local storage ?
|
|
21
|
+
|
|
22
|
+
const styleClasses = {
|
|
23
|
+
bold: "font-bold",
|
|
24
|
+
italic: "italic",
|
|
25
|
+
under: "underline",
|
|
26
|
+
strike: "line-through",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const FloatingComment: React.FunctionComponent<ComponentTypes> = (
|
|
30
|
+
props
|
|
31
|
+
) => {
|
|
32
|
+
const [text, setText] = useState({
|
|
33
|
+
content: "",
|
|
34
|
+
stylings: [], // A styling is an object with 2 indices specifying a substring of text, and the applied effect
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const field = useRef(null)
|
|
38
|
+
|
|
39
|
+
// utility functions
|
|
40
|
+
const getRange = (start, end) => {
|
|
41
|
+
let result = []
|
|
42
|
+
for (let i = start; i <= end; i++) {
|
|
43
|
+
result.push(i)
|
|
44
|
+
}
|
|
45
|
+
return result
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const intersection = (setA, setB) => {
|
|
49
|
+
const _intersection = new Set()
|
|
50
|
+
for (const elem of setB) {
|
|
51
|
+
if (setA.has(elem)) {
|
|
52
|
+
_intersection.add(elem)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return _intersection
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const getMinimum = (array) => {
|
|
59
|
+
return array.sort((a, b) => a - b)[0]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const getMaximum = (array) => {
|
|
63
|
+
return array.sort((a, b) => b - a)[0]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// -1 - types dont match
|
|
67
|
+
// 0 - s1 is surrounded or on the edge of the s2
|
|
68
|
+
// 1 - s1 intersects with s2
|
|
69
|
+
// 2 - s1 does not intersect with s2
|
|
70
|
+
const getCorrelation = (styling1, styling2) => {
|
|
71
|
+
if (styling1.type != styling2.type) return -1
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
styling2.start <= styling1.start &&
|
|
75
|
+
styling2.finish >= styling1.finish
|
|
76
|
+
) {
|
|
77
|
+
return 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let indices1 = new Set(getRange(styling1.start - 1, styling1.finish - 1))
|
|
81
|
+
let indices2 = new Set(getRange(styling2.start - 1, styling2.finish - 1))
|
|
82
|
+
|
|
83
|
+
let result = intersection(indices1, indices2)
|
|
84
|
+
|
|
85
|
+
return result.size == 0 ? 2 : 1
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Correlation handler
|
|
89
|
+
const stylingSplice = (correlations, stylings, current, type) => {
|
|
90
|
+
// Only one surround correlation is possible at one time, so use .find to fetch it
|
|
91
|
+
|
|
92
|
+
let [_, index, styling] = correlations.find(([c, _, __]) => c == 0)
|
|
93
|
+
|
|
94
|
+
// Remove correlated styling
|
|
95
|
+
stylings = stylings.filter((_, _index) => _index != index)
|
|
96
|
+
|
|
97
|
+
// Get splices
|
|
98
|
+
let added = [
|
|
99
|
+
{
|
|
100
|
+
type: type,
|
|
101
|
+
start: getMinimum([styling.start, current.start]),
|
|
102
|
+
finish: getMaximum([styling.start, current.start]),
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
type: type,
|
|
106
|
+
start: getMinimum([styling.finish, current.finish]),
|
|
107
|
+
finish: getMaximum([styling.finish, current.finish]),
|
|
108
|
+
},
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
// Remove empty splices (edge cases)
|
|
112
|
+
added = added.filter((item) => item.start != item.finish)
|
|
113
|
+
|
|
114
|
+
// Add to current stylings
|
|
115
|
+
stylings = [...stylings, ...added]
|
|
116
|
+
|
|
117
|
+
return stylings
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Correlation handler
|
|
121
|
+
const stylingIntersect = (correlations, stylings, current, type) => {
|
|
122
|
+
// Filter out all intersected stylings
|
|
123
|
+
let intersections = correlations
|
|
124
|
+
.filter(([c, _, __]) => c == 1)
|
|
125
|
+
.map(([_, index, styling]) => {
|
|
126
|
+
return [index, styling]
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// Add current styling with no index for the sake for endpoint indices
|
|
130
|
+
intersections.push([-1, current])
|
|
131
|
+
|
|
132
|
+
// Get minimum intersection start index
|
|
133
|
+
let start = intersections
|
|
134
|
+
.map(([_, styling]) => styling.start)
|
|
135
|
+
.sort((a, b) => a - b)[0]
|
|
136
|
+
|
|
137
|
+
// Get maximum intersection start index
|
|
138
|
+
let finish = intersections
|
|
139
|
+
.map(([_, styling]) => styling.finish)
|
|
140
|
+
.sort((a, b) => b - a)[0]
|
|
141
|
+
|
|
142
|
+
// Get indices of all intersection
|
|
143
|
+
let indices = intersections.map((e) => e[0])
|
|
144
|
+
|
|
145
|
+
// Remove all from resulting styling array
|
|
146
|
+
stylings = stylings.filter((_, index) => !indices.includes(index))
|
|
147
|
+
|
|
148
|
+
// Add widest styling which encompasses all intersections
|
|
149
|
+
stylings.push({
|
|
150
|
+
type: type,
|
|
151
|
+
start: start,
|
|
152
|
+
finish: finish,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
return stylings
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const perform = (id) => {
|
|
159
|
+
let stylings = text.stylings.slice()
|
|
160
|
+
var selectionStart = field.current.selectionStart
|
|
161
|
+
var selectionEnd = field.current.selectionEnd
|
|
162
|
+
|
|
163
|
+
if (selectionStart == selectionEnd) return
|
|
164
|
+
|
|
165
|
+
let current = {
|
|
166
|
+
type: id,
|
|
167
|
+
start: selectionStart,
|
|
168
|
+
finish: selectionEnd,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let correlations = []
|
|
172
|
+
|
|
173
|
+
// Check the correlation between this requested styling and all other stylings
|
|
174
|
+
for (let i = 0; i < stylings.length; i++) {
|
|
175
|
+
let styling = stylings[i]
|
|
176
|
+
let correlation = getCorrelation(current, styling)
|
|
177
|
+
|
|
178
|
+
if (correlation != -1) correlations.push([correlation, i, styling])
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let result
|
|
182
|
+
|
|
183
|
+
if (correlations.find(([c, _, __]) => c == 1)) {
|
|
184
|
+
result = stylingIntersect(correlations, stylings, current, id)
|
|
185
|
+
} else if (correlations.find(([c, _, __]) => c == 0)) {
|
|
186
|
+
result = stylingSplice(correlations, stylings, current, id)
|
|
187
|
+
} else if (
|
|
188
|
+
correlations.find(([c, _, __]) => c == 2) ||
|
|
189
|
+
correlations.length == 0
|
|
190
|
+
) {
|
|
191
|
+
result = [...stylings, current]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
setText({
|
|
195
|
+
...text,
|
|
196
|
+
stylings: result,
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Get stylings encompassing an index within it's range
|
|
201
|
+
const getIntersectStylings = (index, startOffset = 0, finishOffset = 0) => {
|
|
202
|
+
// Find all stylings with encompassing range
|
|
203
|
+
let matches = text.stylings.filter(
|
|
204
|
+
({ start, finish }) =>
|
|
205
|
+
index >= start + startOffset && index < finish + finishOffset
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return matches
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Get stylings after an index
|
|
212
|
+
const getSucceedStylings = (index) => {
|
|
213
|
+
// Find all stylings after the index
|
|
214
|
+
let matches = text.stylings.filter(({ start, finish }) => start >= index)
|
|
215
|
+
|
|
216
|
+
return matches
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const getStylingIndex = (styling) => {
|
|
220
|
+
return text.stylings.findIndex(
|
|
221
|
+
(item) =>
|
|
222
|
+
item.start == styling.start &&
|
|
223
|
+
item.finish == styling.finish &&
|
|
224
|
+
item.type == styling.type
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const onChange = (value) => {
|
|
229
|
+
let difference = value.length - text.content.length
|
|
230
|
+
|
|
231
|
+
let selection = field.current.selectionStart - difference
|
|
232
|
+
|
|
233
|
+
let stylings = text.stylings.slice()
|
|
234
|
+
let succeeding = getSucceedStylings(selection)
|
|
235
|
+
let changes = []
|
|
236
|
+
|
|
237
|
+
for (let succeed of succeeding) {
|
|
238
|
+
let index = getStylingIndex(succeed)
|
|
239
|
+
let styling = stylings[index]
|
|
240
|
+
|
|
241
|
+
changes.push([
|
|
242
|
+
index,
|
|
243
|
+
styling.start + difference,
|
|
244
|
+
styling.finish + difference,
|
|
245
|
+
])
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let intersecting = getIntersectStylings(selection, 1, 1)
|
|
249
|
+
|
|
250
|
+
for (let intersect of intersecting) {
|
|
251
|
+
let index = getStylingIndex(intersect)
|
|
252
|
+
let styling = stylings[index]
|
|
253
|
+
|
|
254
|
+
changes.push([index, styling.start, styling.finish + difference])
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let spliced = []
|
|
258
|
+
for (let [index, start, finish] of changes) {
|
|
259
|
+
stylings[index] = {
|
|
260
|
+
...stylings[index],
|
|
261
|
+
start: start,
|
|
262
|
+
finish: finish,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (start >= finish) spliced.push(index)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
stylings = stylings.filter((_, index) => !spliced.includes(index))
|
|
269
|
+
|
|
270
|
+
setText({ ...text, content: value, stylings: stylings })
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
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">
|
|
275
|
+
<div className={clsx("flex w-full flex-row justify-start p-2")}>
|
|
276
|
+
<Property
|
|
277
|
+
name="B"
|
|
278
|
+
onClick={() => {
|
|
279
|
+
perform("bold")
|
|
280
|
+
}}
|
|
281
|
+
/>
|
|
282
|
+
<Property
|
|
283
|
+
name="I"
|
|
284
|
+
onClick={() => {
|
|
285
|
+
perform("italic")
|
|
286
|
+
}}
|
|
287
|
+
/>
|
|
288
|
+
<Property
|
|
289
|
+
name="U"
|
|
290
|
+
onClick={() => {
|
|
291
|
+
perform("under")
|
|
292
|
+
}}
|
|
293
|
+
/>
|
|
294
|
+
<Property
|
|
295
|
+
name="S"
|
|
296
|
+
onClick={() => {
|
|
297
|
+
perform("strike")
|
|
298
|
+
}}
|
|
299
|
+
/>
|
|
300
|
+
</div>
|
|
301
|
+
<div className="h-[1px] w-full bg-slate-600"> </div>
|
|
302
|
+
<div className="w-full">
|
|
303
|
+
<RichTextarea
|
|
304
|
+
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)
|
|
310
|
+
}}
|
|
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
|
+
)
|
|
359
|
+
})
|
|
360
|
+
}}
|
|
361
|
+
</RichTextarea>
|
|
362
|
+
</div>
|
|
363
|
+
<div className="h-[1px] w-full bg-slate-600"> </div>
|
|
364
|
+
<button className="my-1 rounded bg-cyan-800 p-2 py-1 text-white">
|
|
365
|
+
Submit
|
|
366
|
+
</button>
|
|
367
|
+
</div>
|
|
368
|
+
)
|
|
369
|
+
}
|
package/src/elements/index.ts
CHANGED
package/src/styles.css
CHANGED
|
@@ -660,6 +660,9 @@ video {
|
|
|
660
660
|
.z-50 {
|
|
661
661
|
z-index: 50;
|
|
662
662
|
}
|
|
663
|
+
.order-none {
|
|
664
|
+
order: 0;
|
|
665
|
+
}
|
|
663
666
|
.m-0 {
|
|
664
667
|
margin: 0px;
|
|
665
668
|
}
|
|
@@ -771,6 +774,9 @@ video {
|
|
|
771
774
|
.mr-40 {
|
|
772
775
|
margin-right: 10rem;
|
|
773
776
|
}
|
|
777
|
+
.mr-\[5px\] {
|
|
778
|
+
margin-right: 5px;
|
|
779
|
+
}
|
|
774
780
|
.mt-0 {
|
|
775
781
|
margin-top: 0px;
|
|
776
782
|
}
|
|
@@ -885,12 +891,18 @@ video {
|
|
|
885
891
|
.h-96 {
|
|
886
892
|
height: 24rem;
|
|
887
893
|
}
|
|
894
|
+
.h-\[150px\] {
|
|
895
|
+
height: 150px;
|
|
896
|
+
}
|
|
888
897
|
.h-\[1px\] {
|
|
889
898
|
height: 1px;
|
|
890
899
|
}
|
|
891
900
|
.h-\[2\.36rem\] {
|
|
892
901
|
height: 2.36rem;
|
|
893
902
|
}
|
|
903
|
+
.h-\[32px\] {
|
|
904
|
+
height: 32px;
|
|
905
|
+
}
|
|
894
906
|
.h-\[calc\(100\%-3\.5rem\)\] {
|
|
895
907
|
height: calc(100% - 3.5rem);
|
|
896
908
|
}
|
|
@@ -990,6 +1002,12 @@ video {
|
|
|
990
1002
|
.w-8 {
|
|
991
1003
|
width: 2rem;
|
|
992
1004
|
}
|
|
1005
|
+
.w-\[32px\] {
|
|
1006
|
+
width: 32px;
|
|
1007
|
+
}
|
|
1008
|
+
.w-\[400px\] {
|
|
1009
|
+
width: 400px;
|
|
1010
|
+
}
|
|
993
1011
|
.w-\[calc\(1\%\)\] {
|
|
994
1012
|
width: calc(1%);
|
|
995
1013
|
}
|
|
@@ -1104,6 +1122,9 @@ video {
|
|
|
1104
1122
|
.cursor-pointer {
|
|
1105
1123
|
cursor: pointer;
|
|
1106
1124
|
}
|
|
1125
|
+
.resize-none {
|
|
1126
|
+
resize: none;
|
|
1127
|
+
}
|
|
1107
1128
|
.resize {
|
|
1108
1129
|
resize: both;
|
|
1109
1130
|
}
|
|
@@ -1492,6 +1513,10 @@ video {
|
|
|
1492
1513
|
.bg-buttonPrimary-500 {
|
|
1493
1514
|
background-color: var(--button-primary-500);
|
|
1494
1515
|
}
|
|
1516
|
+
.bg-cyan-800 {
|
|
1517
|
+
--tw-bg-opacity: 1;
|
|
1518
|
+
background-color: rgb(21 94 117 / var(--tw-bg-opacity));
|
|
1519
|
+
}
|
|
1495
1520
|
.bg-gray-100 {
|
|
1496
1521
|
--tw-bg-opacity: 1;
|
|
1497
1522
|
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
|
@@ -1504,6 +1529,10 @@ video {
|
|
|
1504
1529
|
--tw-bg-opacity: 1;
|
|
1505
1530
|
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
|
|
1506
1531
|
}
|
|
1532
|
+
.bg-gray-400 {
|
|
1533
|
+
--tw-bg-opacity: 1;
|
|
1534
|
+
background-color: rgb(156 163 175 / var(--tw-bg-opacity));
|
|
1535
|
+
}
|
|
1507
1536
|
.bg-gray-50 {
|
|
1508
1537
|
--tw-bg-opacity: 1;
|
|
1509
1538
|
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
|
|
@@ -1566,6 +1595,10 @@ video {
|
|
|
1566
1595
|
--tw-bg-opacity: 1;
|
|
1567
1596
|
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
|
|
1568
1597
|
}
|
|
1598
|
+
.bg-slate-600 {
|
|
1599
|
+
--tw-bg-opacity: 1;
|
|
1600
|
+
background-color: rgb(71 85 105 / var(--tw-bg-opacity));
|
|
1601
|
+
}
|
|
1569
1602
|
.bg-transparent {
|
|
1570
1603
|
background-color: transparent;
|
|
1571
1604
|
}
|
|
@@ -1812,6 +1845,9 @@ video {
|
|
|
1812
1845
|
.capitalize {
|
|
1813
1846
|
text-transform: capitalize;
|
|
1814
1847
|
}
|
|
1848
|
+
.italic {
|
|
1849
|
+
font-style: italic;
|
|
1850
|
+
}
|
|
1815
1851
|
.leading-4 {
|
|
1816
1852
|
line-height: 1rem;
|
|
1817
1853
|
}
|
|
@@ -1900,6 +1936,10 @@ video {
|
|
|
1900
1936
|
-webkit-text-decoration-line: underline;
|
|
1901
1937
|
text-decoration-line: underline;
|
|
1902
1938
|
}
|
|
1939
|
+
.line-through {
|
|
1940
|
+
-webkit-text-decoration-line: line-through;
|
|
1941
|
+
text-decoration-line: line-through;
|
|
1942
|
+
}
|
|
1903
1943
|
.underline-offset-4 {
|
|
1904
1944
|
text-underline-offset: 4px;
|
|
1905
1945
|
}
|
|
@@ -1945,6 +1985,10 @@ video {
|
|
|
1945
1985
|
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
|
|
1946
1986
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
|
1947
1987
|
}
|
|
1988
|
+
.outline-none {
|
|
1989
|
+
outline: 2px solid transparent;
|
|
1990
|
+
outline-offset: 2px;
|
|
1991
|
+
}
|
|
1948
1992
|
.outline {
|
|
1949
1993
|
outline-style: solid;
|
|
1950
1994
|
}
|
|
@@ -2612,4 +2656,4 @@ body {
|
|
|
2612
2656
|
margin-left: 2.5rem;
|
|
2613
2657
|
margin-right: 2.5rem;
|
|
2614
2658
|
}
|
|
2615
|
-
}
|
|
2659
|
+
}
|
package/src/tailwind.css
CHANGED