// Form controls built for a remote control. // // Everything is a button. Dropdowns and checkboxes are miserable to operate // with a D-pad, so a choice cycles through its values on Enter and a toggle // flips. Text fields are the one exception: focusing one and pressing Enter // brings up the TV's on-screen keyboard, which is the only way to type. (function (global) { 'use strict'; function el(tag, className, text) { var node = document.createElement(tag); if (className) { node.className = className; } if (text !== undefined && text !== null) { node.textContent = String(text); } return node; } // Reads "hyperhdr.host" out of a settings object. function get(obj, path) { var parts = path.split('.'); var cur = obj; for (var i = 0; i < parts.length; i++) { if (cur === null || cur === undefined) { return undefined; } cur = cur[parts[i]]; } return cur; } // Builds { hyperhdr: { host: value } } so setConfig only carries the change. function patchFor(path, value) { var parts = path.split('.'); var root = {}; var cur = root; for (var i = 0; i < parts.length - 1; i++) { cur[parts[i]] = {}; cur = cur[parts[i]]; } cur[parts[parts.length - 1]] = value; return root; } function row(label, hint, control) { var wrap = el('div', 'field'); var text = el('div', 'field-text'); text.appendChild(el('div', 'field-label', label)); if (hint) { text.appendChild(el('div', 'field-hint', hint)); } wrap.appendChild(text); var holder = el('div', 'field-control'); holder.appendChild(control); wrap.appendChild(holder); return wrap; } function button(label, onClick, extraClass) { var b = el('button', 'focusable btn' + (extraClass ? ' ' + extraClass : ''), label); b.type = 'button'; b.addEventListener('click', onClick); return b; } function toggle(value, onChange) { var b = el('button', 'focusable btn toggle'); b.type = 'button'; function paint(v) { b.textContent = v ? 'On' : 'Off'; b.classList.toggle('on', !!v); b.setAttribute('aria-pressed', v ? 'true' : 'false'); } paint(value); b.addEventListener('click', function () { value = !value; paint(value); onChange(value); }); b.setValue = paint; return b; } // `options` is [{ value, label }]. function choice(options, value, onChange) { var b = el('button', 'focusable btn choice'); b.type = 'button'; function indexOf(v) { for (var i = 0; i < options.length; i++) { if (options[i].value === v) { return i; } } return 0; } var index = indexOf(value); function paint() { b.textContent = options.length ? options[index].label : '—'; b.title = options.length ? String(options[index].value) : ''; } paint(); b.addEventListener('click', function () { if (!options.length) { return; } index = (index + 1) % options.length; paint(); onChange(options[index].value); }); b.setValue = function (v) { index = indexOf(v); paint(); }; b.setOptions = function (list, v) { options = list; index = indexOf(v); paint(); }; return b; } function text(value, onChange, opts) { opts = opts || {}; var input = el('input', 'focusable input'); input.type = 'text'; input.value = value === undefined || value === null ? '' : String(value); if (opts.placeholder) { input.placeholder = opts.placeholder; } if (opts.numeric) { input.inputMode = 'numeric'; } function commit() { var raw = input.value.trim(); onChange(opts.numeric ? (raw === '' ? 0 : parseFloat(raw)) : raw); } input.addEventListener('change', commit); input.addEventListener('blur', commit); input.addEventListener('keydown', function (e) { // Let the arrow keys move the caret inside a focused field, but hand Up // and Down back to the navigator so the user can leave it. if (e.keyCode === 37 || e.keyCode === 39) { e.stopPropagation(); } if (e.keyCode === 13) { commit(); } }); input.setValue = function (v) { input.value = v === undefined || v === null ? '' : String(v); }; return input; } // A horizontal bar, 0..1. function bar(className) { var outer = el('div', 'bar ' + (className || '')); var fill = el('div', 'bar-fill'); outer.appendChild(fill); outer.setValue = function (v) { var pct = Math.max(0, Math.min(1, v)) * 100; fill.style.width = pct.toFixed(1) + '%'; }; return outer; } function clear(node) { while (node.firstChild) { node.removeChild(node.firstChild); } } global.UI = { el: el, get: get, patchFor: patchFor, row: row, button: button, toggle: toggle, choice: choice, text: text, bar: bar, clear: clear, }; })(window);