braid-text 0.3.27 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,246 +0,0 @@
1
- // textarea-highlights.js — Render colored highlights behind a <textarea>
2
- //
3
- // No dependencies. Pure DOM/CSS.
4
- //
5
- // Renders colored ranges (selections) and zero-width points (cursors) as
6
- // overlays behind a textarea's text. Works with any textarea regardless
7
- // of its parent's CSS — backdrops are positioned as absolute siblings.
8
- //
9
- // Usage:
10
- // var hl = textarea_highlights(textarea)
11
- // hl.set('peer-1', [{ from: 5, to: 10, color: 'rgba(97,175,239,0.25)' }])
12
- // hl.render()
13
- // hl.remove('peer-1')
14
- // hl.destroy()
15
- //
16
- function textarea_highlights(textarea) {
17
- // Inject CSS once per page
18
- if (!document.getElementById('textarea-highlights-css')) {
19
- var style = document.createElement('style')
20
- style.id = 'textarea-highlights-css'
21
- style.textContent = `
22
- .textarea-hl-backdrop {
23
- position: absolute;
24
- white-space: pre-wrap;
25
- word-wrap: break-word;
26
- overflow-y: auto;
27
- pointer-events: none;
28
- color: transparent;
29
- z-index: 1;
30
- box-sizing: border-box;
31
- scrollbar-width: none;
32
- -ms-overflow-style: none;
33
- }
34
- .textarea-hl-backdrop::-webkit-scrollbar { display: none; }
35
- .textarea-hl-backdrop span { color: transparent; border-radius: 2px; }
36
- .textarea-hl-backdrop span.sel {
37
- padding: var(--sel-pad, 3px) 0;
38
- -webkit-box-decoration-break: clone;
39
- box-decoration-break: clone;
40
- }
41
- .textarea-hl-backdrop .cursor {
42
- border-left: 2px solid var(--cursor-color, #ff5722);
43
- margin-left: -1px;
44
- margin-right: -1px;
45
- }
46
- `
47
- document.head.appendChild(style)
48
- }
49
-
50
- // Save original styles so we can restore on destroy
51
- var original_bg = textarea.style.backgroundColor
52
- var original_position = textarea.style.position
53
- var original_zIndex = textarea.style.zIndex
54
-
55
- // Read the textarea's background color before we make it transparent.
56
- // Walk up the DOM if the textarea itself is transparent.
57
- var bg = getComputedStyle(textarea).backgroundColor
58
- if (!bg || bg === 'rgba(0, 0, 0, 0)') {
59
- var el = textarea.parentElement
60
- while (el) {
61
- var elBg = getComputedStyle(el).backgroundColor
62
- if (elBg && elBg !== 'rgba(0, 0, 0, 0)') { bg = elBg; break }
63
- el = el.parentElement
64
- }
65
- }
66
- bg = bg || 'white'
67
-
68
- // Make textarea transparent so backdrops show through.
69
- // position:relative + z-index puts the textarea text above the backdrops.
70
- textarea.style.backgroundColor = 'transparent'
71
- textarea.style.position = 'relative'
72
- textarea.style.zIndex = '2'
73
-
74
- // Measure font metrics for gap-free selection highlights
75
- var cs = getComputedStyle(textarea)
76
- var test_div = document.createElement('div')
77
- test_div.style.cssText =
78
- 'font-family:' + cs.fontFamily + ';' +
79
- 'font-size:' + cs.fontSize + ';' +
80
- 'line-height:' + cs.lineHeight + ';' +
81
- 'position:absolute;top:-9999px;'
82
- var test_span = document.createElement('span')
83
- test_span.style.backgroundColor = 'red'
84
- test_span.textContent = 'Xg'
85
- test_div.appendChild(test_span)
86
- document.body.appendChild(test_div)
87
- var line_height = parseFloat(getComputedStyle(test_div).lineHeight)
88
- var bg_height = test_span.getBoundingClientRect().height
89
- document.body.removeChild(test_div)
90
- var sel_pad = (line_height - bg_height) / 2
91
-
92
- // State
93
- var layer_data = {} // layer_id -> [{ from, to, color }]
94
- var layer_divs = {} // layer_id -> DOM div
95
-
96
- // Scroll sync
97
- function sync_scroll() {
98
- for (var div of Object.values(layer_divs)) {
99
- div.scrollTop = textarea.scrollTop
100
- div.scrollLeft = textarea.scrollLeft
101
- }
102
- }
103
- textarea.addEventListener('scroll', sync_scroll)
104
-
105
- // Re-render when textarea resizes (user drag, window resize, CSS change)
106
- var resize_observer = new ResizeObserver(do_render)
107
- resize_observer.observe(textarea)
108
-
109
- // Build a backdrop style string matching the textarea's font/padding/border
110
- function backdrop_style() {
111
- var cs = getComputedStyle(textarea)
112
- var bw = parseFloat(cs.borderLeftWidth) + parseFloat(cs.borderRightWidth)
113
- var bh = parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth)
114
- // Use clientWidth/clientHeight (content + padding, excludes scrollbar)
115
- // plus border, so the backdrop's content area matches the textarea's
116
- // even when the textarea reserves space for a scrollbar.
117
- return 'font-family:' + cs.fontFamily + ';' +
118
- 'font-size:' + cs.fontSize + ';' +
119
- 'line-height:' + cs.lineHeight + ';' +
120
- 'padding:' + cs.paddingTop + ' ' + cs.paddingRight + ' ' +
121
- cs.paddingBottom + ' ' + cs.paddingLeft + ';' +
122
- 'border:' + cs.borderTopWidth + ' solid transparent;' +
123
- 'border-radius:' + cs.borderRadius + ';' +
124
- 'width:' + (textarea.clientWidth + bw) + 'px;' +
125
- 'height:' + (textarea.clientHeight + bh) + 'px;' +
126
- '--sel-pad:' + sel_pad + 'px;' +
127
- 'background-color:' + bg + ';'
128
- }
129
-
130
- function escape_html(text) {
131
- var div = document.createElement('div')
132
- div.textContent = text
133
- return div.innerHTML
134
- }
135
-
136
- function build_html(text, highlights) {
137
- var cursors = highlights.filter(h => h.from === h.to)
138
- var sels = highlights.filter(h => h.from !== h.to)
139
-
140
- var items = []
141
- for (var s of sels)
142
- items.push({ type: 'selection', start: s.from, end: s.to, color: s.color })
143
- for (var c of cursors)
144
- items.push({ type: 'cursor', pos: c.from, color: c.color })
145
-
146
- items.sort((a, b) => {
147
- var pa = a.type === 'cursor' ? a.pos : a.start
148
- var pb = b.type === 'cursor' ? b.pos : b.start
149
- return pa - pb
150
- })
151
-
152
- var result = ''
153
- var last = 0
154
-
155
- for (var item of items) {
156
- if (item.type === 'selection') {
157
- if (item.start < last) continue
158
- result += escape_html(text.substring(last, item.start))
159
- var sel_text = text.substring(item.start, item.end)
160
- var sel_html = escape_html(sel_text).replace(/\n/g, ' \n')
161
- result += '<span class="sel" style="background-color:' + item.color + ';">' + sel_html + '</span>'
162
- last = item.end
163
- } else {
164
- if (item.pos < last) continue
165
- result += escape_html(text.substring(last, item.pos))
166
- result += '<span class="cursor" style="--cursor-color:' + item.color + ';"></span>'
167
- last = item.pos
168
- }
169
- }
170
-
171
- result += escape_html(text.substring(last))
172
- if (text.endsWith('\n')) result += '\u200B\n'
173
- return result
174
- }
175
-
176
- // --- render implementation ---
177
-
178
- function do_render() {
179
- var text = textarea.value
180
- var len = text.length
181
- var style_str = backdrop_style()
182
-
183
- // Remove divs for layers that no longer exist
184
- for (var id of Object.keys(layer_divs)) {
185
- if (!layer_data[id]) {
186
- layer_divs[id].remove()
187
- delete layer_divs[id]
188
- }
189
- }
190
-
191
- // Render each layer
192
- for (var id of Object.keys(layer_data)) {
193
- var highlights = layer_data[id].map(h => ({
194
- from: Math.min(h.from, len),
195
- to: Math.min(h.to, len),
196
- color: h.color
197
- }))
198
-
199
- if (!layer_divs[id]) {
200
- // Insert backdrop as previous sibling of textarea.
201
- // position:absolute takes it out of flow so it doesn't
202
- // affect layout. Without top/left set, it naturally sits
203
- // at the textarea's position.
204
- var div = document.createElement('div')
205
- div.className = 'textarea-hl-backdrop'
206
- textarea.parentElement.insertBefore(div, textarea)
207
- layer_divs[id] = div
208
- }
209
-
210
- // Font/padding/border/size are set inline to match textarea;
211
- // positioning/pointer-events/etc come from the CSS class.
212
- layer_divs[id].style.cssText = style_str
213
-
214
- layer_divs[id].innerHTML = build_html(text, highlights)
215
- layer_divs[id].scrollTop = textarea.scrollTop
216
- layer_divs[id].scrollLeft = textarea.scrollLeft
217
- }
218
- }
219
-
220
- return {
221
- set: function(layer_id, highlights) {
222
- layer_data[layer_id] = highlights
223
- },
224
-
225
- remove: function(layer_id) {
226
- delete layer_data[layer_id]
227
- if (layer_divs[layer_id]) {
228
- layer_divs[layer_id].remove()
229
- delete layer_divs[layer_id]
230
- }
231
- },
232
-
233
- render: do_render,
234
-
235
- destroy: function() {
236
- textarea.removeEventListener('scroll', sync_scroll)
237
- resize_observer.disconnect()
238
- for (var div of Object.values(layer_divs)) div.remove()
239
- layer_data = {}
240
- layer_divs = {}
241
- textarea.style.backgroundColor = original_bg
242
- textarea.style.position = original_position
243
- textarea.style.zIndex = original_zIndex
244
- }
245
- }
246
- }