braid-text 0.3.27 → 0.4.0
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/package.json +1 -1
- package/client/cursor-highlights.js +0 -246
package/package.json
CHANGED
|
@@ -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
|
-
}
|