@rip-lang/ui 0.3.19 → 0.3.21
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/README.md +443 -576
- package/accordion.rip +113 -0
- package/alert-dialog.rip +96 -0
- package/autocomplete.rip +141 -0
- package/avatar.rip +37 -0
- package/badge.rip +15 -0
- package/breadcrumb.rip +46 -0
- package/button-group.rip +26 -0
- package/button.rip +23 -0
- package/card.rip +25 -0
- package/carousel.rip +110 -0
- package/checkbox-group.rip +65 -0
- package/checkbox.rip +33 -0
- package/collapsible.rip +50 -0
- package/combobox.rip +155 -0
- package/context-menu.rip +105 -0
- package/date-picker.rip +214 -0
- package/dialog.rip +107 -0
- package/drawer.rip +79 -0
- package/editable-value.rip +80 -0
- package/field.rip +53 -0
- package/fieldset.rip +22 -0
- package/form.rip +39 -0
- package/grid.rip +901 -0
- package/index.rip +16 -0
- package/input-group.rip +28 -0
- package/input.rip +36 -0
- package/label.rip +16 -0
- package/menu.rip +162 -0
- package/menubar.rip +155 -0
- package/meter.rip +36 -0
- package/multi-select.rip +158 -0
- package/native-select.rip +32 -0
- package/nav-menu.rip +129 -0
- package/number-field.rip +162 -0
- package/otp-field.rip +89 -0
- package/package.json +18 -27
- package/pagination.rip +123 -0
- package/popover.rip +143 -0
- package/preview-card.rip +73 -0
- package/progress.rip +25 -0
- package/radio-group.rip +67 -0
- package/resizable.rip +123 -0
- package/scroll-area.rip +145 -0
- package/select.rip +184 -0
- package/separator.rip +17 -0
- package/skeleton.rip +22 -0
- package/slider.rip +165 -0
- package/spinner.rip +17 -0
- package/table.rip +27 -0
- package/tabs.rip +124 -0
- package/textarea.rip +48 -0
- package/toast.rip +87 -0
- package/toggle-group.rip +78 -0
- package/toggle.rip +24 -0
- package/toolbar.rip +46 -0
- package/tooltip.rip +115 -0
- package/dist/rip-ui.min.js +0 -524
- package/dist/rip-ui.min.js.br +0 -0
- package/serve.rip +0 -92
- package/ui.rip +0 -964
package/grid.rip
ADDED
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
# Grid — high-performance headless data grid with virtual scrolling
|
|
2
|
+
#
|
|
3
|
+
# Renders 100K+ rows at 60fps. Semantic <table> with sticky header.
|
|
4
|
+
# Selection, keyboard navigation, inline editing, sorting, column resize.
|
|
5
|
+
# Exposes data-* attributes for all interactive states. Themeable via
|
|
6
|
+
# CSS custom properties (--grid-*). Ships zero layout CSS — bring your own.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# Grid
|
|
10
|
+
# data: employees
|
|
11
|
+
# columns: [
|
|
12
|
+
# { key: 'name', title: 'Name', width: 200 }
|
|
13
|
+
# { key: 'age', title: 'Age', width: 80, align: 'right' }
|
|
14
|
+
# { key: 'role', title: 'Role', width: 150, type: 'select', source: roles }
|
|
15
|
+
# { key: 'active', title: 'Active', width: 60, type: 'checkbox' }
|
|
16
|
+
# ]
|
|
17
|
+
# rowHeight: 32
|
|
18
|
+
# overscan: 5
|
|
19
|
+
# @beforeEdit: (row, col, oldVal, newVal) -> newVal
|
|
20
|
+
# @afterEdit: (row, col, oldVal, newVal) -> null
|
|
21
|
+
|
|
22
|
+
export Grid = component
|
|
23
|
+
|
|
24
|
+
@data := []
|
|
25
|
+
@columns := []
|
|
26
|
+
@rowHeight := 32
|
|
27
|
+
@headerHeight := 36
|
|
28
|
+
@overscan := 5
|
|
29
|
+
@striped := false
|
|
30
|
+
@beforeEdit := null
|
|
31
|
+
@afterEdit := null
|
|
32
|
+
|
|
33
|
+
formatRegistry = []
|
|
34
|
+
|
|
35
|
+
registerFormat = (code, formatFn, parseFn) ->
|
|
36
|
+
id = formatRegistry.length
|
|
37
|
+
formatRegistry.push({ code, format: formatFn, parse: parseFn })
|
|
38
|
+
id
|
|
39
|
+
|
|
40
|
+
formatMap := new Map()
|
|
41
|
+
|
|
42
|
+
_resolveCol = (ci) ->
|
|
43
|
+
fid = formatMap.get("0,#{ci + 1}")
|
|
44
|
+
if fid isnt undefined then return formatRegistry[fid]
|
|
45
|
+
gid = formatMap.get("0,0")
|
|
46
|
+
if gid isnt undefined then formatRegistry[gid] else null
|
|
47
|
+
|
|
48
|
+
colFormatCache ~=
|
|
49
|
+
cache = []
|
|
50
|
+
ci = 0
|
|
51
|
+
while ci < @columns.length
|
|
52
|
+
cache.push(_resolveCol(ci))
|
|
53
|
+
ci++
|
|
54
|
+
cache
|
|
55
|
+
|
|
56
|
+
cellFormat = (row, col) ->
|
|
57
|
+
fid = formatMap.get("#{row + 1},#{col + 1}")
|
|
58
|
+
if fid isnt undefined then formatRegistry[fid] else colFormatCache[col]
|
|
59
|
+
|
|
60
|
+
_ready := false
|
|
61
|
+
scrollTop := 0
|
|
62
|
+
containerHeight := 0
|
|
63
|
+
activeRow := -1
|
|
64
|
+
activeCol := -1
|
|
65
|
+
anchorRow := -1
|
|
66
|
+
anchorCol := -1
|
|
67
|
+
selecting := false
|
|
68
|
+
editing := false
|
|
69
|
+
editValue := ''
|
|
70
|
+
_enterCommit = false
|
|
71
|
+
dataVersion := 0
|
|
72
|
+
sortKeys := []
|
|
73
|
+
|
|
74
|
+
cmp = (a, b) -> (a > b) - (a < b)
|
|
75
|
+
|
|
76
|
+
sortIndex ~=
|
|
77
|
+
dataVersion
|
|
78
|
+
len = @data.length
|
|
79
|
+
idx = new Array(len)
|
|
80
|
+
i = 0
|
|
81
|
+
while i < len
|
|
82
|
+
idx[i] = i
|
|
83
|
+
i++
|
|
84
|
+
if sortKeys.length > 0
|
|
85
|
+
idx.sort (a, b) ->
|
|
86
|
+
ki = 0
|
|
87
|
+
while ki < sortKeys.length
|
|
88
|
+
sk = sortKeys[ki]
|
|
89
|
+
va = @data[a][@columns[sk.col].key]
|
|
90
|
+
vb = @data[b][@columns[sk.col].key]
|
|
91
|
+
result = if sk.dir is 'asc' then cmp(va, vb) else cmp(vb, va)
|
|
92
|
+
if result isnt 0 then return result
|
|
93
|
+
ki++
|
|
94
|
+
a - b
|
|
95
|
+
idx
|
|
96
|
+
|
|
97
|
+
sortInfo ~=
|
|
98
|
+
arrows = new Array(@columns.length).fill('')
|
|
99
|
+
ranks = new Array(@columns.length).fill(0)
|
|
100
|
+
ki = 0
|
|
101
|
+
while ki < sortKeys.length
|
|
102
|
+
sk = sortKeys[ki]
|
|
103
|
+
arrows[sk.col] = if sk.dir is 'asc' then " ↓" else " ↑"
|
|
104
|
+
ranks[sk.col] = ki + 1
|
|
105
|
+
ki++
|
|
106
|
+
{ arrows, ranks }
|
|
107
|
+
|
|
108
|
+
cellRef ~= if activeRow >= 0 and activeCol >= 0 then "R#{activeRow + 1}:C#{activeCol + 1}" else ''
|
|
109
|
+
totalRows ~= @data.length
|
|
110
|
+
startRow ~= Math.max(0, Math.floor(scrollTop / @rowHeight) - @overscan)
|
|
111
|
+
endRow ~= Math.min(totalRows - 1, Math.ceil((scrollTop + containerHeight) / @rowHeight) + @overscan)
|
|
112
|
+
offsetY ~= startRow * @rowHeight
|
|
113
|
+
bottomSpace ~= Math.max(0, (totalRows - endRow - 1) * @rowHeight)
|
|
114
|
+
|
|
115
|
+
# --------------------------------------------------------------------------
|
|
116
|
+
# DOM Recycling — pooled <tr>/<td> elements, imperative updates on scroll
|
|
117
|
+
# --------------------------------------------------------------------------
|
|
118
|
+
#
|
|
119
|
+
# Instead of letting the reactive render loop recreate row components on
|
|
120
|
+
# every scroll frame, we maintain a fixed pool of <tr> elements and update
|
|
121
|
+
# cell textContent directly. This eliminates DOM allocation and GC pressure
|
|
122
|
+
# during fast scrolling — the most performance-critical path in the grid.
|
|
123
|
+
|
|
124
|
+
_trPool = []
|
|
125
|
+
_topSpacer = null
|
|
126
|
+
_botSpacer = null
|
|
127
|
+
_selBorder = null
|
|
128
|
+
|
|
129
|
+
_createSpacer: ->
|
|
130
|
+
tr = document.createElement('tr')
|
|
131
|
+
tr.className = 'spacer'
|
|
132
|
+
td = document.createElement('td')
|
|
133
|
+
td.style.padding = '0'
|
|
134
|
+
td.style.border = 'none'
|
|
135
|
+
td.style.lineHeight = '0'
|
|
136
|
+
td.style.fontSize = '0'
|
|
137
|
+
tr.appendChild(td)
|
|
138
|
+
tr
|
|
139
|
+
|
|
140
|
+
_ensurePool: (needed, numCols) ->
|
|
141
|
+
while _trPool.length < needed
|
|
142
|
+
tr = document.createElement('tr')
|
|
143
|
+
ci = 0
|
|
144
|
+
while ci < numCols
|
|
145
|
+
td = document.createElement('td')
|
|
146
|
+
tr.appendChild(td)
|
|
147
|
+
ci++
|
|
148
|
+
_trPool.push(tr)
|
|
149
|
+
# Add cells if column count grew
|
|
150
|
+
for tr in _trPool
|
|
151
|
+
while tr.children.length < numCols
|
|
152
|
+
tr.appendChild(document.createElement('td'))
|
|
153
|
+
|
|
154
|
+
# Effect: update tbody rows from pool on every viewport/data change
|
|
155
|
+
~>
|
|
156
|
+
return unless _ready
|
|
157
|
+
tbody = @_body
|
|
158
|
+
return unless tbody
|
|
159
|
+
numCols = @columns.length
|
|
160
|
+
count = endRow - startRow + 1
|
|
161
|
+
count = 0 if count < 0
|
|
162
|
+
|
|
163
|
+
@_ensurePool(count, numCols)
|
|
164
|
+
|
|
165
|
+
_topSpacer.children[0].colSpan = numCols
|
|
166
|
+
_topSpacer.children[0].style.height = "#{offsetY}px"
|
|
167
|
+
_botSpacer.children[0].colSpan = numCols
|
|
168
|
+
_botSpacer.children[0].style.height = "#{bottomSpace}px"
|
|
169
|
+
|
|
170
|
+
# Detach all current rows (fast: replaceChildren rebuilds in one
|
|
171
|
+
# reflow instead of N removeChild calls)
|
|
172
|
+
rows = [_topSpacer]
|
|
173
|
+
|
|
174
|
+
i = 0
|
|
175
|
+
n = startRow
|
|
176
|
+
while n <= endRow
|
|
177
|
+
if n >= 0 and n < totalRows
|
|
178
|
+
tr = _trPool[i]
|
|
179
|
+
di = sortIndex[n]
|
|
180
|
+
row = @data[di]
|
|
181
|
+
|
|
182
|
+
# Stripe class
|
|
183
|
+
if @striped and n % 2 is 1
|
|
184
|
+
tr.className = 'even'
|
|
185
|
+
else
|
|
186
|
+
tr.className = ''
|
|
187
|
+
|
|
188
|
+
# Update cells — only textContent, the cheapest DOM mutation
|
|
189
|
+
ci = 0
|
|
190
|
+
while ci < numCols
|
|
191
|
+
td = tr.children[ci]
|
|
192
|
+
c = @columns[ci]
|
|
193
|
+
v = row[c.key]
|
|
194
|
+
fmt = cellFormat(di, ci)
|
|
195
|
+
display = if fmt then fmt.format(v) else v
|
|
196
|
+
|
|
197
|
+
td.style.textAlign = c.align or 'left'
|
|
198
|
+
|
|
199
|
+
if c.type is 'checkbox'
|
|
200
|
+
# Checkbox cells need an <input> — reuse if present
|
|
201
|
+
inp = td.firstChild
|
|
202
|
+
unless inp and inp.tagName is 'INPUT'
|
|
203
|
+
td.textContent = ''
|
|
204
|
+
inp = document.createElement('input')
|
|
205
|
+
inp.type = 'checkbox'
|
|
206
|
+
inp.className = 'cell-checkbox'
|
|
207
|
+
inp.style.pointerEvents = 'none'
|
|
208
|
+
inp.style.margin = '0'
|
|
209
|
+
inp.style.verticalAlign = 'middle'
|
|
210
|
+
td.appendChild(inp)
|
|
211
|
+
inp.checked = !!v
|
|
212
|
+
else
|
|
213
|
+
# Plain text — fastest path
|
|
214
|
+
if td.firstChild and td.firstChild.nodeType is 3
|
|
215
|
+
td.firstChild.nodeValue = if display? then String(display) else ''
|
|
216
|
+
else
|
|
217
|
+
td.textContent = if display? then String(display) else ''
|
|
218
|
+
|
|
219
|
+
ci++
|
|
220
|
+
rows.push(tr)
|
|
221
|
+
i++
|
|
222
|
+
n++
|
|
223
|
+
|
|
224
|
+
rows.push(_botSpacer)
|
|
225
|
+
tbody.replaceChildren(...rows)
|
|
226
|
+
|
|
227
|
+
# Selection overlay — applies data-* attributes to active/selected cells
|
|
228
|
+
~>
|
|
229
|
+
return unless _ready
|
|
230
|
+
tbody = @_body
|
|
231
|
+
return unless tbody
|
|
232
|
+
trs = tbody.querySelectorAll('tr:not(.spacer)')
|
|
233
|
+
|
|
234
|
+
for el in tbody.querySelectorAll('td[data-active], td[data-selected]')
|
|
235
|
+
delete el.dataset.active
|
|
236
|
+
delete el.dataset.selected
|
|
237
|
+
el.style.borderColor = ''
|
|
238
|
+
|
|
239
|
+
unless _selBorder
|
|
240
|
+
_selBorder = document.createElement('div')
|
|
241
|
+
_selBorder.style.cssText = 'position:absolute;pointer-events:none;border:0;outline:1px solid #3b82f6;outline-offset:-1px;z-index:1;display:none'
|
|
242
|
+
@_container.appendChild(_selBorder)
|
|
243
|
+
|
|
244
|
+
if anchorRow >= 0 and activeRow >= 0 and anchorCol >= 0 and activeCol >= 0
|
|
245
|
+
minR = Math.min(anchorRow, activeRow)
|
|
246
|
+
maxR = Math.max(anchorRow, activeRow)
|
|
247
|
+
minC = Math.min(anchorCol, activeCol)
|
|
248
|
+
maxC = Math.max(anchorCol, activeCol)
|
|
249
|
+
multi = minR isnt maxR or minC isnt maxC
|
|
250
|
+
r = Math.max(minR, startRow)
|
|
251
|
+
while r <= Math.min(maxR, endRow)
|
|
252
|
+
tr = trs[r - startRow]
|
|
253
|
+
if tr
|
|
254
|
+
c = minC
|
|
255
|
+
while c <= maxC
|
|
256
|
+
td = tr.children[c]
|
|
257
|
+
if td
|
|
258
|
+
td.dataset.selected = ''
|
|
259
|
+
td.style.borderColor = '#bfdbfe'
|
|
260
|
+
c++
|
|
261
|
+
r++
|
|
262
|
+
|
|
263
|
+
if multi and not selecting
|
|
264
|
+
topTr = trs[Math.max(minR, startRow) - startRow]
|
|
265
|
+
botTr = trs[Math.min(maxR, endRow) - startRow]
|
|
266
|
+
if topTr and botTr
|
|
267
|
+
topTd = topTr.children[minC]
|
|
268
|
+
botTd = botTr.children[maxC]
|
|
269
|
+
if topTd and botTd
|
|
270
|
+
cRect = @_container.getBoundingClientRect()
|
|
271
|
+
tl = topTd.getBoundingClientRect()
|
|
272
|
+
br = botTd.getBoundingClientRect()
|
|
273
|
+
_selBorder.style.top = "#{tl.top - cRect.top + @_container.scrollTop}px"
|
|
274
|
+
_selBorder.style.left = "#{tl.left - cRect.left + @_container.scrollLeft}px"
|
|
275
|
+
_selBorder.style.width = "#{br.right - tl.left}px"
|
|
276
|
+
_selBorder.style.height = "#{br.bottom - tl.top}px"
|
|
277
|
+
_selBorder.style.display = 'block'
|
|
278
|
+
else
|
|
279
|
+
_selBorder.style.display = 'none'
|
|
280
|
+
else
|
|
281
|
+
_selBorder.style.display = 'none'
|
|
282
|
+
else
|
|
283
|
+
_selBorder.style.display = 'none'
|
|
284
|
+
else
|
|
285
|
+
_selBorder.style.display = 'none'
|
|
286
|
+
|
|
287
|
+
cursorRow = if selecting then anchorRow else activeRow
|
|
288
|
+
cursorCol = if selecting then anchorCol else activeCol
|
|
289
|
+
if cursorRow >= startRow and cursorRow <= endRow and cursorCol >= 0
|
|
290
|
+
tr = trs[cursorRow - startRow]
|
|
291
|
+
if tr
|
|
292
|
+
td = tr.children[cursorCol]
|
|
293
|
+
if td
|
|
294
|
+
td.dataset.active = ''
|
|
295
|
+
|
|
296
|
+
_removeEditor: ->
|
|
297
|
+
@_container.querySelector('.rip-grid-editor')?.remove()
|
|
298
|
+
|
|
299
|
+
openEditor: (initial, cursorEnd) ->
|
|
300
|
+
if activeRow < 0 or activeCol < 0 or editing
|
|
301
|
+
return
|
|
302
|
+
col = @columns[activeCol]
|
|
303
|
+
if col.type is 'checkbox'
|
|
304
|
+
@data[sortIndex[activeRow]][col.key] = not @data[sortIndex[activeRow]][col.key]
|
|
305
|
+
dataVersion++
|
|
306
|
+
return
|
|
307
|
+
val = @data[sortIndex[activeRow]][col.key]
|
|
308
|
+
orig = String(val ?? '')
|
|
309
|
+
if initial isnt undefined and col.type isnt 'select'
|
|
310
|
+
editValue = initial
|
|
311
|
+
else
|
|
312
|
+
editValue = orig
|
|
313
|
+
editing = true
|
|
314
|
+
@_removeEditor()
|
|
315
|
+
el = undefined
|
|
316
|
+
if col.type is 'select'
|
|
317
|
+
el = document.createElement('select')
|
|
318
|
+
el.className = 'rip-grid-editor'
|
|
319
|
+
el.style.borderRadius = '0'
|
|
320
|
+
for opt in (col.source or [])
|
|
321
|
+
o = document.createElement('option')
|
|
322
|
+
o.value = opt
|
|
323
|
+
o.textContent = opt
|
|
324
|
+
o.selected = (opt is editValue)
|
|
325
|
+
el.appendChild(o)
|
|
326
|
+
el.addEventListener 'change', (e) =>
|
|
327
|
+
editValue = e.target.value
|
|
328
|
+
@commitEditor()
|
|
329
|
+
el.addEventListener 'keydown', (e) => @_editorKeydown(e)
|
|
330
|
+
else
|
|
331
|
+
el = document.createElement('input')
|
|
332
|
+
el.className = 'rip-grid-editor'
|
|
333
|
+
el.type = 'text'
|
|
334
|
+
el.value = editValue
|
|
335
|
+
align = col.align or 'left'
|
|
336
|
+
el.style.textAlign = align
|
|
337
|
+
el.addEventListener 'input', (e) =>
|
|
338
|
+
editValue = e.target.value
|
|
339
|
+
el.addEventListener 'keydown', (e) => @_editorKeydown(e)
|
|
340
|
+
@_container.appendChild(el)
|
|
341
|
+
requestAnimationFrame =>
|
|
342
|
+
trs = @_body.querySelectorAll('tr:not(.spacer)')
|
|
343
|
+
tr = trs[activeRow - startRow]
|
|
344
|
+
if tr
|
|
345
|
+
td = tr.children[activeCol]
|
|
346
|
+
if td
|
|
347
|
+
tdRect = td.getBoundingClientRect()
|
|
348
|
+
cRect = @_container.getBoundingClientRect()
|
|
349
|
+
el.style.top = "#{tdRect.top - cRect.top + @_container.scrollTop}px"
|
|
350
|
+
el.style.left = "#{tdRect.left - cRect.left + @_container.scrollLeft}px"
|
|
351
|
+
el.style.width = "#{tdRect.width}px"
|
|
352
|
+
el.style.height = "#{tdRect.height}px"
|
|
353
|
+
if el.selectionStart isnt undefined
|
|
354
|
+
el.selectionStart = el.value.length
|
|
355
|
+
el.selectionEnd = el.value.length
|
|
356
|
+
el.focus()
|
|
357
|
+
|
|
358
|
+
commitEditor: ->
|
|
359
|
+
if editing
|
|
360
|
+
di = sortIndex[activeRow]
|
|
361
|
+
col = @columns[activeCol]
|
|
362
|
+
oldVal = @data[di][col.key]
|
|
363
|
+
val = editValue
|
|
364
|
+
fmt = cellFormat(di, activeCol)
|
|
365
|
+
if fmt and fmt.parse
|
|
366
|
+
val = fmt.parse(val)
|
|
367
|
+
if @beforeEdit
|
|
368
|
+
val = @beforeEdit(di, activeCol, oldVal, val)
|
|
369
|
+
if val is false then return @cancelEditor()
|
|
370
|
+
@data[di][col.key] = val
|
|
371
|
+
dataVersion++
|
|
372
|
+
editing = false
|
|
373
|
+
@_removeEditor()
|
|
374
|
+
@_container.focus() if @_container
|
|
375
|
+
if @afterEdit
|
|
376
|
+
@afterEdit(di, activeCol, oldVal, val)
|
|
377
|
+
|
|
378
|
+
cancelEditor: ->
|
|
379
|
+
editing = false
|
|
380
|
+
@_removeEditor()
|
|
381
|
+
anchorRow = activeRow
|
|
382
|
+
anchorCol = activeCol
|
|
383
|
+
@_container.focus() if @_container
|
|
384
|
+
|
|
385
|
+
_editorKeydown: (e) ->
|
|
386
|
+
numCols = @columns.length
|
|
387
|
+
switch e.key
|
|
388
|
+
when 'Enter'
|
|
389
|
+
e.preventDefault()
|
|
390
|
+
e.stopPropagation()
|
|
391
|
+
@commitEditor()
|
|
392
|
+
_enterCommit = true
|
|
393
|
+
when 'Tab'
|
|
394
|
+
e.preventDefault()
|
|
395
|
+
e.stopPropagation()
|
|
396
|
+
@commitEditor()
|
|
397
|
+
if e.shiftKey
|
|
398
|
+
if activeCol > 0
|
|
399
|
+
activeCol = activeCol - 1
|
|
400
|
+
else if activeRow > 0
|
|
401
|
+
activeRow = activeRow - 1
|
|
402
|
+
activeCol = numCols - 1
|
|
403
|
+
@scrollToRow(activeRow)
|
|
404
|
+
else
|
|
405
|
+
if activeCol < numCols - 1
|
|
406
|
+
activeCol = activeCol + 1
|
|
407
|
+
else if activeRow < totalRows - 1
|
|
408
|
+
activeRow = activeRow + 1
|
|
409
|
+
activeCol = 0
|
|
410
|
+
@scrollToRow(activeRow)
|
|
411
|
+
anchorRow = activeRow
|
|
412
|
+
anchorCol = activeCol
|
|
413
|
+
when 'Escape'
|
|
414
|
+
e.preventDefault()
|
|
415
|
+
e.stopPropagation()
|
|
416
|
+
@cancelEditor()
|
|
417
|
+
|
|
418
|
+
_resizeStart: (e) ->
|
|
419
|
+
e.stopPropagation()
|
|
420
|
+
e.preventDefault()
|
|
421
|
+
th = e.target.closest('th')
|
|
422
|
+
if not th then return
|
|
423
|
+
ci = th.cellIndex
|
|
424
|
+
startX = e.clientX
|
|
425
|
+
startW = @columns[ci].width
|
|
426
|
+
handle = e.target
|
|
427
|
+
handle.classList.add('active')
|
|
428
|
+
colEl = @_container.querySelector("colgroup").children[ci]
|
|
429
|
+
onMove = (ev) =>
|
|
430
|
+
w = Math.max(40, startW + ev.clientX - startX)
|
|
431
|
+
colEl.style.width = "#{w}px"
|
|
432
|
+
onUp = (ev) =>
|
|
433
|
+
document.removeEventListener('mousemove', onMove)
|
|
434
|
+
document.removeEventListener('mouseup', onUp)
|
|
435
|
+
handle.classList.remove('active')
|
|
436
|
+
w = Math.max(40, startW + ev.clientX - startX)
|
|
437
|
+
@columns = @columns.map (c, i) -> if i is ci then Object.assign({}, c, { width: w }) else c
|
|
438
|
+
document.addEventListener('mousemove', onMove)
|
|
439
|
+
document.addEventListener('mouseup', onUp)
|
|
440
|
+
|
|
441
|
+
_headerClick: (e) ->
|
|
442
|
+
if e.target.classList.contains('resize-handle') then return
|
|
443
|
+
th = e.target.closest('th')
|
|
444
|
+
if not th then return
|
|
445
|
+
ci = th.cellIndex
|
|
446
|
+
if e.shiftKey
|
|
447
|
+
existing = sortKeys.findIndex (sk) -> sk.col is ci
|
|
448
|
+
if existing >= 0
|
|
449
|
+
if sortKeys[existing].dir is 'asc'
|
|
450
|
+
sortKeys = sortKeys.map (sk, i) -> if i is existing then { col: sk.col, dir: 'desc' } else sk
|
|
451
|
+
else if sortKeys.length > 1
|
|
452
|
+
sortKeys = sortKeys.map (sk, i) -> if i is existing then { col: sk.col, dir: 'asc' } else sk
|
|
453
|
+
else
|
|
454
|
+
sortKeys = sortKeys.filter (_, i) -> i isnt existing
|
|
455
|
+
else
|
|
456
|
+
sortKeys = sortKeys.concat({ col: ci, dir: 'asc' })
|
|
457
|
+
else
|
|
458
|
+
if sortKeys.length is 1 and sortKeys[0].col is ci
|
|
459
|
+
if sortKeys[0].dir is 'asc'
|
|
460
|
+
sortKeys = [{ col: ci, dir: 'desc' }]
|
|
461
|
+
else
|
|
462
|
+
sortKeys = []
|
|
463
|
+
else
|
|
464
|
+
sortKeys = [{ col: ci, dir: 'asc' }]
|
|
465
|
+
dataVersion++
|
|
466
|
+
|
|
467
|
+
onDblclick: (e) ->
|
|
468
|
+
td = e.target.closest('td')
|
|
469
|
+
if td and not td.parentElement.classList.contains('spacer')
|
|
470
|
+
@openEditor()
|
|
471
|
+
|
|
472
|
+
onScroll: (e) ->
|
|
473
|
+
@commitEditor() if editing
|
|
474
|
+
@_nextST = e.currentTarget.scrollTop
|
|
475
|
+
if not @_rafPending
|
|
476
|
+
@_rafPending = true
|
|
477
|
+
requestAnimationFrame =>
|
|
478
|
+
scrollTop = @_nextST
|
|
479
|
+
@_rafPending = false
|
|
480
|
+
|
|
481
|
+
scrollToRow: (row) ->
|
|
482
|
+
el = @_container
|
|
483
|
+
if el
|
|
484
|
+
top = row * @rowHeight
|
|
485
|
+
if top < el.scrollTop
|
|
486
|
+
el.scrollTop = top
|
|
487
|
+
else if top + @rowHeight + @headerHeight > el.scrollTop + containerHeight
|
|
488
|
+
el.scrollTop = top + @rowHeight + @headerHeight - containerHeight
|
|
489
|
+
|
|
490
|
+
onMousedown: (e) ->
|
|
491
|
+
if e.button isnt 0
|
|
492
|
+
return
|
|
493
|
+
if e.target.classList.contains('rip-grid-editor')
|
|
494
|
+
return
|
|
495
|
+
@commitEditor() if editing
|
|
496
|
+
td = e.target.closest('td')
|
|
497
|
+
if td and not td.parentElement.classList.contains('spacer')
|
|
498
|
+
e.preventDefault()
|
|
499
|
+
@_container.focus() if @_container
|
|
500
|
+
selecting = true
|
|
501
|
+
el = @_container
|
|
502
|
+
rect = el.getBoundingClientRect()
|
|
503
|
+
ri = Math.floor((e.clientY - rect.top + el.scrollTop - @headerHeight) / @rowHeight)
|
|
504
|
+
if ri >= 0 and ri < totalRows
|
|
505
|
+
activeRow = ri
|
|
506
|
+
activeCol = td.cellIndex
|
|
507
|
+
if not e.shiftKey
|
|
508
|
+
anchorRow = ri
|
|
509
|
+
anchorCol = td.cellIndex
|
|
510
|
+
|
|
511
|
+
onMouseup: (e) ->
|
|
512
|
+
if selecting
|
|
513
|
+
r = anchorRow; c = anchorCol
|
|
514
|
+
anchorRow = activeRow; anchorCol = activeCol
|
|
515
|
+
activeRow = r; activeCol = c
|
|
516
|
+
selecting = false
|
|
517
|
+
|
|
518
|
+
onMousemove: (e) ->
|
|
519
|
+
if not selecting or e.buttons isnt 1
|
|
520
|
+
return
|
|
521
|
+
el = @_container
|
|
522
|
+
rect = el.getBoundingClientRect()
|
|
523
|
+
ri = Math.floor((e.clientY - rect.top + el.scrollTop - @headerHeight) / @rowHeight)
|
|
524
|
+
ri = Math.max(0, Math.min(totalRows - 1, ri))
|
|
525
|
+
td = e.target.closest('td')
|
|
526
|
+
ci = if td and not td.parentElement.classList.contains('spacer') then td.cellIndex else activeCol
|
|
527
|
+
activeRow = ri
|
|
528
|
+
activeCol = ci
|
|
529
|
+
@scrollToRow(ri)
|
|
530
|
+
|
|
531
|
+
_jumpToEdge: (row, col, dr, dc) ->
|
|
532
|
+
key = @columns[col]?.key
|
|
533
|
+
return { row, col } unless key
|
|
534
|
+
cur = @data[sortIndex[row]]?[key]
|
|
535
|
+
empty = not cur? or cur is ''
|
|
536
|
+
r = row + dr
|
|
537
|
+
c = col + dc
|
|
538
|
+
while r >= 0 and r < totalRows and c >= 0 and c < @columns.length
|
|
539
|
+
k = @columns[c].key
|
|
540
|
+
v = @data[sortIndex[r]]?[k]
|
|
541
|
+
ve = not v? or v is ''
|
|
542
|
+
if empty
|
|
543
|
+
return { row: r, col: c } unless ve
|
|
544
|
+
else
|
|
545
|
+
if ve then return { row: r - dr, col: c - dc }
|
|
546
|
+
r += dr
|
|
547
|
+
c += dc
|
|
548
|
+
row: Math.max(0, Math.min(totalRows - 1, r - dr))
|
|
549
|
+
col: Math.max(0, Math.min(@columns.length - 1, c - dc))
|
|
550
|
+
|
|
551
|
+
onKeydown: (e) ->
|
|
552
|
+
if editing
|
|
553
|
+
return
|
|
554
|
+
numCols = @columns.length
|
|
555
|
+
key = e.key
|
|
556
|
+
_enterCommit = false if key isnt 'Enter'
|
|
557
|
+
switch key
|
|
558
|
+
when 'ArrowDown'
|
|
559
|
+
e.preventDefault()
|
|
560
|
+
if activeRow < 0
|
|
561
|
+
activeRow = 0
|
|
562
|
+
activeCol = 0 if activeCol < 0
|
|
563
|
+
else if e.ctrlKey or e.metaKey
|
|
564
|
+
pos = @_jumpToEdge(activeRow, activeCol, 1, 0)
|
|
565
|
+
activeRow = pos.row
|
|
566
|
+
else if activeRow < totalRows - 1
|
|
567
|
+
activeRow = activeRow + 1
|
|
568
|
+
@scrollToRow(activeRow)
|
|
569
|
+
when 'ArrowUp'
|
|
570
|
+
e.preventDefault()
|
|
571
|
+
if e.ctrlKey or e.metaKey
|
|
572
|
+
pos = @_jumpToEdge(activeRow, activeCol, -1, 0)
|
|
573
|
+
activeRow = pos.row
|
|
574
|
+
activeCol = 0 if activeCol < 0
|
|
575
|
+
else if activeRow > 0
|
|
576
|
+
activeRow = activeRow - 1
|
|
577
|
+
@scrollToRow(activeRow)
|
|
578
|
+
when 'ArrowRight'
|
|
579
|
+
e.preventDefault()
|
|
580
|
+
if activeCol < 0
|
|
581
|
+
activeCol = 0
|
|
582
|
+
activeRow = 0 if activeRow < 0
|
|
583
|
+
else if e.ctrlKey or e.metaKey
|
|
584
|
+
pos = @_jumpToEdge(activeRow, activeCol, 0, 1)
|
|
585
|
+
activeCol = pos.col
|
|
586
|
+
else if activeCol < numCols - 1
|
|
587
|
+
activeCol = activeCol + 1
|
|
588
|
+
when 'ArrowLeft'
|
|
589
|
+
e.preventDefault()
|
|
590
|
+
if e.ctrlKey or e.metaKey
|
|
591
|
+
pos = @_jumpToEdge(activeRow, activeCol, 0, -1)
|
|
592
|
+
activeCol = pos.col
|
|
593
|
+
activeRow = 0 if activeRow < 0
|
|
594
|
+
else if activeCol > 0
|
|
595
|
+
activeCol = activeCol - 1
|
|
596
|
+
when 'Home'
|
|
597
|
+
e.preventDefault()
|
|
598
|
+
if e.ctrlKey or e.metaKey
|
|
599
|
+
activeRow = 0
|
|
600
|
+
activeCol = 0
|
|
601
|
+
@scrollToRow(0)
|
|
602
|
+
else
|
|
603
|
+
activeRow = 0 if activeRow < 0
|
|
604
|
+
activeCol = 0
|
|
605
|
+
when 'End'
|
|
606
|
+
e.preventDefault()
|
|
607
|
+
if e.ctrlKey or e.metaKey
|
|
608
|
+
activeRow = totalRows - 1
|
|
609
|
+
activeCol = numCols - 1
|
|
610
|
+
@scrollToRow(activeRow)
|
|
611
|
+
else
|
|
612
|
+
activeRow = 0 if activeRow < 0
|
|
613
|
+
activeCol = numCols - 1
|
|
614
|
+
when 'Tab'
|
|
615
|
+
e.preventDefault()
|
|
616
|
+
if activeRow < 0 or activeCol < 0
|
|
617
|
+
activeRow = 0
|
|
618
|
+
activeCol = 0
|
|
619
|
+
else if e.shiftKey
|
|
620
|
+
if activeCol > 0
|
|
621
|
+
activeCol = activeCol - 1
|
|
622
|
+
else if activeRow > 0
|
|
623
|
+
activeRow = activeRow - 1
|
|
624
|
+
activeCol = numCols - 1
|
|
625
|
+
@scrollToRow(activeRow)
|
|
626
|
+
else
|
|
627
|
+
if activeCol < numCols - 1
|
|
628
|
+
activeCol = activeCol + 1
|
|
629
|
+
else if activeRow < totalRows - 1
|
|
630
|
+
activeRow = activeRow + 1
|
|
631
|
+
activeCol = 0
|
|
632
|
+
@scrollToRow(activeRow)
|
|
633
|
+
when 'Enter'
|
|
634
|
+
e.preventDefault()
|
|
635
|
+
if activeRow < 0 or activeCol < 0
|
|
636
|
+
activeRow = 0
|
|
637
|
+
activeCol = 0
|
|
638
|
+
else if _enterCommit
|
|
639
|
+
_enterCommit = false
|
|
640
|
+
if e.shiftKey
|
|
641
|
+
if activeRow > 0
|
|
642
|
+
activeRow = activeRow - 1
|
|
643
|
+
anchorRow = activeRow
|
|
644
|
+
@scrollToRow(activeRow)
|
|
645
|
+
else
|
|
646
|
+
if activeRow < totalRows - 1
|
|
647
|
+
activeRow = activeRow + 1
|
|
648
|
+
anchorRow = activeRow
|
|
649
|
+
@scrollToRow(activeRow)
|
|
650
|
+
@openEditor()
|
|
651
|
+
return
|
|
652
|
+
else
|
|
653
|
+
@openEditor()
|
|
654
|
+
return
|
|
655
|
+
when 'F2'
|
|
656
|
+
e.preventDefault()
|
|
657
|
+
if activeRow >= 0 and activeCol >= 0
|
|
658
|
+
@openEditor(undefined, true)
|
|
659
|
+
return
|
|
660
|
+
when 'PageDown'
|
|
661
|
+
e.preventDefault()
|
|
662
|
+
rows = Math.floor(containerHeight / @rowHeight)
|
|
663
|
+
activeRow = Math.min(totalRows - 1, activeRow + rows)
|
|
664
|
+
activeCol = 0 if activeCol < 0
|
|
665
|
+
@scrollToRow(activeRow)
|
|
666
|
+
when 'PageUp'
|
|
667
|
+
e.preventDefault()
|
|
668
|
+
rows = Math.floor(containerHeight / @rowHeight)
|
|
669
|
+
activeRow = Math.max(0, activeRow - rows)
|
|
670
|
+
activeCol = 0 if activeCol < 0
|
|
671
|
+
@scrollToRow(activeRow)
|
|
672
|
+
when 'Escape'
|
|
673
|
+
if anchorRow isnt activeRow or anchorCol isnt activeCol
|
|
674
|
+
anchorRow = activeRow
|
|
675
|
+
anchorCol = activeCol
|
|
676
|
+
else
|
|
677
|
+
activeRow = -1
|
|
678
|
+
activeCol = -1
|
|
679
|
+
anchorRow = -1
|
|
680
|
+
anchorCol = -1
|
|
681
|
+
return
|
|
682
|
+
when ' '
|
|
683
|
+
if activeRow >= 0 and activeCol >= 0
|
|
684
|
+
col = @columns[activeCol]
|
|
685
|
+
if col.type is 'checkbox'
|
|
686
|
+
e.preventDefault()
|
|
687
|
+
@data[sortIndex[activeRow]][col.key] = not @data[sortIndex[activeRow]][col.key]
|
|
688
|
+
dataVersion++
|
|
689
|
+
return
|
|
690
|
+
when 'Delete', 'Backspace'
|
|
691
|
+
if activeRow >= 0 and activeCol >= 0 and not e.ctrlKey and not e.metaKey
|
|
692
|
+
e.preventDefault()
|
|
693
|
+
col = @columns[activeCol]
|
|
694
|
+
if col.type is 'checkbox'
|
|
695
|
+
@data[sortIndex[activeRow]][col.key] = false
|
|
696
|
+
else
|
|
697
|
+
fmt = cellFormat(sortIndex[activeRow], activeCol)
|
|
698
|
+
@data[sortIndex[activeRow]][col.key] = if fmt and fmt.parse then fmt.parse('') else ''
|
|
699
|
+
dataVersion++
|
|
700
|
+
return
|
|
701
|
+
when 'a'
|
|
702
|
+
if e.ctrlKey or e.metaKey
|
|
703
|
+
e.preventDefault()
|
|
704
|
+
anchorRow = 0
|
|
705
|
+
anchorCol = 0
|
|
706
|
+
activeRow = totalRows - 1
|
|
707
|
+
activeCol = numCols - 1
|
|
708
|
+
return
|
|
709
|
+
when 'c'
|
|
710
|
+
if (e.ctrlKey or e.metaKey) and activeRow >= 0 and activeCol >= 0
|
|
711
|
+
e.preventDefault()
|
|
712
|
+
@copySelection()
|
|
713
|
+
return
|
|
714
|
+
when 'v'
|
|
715
|
+
if e.ctrlKey or e.metaKey
|
|
716
|
+
e.preventDefault()
|
|
717
|
+
@pasteAtActive()
|
|
718
|
+
return
|
|
719
|
+
when 'x'
|
|
720
|
+
if (e.ctrlKey or e.metaKey) and activeRow >= 0 and activeCol >= 0
|
|
721
|
+
e.preventDefault()
|
|
722
|
+
@cutSelection()
|
|
723
|
+
return
|
|
724
|
+
else
|
|
725
|
+
if key.length is 1 and not e.ctrlKey and not e.metaKey and not e.altKey
|
|
726
|
+
if activeRow >= 0 and activeCol >= 0
|
|
727
|
+
ct = @columns[activeCol].type
|
|
728
|
+
if ct isnt 'checkbox' and ct isnt 'select'
|
|
729
|
+
e.preventDefault()
|
|
730
|
+
@openEditor(key)
|
|
731
|
+
return
|
|
732
|
+
|
|
733
|
+
extending = e.shiftKey and key isnt 'Tab' and key isnt 'Enter'
|
|
734
|
+
if not extending
|
|
735
|
+
anchorRow = activeRow
|
|
736
|
+
anchorCol = activeCol
|
|
737
|
+
|
|
738
|
+
# --------------------------------------------------------------------------
|
|
739
|
+
# Clipboard — copy, cut, paste via navigator.clipboard (TSV format)
|
|
740
|
+
# --------------------------------------------------------------------------
|
|
741
|
+
|
|
742
|
+
_selectionBounds: ->
|
|
743
|
+
minR = Math.min(anchorRow, activeRow)
|
|
744
|
+
maxR = Math.max(anchorRow, activeRow)
|
|
745
|
+
minC = Math.min(anchorCol, activeCol)
|
|
746
|
+
maxC = Math.max(anchorCol, activeCol)
|
|
747
|
+
{ minR, maxR, minC, maxC }
|
|
748
|
+
|
|
749
|
+
_selectionToTSV: ->
|
|
750
|
+
{ minR, maxR, minC, maxC } = @_selectionBounds()
|
|
751
|
+
lines = []
|
|
752
|
+
r = minR
|
|
753
|
+
while r <= maxR
|
|
754
|
+
di = sortIndex[r]
|
|
755
|
+
row = @data[di]
|
|
756
|
+
cells = []
|
|
757
|
+
c = minC
|
|
758
|
+
while c <= maxC
|
|
759
|
+
v = row[@columns[c].key]
|
|
760
|
+
s = if v? then String(v) else ''
|
|
761
|
+
if s.includes('\t') or s.includes('\n') or s.includes('"')
|
|
762
|
+
s = '"' + s.replace(/"/g, '""') + '"'
|
|
763
|
+
cells.push(s)
|
|
764
|
+
c++
|
|
765
|
+
lines.push(cells.join('\t'))
|
|
766
|
+
r++
|
|
767
|
+
lines.join('\n')
|
|
768
|
+
|
|
769
|
+
_parseTSV: (text) ->
|
|
770
|
+
rows = []
|
|
771
|
+
lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n')
|
|
772
|
+
for line, li in lines
|
|
773
|
+
continue if line is '' and li is lines.length - 1
|
|
774
|
+
if line.includes('"')
|
|
775
|
+
cells = []
|
|
776
|
+
i = 0
|
|
777
|
+
while i < line.length
|
|
778
|
+
if line[i] is '"'
|
|
779
|
+
j = i + 1
|
|
780
|
+
val = ''
|
|
781
|
+
while j < line.length
|
|
782
|
+
if line[j] is '"'
|
|
783
|
+
if line[j + 1] is '"'
|
|
784
|
+
val += '"'
|
|
785
|
+
j += 2
|
|
786
|
+
else
|
|
787
|
+
j++
|
|
788
|
+
break
|
|
789
|
+
else
|
|
790
|
+
val += line[j]
|
|
791
|
+
j++
|
|
792
|
+
cells.push(val)
|
|
793
|
+
i = j + 1
|
|
794
|
+
else
|
|
795
|
+
j = line.indexOf('\t', i)
|
|
796
|
+
if j is -1
|
|
797
|
+
cells.push(line.slice(i))
|
|
798
|
+
break
|
|
799
|
+
else
|
|
800
|
+
cells.push(line.slice(i, j))
|
|
801
|
+
i = j + 1
|
|
802
|
+
rows.push(cells)
|
|
803
|
+
else
|
|
804
|
+
rows.push(line.split('\t'))
|
|
805
|
+
rows
|
|
806
|
+
|
|
807
|
+
copySelection: ->
|
|
808
|
+
return if activeRow < 0 or activeCol < 0
|
|
809
|
+
tsv = @_selectionToTSV()
|
|
810
|
+
navigator.clipboard.writeText(tsv) if navigator.clipboard
|
|
811
|
+
|
|
812
|
+
cutSelection: ->
|
|
813
|
+
return if activeRow < 0 or activeCol < 0
|
|
814
|
+
@copySelection()
|
|
815
|
+
{ minR, maxR, minC, maxC } = @_selectionBounds()
|
|
816
|
+
r = minR
|
|
817
|
+
while r <= maxR
|
|
818
|
+
di = sortIndex[r]
|
|
819
|
+
c = minC
|
|
820
|
+
while c <= maxC
|
|
821
|
+
col = @columns[c]
|
|
822
|
+
if col.type is 'checkbox'
|
|
823
|
+
@data[di][col.key] = false
|
|
824
|
+
else
|
|
825
|
+
fmt = cellFormat(di, c)
|
|
826
|
+
@data[di][col.key] = if fmt and fmt.parse then fmt.parse('') else ''
|
|
827
|
+
c++
|
|
828
|
+
r++
|
|
829
|
+
dataVersion++
|
|
830
|
+
|
|
831
|
+
pasteAtActive: ->
|
|
832
|
+
return if activeRow < 0 or activeCol < 0
|
|
833
|
+
return unless navigator.clipboard
|
|
834
|
+
navigator.clipboard.readText().then (text) =>
|
|
835
|
+
return unless text
|
|
836
|
+
rows = @_parseTSV(text)
|
|
837
|
+
return unless rows.length
|
|
838
|
+
numCols = @columns.length
|
|
839
|
+
r = 0
|
|
840
|
+
while r < rows.length
|
|
841
|
+
ri = activeRow + r
|
|
842
|
+
break if ri >= totalRows
|
|
843
|
+
di = sortIndex[ri]
|
|
844
|
+
c = 0
|
|
845
|
+
while c < rows[r].length
|
|
846
|
+
ci = activeCol + c
|
|
847
|
+
break if ci >= numCols
|
|
848
|
+
col = @columns[ci]
|
|
849
|
+
val = rows[r][c]
|
|
850
|
+
if col.type is 'checkbox'
|
|
851
|
+
@data[di][col.key] = val is 'true' or val is '1' or val is 'yes'
|
|
852
|
+
else
|
|
853
|
+
fmt = cellFormat(di, ci)
|
|
854
|
+
@data[di][col.key] = if fmt and fmt.parse then fmt.parse(val) else val
|
|
855
|
+
c++
|
|
856
|
+
r++
|
|
857
|
+
dataVersion++
|
|
858
|
+
|
|
859
|
+
# Public API
|
|
860
|
+
getCell: (row, col) -> @data[row][@columns[col].key]
|
|
861
|
+
setCell: (row, col, value) -> @data[row][@columns[col].key] = value; dataVersion++
|
|
862
|
+
getData: -> @data
|
|
863
|
+
setData: (d) -> @data = d; dataVersion++
|
|
864
|
+
sort: (col, dir) -> sortKeys = if dir then [{ col, dir }] else []; dataVersion++
|
|
865
|
+
|
|
866
|
+
mounted: ->
|
|
867
|
+
_topSpacer = @_createSpacer()
|
|
868
|
+
_botSpacer = @_createSpacer()
|
|
869
|
+
_ready = true
|
|
870
|
+
if @_container
|
|
871
|
+
handler = (entries) =>
|
|
872
|
+
for entry in entries
|
|
873
|
+
containerHeight = entry.contentRect.height
|
|
874
|
+
@_resizeObs = new ResizeObserver(handler)
|
|
875
|
+
@_resizeObs.observe(@_container)
|
|
876
|
+
|
|
877
|
+
unmounted: ->
|
|
878
|
+
if @_resizeObs
|
|
879
|
+
@_resizeObs.disconnect()
|
|
880
|
+
|
|
881
|
+
render
|
|
882
|
+
div.grid-container ref: "_container", role: "grid", tabindex: "0",
|
|
883
|
+
$editing: editing?!,
|
|
884
|
+
$selecting: selecting?!
|
|
885
|
+
table.rip-grid
|
|
886
|
+
colgroup
|
|
887
|
+
for c in @columns
|
|
888
|
+
col style: "width: #{c.width}px"
|
|
889
|
+
thead
|
|
890
|
+
tr @click: @_headerClick
|
|
891
|
+
for c, ci in @columns
|
|
892
|
+
th style: "text-align: #{c.align or 'left'}; cursor: pointer",
|
|
893
|
+
$sorted: (if sortInfo.arrows[ci] then (if sortInfo.arrows[ci].includes('↓') then 'asc' else 'desc') else undefined)
|
|
894
|
+
= c.title
|
|
895
|
+
span.sort-arrow
|
|
896
|
+
sortInfo.arrows[ci]
|
|
897
|
+
if sortKeys.length > 1 and sortInfo.ranks[ci] > 0
|
|
898
|
+
span.sort-badge
|
|
899
|
+
"#{sortInfo.ranks[ci]}"
|
|
900
|
+
span.resize-handle @mousedown: @_resizeStart
|
|
901
|
+
tbody ref: "_body"
|