mirror of
https://github.com/HifiExperiments/overte.git
synced 2025-05-08 05:29:53 +02:00
895 lines
36 KiB
JavaScript
895 lines
36 KiB
JavaScript
// 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('<div class="inner-ui-slider-handle"></div>');
|
|
},
|
|
});
|
|
}
|
|
|
|
// 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, '<br />'));
|
|
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(/</g,'<'));
|
|
}
|
|
return json;
|
|
}
|
|
})(this);
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// manage jquery-tooltipster hover tooltips
|
|
TooltipManager = (function(global) {
|
|
function TooltipManager(options) {
|
|
this.options = options;
|
|
assert(options.elements, 'TooltipManager constructor expects options.elements');
|
|
assert(options.tooltips, 'TooltipManager constructor expects options.tooltips');
|
|
var TOOLTIPS = this.TOOLTIPS = {};
|
|
$(options.tooltips).find('[for]').each(function() {
|
|
var id = $(this).attr('for');
|
|
TOOLTIPS[id] = $(this).html();
|
|
});
|
|
|
|
var _self = this;
|
|
options.elements.each(function() {
|
|
var element = $($(this).closest('.tooltip-target').get(0) || this),
|
|
input = element.is('input, button') ? element : element.find('input, button'),
|
|
parent = element.is('.row, button') ? element : element.parent(),
|
|
id = element.attr('for') || input.prop('id'),
|
|
tip = TOOLTIPS[id] || element.prop('title');
|
|
|
|
var tooltipSide = element.data('tooltipSide');
|
|
|
|
log('binding tooltip', tooltipSide, element[0].nodeName, id || element[0], tip);
|
|
if(!tip)
|
|
return log('missing tooltip: ' + (id || this.id || this.name || this.nodeName));
|
|
if(element.is('.tooltipstered')) {
|
|
console.info('already tooltipstered!?', this.id, this.name, id);
|
|
return;
|
|
}
|
|
var instance = element.tooltipster({
|
|
theme: ['tooltipster-noir'],
|
|
side: tooltipSide || (
|
|
input.is('button') ? 'top' :
|
|
input.closest('.slider').get(0) || input.closest('.column').get(0) ? ['top','bottom'] :
|
|
['right','top','bottom', 'left']
|
|
),
|
|
content: tip,
|
|
updateAnimation: 'scale',
|
|
trigger: !options.testmode ? 'hover' : 'click',
|
|
distance: element.is('.slider.row') ? -20 : undefined,//element
|
|
delay: [500, 1000],
|
|
contentAsHTML: true,
|
|
interactive: options.testmode,
|
|
minWidth: options.viewport && options.viewport.min.x,
|
|
maxWidth: options.viewport && options.viewport.max.w,
|
|
}).tooltipster('instance');
|
|
|
|
instance.on('close', function(event) {
|
|
if (options.keepopen === element) {
|
|
debugPrint(event.type, 'canceling close keepopen === element', id);
|
|
event.stop();
|
|
options.keepopen = null;
|
|
}
|
|
});
|
|
instance.on('before', function(event) {
|
|
debugPrint(event.type, 'before', event);
|
|
!options.testmode && _self.closeAll();
|
|
!options.enabled && event.stop();
|
|
return;
|
|
});
|
|
parent.find(':focusable, input, [tabindex], button, .control')
|
|
.add(parent)
|
|
.add(input.closest(':focusable, input, [tabindex]'))
|
|
.on({
|
|
click: function(evt) {
|
|
if (input.is('button')) return setTimeout(instance.close.bind(instance,null),50);
|
|
options.keepopen = element; 0&&instance.open();
|
|
},
|
|
focus: instance.open.bind(instance, null),
|
|
blur: function(evt) { instance.close(); _self.openFocusedTooltip(); },
|
|
});
|
|
});
|
|
Object.assign(this, {
|
|
openFocusedTooltip: function() {
|
|
if (!this.options.enabled)
|
|
return;
|
|
setTimeout(function() {
|
|
if (!document.activeElement || document.activeElement === document.body ||
|
|
!$(document.activeElement).closest('section')) {
|
|
return;
|
|
}
|
|
var tip = $([])
|
|
.add($(document.activeElement))
|
|
.add($(document.activeElement).find('.tooltipstered'))
|
|
.add($(document.activeElement).closest('.tooltipstered'))
|
|
.filter('.tooltipstered');
|
|
if (tip.is('.tooltipstered')) {
|
|
log('opening focused tooltip', tip.length, tip[0].id);
|
|
tip.tooltipster('open');
|
|
}
|
|
},1);
|
|
},
|
|
rapidClose: function(instance, reopen) {
|
|
if (!instance.status().open) {
|
|
return;
|
|
}
|
|
instance.elementTooltip() && $(instance.elementTooltip()).hide();
|
|
instance.close(function() { reopen && instance.open(); });
|
|
},
|
|
openAll: function() {
|
|
$('.tooltipstered').tooltipster('open');
|
|
},
|
|
closeAll: function() {
|
|
$.tooltipster.instances().forEach(function(instance) {
|
|
this.rapidClose(instance);
|
|
}.bind(this));
|
|
},
|
|
updateViewport: function(viewport) {
|
|
var options = {
|
|
minWidth: viewport.min.x,
|
|
maxWidth: viewport.max.x,
|
|
};
|
|
$.tooltipster.setDefaults(options);
|
|
log('updating tooltipster options', JSON.stringify(options, 0, 2));
|
|
$.tooltipster.instances().forEach(function(instance) {
|
|
instance.option('minWidth', options.minWidth);
|
|
instance.option('maxWidth', options.maxWidth);
|
|
this.rapidClose(instance, instance.status().open);
|
|
}.bind(this));
|
|
},
|
|
enable: function() {
|
|
this.options.enabled = true;
|
|
if (this.options.testmode)
|
|
this.openAll();
|
|
},
|
|
disable: function() {
|
|
this.options.enabled = false;
|
|
this.closeAll();
|
|
},
|
|
});
|
|
}
|
|
return TooltipManager;
|
|
})(this);
|
|
|
|
function setupUI() {
|
|
$(window).trigger('resize'); // populate viewport
|
|
|
|
$('#debug-menu button, footer button').button();
|
|
|
|
var $json = JQuerySettings.$json;
|
|
|
|
var buttonHandlers = {
|
|
'page-reload': function() {
|
|
log('triggering location.reload');
|
|
location.reload();
|
|
},
|
|
'script-reload': function() {
|
|
log('triggering script.reload');
|
|
bridgedSettings.sendEvent({ method: 'reloadClientScript' });
|
|
},
|
|
'reset-sensors': function() {
|
|
log('resetting avatar orientation');
|
|
bridgedSettings.sendEvent({ method: 'resetSensors' });
|
|
},
|
|
'reset-to-defaults': function() {
|
|
tooltipManager.closeAll();
|
|
document.activeElement && document.activeElement.blur();
|
|
document.body.focus();
|
|
setTimeout(function() {
|
|
bridgedSettings.sendEvent({ method: 'reset' });
|
|
},1);
|
|
},
|
|
'copy-json': $json.showSettings.bind($json, jquerySettings, null),
|
|
'paste-json': function() {
|
|
$json.applyJSON(
|
|
jquerySettings,
|
|
'pasted',
|
|
$json.promptJSON()
|
|
);
|
|
},
|
|
'toggle-advanced-options': function(evt) {
|
|
var on = $('body').hasClass('ui-show-advanced-options');
|
|
bridgedSettings.setValue('ui-show-advanced-options', !on);
|
|
evt.stopPropagation();
|
|
evt.preventDefault();
|
|
$(this).tooltipster('instance').content((on ? 'show' : 'hide') + ' advanced options');
|
|
if (!on) {
|
|
$('.scrollable').delay(100).animate({
|
|
scrollTop: innerHeight - $('header').innerHeight() - 24
|
|
}, 1500);
|
|
}
|
|
},
|
|
'appVersion': function(evt) { evt.shiftKey && $json.showSettings(jquerySettings); },
|
|
'errors': function() {
|
|
$(this).find('.output').text('').end().hide();
|
|
},
|
|
};
|
|
Object.keys(buttonHandlers).forEach(function(p) {
|
|
$('#' + p).css('cursor', 'pointer').on('click', buttonHandlers[p]);
|
|
});
|
|
|
|
// ----------------------------------------------------------------
|
|
tooltipManager = new TooltipManager({
|
|
enabled: false,
|
|
testmode: PARAMS.tooltiptest,
|
|
viewport: PARAMS.viewport,
|
|
tooltips: $('#tooltips'),
|
|
elements: $($.unique($('input.setting, button').map(function() {
|
|
var input = $(this),
|
|
button = input.is('button') && input,
|
|
row = input.closest('.row').is('.slider') && input.closest('.row'),
|
|
label = input.is('[type=checkbox],[type=radio]') && input.parent().find('label');
|
|
|
|
return (button || label || row || input).get(0);
|
|
}))),
|
|
});
|
|
|
|
$( "input[data-type=number]" ).each(function() {
|
|
// set up default numeric precisions
|
|
var step = $(this).prop('step') || .01;
|
|
var precision = ((1 / step).toString().length - 1);
|
|
$(this).prop('step', step || Math.pow(10, -precision));
|
|
});
|
|
|
|
$('input.setting').each(function() {
|
|
// set up the bidirectional mapping between DOM and Settings
|
|
jquerySettings.registerNode(this);
|
|
}).on('change', function(evt) {
|
|
// set up change detection
|
|
var input = $(this),
|
|
key = assert(jquerySettings.getKey(this.id)),
|
|
type = this.dataset.type || input.prop('type'),
|
|
valid = (this.value !== '' && (type !== 'number' || isFinite(this.value)));
|
|
|
|
debugPrint('input.setting.change', key, evt, this);
|
|
$(this).closest('.row').toggleClass('invalid', !valid);
|
|
|
|
if (!valid) {
|
|
debugPrint('input.setting.change: ignoring invalid value ' + this.value + ' for ' + key);
|
|
return;
|
|
}
|
|
|
|
jquerySettings.resyncValue(key, 'input.setting.change');
|
|
|
|
// radio buttons are codependent (ie: TCOBO selected at a time)
|
|
// so if under a control group, trigger a display refresh
|
|
$(evt.target).closest(':ui-hifiControlGroup').find(':ui-hifiCheckboxRadio').hifiCheckboxRadio('refresh');
|
|
});
|
|
|
|
if (PARAMS.debug || true) {
|
|
// support hitting 'r' key to refresh during development
|
|
$(window).on('keypress.debug', function(evt) {
|
|
if (!$(evt.target).is('input')) {
|
|
var ch = String.fromCharCode(evt.keyCode);
|
|
if (evt.originalEvent.keyIdentifier === 'U+0057')
|
|
ch = 'w';
|
|
switch(ch) {
|
|
case 'r': {
|
|
log('... "r" reloading', evt.keyCode);
|
|
location.reload()
|
|
} break;
|
|
case 'w': {
|
|
if (evt.ctrlKey) {
|
|
log('ctrl-w detected; closing via client script');
|
|
bridgedSettings.sendEvent({ method: 'window.close' });
|
|
}
|
|
} break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// numeric fields
|
|
$( ".number.row input.setting" )
|
|
.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));
|
|
|
|
var options = input.hifiSpinner('instance').options;
|
|
options.min = options.min || 0.0;
|
|
|
|
bridgedSettings.getValueAsync(key, function(err, result) {
|
|
input.hifiSpinner('enable');
|
|
jquerySettings.setValue(key, result);
|
|
});
|
|
},
|
|
});
|
|
|
|
// radio button groups
|
|
$( ".radio.row" )
|
|
.hifiControlGroup({
|
|
direction: 'horizontal',
|
|
disabled: true,
|
|
create: function() {
|
|
var group = $(this), id = this.id;
|
|
this.setAttribute('type', this.type = this.dataset.type = 'radio-group');
|
|
var key = assert(jquerySettings.getKey(id));
|
|
bridgedSettings.getValueAsync(key, function(err, result) {
|
|
debugPrint('> 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 '<kbd>' + key.replace('Shifted','Shift') + '</kbd>'; })
|
|
.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("<pre>").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;
|
|
};
|
|
}
|