overte-JulianGro/unpublishedScripts/marketplace/camera-move/app.js
2017-06-21 15:57:28 -04:00

613 lines
24 KiB
JavaScript

// app.js -- support functions
/* eslint-env console, jquery, browser, shared-node-browser */
/* eslint-disable comma-dangle */
/* global Mousetrap, TooltipManager, SettingsJSON, PARAMS, signal, assert, log, debugPrint */
// ----------------------------------------------------------------------------
var viewportUpdated, bridgedSettings, jquerySettings, tooltipManager, lessManager;
function setupUI() {
$('#debug-menu button, footer button')
.hifiButton({
create: function() {
$(this).addClass('tooltip-target')
.data('tooltipster', { side: ['top','bottom'] });
}
});
var $json = SettingsJSON;
window.buttonHandlers = {
'test-event-bridge': function() {
log('bridgedSettings.eventBridge === Window.EventBridge', bridgedSettings.eventBridge === window.EventBridge);
bridgedSettings.sendEvent({ method: 'test-event-bridge' });
EventBridge.emitWebEvent('EventBridge.emitWebEvent: testing 1..2..3..');
},
'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 && 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 checkbox = $(this).hifiButton('instance').checkbox;
var on = checkbox.value(!checkbox.value());
$('body').toggleClass('ui-show-advanced-options', on);
evt.stopPropagation();
evt.preventDefault();
if ($(this).is('.tooltipstered')) {
$(this).tooltipster('instance').content((on ? 'hide' : 'show') + ' advanced options');
}
if (checkbox.value()) {
$('.scrollable').delay(100).animate({
scrollTop: innerHeight - $('header').innerHeight() - 24
}, 1500);
}
},
'appVersion': function(evt) {
evt.shiftKey && $json.showSettings(jquerySettings);
},
'errors': function(evt) {
$(evt.target).is('button') && $(this).find('.output').text('').end().hide();
},
};
buttonHandlers['button-toggle-advanced-options'] =
buttonHandlers['toggle-advanced-options'];
Object.keys(window.buttonHandlers).forEach(function(p) {
$('#' + p).on('click', window.buttonHandlers[p]);
});
// ----------------------------------------------------------------
// trim whitespace in labels
$('label').contents().filter(function() {
if (this.nodeType !== window.Node.TEXT_NODE) {
return false;
}
this.textContent = this.textContent.trim();
return !this.textContent.length;
}).remove();
var settingsNodes = $('fieldset,button[data-for],input:not([type=radio])');
settingsNodes.each(function() {
// set up the bidirectional mapping between DOM and Settings
jquerySettings.registerNode(this);
});
var spinnerOptions = {
disabled: true,
create: function() {
var input = $(this),
key = assert(jquerySettings.getKey(input.data('for')));
var options = input.hifiSpinner('instance').options;
options.min = options.min || 0.0;
bridgedSettings.getValueAsync(key, function(err, result) {
input.filter(':not([data-autoenable=false])').hifiSpinner('enable');
jquerySettings.setValue(key, result);
});
},
};
$( ".rows > label" ).each(function() {
var label = $(this),
input = label.find('input'),
type = input.data('type') || input.attr('type');
label.wrap('<div>').parent().addClass(['hifi-'+type, type, 'row'].join(' '))
.on('focus.row, click.row, hover.row', function() {
$(this).find('.tooltipstered').tooltipster('open');
});
});
debugPrint('initializing hifiSpinners');
// numeric settings
$( ".number.row" )
.find( "input[data-type=number]" )
.addClass('setting')
.hifiSpinner(spinnerOptions);
// radio groups settings
$( ".radio.rows" )
.find('label').addClass('tooltip-target').end()
.addClass('setting')
.hifiRadioGroup({
direction: 'vertical',
disabled: true,
create: function() {
assert(this !== window);
var group = $(this), id = this.id;
var key = assert(jquerySettings.getKey(group.data('for')));
bridgedSettings.getValueAsync(key, function(err, result) {
debugPrint('> GOT RADIO', key, id, result);
group.filter(':not([data-autoenable=false])').hifiRadioGroup('enable');
jquerySettings.setValue(key, result);
group.change();
});
},
})
// checkbox settings
$( "input[type=checkbox]" )
.addClass('setting')
.hifiCheckbox({
disabled: true,
create: function() {
var key = assert(jquerySettings.getKey(this.id)),
input = $(this);
input.closest('label').addClass('tooltip-target');
bridgedSettings.getValueAsync(key, function(err, result) {
input.filter(':not([data-autoenable=false])').hifiCheckbox('enable');
jquerySettings.setValue(key, result);
});
},
});
// slider + numeric settings
// use the whole row as a tooltip target
$( ".slider.row" ).addClass('tooltip-target').data('tooltipster', {
distance: -20,
side: ['top', 'bottom'],
}).each(function(ent) {
var element = $(this).find( ".control" ),
input = $(this).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),
autoenable: input.data('autoenable') !== 'false',
};
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
}, commonOptions)).hifiSlider('instance');
debugPrint('initializing hifiSlider->hifiSpinner');
// setup chrome up/down arrow steps and propagate input field -> slider
var spinner = input.on('change', function() {
var value = spinner.value();
if (isFinite(value) && slider.value() !== value) {
slider.value(value);
}
}).addClass('setting')
.hifiSpinner(
Object.assign({}, commonOptions, { max: 1e4 })
).hifiSpinner('instance');
bridgedSettings.getValueAsync(key, function(err, result) {
slider.options.autoenable !== false && slider.enable();
spinner.options.autoenable !== false && spinner.enable();
spinner.value(result);
});
});
$('#fps').hifiSpinner(spinnerOptions).closest('.row').css('pointer-events', 'all').on('click.subinput', function(evt) {
jquerySettings.setValue('thread-update-mode', 'requestAnimationFrame');
evt.target.focus();
});
// detect invalid numbers entered into spinner fields
$(':ui-hifiSpinner').on('change.validation', function(evt) {
var spinner = $(this).hifiSpinner('instance');
$(this).closest('.row').toggleClass('invalid', !spinner.isValid());
});
// ----------------------------------------------------------------------------
// allow tabbing between checkboxes using the container row
$(':ui-hifiCheckbox,:ui-hifiRadioButton').prop('tabindex', -1).closest('.row').prop('tabindex', 0);
// select the input field text when first focused
$('input').not('input[type=radio],input[type=checkbox]').on('focus', function () {
var dt = (new Date - this.blurredAt);
if (!(dt < 5)) { // debounce
this.blurredAt = +new Date;
$(this).one('mouseup.selectit', function() {
$(this).select();
return false;
}).select();
}
}).on('blur', function(e) {
this.blurredAt = new Date;
});
// monitor changes to specific settings that affect the UI
var monitors = {
// advanced options toggle
'ui-show-advanced-options': function onChange(value) {
function handle(err, result) {
log('** ui-show-advanced-options updated', result+'');
$('body').toggleClass('ui-show-advanced-options', !!result);
jquerySettings.setValue('ui-show-advanced-options', result)
}
if (!onChange.fetched) {
bridgedSettings.getValueAsync('ui-show-advanced-options', handle);
return onChange.fetched = true;
}
handle(null, value);
},
// UI tooltips toggle
'ui-enable-tooltips': function(value) {
if (!tooltipManager) return;
if (value) {
tooltipManager.enable();
tooltipManager.openFocusedTooltip();
} else {
tooltipManager.disable();
}
},
// enable/disable fps field (based on whether thread mode is requestAnimationFrame)
'thread-update-mode': function(value) {
var enabled = (value === 'requestAnimationFrame'), fps = $('#fps');
fps.hifiSpinner(enabled ? 'enable' : 'disable');
fps.closest('.row').toggleClass('disabled', !enabled);
},
// flag BODY with CSS class to indicate active camera move mode
'camera-move-enabled': function(value) {
$('body').toggleClass('camera-move-enabled', value);
},
// update the "keybinding" and #appVersion extraParams displays
'.extraParams': function extraParams(value, other) {
value = bridgedSettings.extraParams;
if (value.mode) {
for (var p in value.mode) {
// tablet-mode, hmd-mode, etc.
$('body').toggleClass(p + '-mode', value.mode[p]);
}
document.oncontextmenu = value.mode.tablet ? function(evt) { return evt.preventDefault(),false; } : null;
$('[data-type=number]').prop('type', value.mode.tablet ? 'number' : 'text');
}
var versionDisplay = [
value.appVersion || '(unknown appVersion)',
PARAMS.debug && '(debug)',
value.mode && value.mode.tablet ? '(tablet)' : '',
].filter(Boolean).join(' | ');
$('#appVersion').find('.output').text(versionDisplay).end().show();
if (value.toggleKey) {
$('#toggleKey').find('.binding').empty()
.append(getKeysHTML(value.toggleKey)).end().show();
}
var activateLookAtOption = value.MyAvatar && value.MyAvatar.supportsLookAtSnappingEnabled;
$(jquerySettings.findNodeByKey('Avatar/lookAtSnappingEnabled'))
.hifiCheckbox(activateLookAtOption ? 'enable' : 'disable')
.closest('.row').toggleClass('disabled', !activateLookAtOption)
.css('pointer-events', 'all') // so tooltips display regardless
var activateCursorOption = value.Reticle && value.Reticle.supportsScale;
$('#minimal-cursor:ui-hifiCheckbox')
.hifiCheckbox(activateCursorOption ? 'enable' : 'disable')
.closest('.row').toggleClass('disabled', !activateCursorOption)
.css('pointer-events', 'all') // so tooltips display regardless
},
// 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) {
debugPrint(localValue, '... Keyboard.RightMouseButton:' + value);
window.active = !value;
},
};
monitors['toggle-advanced-options'] = monitors['ui-toggle-advanced-options'];
monitorSettings(monitors);
// disable selection
// $('input').css('-webkit-user-select', 'none');
viewportUpdated.connect(lessManager, 'onViewportUpdated');
setupTooltips();
$(window).trigger('resize'); // populate viewport
// set up DOM MutationObservers
settingsNodes.each(function tcobo() {
if (this.dataset.hifiType === 'hifiButton') {
return;
}
var id = assert(this.dataset['for'] || this.id, 'could not id for node: ' + this.outerHTML);
assert(!tcobo[id]); // detect dupes
tcobo[id] = true;
debugPrint('OBSERVING NODE', id, this.id || this.getAttribute('for'));
jquerySettings.observeNode(this);
});
// set up key bindings
setupMousetrapKeys();
function getKeysHTML(binding) {
var text = binding.text || ('(#' + binding.key + ')');
// translate hifi's proprietary key scheme into human-friendly KBDs
return [ 'Control', 'Meta', 'Alt', 'Super', 'Menu', 'Shifted' ]
.map(function(flag) {
return binding['is' + flag] && flag;
})
.concat(text)
.filter(Boolean)
.map(function(key) {
return '<kbd>' + key.replace('Shifted','Shift') + '</kbd>';
})
.join('-');
}
} // setupUI
function setupTooltips() {
// extract the tooltip captions
var tooltips = window.tooltips = {};
var target = '[id], [data-for], [for]';
$('.tooltip')
.removeClass('tooltip').addClass('x-tooltip')
.each(function() {
var element = $(this),
input = $(element.parent().find('input').get(0) ||
element.closest('button').get(0));
id = element.prop('id') || element.data('for') ||
input.prop('id') || input.data('for');
assert(id);
tooltips[id] = this.outerHTML;
}).hide();
tooltipManager = new TooltipManager({
enabled: false,
testmode: PARAMS.tooltiptest,
viewport: PARAMS.viewport,
tooltips: tooltips,
elements: '#reset-to-defaults, button, input',
});
viewportUpdated.connect(tooltipManager, 'updateViewport');
// tooltips aren't needed right away, so defer initializing for better page load times
window.setTimeout(tooltipManager.initialize.bind(tooltipManager), 1000);
}
// helper for instrumenting local jquery onchange handlers
function monitorSettings(options) {
return Object.keys(options).reduce(function(out, id) {
var key = bridgedSettings.resolve(id),
domId = jquerySettings.getId(key, true);
if (!domId) {
var placeholder = {
id: id,
type: 'placeholder',
toString: function() {
return this.id;
},
value: undefined,
};
jquerySettings.registerSetting(placeholder, key);
debugPrint('registered placeholder value for setting', id, key);
assert(jquerySettings.findNodeByKey(key) === placeholder);
}
// if (domId === 'toggle-advanced-options') alert([key,id,domId, jquerySettings.findNodeByKey(key)])
assert(function assertion(){
return typeof key === 'string';
}, 'monitorSettings -- received invalid key type');
var context = {
id: id,
key: key,
domId: domId,
options: options,
lastValue: undefined,
initializer: function(hint) {
var key = this.key,
lastValue = this.lastValue;
if (lastValue !== undefined) {
return log('skipping repeat initializer', key, hint);
}
this.lastValue = lastValue = jquerySettings.getValue(key);
this._onChange.call(jquerySettings, key, lastValue, undefined, hint);
},
_onChange: function _onChange(key, value) {
var currentValue = this.getValue(context.id),
jsonCurrentValue = JSON.stringify(currentValue);
if (jsonCurrentValue === context.jsonLastValue) {
if (jsonCurrentValue !== undefined) {
debugPrint([context.key, '_onChange', this, 'not triggering _onChange for duplicated value']);
}
return;
}
context.jsonLastValue = jsonCurrentValue;
var args = [].slice.call(arguments, 0);
debugPrint('monitorSetting._onChange', context.key, value, [].concat(args).pop());
context.options[context.id].apply(this, [ currentValue ].concat(args));
},
onValueReceived: function(key) {
if (key === this.key) {
this._onChange.apply(bridgedSettings, arguments);
}
},
onMutationEvent: function(event) {
if (event.key === this.key) {
context._onChange.call(jquerySettings, event.key, event.value, event.oldValue, event.hifiType+':mutation');
}
},
onPendingRequestsFinished: function onPendingRequestsFinished() {
bridgedSettings.pendingRequestsFinished.disconnect(this, 'onPendingRequestsFinished');
this.initializer('pendingRequestsFinished');
},
};
bridgedSettings.valueReceived.connect(context, 'onValueReceived');
jquerySettings.mutationEvent.connect(context, 'onMutationEvent');
if (bridgedSettings.pendingRequestCount()) {
bridgedSettings.pendingRequestsFinished.connect(context, 'onPendingRequestsFinished');
} else {
window.setTimeout(context.initializer.bind(context, 'monitorSettings init'), 1);
}
return context;
}, {});
}
function initializeDOM() {
Object.defineProperty(window, 'active', {
get: function() {
return window._active;
},
set: function(nv) {
nv = !!nv;
window._active = nv;
debugPrint('window.active == ' + nv);
if (!nv) {
document.activeElement && document.activeElement.blur();
document.body.focus();
tooltipManager && tooltipManager.disable();
debugPrint('TOOLTIPS DISABLED');
} else if (tooltipManager && bridgedSettings) {
if (bridgedSettings.getValue('ui-enable-tooltips')){
tooltipManager.enable();
debugPrint('TOOLTIPS RE-ENABLED');
}
}
$('body').toggleClass('active', window._active);
},
});
$('body').toggleClass('active', window._active = true);
function checkAnim(evt) {
if (!checkAnim.disabled) {
if ($('.scrollable').is(':animated')) {
$('.scrollable').stop();
log(evt.type, 'stop animation');
}
}
}
viewportUpdated = signal(function viewportUpdated(viewport) {});
function triggerViewportUpdate() {
var viewport = {
inner: { width: innerWidth, height: innerHeight },
client: {
width: document.body.clientWidth || window.innerWidth,
height: document.body.clientHeight || window.innerHeight,
},
min: { width: window.innerWidth / 3, height: 32 },
max: { width: window.innerWidth * 7/8, height: window.innerHeight * 7/8 },
};
debugPrint('viewportUpdated', viewport);
PARAMS.viewport = Object.assign(PARAMS.viewport||{}, viewport);
viewportUpdated(viewport, triggerViewportUpdate.lastViewport);
triggerViewportUpdate.lastViewport = viewport;
}
$(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 ** ');
$('body').addClass('window-blurred');
document.body.focus();
document.activeElement && document.activeElement.blur();
// tooltipManager.closeAll();
},
focus: function() {
log('** FOCUS **');
$('body').removeClass('window-blurred');
},
});
}
function setupMousetrapKeys() {
if (!window.Mousetrap) {
return log('WARNING: window.Mousetrap not found; not configurating keybindings');
}
mousetrapMultiBind({
'ctrl+a, option+a': function global(evt, combo) {
$(document.activeElement).filter('input').select();
},
'enter': function global(evt, combo) {
var node = document.activeElement;
if ($(node).is('input')) {
log('enter on input element');
tooltipManager.closeAll();
node.blur();
var nexts = $('[tabindex],input,:focusable').not('[tabindex=-1],.ui-slider-handle');
nexts.add(nexts.find('input'));
nexts = nexts.toArray();
if (~nexts.indexOf(node)) {
var nextActive = nexts[nexts.indexOf(node)+1];
log('setting focus to', nextActive);
$(nextActive).focus();
} else {
log('could not deduce next tabbable element', nexts.length, this);
}
}
return true;
},
'ctrl+w': bridgedSettings.sendEvent.bind(bridgedSettings, { method: 'window.close' }),
'r': location.reload.bind(location),
'space': function global(evt, combo) {
log('SPACE', evt.target, document.activeElement);
$(document.activeElement).filter('.row').find(':ui-hifiCheckbox,:ui-hifiRadioButton').click();
if (!$(document.activeElement).is('input,.ui-widget')) {
return false;
}
return true;
},
});
// $('input').addClass('mousetrap');
function mousetrapMultiBind(a, b) {
var obj = typeof a === 'object' ? a :
Object.defineProperty({}, a, {enumerable: true, value: b });
Object.keys(obj).forEach(function(key) {
var method = obj[key].name === 'global' ? 'bindGlobal' : 'bind';
key.split(/\s*,\s*/).forEach(function(combo) {
debugPrint('Mousetrap', method, combo, typeof obj[key]);
Mousetrap[method](combo, function(evt, combo) {
debugPrint('Mousetrap', method, combo);
return obj[key].apply(this, arguments);
});
});
});
}
}
// support the URL having a #node-id (or #debug=1&node-id) hash fragment jumping to that element
function jumpToAnchor(id) {
id = JSON.stringify(id);
$('[id='+id+'],[name='+id+']').first().each(function() {
log('jumpToAnchor', id);
$(this).show();
this.scrollIntoView({ behavior: 'smooth' });
});
};