var Settings = { showAdvanced: false, METAVERSE_URL: 'http://localhost:3000', ADVANCED_CLASS: 'advanced-setting', TRIGGER_CHANGE_CLASS: 'trigger-change', DATA_ROW_CLASS: 'value-row', DATA_COL_CLASS: 'value-col', ADD_ROW_BUTTON_CLASS: 'add-row', ADD_ROW_SPAN_CLASSES: 'glyphicon glyphicon-plus add-row', DEL_ROW_BUTTON_CLASS: 'del-row', DEL_ROW_SPAN_CLASSES: 'glyphicon glyphicon-remove del-row', MOVE_UP_BUTTON_CLASS: 'move-up', MOVE_UP_SPAN_CLASSES: 'glyphicon glyphicon-chevron-up move-up', MOVE_DOWN_BUTTON_CLASS: 'move-down', MOVE_DOWN_SPAN_CLASSES: 'glyphicon glyphicon-chevron-down move-down', TABLE_BUTTONS_CLASS: 'buttons', ADD_DEL_BUTTONS_CLASS: 'add-del-buttons', ADD_DEL_BUTTONS_CLASSES: 'buttons add-del-buttons', REORDER_BUTTONS_CLASS: 'reorder-buttons', REORDER_BUTTONS_CLASSES: 'buttons reorder-buttons', NEW_ROW_CLASS: 'new-row', CONNECT_ACCOUNT_BTN_ID: 'connect-account-btn', DISCONNECT_ACCOUNT_BTN_ID: 'disconnect-account-btn', CREATE_DOMAIN_ID_BTN_ID: 'create-domain-btn', CHOOSE_DOMAIN_ID_BTN_ID: 'choose-domain-btn' }; var viewHelpers = { getFormGroup: function(keypath, setting, values, isAdvanced, isLocked) { form_group = "
"; setting_value = _(values).valueForKeyPath(keypath); if (typeof setting_value == 'undefined' || setting_value === null) { if (_.has(setting, 'default')) { setting_value = setting.default; } else { setting_value = ""; } } label_class = 'control-label'; if (isLocked) { label_class += ' locked'; } function common_attrs(extra_classes) { extra_classes = (typeof extra_classes !== 'undefined' ? extra_classes : ""); return " class='" + (setting.type !== 'checkbox' ? 'form-control' : '') + " " + Settings.TRIGGER_CHANGE_CLASS + " " + extra_classes + "' data-short-name='" + setting.name + "' name='" + keypath + "' " + "id='" + (typeof setting.id !== 'undefined' ? setting.id : keypath) + "'"; } if (setting.type === 'checkbox') { if (setting.label) { form_group += "" } form_group += "
" form_group += ""; form_group += "
" } else { input_type = _.has(setting, 'type') ? setting.type : "text" if (setting.label) { form_group += ""; } if (input_type === 'table') { form_group += makeTable(setting, keypath, setting_value, isLocked) } else { if (input_type === 'select') { form_group += "" form_group += "" } else if (input_type === 'button') { // Is this a button that should link to something directly? // If so, we use an anchor tag instead of a button tag if (setting.href) { form_group += "" + setting.button_label + ""; } else { form_group += ""; } } else { if (input_type == 'integer') { input_type = "text" } form_group += "" } form_group += "" + setting.help + "" } } form_group += "
" return form_group } } $(document).ready(function(){ /* * Clamped-width. * Usage: *
This long content will force clamped width
* * Author: LV */ $('[data-clampedwidth]').each(function () { var elem = $(this); var parentPanel = elem.data('clampedwidth'); var resizeFn = function () { var sideBarNavWidth = $(parentPanel).width() - parseInt(elem.css('paddingLeft')) - parseInt(elem.css('paddingRight')) - parseInt(elem.css('marginLeft')) - parseInt(elem.css('marginRight')) - parseInt(elem.css('borderLeftWidth')) - parseInt(elem.css('borderRightWidth')); elem.css('width', sideBarNavWidth); }; resizeFn(); $(window).resize(resizeFn); }); $('#settings-form').on('click', '.' + Settings.ADD_ROW_BUTTON_CLASS, function(){ addTableRow(this); }); $('#settings-form').on('click', '.' + Settings.DEL_ROW_BUTTON_CLASS, function(){ deleteTableRow(this); }); $('#settings-form').on('click', '.' + Settings.MOVE_UP_BUTTON_CLASS, function(){ moveTableRow(this, true); }); $('#settings-form').on('click', '.' + Settings.MOVE_DOWN_BUTTON_CLASS, function(){ moveTableRow(this, false); }); $('#settings-form').on('keypress', 'table input', function(e){ if (e.keyCode == 13) { // capture enter in table input // if we have a sibling next to us that has an input, jump to it, otherwise check if we have a glyphicon for add to click sibling = $(this).parent('td').next(); if (sibling.hasClass(Settings.DATA_COL_CLASS)) { // set focus to next input sibling.find('input').focus() } else if (sibling.hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) { sibling.find('.' + Settings.ADD_ROW_BUTTON_CLASS).click() // set focus to the first input in the new row $(this).closest('table').find('tr.inputs input:first').focus() } } }); $('#settings-form').on('change', '.' + Settings.TRIGGER_CHANGE_CLASS , function(){ // this input was changed, add the changed data attribute to it $(this).attr('data-changed', true) badgeSidebarForDifferences($(this)) }) $('.advanced-toggle').click(function(){ Settings.showAdvanced = !Settings.showAdvanced var advancedSelector = $('.' + Settings.ADVANCED_CLASS) if (Settings.showAdvanced) { advancedSelector.show(); $(this).html("Hide advanced") } else { advancedSelector.hide(); $(this).html("Show advanced") } $(this).blur(); }) $('#settings-form').on('click', '#' + Settings.CREATE_DOMAIN_ID_BTN_ID, function(){ showDomainCreationAlert(false); }) $('#settings-form').on('click', '#' + Settings.CHOOSE_DOMAIN_ID_BTN_ID, function(){ chooseFromHighFidelityDomains($(this)) }); $('#settings-form').on('change', 'select', function(){ $("input[name='" + $(this).attr('data-hidden-input') + "']").val($(this).val()).change() }); $('#settings-form').on('click', '#' + Settings.DISCONNECT_ACCOUNT_BTN_ID, function(e){ disonnectHighFidelityAccount(); e.preventDefault(); }); $('#settings-form').on('click', '#' + Settings.CONNECT_ACCOUNT_BTN_ID, function(e){ $(this).blur(); prepareAccessTokenPrompt(); }); var panelsSource = $('#panels-template').html() Settings.panelsTemplate = _.template(panelsSource) var sidebarTemplate = $('#list-group-template').html() Settings.sidebarTemplate = _.template(sidebarTemplate) // $('body').scrollspy({ target: '#setup-sidebar'}) reloadSettings(); }); function postSettings(jsonSettings) { // POST the form JSON to the domain-server settings.json endpoint so the settings are saved $.ajax('/settings.json', { data: JSON.stringify(jsonSettings), contentType: 'application/json', type: 'POST' }).done(function(data){ if (data.status == "success") { showRestartModal(); } else { showErrorMessage("Error", SETTINGS_ERROR_MESSAGE) reloadSettings(); } }).fail(function(){ showErrorMessage("Error", SETTINGS_ERROR_MESSAGE) reloadSettings(); }); } function setupHFAccountButton() { // figure out how we should handle the HF connect button var accessToken = Settings.data.values.metaverse.access_token; // setup an object for the settings we want our button to have var buttonSetting = { type: 'button', name: 'connected_account', label: 'Connected Account', } var hasAccessToken = accessToken.length > 0; if (hasAccessToken) { buttonSetting.help = "Click the button above to clear your OAuth token and disconnect your High Fidelity account."; buttonSetting.classes = "btn-danger"; buttonSetting.button_label = "Disconnect High Fidelity Account"; buttonSetting.id = Settings.DISCONNECT_ACCOUNT_BTN_ID; } else { buttonSetting.help = "Click the button above to connect your High Fidelity account."; buttonSetting.classes = "btn-primary"; buttonSetting.button_label = "Connect High Fidelity Account"; buttonSetting.id = Settings.CONNECT_ACCOUNT_BTN_ID; buttonSetting.href = Settings.METAVERSE_URL + "/user/tokens/new?for_domain_server=true"; // since we do not have an access token we change hide domain ID and auto networking settings // without an access token niether of them can do anything $("[data-keypath='metaverse.id']").hide(); $("[data-keypath='metaverse.automatic_networking']").hide(); } var tokenLocked = _(Settings.data).valueForKeyPath("locked.metaverse.access_token"); // use the existing getFormGroup helper to ask for a button var buttonGroup = viewHelpers.getFormGroup('', buttonSetting, Settings.data.values, false, tokenLocked); // add the button group to the top of the metaverse panel $('#metaverse .panel-body').prepend(buttonGroup); } function postNewAccessToken(access_token) { var newAccessToken = { "metaverse": { "access_token": access_token } }; postSettings(newAccessToken); } function disonnectHighFidelityAccount() { // the user clicked on the disconnect account btn - give them a sweet alert to make sure this is what they want to do swal({ title: "Are you sure?", text: "This will remove your domain-server OAuth access token." + "

This could cause your domain to appear offline and no longer be reachable via any place names.", type: "warning", html: true, showCancelButton: true, }, function(){ // we need to post to settings to clear the access-token postNewAccessToken(""); }); } function prepareAccessTokenPrompt() { swal({ title: "Connect Account", type: "input", text: "Paste your created access token here." + "

If you did not successfully create an access token click cancel below and attempt to connect your account again.

", showCancelButton: true, closeOnConfirm: false, html: true }, function(inputValue){ if (inputValue === false) return false; if (inputValue === "") { swal.showInputError("Please paste your access token in the input field.") return false } // we have an input value - set the access token input with this and save settings $("[name='metaverse.access_token']").val(inputValue).change(); // if the user doesn't have a domain ID set, give them the option to create one now if (!Settings.data.values.metaverse.id) { // show domain ID selection alert showDomainIDChoiceAlert(); } else { swal.close(); saveSettings(); } }); } function showDomainIDChoiceAlert() { swal({ title: 'Domain ID', type: 'info', text: "You do not currently have a domain ID." + "

This is required to point place names at your domain and to use automatic networking

" + "Would you like to create a domain ID via the Metaverse API?

", showCancelButton: true, confirmButtonText: "Create new domain ID", cancelButtonText: "Skip", closeOnConfirm: false, html: true }, function(isConfirm){ if (isConfirm) { // show the swal to create a new domain via API showDomainCreationAlert(true); } else { // user cancelled, close this swal and save the access token we got swal.close(); saveSettings(); } }); } function showDomainCreationAlert(justConnected) { swal({ title: 'Create new domain ID', type: 'input', text: 'Enter a short description for this machine.

This will help you identify which domain ID belongs to which machine.

', showCancelButton: true, confirmButtonText: "Create", closeOnConfirm: false, html: true }, function(inputValue){ if (inputValue === false) { swal.close(); // user cancelled domain ID creation - if we're supposed to save after cancel then save here if (justConnected) { saveSettings(); } } else { // we're going to change the alert to a new one with a spinner while we create this domain swal({ title: 'Creating domain ID', text: '
', html: true, showConfirmButton: false, allowEscapeKey: false }); createNewDomainID(inputValue, justConnected); } }); } function createNewDomainID(description, justConnected) { // get the JSON object ready that we'll use to create a new domain var domainJSON = { "domain": { "description": description }, "access_token": $("[name='metaverse.access_token']").val() } $.post(Settings.METAVERSE_URL + "/api/v1/domains", domainJSON, function(data){ if (data.status == "success") { // we successfully created a domain ID, set it on that field var domainID = data.domain.id; $("[name='metaverse.id']").val(domainID).change(); if (justConnected) { var successText = "We connnected your High Fidelity account and created a new domain ID for this machine." } else { var successText = "We created a new domain ID for this machine." } successText += "

Click the button below to save your new settings and restart your domain-server."; // show a sweet alert to say we are all finished up and that we need to save swal({ title: 'Success!', type: 'success', text: successText, html: true, confirmButtonText: 'Save' }, function(){ saveSettings(); }); } }).fail(function(){ var errorText = "There was a problem creating your new domain ID. Do you want to try again or"; if (justConnected) { errorText += " just save your new access token?

You can always create a new domain ID later."; } else { errorText += " cancel?" } // we failed to create the new domain ID, show a sweet-alert that lets them try again or cancel swal({ title: '', type: 'error', text: errorText, html: true, confirmButtonText: 'Try again', showCancelButton: true, closeOnConfirm: false }, function(isConfirm){ if (isConfirm) { // they want to try creating a domain ID again showDomainCreationAlert(justConnected); } else { // they want to cancel if (justConnected) { // since they just connected we need to save the access token here saveSettings(); } } }); }); } function reloadSettings() { $.getJSON('/settings.json', function(data){ _.extend(data, viewHelpers) $('.nav-stacked').html(Settings.sidebarTemplate(data)) $('#panels').html(Settings.panelsTemplate(data)) Settings.data = data; Settings.initialValues = form2js('settings-form', ".", false, cleanupFormValues, true); if (!_.has(data["locked"], "metaverse") && !_.has(data["locked"]["metaverse"], "id")) { // append the domain selection modal, as long as it's not locked appendDomainIDButtons(); } // call our method to setup the HF account button setupHFAccountButton(); // add tooltip to locked settings $('label.locked').tooltip({ placement: 'right', title: 'This setting is in the master config file and cannot be changed' }); }); } function appendDomainIDButtons() { var metaverseInput = $("[name='metaverse.id']"); var createButton = $(""); var chooseButton = $(""); metaverseInput.after(chooseButton); metaverseInput.after(createButton); } var SETTINGS_ERROR_MESSAGE = "There was a problem saving domain settings. Please try again!"; function saveSettings() { // disable any inputs not changed $("input:not([data-changed])").each(function(){ $(this).prop('disabled', true); }); // grab a JSON representation of the form via form2js var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true); console.log(formJSON); // re-enable all inputs $("input").each(function(){ $(this).prop('disabled', false); }); // remove focus from the button $(this).blur(); // POST the form JSON to the domain-server settings.json endpoint so the settings are saved postSettings(formJSON); } $('body').on('click', '.save-button', function(e){ saveSettings(); return false; }); function makeTable(setting, keypath, setting_value, isLocked) { var isArray = !_.has(setting, 'key') if (!isArray && setting.can_order) { setting.can_order = false; } var html = ""; if (setting.help) { html += "" + setting.help + "" } html += "" // Column names html += "" if (setting.numbered === true) { html += "" // Row number } if (setting.key) { html += "" // Key } _.each(setting.columns, function(col) { html += "" // Data }) if (!isLocked) { if (setting.can_order) { html += ""; } html += "" } // populate rows in the table from existing values var row_num = 1 _.each(setting_value, function(row, indexOrName) { html += "" if (setting.numbered === true) { html += "" } if (setting.key) { html += "" } _.each(setting.columns, function(col) { html += "" }) if (!isLocked) { if (setting.can_order) { html += "" } html += "" } html += "" row_num++ }); // populate inputs in the table for new values if (!isLocked) { html += makeTableInputs(setting) } html += "
#" + setting.key.label + "" + col.label + "
" + row_num + "" + indexOrName + "" if (isArray) { rowIsObject = setting.columns.length > 1 colValue = rowIsObject ? row[col.name] : row html += colValue // for arrays we add a hidden input to this td so that values can be posted appropriately html += "" } else if (row.hasOwnProperty(col.name)) { html += row[col.name] } html += "
" return html; } function makeTableInputs(setting) { var html = "" if (setting.numbered === true) { html += "" } if (setting.key) { html += "\ \ " } _.each(setting.columns, function(col) { html += "\ \ " }) if (setting.can_order) { html += "" } html += "" html += "" return html } function badgeSidebarForDifferences(changedElement) { // figure out which group this input is in var panelParentID = changedElement.closest('.panel').attr('id') // if the panel contains non-grouped settings, the initial value is Settings.initialValues var isGrouped = $(panelParentID).hasClass('grouped'); if (isGrouped) { var initialPanelJSON = Settings.initialValues[panelParentID]; // get a JSON representation of that section var panelJSON = form2js(panelParentID, ".", false, cleanupFormValues, true)[panelParentID]; } else { var initialPanelJSON = Settings.initialValues; // get a JSON representation of that section var panelJSON = form2js(panelParentID, ".", false, cleanupFormValues, true); } var badgeValue = 0 // badge for any settings we have that are not the same or are not present in initialValues for (var setting in panelJSON) { if ((!_.has(initialPanelJSON, setting) && panelJSON[setting] !== "") || (!_.isEqual(panelJSON[setting], initialPanelJSON[setting]) && (panelJSON[setting] !== "" || _.has(initialPanelJSON, setting)))) { badgeValue += 1 } } // update the list-group-item badge to have the new value if (badgeValue == 0) { badgeValue = "" } $("a[href='#" + panelParentID + "'] .badge").html(badgeValue); } function addTableRow(add_glyphicon) { var row = $(add_glyphicon).closest('tr') var table = row.parents('table') var isArray = table.data('setting-type') === 'array' var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS) if (!isArray) { // Check key spaces var key = row.children(".key").children("input").val() if (key.indexOf(' ') !== -1) { showErrorMessage("Error", "Key contains spaces") return } // Check keys with the same name var equals = false; _.each(columns.children(".key"), function(element) { if ($(element).text() === key) { equals = true return } }) if (equals) { showErrorMessage("Error", "Two keys cannot be identical") return } } // Check empty fields var empty = false; _.each(row.children('.' + Settings.DATA_COL_CLASS + ' input'), function(element) { if ($(element).val().length === 0) { empty = true return } }) if (empty) { showErrorMessage("Error", "Empty field(s)") return } var input_clone = row.clone() // Change input row to data row var table = row.parents("table") var setting_name = table.attr("name") var full_name = setting_name + "." + key row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS) row.removeClass("inputs") _.each(row.children(), function(element) { if ($(element).hasClass("numbered")) { // Index row var numbers = columns.children(".numbered") if (numbers.length > 0) { $(element).html(parseInt(numbers.last().text()) + 1) } else { $(element).html(1) } } else if ($(element).hasClass(Settings.REORDER_BUTTONS_CLASS)) { $(element).html("") } else if ($(element).hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) { // Change buttons var span = $(element).children("span") span.removeClass(Settings.ADD_ROW_SPAN_CLASSES) span.addClass(Settings.DEL_ROW_SPAN_CLASSES) } else if ($(element).hasClass("key")) { var input = $(element).children("input") $(element).html(input.val()) input.remove() } else if ($(element).hasClass(Settings.DATA_COL_CLASS)) { // Hide inputs var input = $(element).children("input") input.attr("type", "hidden") if (isArray) { var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length var key = $(element).attr('name') // are there multiple columns or just one? // with multiple we have an array of Objects, with one we have an array of whatever the value type is var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) } else { input.attr("name", full_name + "." + $(element).attr("name")) } input.attr("data-changed", "true") $(element).append(input.val()) } else { console.log("Unknown table element") } }) input_clone.find('input').each(function(){ $(this).val($(this).attr('data-default')); }); if (isArray) { updateDataChangedForSiblingRows(row, true) // the addition of any table row should remove the empty-array-row row.siblings('.empty-array-row').remove() } badgeSidebarForDifferences($(table)) row.parent().append(input_clone) } function deleteTableRow(delete_glyphicon) { var row = $(delete_glyphicon).closest('tr') var table = $(row).closest('table') var isArray = table.data('setting-type') === 'array' row.empty(); if (!isArray) { row.html(""); } else { if (table.find('.' + Settings.DATA_ROW_CLASS).length > 1) { updateDataChangedForSiblingRows(row) // this isn't the last row - we can just remove it row.remove() } else { // this is the last row, we can't remove it completely since we need to post an empty array row.removeClass(Settings.DATA_ROW_CLASS).removeClass(Settings.NEW_ROW_CLASS) row.addClass('empty-array-row') row.html(""); } } // we need to fire a change event on one of the remaining inputs so that the sidebar badge is updated badgeSidebarForDifferences($(table)) } function moveTableRow(move_glyphicon, move_up) { var row = $(move_glyphicon).closest('tr') var table = $(row).closest('table') var isArray = table.data('setting-type') === 'array' if (!isArray) { return; } if (move_up) { var prev_row = row.prev() if (prev_row.hasClass(Settings.DATA_ROW_CLASS)) { prev_row.before(row) } } else { var next_row = row.next() if (next_row.hasClass(Settings.DATA_ROW_CLASS)) { next_row.after(row) } } // we need to fire a change event on one of the remaining inputs so that the sidebar badge is updated badgeSidebarForDifferences($(table)) } function updateDataChangedForSiblingRows(row, forceTrue) { // anytime a new row is added to an array we need to set data-changed for all sibling row inputs to true // unless it matches the inital set of values if (!forceTrue) { // figure out which group this row is in var panelParentID = row.closest('.panel').attr('id') // get the short name for the setting from the table var tableShortName = row.closest('table').data('short-name') // get a JSON representation of that section var panelSettingJSON = form2js(panelParentID, ".", false, cleanupFormValues, true)[panelParentID][tableShortName] var initialPanelSettingJSON = Settings.initialValues[panelParentID][tableShortName] // if they are equal, we don't need data-changed isTrue = !_.isEqual(panelSettingJSON, initialPanelSettingJSON) } else { isTrue = true } row.siblings('.' + Settings.DATA_ROW_CLASS).each(function(){ var hiddenInput = $(this).find('td.' + Settings.DATA_COL_CLASS + ' input') if (isTrue) { hiddenInput.attr('data-changed', isTrue) } else { hiddenInput.removeAttr('data-changed') } }) } function showRestartModal() { $('#restart-modal').modal({ backdrop: 'static', keyboard: false }); var secondsElapsed = 0; var numberOfSecondsToWait = 3; var refreshSpan = $('span#refresh-time') refreshSpan.html(numberOfSecondsToWait + " seconds"); // call ourselves every 1 second to countdown var refreshCountdown = setInterval(function(){ secondsElapsed++; secondsLeft = numberOfSecondsToWait - secondsElapsed refreshSpan.html(secondsLeft + (secondsLeft == 1 ? " second" : " seconds")) if (secondsElapsed == numberOfSecondsToWait) { location.reload(true); clearInterval(refreshCountdown); } }, 1000); } function cleanupFormValues(node) { if (node.type && node.type === 'checkbox') { return { name: node.name, value: node.checked ? true : false }; } else { return false; } } function showErrorMessage(title, message) { swal(title, message) } function chooseFromHighFidelityDomains(clickedButton) { // setup the modal to help user pick their domain if (Settings.initialValues.metaverse.access_token) { // add a spinner to the choose button clickedButton.html("Loading domains...") clickedButton.attr('disabled', 'disabled') // get a list of user domains from data-web data_web_domains_url = Settings.METAVERSE_URL + "/api/v1/domains?access_token=" $.getJSON(data_web_domains_url + Settings.initialValues.metaverse.access_token, function(data){ modal_buttons = { cancel: { label: 'Cancel', className: 'btn-default' } } if (data.data.domains.length) { // setup a select box for the returned domains modal_body = "

Choose the High Fidelity domain you want this domain-server to represent.
This will set your domain ID on the settings page.

" domain_select = $("") _.each(data.data.domains, function(domain){ domain_select.append(""); }) modal_body += "" + domain_select[0].outerHTML modal_buttons["success"] = { label: 'Choose domain', callback: function() { domainID = $('#domain-name-select').val() // set the domain ID on the form $("[name='metaverse.id']").val(domainID).change(); } } } else { modal_buttons["success"] = { label: 'Create new domain', callback: function() { window.open("https://metaverse.highfidelity.com/user/domains", '_blank'); } } modal_body = "

You do not have any domains in your High Fidelity account." + "

Go to your domains page to create a new one. Once your domain is created re-open this dialog to select it.

" } bootbox.dialog({ title: "Choose matching domain", message: modal_body, buttons: modal_buttons }) // remove the spinner from the choose button clickedButton.html("Choose from my domains") clickedButton.removeAttr('disabled') }) } else { bootbox.alert({ message: "You must have an access token to query your High Fidelity domains.

" + "Please follow the instructions on the settings page to add an access token.", title: "Access token required" }) } }