-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathextended_toc.js
More file actions
759 lines (669 loc) · 26.8 KB
/
extended_toc.js
File metadata and controls
759 lines (669 loc) · 26.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
// Extended Table of Contents JavaScript functionality
// Designed to be included directly in a <script> tag
// This file contains all the JavaScript code for the ExtendedTableOfContents features, but is not per se a valid/executable as javascript directly
// This file assumes that some variables are already defined in the preamble of the <script> tag like `scrollIntoView`, these are directly added in the julia function in `extended_toc_simple.jl`
function scroll_to(h, config = {
behavior: 'smooth',
block: 'start',
}) {
scrollIntoView(h, config).then(() =>
// sometimes it doesn't scroll to the right place
// solution: try a second time!
scrollIntoView(h, config)
)
}
// Basic functionality
let cell = currentScript.closest('pluto-cell')
let pluto_actions = cell._internal_pluto_actions
let toc = document.querySelector('nav.plutoui-toc')
function getRow(el) {
const row = el?.closest('.toc-row')
return row
}
function get_link_id(el) {
const row = getRow(el)
if (_.isNil(row)) { return null }
return row.linkedHeadingId
}
function getHeadingLevel(row) {
const a = row.querySelector('a')
// We return the link class without the first H
return Number(a.classList[0].slice(1))
}
function generateChecker(selector) {
switch (typeof selector) {
case 'string':
const func = el => {
return el.matches(selector)
}
return func
case 'function':
return selector
default:
console.error(`The type (${typeof selector}) of the provided argument is not valid`)
}
}
// Get next and previous sibling, adapted from:
// https://gomakethings.com/finding-the-next-and-previous-sibling-elements-that-match-a-selector-with-vanilla-js/
var getNextSibling = function (elem, selector) {
// Added to return undefined is called with undefined
if (_.isNil(elem)) {return undefined}
// Get the next sibling element
var sibling = elem.nextElementSibling;
// If there's no selector, return the first sibling
if (!selector) return sibling;
const checker = generateChecker(selector)
// If the sibling matches our selector, use it
// If not, jump to the next sibling and continue the loop
while (sibling) {
if (checker(sibling)) return sibling;
sibling = sibling.nextElementSibling
}
};
var getPreviousSibling = function (elem, selector) {
// Added to return undefined is called with undefined
if (_.isNil(elem)) {return undefined}
// Get the next sibling element
var sibling = elem.previousElementSibling;
// If there's no selector, return the first sibling
if (!selector) return sibling;
const checker = generateChecker(selector)
// If the sibling matches our selector, use it
// If not, jump to the next sibling and continue the loop
while (sibling) {
if (checker(sibling)) return sibling;
sibling = sibling.previousElementSibling;
}
};
// Get the last toc entry descendant from the provided one
function getLastDescendant(el) {
const row = getRow(el)
if (_.isNil(row)) {return}
const children = row.directChildren
if (_.isEmpty(children)) {
return row
} else {
return getLastDescendant(_.last(children))
}
}
// Find all the cell ids contained within the target toc rows and all its descendants
function getBlockIds(el) {
const row = getRow(el)
if (_.isNil(row)) {return}
function getIndex(row) {
return editor_state.notebook.cell_order.indexOf(get_link_id(row))
}
const start = getIndex(row)
const lastChild = getLastDescendant(row)
const end = getIndex(getNextSibling(lastChild, '.toc-row'))
return editor_state.notebook.cell_order.slice(start, end < 0 ? Infinity : end)
}
window.toc_utils = {
getNextSibling,
getPreviousSibling,
getLastDescendant,
getBlockIds,
}
// Functions to set and propagate hidden and collapsed states
function propagate_parent(div, parent=null) {
if (parent != null) {
div.allParents = _.union(div.allParents, [parent])
}
if (_.isEmpty(div.directChildren)) {return}
for (const child of div.directChildren) {
propagate_parent(child, parent ?? div)
}
}
// Returns true if the current hidden/collapsed toc state is different from the one saved in the file within cell metadata
function stateDiffersFile(state) {
for (const [id, st] of _.entries(state)) {
if (has_cell_attribute(id, 'toc-hidden') != st.hidden) { return true }
if (has_cell_attribute(id, 'toc-collapsed') != st.collapsed) { return true }
}
return false
}
function set_state(div, state, value, init = false) {
div.classList.toggle(state, value)
if (!init) {
window.toc_state[get_link_id(div)][state] = value
toc.classList.toggle('file-state-differs', stateDiffersFile(window.toc_state))
}
if (_.isEmpty(div.directChildren)) {return}
for (const child of div.directChildren) {
propagate_state(child, state)
}
}
function propagate_state(div, state) {
let new_state = `parent-${state}`
div.classList.toggle(new_state, false)
// Check the parents for the state
for (const parent of div.allParents) {
if (parent.classList.contains(state)) {
div.classList.toggle(new_state, true)
break
}
}
if (_.isEmpty(div.directChildren)) {return}
for (const child of div.directChildren) {
propagate_state(child, state)
}
}
// Floating UI functionality
const floating_ui = await import('https://esm.sh/@floating-ui/dom@1.7.4')
// Cell attributes modification
function has_cell_attribute(cell_id, attr) {
const md = editor_state.notebook.cell_inputs[cell_id].metadata
return _.includes(md["custom_attrs"], attr)
}
function add_cell_attributes(cell_id, attrs) {
pluto_actions.update_notebook((notebook) => {
let md = notebook.cell_inputs[cell_id].metadata
md["custom_attrs"] = _.union(md["custom_attrs"], attrs)
})
let cell = document.getElementById(cell_id)
for (let attr of attrs) {
cell.toggleAttribute(attr, true)
}
}
function remove_cell_attributes(cell_id, attrs) {
pluto_actions.update_notebook((notebook) => {
let md = notebook.cell_inputs[cell_id].metadata
let after = _.difference(md["custom_attrs"], attrs)
if (_.isEmpty(after)) {
delete md["custom_attrs"]
} else {
md["custom_attrs"] = after
}
})
let cell = document.getElementById(cell_id)
for (let attr of attrs) {
cell.toggleAttribute(attr, false)
}
}
function toggle_cell_attribute(cell_id, attr, force='toggle') {
pluto_actions.update_notebook((notebook) => {
let md = notebook.cell_inputs[cell_id].metadata
let f = force == 'toggle' ? _.xor : force ? _.union : _.difference
let after = f(md["custom_attrs"], [attr])
if (_.isEmpty(after)) {
delete md["custom_attrs"]
} else {
md["custom_attrs"] = after
}
})
let cell = document.getElementById(cell_id)
force == 'toggle' ? cell.toggleAttribute(attr) : cell.toggleAttribute(attr, force)
}
// Notebook attributes modification
function add_notebook_attributes(attrs) {
pluto_actions.update_notebook((notebook) => {
let md = notebook.metadata
md["custom_attrs"] = _.union(md["custom_attrs"], attrs)
})
let notebook = document.querySelector('pluto-notebook')
for (let attr of attrs) {
notebook.toggleAttribute(attr, true)
}
}
function remove_notebook_attributes(attrs) {
pluto_actions.update_notebook((notebook) => {
let md = notebook.metadata
let after = _.difference(md["custom_attrs"], attrs)
if (_.isEmpty(after)) {
delete md["custom_attrs"]
} else {
md["custom_attrs"] = after
}
})
let notebook = document.querySelector('pluto-notebook')
for (let attr of attrs) {
notebook.toggleAttribute(attr, false)
}
}
function toggle_notebook_attribute(attr, force='toggle') {
pluto_actions.update_notebook((notebook) => {
let md = notebook.metadata
let f = force == 'toggle' ? _.xor : force ? _.union : _.difference
let after = f(md["custom_attrs"], [attr])
if (_.isEmpty(after)) {
delete md["custom_attrs"]
} else {
md["custom_attrs"] = after
}
})
let notebook = document.querySelector('pluto-notebook')
force == 'toggle' ? notebook.toggleAttribute(attr) : notebook.toggleAttribute(attr, force)
}
if (force_hide_enabled) {
toggle_notebook_attribute('hide-enabled',true)
}
// Mutation observer functionality
function toggle_state(name) {
return (e) => {
e.preventDefault()
e.stopPropagation()
let div = e.target.closest('div')
const new_val = !div.classList.contains(name)
set_state(div, name, new_val)
}
}
// This function will return a vector of cells indices (intended to index the list of cells in their order as return by `document.querySelectorAll('pluto-cell')`) to identify start and end index of cells to hide because their corresponding ToC row is hidden
function tocrows_hidden_indices() {
let hidden_start_stop_idxs = [] // This will track the start and end index of cells to hide because their corresponding ToC row is hidden. The cell at the end idx should already be considered as `shown`
let current_hidden_status = false // This signals whether the current is hidden or not
if (hide_preamble) {
hidden_start_stop_idxs.push(-1)
current_hidden_status = true
}
const divs = toc.querySelectorAll('div.toc-row')
const cells = document.querySelectorAll('pluto-cell')
let running_idx = 0 // This will track the index of the main cell
for (const div of divs) {
const hidden = div.classList.contains('hidden') || div.classList.contains('parent-hidden')
const linked_cell_id = get_link_id(div)
// We iterate through the main cells till we find the one with the linked cell id
while (running_idx < cells.length && cells[running_idx].id !== linked_cell_id) {
running_idx++
}
if (running_idx >= cells.length) {
console.warn(`Cell with id ${linked_cell_id} not found`)
continue
}
if (hidden != current_hidden_status) {
hidden_start_stop_idxs.push(running_idx)
current_hidden_status = hidden
}
}
// If the number of elements is odd, we just add as last element the number of cells as we want consistent start and stop indices
if (hidden_start_stop_idxs.length % 2 !== 0) {
hidden_start_stop_idxs.push(cells.length)
}
return hidden_start_stop_idxs
}
// This function takes as input a vector of start/stop indices of hidden cells (based on toc rows) and apply the `toc-hidden` cell attribute (we don't use a class as pluto constantly rewrites the cell class) to identify that a cell must be hidden
function hide_main_cells(indices) {
const cells = document.querySelectorAll('pluto-cell')
let current_hidden_status = false
let next_target = indices.shift()
for (let i = 0; i < cells.length; i++) {
while (i > next_target) {
next_target = indices.shift()
current_hidden_status = !current_hidden_status
}
if (i == next_target) {
next_target = indices.shift()
current_hidden_status = !current_hidden_status
}
cells[i].toggleAttribute('toc-hidden', current_hidden_status)
}
}
window.toc_utils.tocrows_hidden_indices = tocrows_hidden_indices
function update_hidden_tocrows() {
hide_main_cells(tocrows_hidden_indices())
}
// Reposition the hide_container using the floating-ui library
function repositionTooltip(e) {
const { computePosition, offset } = floating_ui
const ref = e.target
const tooltip = ref.querySelector('.toc-hide-container')
if (_.isNil(tooltip)) {
console.warn("Something went wrong, no tooltip found")
return
}
// Get the scroll container - this is crucial for proper offset calculation
const scrollContainer = document.querySelector('main') || document.documentElement
const scrollTop = scrollContainer.scrollTop || window.pageYOffset
computePosition(ref, tooltip, {
placement: "left",
strategy: "fixed",
}).then(pos => {
tooltip.style.top = pos.y - scrollTop + "px"
})
}
function process_row(div, history, old_state, new_state) {
// We find all the mainHeadings and tocRows to find matching pairs
// Try the original selector first
const tocRows = Array.from(toc.querySelectorAll('section div.toc-row'))
const mainHeadings = Array.from(document.querySelectorAll('main h1, main h2, main h3, main h4, main h5, main h6'))
const headingIdx = tocRows.indexOf(div)
const correspondingHeading = mainHeadings[headingIdx]
div.linkedHeading = correspondingHeading
div.linkedHeadingId = correspondingHeading.closest('pluto-cell').id
// We add the separator
div.insertAdjacentElement('beforebegin', html`<div class='toc-row-separator'></div>`)
// If we are just processing the first element (so the last row) we also add a separator at the bottom
if (_.isEmpty(new_state) && _.every(history, _.isEmpty)) {
div.insertAdjacentElement('afterend', html`<div class='toc-row-separator'></div>`)
}
// We add the reposition event to the row
div.addEventListener('mouseenter', repositionTooltip)
let id = get_link_id(div)
const a = div.querySelector('a')
let old_f = a.onclick;
a.onclick = (e) => {
e.preventDefault()
// We avoid triggering the click if coming out of a drag
if (toc.classList.contains('recent-drag')) { return }
old_f(e)
}
const level = getHeadingLevel(div)
if (level > 1) {
history[level].unshift(div)
}
// We iterate through the history and assign the direct children if they exist, while clearing lower levels history
for (let i = 6; i > level; i--) {
if (_.isEmpty(history[i])) {continue}
if (div.directChildren != undefined) {throw('multiple level with children, unexpected!')}
div.directChildren = history[i]
history[i] = [] // empty array
}
const collapse_span = a.insertAdjacentElement('afterbegin', html`<span class='toc-icon toc-collapse'>`)
let hide_style = `--height: ${a.clientHeight}px`
const hide_container = div.insertAdjacentElement('afterbegin', html`<span class='toc-hide-container' style='${hide_style}'>`)
const hide_span = hide_container.insertAdjacentElement('afterbegin', html`<span class='toc-icon toc-hide'>`)
hide_span.addEventListener('click', (e) => {
toggle_state('hidden')(e)
update_hidden_tocrows()
})
if (div.directChildren == undefined) {
collapse_span.classList.toggle('no-children', true)
} else {
propagate_parent(div)
collapse_span.addEventListener('click', toggle_state('collapsed'))
}
let md = editor_state.notebook.cell_inputs[id].metadata
let collapsed = old_state[id]?.collapsed ?? _.includes(md['custom_attrs'], 'toc-collapsed')
let hidden = old_state[id]?.hidden ?? _.includes(md['custom_attrs'], 'toc-hidden')
set_state(div, 'collapsed', collapsed, true)
set_state(div, 'hidden', hidden, true)
new_state[id] = { collapsed, hidden }
}
const observer = new MutationObserver(() => {
console.log('=== ExtendedTableOfContents MutationObserver triggered ===')
const rows = toc.querySelectorAll('section div.toc-row')
let old_state = window.toc_state ?? {}
let new_state = {}
let history = {
2: [],
3: [],
4: [],
5: [],
6: [],
}
for (const row of [...rows].reverse()) {
process_row(row, history, old_state, new_state)
}
window.toc_state = new_state
toc.classList.toggle('file-state-differs', stateDiffersFile(new_state))
update_hidden_tocrows()
})
observer.observe(toc, {childList: true})
// Move entries handler functionality
const { default: interact } = await import('https://esm.sh/interactjs')
// We have to enable dynamicDrop to have dropzone recomputed on dragmove
interact.dynamicDrop(true)
function dragEnabler(e) {
if (!toc.classList.contains('drag_enabled') || e.key !== 'Shift') { return true }
switch (e.type) {
case "keydown":
toc.classList.add('allow_all_drop')
break;
case "keyup":
toc.classList.remove('allow_all_drop')
break;
}
updateActiveSeparator()
}
const window_events = {
keydown: dragEnabler,
keyup: dragEnabler,
}
// Function to add all event listeners from an events object
function addWindowEventListeners(events) {
for (const [eventType, handler] of Object.entries(events)) {
window.addEventListener(eventType, handler)
}
}
// Function to remove all event listeners from an events object
function removeWindowEventListeners(events) {
for (const [eventType, handler] of Object.entries(events)) {
window.removeEventListener(eventType, handler)
}
}
// Add event listeners directly
addWindowEventListeners(window_events)
// Interact.js part
let activeDrop = undefined
function tagAdjacentSeparators(el, active) {
const next = getNextSibling(el, '.toc-row-separator')
const prev = getPreviousSibling(el, '.toc-row-separator')
if (active) {
next?.classList.add('noshow')
prev?.classList.add('noshow')
} else {
next?.classList.remove('noshow')
prev?.classList.remove('noshow')
}
}
function getSeparator(startElement, below, allowAll, headingLevel = 8) {
let separator
if (below) {
const selector = '.toc-row:not(.parent-collapsed)'
const checkerFunc = allowAll ? generateChecker(selector) : (el) => {
if (!el.matches(selector)) { return false }
// Check for the right heading level
for (let i = headingLevel; i > 0; i--) {
const cl = "H" + i
if (el.classList.contains(cl)) { return true }
}
return false
}
const validRow = getNextSibling(startElement, checkerFunc)
// If the activeDrop is the last row or the the last non-collapsed one, the validRow will be `undefined`, so in that case we take the last separator
separator = getPreviousSibling(validRow, '.toc-row-separator') ?? _.last(toc.querySelectorAll('.toc-row-separator'))
} else {
separator = getPreviousSibling(startElement, '.toc-row-separator')
}
return separator
}
function getHigherParent(row, level) {
const currentLevel = getHeadingLevel(row)
if (currentLevel <= level) {return row}
for (const par of row.allParents) {
// Parents cycle from higher level to lower levels
if (getHeadingLevel(par) <= level) {return par}
}
return row
}
let uncollapsed = []
function reCollapse(row) {
const parents = row?.allParents ?? []
const toRemove = _.difference(uncollapsed, [...parents, row])
for (const el of toRemove) {
// debugger
set_state(el, "collapsed", true)
_.remove(uncollapsed, x => x === el)
}
}
function updateDropZone(row) {
const prev = toc.querySelector('.toc-row.active_drop')
if (_.isNil(row) || prev === row) {return}
if (prev?.timeoutId) {
clearTimeout(prev.timeoutId)
prev.timeoutId = undefined
}
prev?.classList.remove('active_drop')
row.classList.add('active_drop')
reCollapse(row)
if (row.classList.contains('collapsed')) {
row.timeoutId = setTimeout(() => {
uncollapsed.push(row)
set_state(row, "collapsed", false)
updateActiveSeparator()
}, 500)
}
activeDrop = row
}
function updateActiveSeparator() {
const e = toc.lastDragEvent
if (_.isNil(e)) { return }
const elBelow = document.elementFromPoint(e.client.x, e.client.y)
if (!elBelow.matches('.plutoui-toc :scope')) {
// We are out of the ToC, recollapse and remove active separator
reCollapse(undefined)
toc.querySelector('.toc-row-separator.active')?.classList.remove('active')
return
}
const rowBelow = getRow(elBelow)
updateDropZone(rowBelow)
if (_.isNil(activeDrop)) {return}
const allowAll = toc.classList.contains('allow_all_drop')
const headingLevel = getHeadingLevel(toc.draggedElement)
const { y, height } = activeDrop.getBoundingClientRect()
let thresholdY = y + height/2
if (!allowAll) {
// We only allow putting the dragged element above/below rows with equal or higher heading level
const currentHeadingLevel = getHeadingLevel(activeDrop)
if (currentHeadingLevel > headingLevel) {
// We update the threshold based on the relevant parent
const par = getHigherParent(activeDrop, headingLevel)
const { y, height } = par.getBoundingClientRect()
thresholdY = y + height/2
}
}
// Check if the current position of the mouse is below or above the middle of the active drop zone
const isBelow = e.client.y > thresholdY
const newSep = getSeparator(activeDrop, isBelow, allowAll, headingLevel)
const currentSep = toc.querySelector('.toc-row-separator.active') ?? newSep
if (currentSep !== newSep) {
currentSep.classList.remove('active')
}
newSep.classList.add('active')
}
const dragHandles = interact('.toc-row').draggable({
cursorChecker (action, interactable, element, interacting) {
// console.log({action, interactable, element, interacting})
return null
},
manualStart: true, // needed for consistent start after hold
listeners: {
start: function (e) {
toc.classList.add('drag_enabled')
const row = e.target
// console.log('start: ', e)
toc.lastDragEvent = e
row.classList.add('dragged')
toc.draggedElement = row
tagAdjacentSeparators(row, true)
},
move: function (e) {
toc.lastDragEvent = e
updateActiveSeparator()
},
// move: function (e) {console.log('move: ',e)},
end: function (e) {
activeDrop = undefined
e.preventDefault()
toc.lastDragEvent = e
// console.log('end: ', e)
const row = e.target
// Cleanup
row.classList.remove('dragged')
toc.classList.remove('drag_enabled')
for (const el of toc.querySelectorAll('.active_drop')) {
el.classList.remove('active_drop')
}
reCollapse()
tagAdjacentSeparators(row, false)
toc.classList.remove('allow_all_drop')
// We temporary set the recentDrag flag
toc.classList.add('recent-drag')
setTimeout(() => {
toc.classList.remove('recent-drag')
}, 300)
// Check if there is an active dropzone
const dropZone = toc.querySelector('.toc-row-separator.active')
if (_.isNil(dropZone) || dropZone.classList.contains('noshow')) {return}
dropZone.classList.remove('active')
// We find the cell after the active separator and move the dragged row before that
const rowAfter = getNextSibling(dropZone)
const cellIdsToMove = getBlockIds(row)
// Find the index of the cell that will stay after our moved block
const end = editor_state.notebook.cell_order.indexOf(get_link_id(rowAfter))
// If we got -1, it means we have to put the cells at the end
pluto_actions.move_remote_cells(cellIdsToMove, end < 0 ? Infinity : end)
toc.draggedElement = undefined
},
}
}).on('hold',function (e) {
if (document.body.classList.contains('disable_ui')) { console.log('UI disabled, no interaction!'); return }
e.preventDefault()
e.stopImmediatePropagation()
e.stopPropagation()
// console.log('this is hold', e)
var interaction = e.interaction
if (!interaction.interacting()) {
interaction.start(
{ name: 'drag' },
e.interactable,
e.currentTarget,
)
}
})
// Header manipulation functionality
const header = toc.querySelector('header')
const header_container = header.insertAdjacentElement('afterbegin', html`<span class='toc-header-container'>`)
const notebook_hide_icon = header_container.insertAdjacentElement('beforeend', html`<span class='toc-header-icon toc-header-hide'>`)
const save_file_icon = header_container.insertAdjacentElement('beforeend', html`<span class='toc-header-icon toc-header-save'>`)
save_file_icon.addEventListener('click', save_to_file)
header_container.insertAdjacentElement('beforeend', html`<span class='toc-header-filler'>`)
header.addEventListener('click', e => {
if (e.target != header) {return}
scroll_to(cell, {block: 'center', behavior: 'smooth'})
})
header.addEventListener('mouseenter', (e) => {
const { computePosition } = floating_ui
// Get the scroll container - this is crucial for proper offset calculation
const scrollContainer = document.querySelector('main') || document.documentElement
const scrollTop = scrollContainer.scrollTop || window.pageYOffset
computePosition(header, header_container, {
placement: "left",
strategy: "fixed",
}).then(pos => {
header_container.style.top = pos.y - scrollTop + "px"
// header_container.style.left = pos.x + "px"
// header_container.style.right = `calc(1rem + min(80vw, 300px))`
})
})
notebook_hide_icon.addEventListener('click', (e) => {
// We find the x coordinate of the pluto-notebook element, to avoid missing the cell when UI is disabled
const { x } = document.querySelector('pluto-notebook').getBoundingClientRect()
const ref = document.elementFromPoint(x+1,100).closest('pluto-cell')
const { y } = ref.getBoundingClientRect()
toggle_notebook_attribute('hide-enabled')
const dy = ref.getBoundingClientRect().y - y
window.scrollBy(0, dy)
})
// Save to file functionality
function save_to_file() {
const state = window.toc_state
for (const [k,v] of Object.entries(state)) {
toggle_cell_attribute(k, 'toc-hidden', v.hidden)
toggle_cell_attribute(k, 'toc-collapsed', v.collapsed)
}
setTimeout(() => {
toc.classList.toggle('file-state-differs', stateDiffersFile(state))
}, 500)
}
// Make functions available globally
window.scroll_to = scroll_to;
window.save_to_file = save_to_file;
window.floating_ui = floating_ui;
// Set cell attribute for always-show-output
cell.toggleAttribute('always-show-output', true)
invalidation.then(() => {
observer.disconnect()
dragHandles.unset()
// Clean up event listeners
removeWindowEventListeners(window_events)
})