function xySlider(options = {}) {
const {
min = 0, max = 1, step = 'any', value = [null, null],
xmin = min, xmax = max, xstep = step, xvalue = value[0],
ymin = min, ymax = max, ystep = step, yvalue = value[1],
theme = theme_default,
} = options;
const ix = DOM.element('input', {type: 'range', min: xmin, max: xmax, step: xstep, value: xvalue});
const iy = DOM.element('input', {type: 'range', min: ymin, max: ymax, step: ystep, value: yvalue});
const controls = {};
const scope = DOM.uid('scope').id;
const dom = html`
<div class="${scope} xy-picker">
${controls.zone = html`<div class=zone>
${controls.thumb = html`<div class=thumb>`}
`}
${html`<style>${theme.replace(/:scope\b/g, '.' + scope)}`}
`;
dom.value = Array(2);
for(let [k, v] of Object.entries({xmin, xmax, xstep, ymin, ymax, ystep})) dom.style.setProperty(`--${k}`, v);
const updateRange = () => {
dom.style.setProperty('--x', `${(dom.value[0] - xmin) / (xmax-xmin) * 100}%`);
dom.style.setProperty('--y', `${(dom.value[1] - ymin) / (ymax-ymin) * 100}%`);
};
const setValue = (vx, vy) => {
const [px, py] = dom.value;
dom.value[0] = vx = (ix.value = vx, ix.valueAsNumber);
dom.value[1] = vy = (iy.value = vy, iy.valueAsNumber);
updateRange();
if(px !== vx || py !== vy) {
dom.dispatchEvent(new CustomEvent('input', {bubbles: true}));
}
};
setValue(xvalue, yvalue);
const mouseOffset = (clientX, clientY) => {
const r = controls.zone.getBoundingClientRect();
return [
(clientX - r.left) / r.width,
(clientY - r.top) / r.height,
];
};
let ov, ot;
const moveTo = (tx, ty) => {
setValue(
xmin + (xmax - xmin) * tx,
ymin + (ymax - ymin) * ty,
);
};
const moveBy = (tx, ty) => {
setValue(
ov[0] + (xmax - xmin) * (tx - ot[0]),
ov[1] + (ymax - ymin) * (ty - ot[1])
);
};
// Returns client offset object.
const pointer = e => e.touches ? e.touches[0] : e;
// Note: Chrome defaults passive for touch events to true.
const on = (e, fn) => e.split(' ').map(e => document.addEventListener(e, fn, {passive: false}));
const off = (e, fn) => e.split(' ').map(e => document.removeEventListener(e, fn, {passive: false}));
const handleDrag = e => {
e.preventDefault();
const p = pointer(e);
const [tx, ty] = mouseOffset(p.clientX, p.clientY);
e.shiftKey ? moveBy(tx, ty) : moveTo(tx, ty);
};
const handleDragStop = e => {
off('mousemove touchmove', handleDrag);
off('mouseup touchend', handleDragStop);
};
invalidation.then(handleDragStop);
dom.ontouchstart = dom.onmousedown = e => {
on('mousemove touchmove', handleDrag);
on('mouseup touchend', handleDragStop);
e.preventDefault();
e.stopPropagation();
ov = dom.value.slice();
const p = pointer(e);
ot = mouseOffset(p.clientX, p.clientY);
if(!e.shiftKey) moveTo(...ot);
};
return dom;
}