/**
* openvibe-shared/kit-data-web.js — Kit 2's behaviour in the page (split from kit-web.js in 4.37.0):
*
* charts .ovk-chart-plot[data-ovk-chart]: the values under the pointer, a tap, or ← → Home End
* scatter labels a label that would cover another, or cross the right edge, is hidden or moved
* drop zones [data-ovk-drop]: drag and drop, paste, the camera on phones, the file's name and picture
* tool frame [data-ovk-clear] shows while its box has text and empties it; a[data-ovk-try] fills the box and runs
* the tool (its form's submit, else the [data-ovk-run] button); without this it is a ?name=value link
*
* It runs by itself when it loads, again on a boosted page swap (ov:boost:load), and for markup added later through
* OpenVibeKitData.enhance(container). Idempotent and safe on a page that uses none of it. Load it on a page that draws
* kit.chart, kit.drop or the tool frame (kit.steps, table, rank, meter and receipt need no script).
*/
(function (root) {
'use strict';
function enhance(rootEl) {
if (typeof document === 'undefined') return;
const scope = rootEl || document;
const all = (sel) => Array.prototype.slice.call(scope.querySelectorAll ? scope.querySelectorAll(sel) : []);
// The mark has a name of its own (…Wired), apart from the data-ovk-* attribute that asks for the behaviour (4.55.0).
const once = (el, key) => { const k = `${key}Wired`; if (el.dataset[k]) return false; el.dataset[k] = '1'; return true; };
// Charts (Kit 2): a rule, a dot per series and the values under the pointer or the arrow keys, on lines and areas.
all('.ovk-chart-plot[data-ovk-chart]').forEach((plot) => {
if (!once(plot, 'ovkChart')) return;
let data; try { data = JSON.parse(plot.getAttribute('data-ovk-chart')); } catch (e) { return; }
if (!data || !data.x || !data.x.length) return;
const rule = plot.querySelector('.ovk-chart-rule'), tip = plot.querySelector('.ovk-chart-tip');
if (!rule || !tip) return;
let at = -1;
const show = (i) => {
at = Math.max(0, Math.min(data.x.length - 1, i));
const [px, label] = data.x[at];
rule.style.left = `${px}%`; rule.hidden = false;
rule.innerHTML = data.s.map((s) => (s.v[at] ? `` : '')).join('');
tip.textContent = '';
const b = document.createElement('b'); b.textContent = label; tip.appendChild(b);
data.s.forEach((s) => { if (!s.v[at]) return; const line = document.createElement('span'); line.className = `ovk-chart-s${s.c}`; line.innerHTML = ''; line.appendChild(document.createTextNode(`${s.n ? `${s.n} ` : ''}${s.v[at][1]}`)); tip.appendChild(line); });
tip.hidden = false;
const w = plot.clientWidth, tw = tip.offsetWidth, x = (px / 100) * w;
tip.style.left = `${Math.max(0, Math.min(w - tw, x + 12 + tw > w ? x - tw - 12 : x + 12))}px`;
};
const hide = () => { rule.hidden = true; tip.hidden = true; };
const nearest = (clientX) => {
const r = plot.getBoundingClientRect(); const p = ((clientX - r.left) / r.width) * 100;
let best = 0; for (let i = 1; i < data.x.length; i++) if (Math.abs(data.x[i][0] - p) < Math.abs(data.x[best][0] - p)) best = i;
return best;
};
plot.addEventListener('pointermove', (e) => show(nearest(e.clientX)));
plot.addEventListener('pointerdown', (e) => show(nearest(e.clientX)));
plot.addEventListener('pointerleave', hide);
plot.addEventListener('blur', hide);
plot.addEventListener('focus', () => show(at < 0 ? data.x.length - 1 : at));
plot.addEventListener('keydown', (e) => {
const step = { ArrowLeft: -1, ArrowRight: 1, Home: -1e9, End: 1e9 }[e.key];
if (step == null) { if (e.key === 'Escape') hide(); return; }
e.preventDefault(); show((at < 0 ? data.x.length - 1 : at) + step);
});
});
// Scatter labels (Kit 2): a label that would sit on one already shown is hidden (best points first); its dot still says it.
all('.ovk-chart--scatter .ovk-chart-plot').forEach((plot) => {
const names = Array.prototype.slice.call(plot.querySelectorAll('.ovk-chart-name'));
if (names.length < 2) return;
const place = () => {
names.forEach((n) => { n.classList.remove('is-hidden'); n.parentNode.classList.remove('is-flip'); });
// A label that would run past the plot's right edge goes to the left of its dot.
const edge = plot.getBoundingClientRect().right + 4;
names.forEach((n) => { if (!n.parentNode.classList.contains('is-right') && n.getBoundingClientRect().right > edge) n.parentNode.classList.add('is-flip'); });
const order = names.slice().sort((a, b) => (b.parentNode.classList.contains('is-best') - a.parentNode.classList.contains('is-best')) || (parseFloat(b.parentNode.style.bottom) - parseFloat(a.parentNode.style.bottom)));
const kept = [];
order.forEach((n) => {
const r = n.getBoundingClientRect();
const hit = kept.some((k) => r.left < k.right + 4 && r.right + 4 > k.left && r.top < k.bottom + 2 && r.bottom + 2 > k.top);
if (hit) n.classList.add('is-hidden'); else kept.push(r);
});
};
if (!once(plot, 'ovkLabels')) return;
place();
if (typeof ResizeObserver !== 'undefined') new ResizeObserver(place).observe(plot);
});
// Drop zones (Kit 2): drag and drop, paste, the camera on phones, the chosen file's name and picture.
all('[data-ovk-drop]').forEach((box) => {
if (!once(box, 'ovkDrop')) return;
const input = box.querySelector('.ovk-drop-input'), zone = box.querySelector('.ovk-drop-zone'), name = box.querySelector('.ovk-drop-name');
if (!input || !zone) return;
box.classList.add('is-js');
let url = null;
const shown = () => {
const files = Array.prototype.slice.call(input.files || []);
box.classList.toggle('has-file', files.length > 0);
if (!name) return;
name.textContent = '';
if (url) { URL.revokeObjectURL(url); url = null; }
if (!files.length) return;
if (files.length === 1 && /^image\//.test(files[0].type) && window.URL) {
const img = document.createElement('img'); url = URL.createObjectURL(files[0]); img.src = url; img.alt = ''; name.appendChild(img);
}
name.appendChild(document.createTextNode(files.length === 1 ? files[0].name : `${files.length} files`));
};
const take = (list) => {
if (!list || !list.length || typeof DataTransfer === 'undefined') return;
const dt = new DataTransfer();
Array.prototype.slice.call(list, 0, input.multiple ? list.length : 1).forEach((f) => dt.items.add(f));
input.files = dt.files;
input.dispatchEvent(new Event('change', { bubbles: true }));
};
input.addEventListener('change', () => {
shown();
if (box.hasAttribute('data-ovk-auto') && input.files && input.files.length && input.form) (input.form.requestSubmit ? input.form.requestSubmit() : input.form.submit());
});
['dragenter', 'dragover'].forEach((t) => zone.addEventListener(t, (e) => { e.preventDefault(); box.classList.add('is-over'); }));
['dragleave', 'dragend'].forEach((t) => zone.addEventListener(t, () => box.classList.remove('is-over')));
zone.addEventListener('drop', (e) => { e.preventDefault(); box.classList.remove('is-over'); take(e.dataTransfer && e.dataTransfer.files); });
// Paste: only while this is the page's one drop zone, and never into a field someone is typing in.
document.addEventListener('paste', (e) => {
if (document.querySelectorAll('[data-ovk-drop]').length !== 1) return;
const t = e.target; if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;
const files = e.clipboardData && e.clipboardData.files; if (files && files.length) { e.preventDefault(); take(files); }
});
const cam = box.querySelector('.ovk-drop-cam'), camInput = box.querySelector('[data-ovk-drop-cam]');
if (cam && camInput && window.matchMedia && matchMedia('(pointer: coarse)').matches) {
cam.hidden = false;
camInput.addEventListener('change', () => { take(camInput.files); camInput.value = ''; });
}
});
// The tool frame: a clear button shows while its box has text; a "Try" example fills the box and runs the tool
// (its form's submit, else the run button it names). Without this, the example is a link to ?name=value.
all('[data-ovk-clear]').forEach((b) => {
const f = document.querySelector(b.getAttribute('data-ovk-clear'));
if (!f || !once(b, 'ovkClear')) return;
const sync = () => { b.hidden = !f.value; };
f.addEventListener('input', sync); sync();
b.addEventListener('click', () => { f.value = ''; sync(); f.dispatchEvent(new Event('input', { bubbles: true })); f.focus(); });
});
all('a[data-ovk-try]').forEach((a) => {
if (!once(a, 'ovkTry')) return;
a.addEventListener('click', (e) => {
const sel = a.getAttribute('data-ovk-for');
const form = a.closest('form');
const f = (sel && document.querySelector(sel)) || (form && form.elements.namedItem(a.getAttribute('data-ovk-name') || 'q'));
if (!f || !('value' in f)) return;
e.preventDefault();
f.value = a.getAttribute('data-ovk-try');
f.dispatchEvent(new Event('input', { bubbles: true }));
const run = a.getAttribute('data-ovk-run');
const btn = run && document.querySelector(run);
if (btn) btn.click();
else if (f.form) { if (f.form.requestSubmit) f.form.requestSubmit(); else f.form.submit(); }
});
});
}
const api = { enhance };
root.OpenVibeKitData = api;
if (typeof module === 'object' && module.exports) { module.exports = api; return; }
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => enhance(document));
else enhance(document);
document.addEventListener('ov:boost:load', () => enhance(document));
}
})(typeof globalThis !== 'undefined' ? globalThis : this);