113 lines
3.9 KiB
JavaScript
113 lines
3.9 KiB
JavaScript
/* part: split (behaviour) — the drag half of split.css.
|
|
*
|
|
* Derived from common/ui/src/components/SplitPane.vue's pointer handlers. Plain
|
|
* DOM, no framework, no build step, no imports: a <script> tag on a page opened
|
|
* over file:// runs this as-is.
|
|
*
|
|
* The drag-delta idiom this implements is currently written FOUR times in
|
|
* semester/ — SplitPane.vue, ResizeHandle.vue, mpr's FrameStrip.vue, and
|
|
* mpr/ui/timeline's Timeline.tsx. This is the plain-HTML one, so the ad-hoc
|
|
* pages stop making it five.
|
|
*
|
|
* USAGE <script src="split.js"></script> (or paste it; it self-starts)
|
|
*
|
|
* <div class="split-pane horizontal" data-split
|
|
* data-size="1.4" data-min="0.4" data-max="4">
|
|
*
|
|
* data-split marks the element. Required.
|
|
* data-size (default 1) initial size of the anchored pane
|
|
* data-mode ratio | px (default ratio)
|
|
* data-anchor first | second (default first)
|
|
* data-min / data-max clamps, in the same unit as data-size
|
|
*
|
|
* Direction comes from the `horizontal` / `vertical` class, so CSS and JS
|
|
* cannot disagree about it.
|
|
*/
|
|
|
|
(function () {
|
|
'use strict'
|
|
|
|
function setup(root) {
|
|
var divider = root.querySelector(':scope > .split-divider')
|
|
if (!divider) return // no divider: a fixed split, deliberately
|
|
|
|
var first = root.querySelector(':scope > .split-first')
|
|
var second = root.querySelector(':scope > .split-second')
|
|
if (!first || !second) return
|
|
|
|
var horizontal = !root.classList.contains('vertical')
|
|
var mode = root.dataset.mode === 'px' ? 'px' : 'ratio'
|
|
var anchor = root.dataset.anchor === 'second' ? 'second' : 'first'
|
|
var size = parseFloat(root.dataset.size)
|
|
if (isNaN(size)) size = 1
|
|
var min = parseFloat(root.dataset.min)
|
|
if (isNaN(min)) min = mode === 'px' ? 0 : 0.1
|
|
var max = parseFloat(root.dataset.max)
|
|
if (isNaN(max)) max = mode === 'px' ? Infinity : 10
|
|
|
|
var sized = anchor === 'second' ? second : first
|
|
var flexed = anchor === 'second' ? first : second
|
|
var dragging = false
|
|
var startPos = 0
|
|
|
|
function apply() {
|
|
flexed.style.flex = '1'
|
|
if (mode === 'px') {
|
|
sized.style.flex = '0 0 auto'
|
|
sized.style[horizontal ? 'width' : 'height'] = size + 'px'
|
|
} else {
|
|
sized.style.flex = String(size)
|
|
}
|
|
}
|
|
|
|
divider.addEventListener('pointerdown', function (e) {
|
|
dragging = true
|
|
startPos = horizontal ? e.clientX : e.clientY
|
|
divider.classList.add('dragging')
|
|
divider.setPointerCapture(e.pointerId)
|
|
})
|
|
|
|
divider.addEventListener('pointermove', function (e) {
|
|
if (!dragging) return
|
|
var pos = horizontal ? e.clientX : e.clientY
|
|
var delta = pos - startPos
|
|
startPos = pos
|
|
|
|
// Dragging right/down grows the first pane. When the SECOND pane is the
|
|
// anchored one, that same gesture must shrink it, so invert.
|
|
if (anchor === 'second') delta = -delta
|
|
|
|
// Ratio mode is unitless, so pixels are scaled into it. The two constants
|
|
// are the SFC's, kept rather than re-derived: they are what the existing
|
|
// panes were tuned against, and vertical drags cover less travel.
|
|
var step = mode === 'px' ? delta : delta * (horizontal ? 0.01 : 0.02)
|
|
size = Math.max(min, Math.min(max, size + step))
|
|
apply()
|
|
})
|
|
|
|
function end(e) {
|
|
if (!dragging) return
|
|
dragging = false
|
|
divider.classList.remove('dragging')
|
|
if (e && e.pointerId !== undefined && divider.hasPointerCapture(e.pointerId)) {
|
|
divider.releasePointerCapture(e.pointerId)
|
|
}
|
|
}
|
|
divider.addEventListener('pointerup', end)
|
|
divider.addEventListener('pointercancel', end)
|
|
|
|
apply()
|
|
}
|
|
|
|
function start() {
|
|
var panes = document.querySelectorAll('[data-split]')
|
|
for (var i = 0; i < panes.length; i++) setup(panes[i])
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', start)
|
|
} else {
|
|
start()
|
|
}
|
|
})()
|