// app.js -- jquery support functions /* eslint-env commonjs, browser */ // ---------------------------------------------------------------------------- function defineCustomWidgets() { $.widget('ui.hifiCheckboxRadio', $.ui.checkboxradio, { _create: function() { this._super(); this.element[0].value = this.element[0].id; debugPrint('ui.hifiCheckboxRadio._create', this.element[0].type, this.element[0].id, this.element[0].value); }, });//$.fn.hifiCheckboxRadio = $.fn.checkboxradio; $.widget('ui.hifiControlGroup', $.ui.controlgroup, { _create: function(x) { debugPrint('ui.hifiControlGroup._create', this.element[0]) var tmp = this.options.items.checkboxradio; delete this.options.items.checkboxradio; this.options.items.hifiCheckboxRadio = tmp; this._super(); Object.defineProperty(this.element[0], 'value', { enumerable: true, get: function() { assert(false, 'attempt to access hifiControlGroup.element[0].value...' +[this.id,this.name]); }, set: function(nv) { assert(false, 'attempt to set hifiControlGroup.element[0].value...' +[this.id,this.name]); }, }); }, }); $.widget('ui.hifiSpinner', $.ui.spinner, { _create: function() { debugPrint('ui.hifiSpinner._create', this.element[0]) this.previous = null; this._super(); }, _spin: function( step, event ) { if (event.type === 'mousewheel') { if (!event.shiftKey) { step *= ('1e'+Math.max(1,this._precision()))/10; } if (event.ctrlKey) { step *= 10; } } return this._super( step, event ); }, _stop: function( event, ui ) { try { return this._super(event, ui); } finally { if (/mouse/.test(event && event.type)) { var value = this.element.val(); if (value != "" && !isNaN(value) && this.previous !== null && this.previous !== value) { debugPrint(this.element[0].id, 'spinner.changed', event.type, JSON.stringify({ previous: isNaN(this.previous) ? this.previous+'' : this.previous, val: isNaN(value) ? value+'' : value, })); this.element.change(); } this.previous = value; } } }, _format: function(n) { var precision = this._precision() return parseFloat(n).toFixed(precision); }, _events: { mousewheel: function(event, delta) { if (document.activeElement !== this.element[0]) return; // fix broken mousewheel on Chrome / webkit delta = delta === undefined ? event.originalEvent.deltaY : delta; $.ui.spinner.prototype._events.mousewheel.call(this, event, delta); } } }); $.widget('ui.hifiSlider', $.ui.slider, { _create: function() { this._super(); // add the inner circle and border (per design specs) to jquery-ui's existing slider handle this.element.find('.ui-slider-handle') .html('
'); }, }); } // JSON export / import helpers proto module JQuerySettings.$json = (function() { return { setPath: setPath, rollupPaths: rollupPaths, encodeNodes: encodeNodes, exportAll: exportAll, showSettings: showSettings, applyJSON: applyJSON, promptJSON: promptJSON, popupJSON, popupJSON, }; function encodeNodes(resolver, elements) { return elements.toArray().reduce((function(out, input, i) { log('input['+i+']', input.id); var id = input.type === 'radio' ? $(input).closest(':ui-hifiControlGroup').prop('id') : input.id; var key = resolver.getKey(id); log('toJSON', id, key, input.id); setPath(out, key.split('/'), resolver.getValue(key)); return out; }).bind(this), {}); } function setPath(obj, path, value) { var key = path.pop(); obj = path.reduce(function(obj, subkey) { return obj[subkey] = obj[subkey] || {}; }, obj); debugPrint('setPath', key, Object.keys(obj)); obj[key] = value; } function rollupPaths(obj, output, path) { path = path || []; output = output || {}; log('rollupPaths', Object.keys(obj||{}), Object.keys(output), path); for (var p in obj) { path.push(p); var value = obj[p]; if (value && typeof value === 'object') { rollupPaths(obj[p], output, path); } else { output[path.join('/')] = value; } path.pop(); } return output; } function exportAll(resolver, name) { return { version: VERSION, name: name || undefined, settings: encodeNodes(resolver, $('input')), _metadata: { timestamp: new Date(), PARAMS: PARAMS, url: location.href, } }; }; function showSettings(resolver, saveName) { JQuerySettings.$json.popupJSON(saveName || '(current settings)', Object.assign(JQuerySettings.$json.exportAll(resolver, saveName), { extraParams: bridgedSettings.extraParams, })); }; function popupJSON(title, tmp) { var HTML = POPUP.innerHTML .replace(/\bxx-script\b/g, 'script') .replace('JSON', JSON.stringify(tmp, 0, 2).replace(/\n/g, '
')); if (0) { bridgedSettings.sendEvent({ method: 'overlayWebWindow', options: { title: 'app-camera-move-export' + (title ? '::'+title : ''), content: HTML, }, }); } else { // make the browser address bar less ugly by putting spaces and friedly name as a "URL footer" var footer = '<\!-- #' + HTML.substr(0,256).replace(/./g,' ') + (title || 'Camera Move Settings'); window.open("data:text/html;escape," + encodeURIComponent(HTML) + footer,"app-camera-move-export"); } } function applyJSON(resolver, name, tmp) { assert('version' in tmp && 'settings' in tmp, 'invalid settings record: ' + JSON.stringify(tmp)) var settings = rollupPaths(tmp.settings); for(var p in settings) { var key = resolver.getId(p, true); if (!key) { log('$applySettings -- skipping unregistered Settings key: ', p); } else { resolver.setValue(p, settings[p], name+'.settings.'+p); } } }; function promptJSON() { var json = window.prompt('(paste JSON here)', ''); if (!json) return; try { json = JSON.parse(json); } catch(e) { throw new Error('Could not parse pasted JSON: ' + e + '\n\n' + (json||'').replace(/ GOT RADIO', key, id, result); group.hifiControlGroup('enable'); jquerySettings.setValue(key, result); group.change(); }); }, }); // checkbox fields $( ".bool.row input.setting" ) .hifiCheckboxRadio({ disabled: true }) .each(function() { var key = assert(jquerySettings.getKey(this.id)), input = $(this); bridgedSettings.getValueAsync(key, function(err, result) { input.hifiCheckboxRadio('enable'); jquerySettings.setValue(key, result); }); }); // slider + numeric field sets $( ".slider.row .control" ).each(function(ent) { var element = $(this), input = element.parent().find('input'), id = input.prop('id'), key = assert(jquerySettings.getKey(id)); var commonOptions = { disabled: true, min: parseFloat(input.prop('min') || 0), max: parseFloat(input.prop('max') || 10), step: parseFloat(input.prop('step') || 0.01), }; debugPrint('commonOptions', commonOptions); // see: https://api.jqueryui.com/slider/ for more options var slider = element.hifiSlider(Object.assign({ orientation: "horizontal", range: "min", animate: 'fast', value: 0.0, start: function(event, ui) { this.$startValue = ui.value; }, slide: function(event, ui) { input.hifiSpinner('value', ui.value); }, stop: function(event, ui) { if (ui.value != this.$startValue) { jquerySettings.setValue(key, ui.value, 'slider.stop'); } }, }, commonOptions)); // setup chrome up/down arrow steps and change event for propagating input field -> slider input.on('change', function() { element.hifiSlider("value", this.value); }).hifiSpinner(Object.assign({}, commonOptions, { max: Infinity })); bridgedSettings.getValueAsync(key, function(err, result) { input.hifiSpinner('enable'); element.hifiSlider('enable'); jquerySettings.setValue(key, result); }); }); // make all other numeric fields into custom jquery spinners $('input[data-type=number]:not(:ui-hifiSpinner)').hifiSpinner({ disabled: true, create: function() { var input = $(this), id = assert(this.id, '.number input.setting without id attribute: ' + this), key = assert(jquerySettings.getKey(this.id)); log('======================================', id, key); var options = input.hifiSpinner('instance').options; options.min = options.min || 0.0; bridgedSettings.getValueAsync(key, function(err, result) { jquerySettings.setValue(key, result); }); }, }); // ---------------------------------------------------------------------------- // allow spacebar to toggle checkbox / radiobutton fields $('[type=checkbox]:ui-hifiCheckboxRadio').parent().prop('tabindex',0) .on('keydown.toggle', function spaceToggle(evt) { if (evt.keyCode === 32) { var input = $(this).find(':ui-hifiCheckboxRadio'), id = input.prop('id'), key = assert(jquerySettings.getKey(id)); log('spaceToggle', evt.target+'', id, key); if (!input.is('[type=radio]') || !input.prop('checked')) { input.prop('checked', !input.prop('checked')); input.change(); } evt.preventDefault(); evt.stopPropagation(); } }); // when user presses ENTER on a number field, blur it to provide visual feedback $('input[data-type=number]').on('keydown.enter', function(evt) { if (evt.keyCode === 13 && $(document.activeElement).is('input')) { tooltipManager.closeAll(); document.activeElement.blur(); var nexts = $('[tabindex],input').not('[tabindex=-1],.ui-slider-handle').toArray(); if (~nexts.indexOf(this)) { var nextActive = nexts[nexts.indexOf(this)+1]; $(nextActive).focus(); } } }); // by default webkit spacebar behaves like PageDown; this disables that to avoid confusion window.onkeydown = function(evt) { if (evt.keyCode === 32 && document.activeElement !== document.body) { log('snarfing spacebar event', document.activeElement); return evt.preventDefault(), evt.stopPropagation(), false; } }; // select-all text when an input field is first focused $('input').not('input[type=radio],input[type=checkbox]').on('focus', function (e) { var dt = (new Date - this.blurredAt); if (!(dt < 5)) { // debounce this.blurredAt = +new Date; //log('FOCUS', dt, e.target === document.activeElement, this === document.activeElement); $(this).one('mouseup.selectit', function () { $(this).select(); return false; }).select(); } }).on('blur', function(e) { this.blurredAt = new Date; //log('BLUR', e.target === document.activeElement, this === document.activeElement); }); // monitor specific settings for live changes jquerySettings.registerSetting({ type: 'placeholder' }, '.extraParams'); jquerySettings.registerSetting({ type: 'placeholder' }, 'ui-show-advanced-options'); jquerySettings.registerSetting({ type: 'placeholder' }, 'Keyboard.RightMouseButton'); monitorSettings({ // advanced options toggle 'ui-show-advanced-options': function(value) { function handle(err, result) { log('***************************** ui-show-advanced-options updated', result+''); $('body').toggleClass('ui-show-advanced-options', !!result); } if (value !== undefined) { handle(null, value); } else { bridgedSettings.getValueAsync('ui-show-advanced-options', handle); } }, // UI tooltips checkbox 'ui-enable-tooltips': function(value) { if (value) { tooltipManager.enable(); tooltipManager.openFocusedTooltip(); } else { tooltipManager.disable(); } }, // enable/disable fps field based on thread update mode 'thread-update-mode': function(value) { var enabled = value === 'requestAnimationFrame', fps = $('#fps'); log('onThreadModeChanged', value, enabled); fps.hifiSpinner(enabled ? 'enable' : 'disable'); fps.closest('.tooltip-target').toggleClass('disabled', !enabled); }, // apply CSS to body based on whether camera move mode is currently enabled 'camera-move-enabled': function(value) { $('body').toggleClass('camera-move-enabled', value); }, // update keybinding and appVersion whenever extraParams arrives '.extraParams': function(value, other) { value = bridgedSettings.extraParams; //log('.extraParams', value, other, arguments); if (value.mode) { $('body').toggleClass('tablet-mode', value.mode.tablet); $('body').toggleClass('hmd-mode', value.mode.hmd); $('body').toggleClass('desktop-mode', value.mode.desktop); $('body').toggleClass('toolbar-mode', value.mode.toolbar); //setupTabletModeScrolling(value.mode.tablet); } var versionDisplay = [ value.appVersion || '(unknown appVersion)', PARAMS.debug && '(debug)', value.mode && value.mode.tablet ? '(tablet)' : '', ].filter(Boolean).join(' | '); $('#appVersion').find('.output').text(versionDisplay).end().fadeIn(); if (value.toggleKey) { function getKeysHTML(binding) { return [ 'Control', 'Meta', 'Alt', 'Super', 'Menu', 'Shifted' ] .map(function(flag) { return binding['is' + flag] && flag; }) .concat(binding.text || ('(#' + binding.key + ')')) .filter(Boolean) .map(function(key) { return '' + key.replace('Shifted','Shift') + ''; }) .join('-'); } $('#toggleKey').find('.binding').empty() .append(getKeysHTML(value.toggleKey)).end().fadeIn(); } }, // gray out / ungray out page content if user is mouse looking around in Interface // (otherwise the cursor still interacts with web content...) 'Keyboard.RightMouseButton': function(localValue, key, value) { log('... Keyboard.RightMouseButton:' + value); window.active = !value; }, }); $('input').css('-webkit-user-select', 'none'); } // setupUI // helper for instrumenting local jquery onchange handlers function monitorSettings(options) { return Object.keys(options).reduce(function(out, id) { var key = bridgedSettings.resolve(id); assert(function assertion(){ return typeof key === 'string' }, 'monitorSettings -- received invalid key type') function onChange(varargs) { debugPrint('onChange', id, typeof varargs === 'string' ? varargs : typeof varargs); var args = [].slice.call(arguments); options[id].apply(this, [ jquerySettings.getValue(id) ].concat(args)); } if (bridgedSettings.pendingRequestCount()) { bridgedSettings.pendingRequestsFinished.connect(function once() { bridgedSettings.pendingRequestsFinished.disconnect(once); onChange('pendingRequestsFinished'); }); } else { onChange('initialization'); } function _onValueUpdated(_key) { _key === key && onChange.apply(this, arguments); } bridgedSettings.valueUpdated.connect(bridgedSettings, _onValueUpdated); jquerySettings.valueUpdated.connect(jquerySettings, _onValueUpdated); return out; }, {}); } function logValueUpdate(hint, key, value, oldValue, origin) { if (0 === key.indexOf('.')) { return; } oldValue = JSON.stringify(oldValue), value = JSON.stringify(value); _debugPrint('[ ' + hint +' @ ' + origin + '] ' + key + ' = ' + value + ' (was: ' + oldValue + ')'); } function initializeDOM() { // DOM initialization window.viewportUpdated = signal(function viewportUpdated(viewport) {}); function triggerViewportUpdate() { var viewport = { geometry: { x: innerWidth, y: innerHeight }, min: { x: window.innerWidth / 3, y: 32 }, max: { x: window.innerWidth * 7/8, y: window.innerHeight * 7/8 }, }; viewportUpdated(viewport, triggerViewportUpdate.lastViewport); triggerViewportUpdate.lastViewport = viewport; } viewportUpdated.connect(PARAMS, function(viewport, oldViewport) { log('viewportUpdated', viewport); Object.assign(PARAMS, { viewport: Object.assign(PARAMS.viewport||{}, viewport), }); tooltipManager.updateViewport(viewport); }); document.onselectstart = document.ondragstart = document.body.ondragstart = document.body.onselectstart = function(){ return false; }; document.body.oncontextmenu = document.oncontextmenu = document.body.ontouchstart = function(evt) { evt.stopPropagation(); evt.preventDefault(); return false; }; $('.scrollable').on('mousemove.unselect', function() { if (!$(document.activeElement).is('input')) { if (document.selection) { log('snarfing mousemove.unselect.selection'); document.selection.empty() } else { window.getSelection().removeAllRanges() } } }).on('selectstart.unselect', function(evt) { if (!$(document.activeElement).is('input')) { log('snarfing selectstart'); evt.stopPropagation(); evt.preventDefault(); return false; } }); Object.defineProperty(window, 'active', { get: function() { return window._active; }, set: function(nv) { nv = !!nv; window._active = nv; log('window.active == ' + nv); if (!nv) { document.activeElement && document.activeElement.blur(); document.body.focus(); $('body').toggleClass('active', nv); } else { $('body').toggleClass('active', nv); } }, }); window.active = true; function checkAnim(evt) { if (!checkAnim.disabled) { if ($('.scrollable').is(':animated')) { $('.scrollable').stop(); log(evt.type, 'stop animation'); } } } $(window).on({ resize: function resize() { window.clearTimeout(resize.to); resize.to = window.setTimeout(triggerViewportUpdate, 100); }, mousedown: checkAnim, mouseup: checkAnim, scroll: checkAnim, wheel: checkAnim, blur: function() { log('** BLUR ** '); document.body.focus(); document.activeElement && document.activeElement.blur(); tooltipManager.disable(); //tooltipManager.closeAll(); }, focus: function() { log('** FOCUS **'); bridgedSettings.getValue('ui-enable-tooltips') && tooltipManager.enable(); }, }); } function preconfigureLESS() { window.lessOriginalNodes = $('style[type="text/less"]').remove(); window.lessGlobalVars = Object.assign({ debug: !!PARAMS.debug, localhost: 1||/^\b(?:localhost|127[.])/.test(location), hash: '', }, { 'header-height': 48, 'footer-height': 32, 'custom-font-family': 'Raleway-Regular', 'input-font-family': 'FiraSans-Regular', 'color-highlight': '#009bd5', 'color-text': '#afafaf', 'color-bg': '#393939', 'color-bg-darker': '#252525', 'color-bg-icon': '#545454', 'color-primary-button': 'darkblue', 'color-alt-button': 'green', 'color-caution-button': 'darkred', }); window.less = { //poll: 1000, //watch: true, globalVars: lessGlobalVars, }; preconfigureLESS.onViewportUpdated = function onViewportUpdated(viewport) { if (onViewportUpdated.to) { clearTimeout(onViewportUpdated.to); } else if (lessGlobalVars.hash) { onViewportUpdated.to = setTimeout(onViewportUpdated, 500); // debounce return; } delete lessGlobalVars.hash; Object.assign(lessGlobalVars, { 'interface-mode': /highfidelity/i.test(navigator.userAgent), 'inner-width': window.innerWidth, 'inner-height': window.innerHeight, 'client-width': document.body.clientWidth || window.innerWidth, 'client-height': document.body.clientHeight || window.innerHeight, 'hash': '', }); lessGlobalVars.hash = JSON.stringify(JSON.stringify(lessGlobalVars,0,2)).replace(/\\n/g , '\\000a'); var hash = JSON.stringify(lessGlobalVars, 0, 2); log('onViewportUpdated', JSON.parse(onViewportUpdated.lastHash||'{}')['inner-width'], JSON.parse(hash)['inner-width']); if (onViewportUpdated.lastHash !== hash) { //log('updating lessVars', 'less.modifyVars:' + typeof less.modifyVars, JSON.stringify(lessGlobalVars, 0, 2)); PARAMS.debug && $('#errors').show().html("
").children(0).text(hash);
            // LESS needs some help to recompile inline styles, so a fresh copy of the source nodes is swapped-in
            var newNodes = lessOriginalNodes.clone().appendTo(document.body);
            less.modifyVars && less.modifyVars(true, lessGlobalVars);
            var oldNodes = onViewportUpdated.lastNodes;
            oldNodes && oldNodes.remove();
            onViewportUpdated.lastNodes = newNodes;
        }
        onViewportUpdated.lastHash = hash;
    };
}