UNPKG

dat.gui

Version:

A lightweight graphical user interface for changing variables in JavaScript.

1,411 lines (1,174 loc) 38.4 kB
/** * dat-gui JavaScript Controller Library * https://github.com/dataarts/dat.gui * * Copyright 2011 Data Arts Team, Google Creative Lab * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 */ import css from '../utils/css'; import saveDialogueContents from './saveDialogue.html'; import ControllerFactory from '../controllers/ControllerFactory'; import Controller from '../controllers/Controller'; import BooleanController from '../controllers/BooleanController'; import FunctionController from '../controllers/FunctionController'; import NumberControllerBox from '../controllers/NumberControllerBox'; import NumberControllerSlider from '../controllers/NumberControllerSlider'; import ColorController from '../controllers/ColorController'; import requestAnimationFrame from '../utils/requestAnimationFrame'; import CenteredDiv from '../dom/CenteredDiv'; import dom from '../dom/dom'; import common from '../utils/common'; import styleSheet from './style.scss'; // CSS to embed in build css.inject(styleSheet); /** @ignore Outer-most className for GUI's */ const CSS_NAMESPACE = 'dg'; const HIDE_KEY_CODE = 72; /** @ignore The only value shared between the JS and SCSS. Use caution. */ const CLOSE_BUTTON_HEIGHT = 20; const DEFAULT_DEFAULT_PRESET_NAME = 'Default'; const SUPPORTS_LOCAL_STORAGE = (function() { try { return !!window.localStorage; } catch (e) { return false; } }()); let SAVE_DIALOGUE; /** @ignore Have we yet to create an autoPlace GUI? */ let autoPlaceVirgin = true; /** @ignore Fixed position div that auto place GUI's go inside */ let autoPlaceContainer; /** @ignore Are we hiding the GUI's ? */ let hide = false; /** @private GUI's which should be hidden */ const hideableGuis = []; /** * @class A lightweight controller library for JavaScript. It allows you to easily * manipulate variables and fire functions on the fly. * * @typicalname gui * * @example * // Creating a GUI with options. * var gui = new dat.GUI({name: 'My GUI'}); * * @example * // Creating a GUI and a subfolder. * var gui = new dat.GUI(); * var folder1 = gui.addFolder('Flow Field'); * * @param {Object} [params] * @param {String} [params.name] The name of this GUI. * @param {Object} [params.load] JSON object representing the saved state of * this GUI. * @param {dat.gui.GUI} [params.parent] The GUI I'm nested in. * @param {Boolean} [params.autoPlace=true] * @param {Boolean} [params.hideable=true] If true, GUI is shown/hidden by <kbd>h</kbd> keypress. * @param {Boolean} [params.closed=false] If true, starts closed * @param {Boolean} [params.closeOnTop=false] If true, close/open button shows on top of the GUI */ const GUI = function(pars) { const _this = this; let params = pars || {}; /** * Outermost DOM Element * @type {DOMElement} */ this.domElement = document.createElement('div'); this.__ul = document.createElement('ul'); this.domElement.appendChild(this.__ul); dom.addClass(this.domElement, CSS_NAMESPACE); /** * Nested GUI's by name * @ignore */ this.__folders = {}; this.__controllers = []; /** * List of objects I'm remembering for save, only used in top level GUI * @ignore */ this.__rememberedObjects = []; /** * Maps the index of remembered objects to a map of controllers, only used * in top level GUI. * * @private * @ignore * * @example * [ * { * propertyName: Controller, * anotherPropertyName: Controller * }, * { * propertyName: Controller * } * ] */ this.__rememberedObjectIndecesToControllers = []; this.__listening = []; // Default parameters params = common.defaults(params, { closeOnTop: false, autoPlace: true, width: GUI.DEFAULT_WIDTH }); params = common.defaults(params, { resizable: params.autoPlace, hideable: params.autoPlace }); if (!common.isUndefined(params.load)) { // Explicit preset if (params.preset) { params.load.preset = params.preset; } } else { params.load = { preset: DEFAULT_DEFAULT_PRESET_NAME }; } if (common.isUndefined(params.parent) && params.hideable) { hideableGuis.push(this); } // Only root level GUI's are resizable. params.resizable = common.isUndefined(params.parent) && params.resizable; if (params.autoPlace && common.isUndefined(params.scrollable)) { params.scrollable = true; } // params.scrollable = common.isUndefined(params.parent) && params.scrollable === true; // Not part of params because I don't want people passing this in via // constructor. Should be a 'remembered' value. let useLocalStorage = SUPPORTS_LOCAL_STORAGE && localStorage.getItem(getLocalStorageHash(this, 'isLocal')) === 'true'; let saveToLocalStorage; let titleRow; Object.defineProperties(this, /** @lends GUI.prototype */ { /** * The parent <code>GUI</code> * @type dat.gui.GUI */ parent: { get: function() { return params.parent; } }, scrollable: { get: function() { return params.scrollable; } }, /** * Handles <code>GUI</code>'s element placement for you * @type Boolean */ autoPlace: { get: function() { return params.autoPlace; } }, /** * Handles <code>GUI</code>'s position of open/close button * @type Boolean */ closeOnTop: { get: function() { return params.closeOnTop; } }, /** * The identifier for a set of saved values * @type String */ preset: { get: function() { if (_this.parent) { return _this.getRoot().preset; } return params.load.preset; }, set: function(v) { if (_this.parent) { _this.getRoot().preset = v; } else { params.load.preset = v; } setPresetSelectIndex(this); _this.revert(); } }, /** * The width of <code>GUI</code> element * @type Number */ width: { get: function() { return params.width; }, set: function(v) { params.width = v; setWidth(_this, v); } }, /** * The name of <code>GUI</code>. Used for folders. i.e * a folder's name * @type String */ name: { get: function() { return params.name; }, set: function(v) { // TODO Check for collisions among sibling folders params.name = v; if (titleRow) { titleRow.innerHTML = params.name; } } }, /** * Whether the <code>GUI</code> is collapsed or not * @type Boolean */ closed: { get: function() { return params.closed; }, set: function(v) { params.closed = v; if (params.closed) { dom.addClass(_this.__ul, GUI.CLASS_CLOSED); } else { dom.removeClass(_this.__ul, GUI.CLASS_CLOSED); } // For browsers that aren't going to respect the CSS transition, // Lets just check our height against the window height right off // the bat. this.onResize(); if (_this.__closeButton) { _this.__closeButton.innerHTML = v ? GUI.TEXT_OPEN : GUI.TEXT_CLOSED; } } }, /** * Contains all presets * @type Object */ load: { get: function() { return params.load; } }, /** * Determines whether or not to use <a href="https://developer.mozilla.org/en/DOM/Storage#localStorage">localStorage</a> as the means for * <code>remember</code>ing * @type Boolean */ useLocalStorage: { get: function() { return useLocalStorage; }, set: function(bool) { if (SUPPORTS_LOCAL_STORAGE) { useLocalStorage = bool; if (bool) { dom.bind(window, 'unload', saveToLocalStorage); } else { dom.unbind(window, 'unload', saveToLocalStorage); } localStorage.setItem(getLocalStorageHash(_this, 'isLocal'), bool); } } } }); // Are we a root level GUI? if (common.isUndefined(params.parent)) { this.closed = params.closed || false; dom.addClass(this.domElement, GUI.CLASS_MAIN); dom.makeSelectable(this.domElement, false); // Are we supposed to be loading locally? if (SUPPORTS_LOCAL_STORAGE) { if (useLocalStorage) { _this.useLocalStorage = true; const savedGui = localStorage.getItem(getLocalStorageHash(this, 'gui')); if (savedGui) { params.load = JSON.parse(savedGui); } } } this.__closeButton = document.createElement('div'); this.__closeButton.innerHTML = GUI.TEXT_CLOSED; dom.addClass(this.__closeButton, GUI.CLASS_CLOSE_BUTTON); if (params.closeOnTop) { dom.addClass(this.__closeButton, GUI.CLASS_CLOSE_TOP); this.domElement.insertBefore(this.__closeButton, this.domElement.childNodes[0]); } else { dom.addClass(this.__closeButton, GUI.CLASS_CLOSE_BOTTOM); this.domElement.appendChild(this.__closeButton); } dom.bind(this.__closeButton, 'click', function() { _this.closed = !_this.closed; }); // Oh, you're a nested GUI! } else { if (params.closed === undefined) { params.closed = true; } const titleRowName = document.createTextNode(params.name); dom.addClass(titleRowName, 'controller-name'); titleRow = addRow(_this, titleRowName); const onClickTitle = function(e) { e.preventDefault(); _this.closed = !_this.closed; return false; }; dom.addClass(this.__ul, GUI.CLASS_CLOSED); dom.addClass(titleRow, 'title'); dom.bind(titleRow, 'click', onClickTitle); if (!params.closed) { this.closed = false; } } if (params.autoPlace) { if (common.isUndefined(params.parent)) { if (autoPlaceVirgin) { autoPlaceContainer = document.createElement('div'); dom.addClass(autoPlaceContainer, CSS_NAMESPACE); dom.addClass(autoPlaceContainer, GUI.CLASS_AUTO_PLACE_CONTAINER); document.body.appendChild(autoPlaceContainer); autoPlaceVirgin = false; } // Put it in the dom for you. autoPlaceContainer.appendChild(this.domElement); // Apply the auto styles dom.addClass(this.domElement, GUI.CLASS_AUTO_PLACE); } // Make it not elastic. if (!this.parent) { setWidth(_this, params.width); } } this.__resizeHandler = function() { _this.onResizeDebounced(); }; dom.bind(window, 'resize', this.__resizeHandler); dom.bind(this.__ul, 'webkitTransitionEnd', this.__resizeHandler); dom.bind(this.__ul, 'transitionend', this.__resizeHandler); dom.bind(this.__ul, 'oTransitionEnd', this.__resizeHandler); this.onResize(); if (params.resizable) { addResizeHandle(this); } saveToLocalStorage = function() { if (SUPPORTS_LOCAL_STORAGE && localStorage.getItem(getLocalStorageHash(_this, 'isLocal')) === 'true') { localStorage.setItem(getLocalStorageHash(_this, 'gui'), JSON.stringify(_this.getSaveObject())); } }; // expose this method publicly this.saveToLocalStorageIfPossible = saveToLocalStorage; function resetWidth() { const root = _this.getRoot(); root.width += 1; common.defer(function() { root.width -= 1; }); } if (!params.parent) { resetWidth(); } }; GUI.toggleHide = function() { hide = !hide; common.each(hideableGuis, function(gui) { gui.domElement.style.display = hide ? 'none' : ''; }); }; GUI.CLASS_AUTO_PLACE = 'a'; GUI.CLASS_AUTO_PLACE_CONTAINER = 'ac'; GUI.CLASS_MAIN = 'main'; GUI.CLASS_CONTROLLER_ROW = 'cr'; GUI.CLASS_TOO_TALL = 'taller-than-window'; GUI.CLASS_CLOSED = 'closed'; GUI.CLASS_CLOSE_BUTTON = 'close-button'; GUI.CLASS_CLOSE_TOP = 'close-top'; GUI.CLASS_CLOSE_BOTTOM = 'close-bottom'; GUI.CLASS_DRAG = 'drag'; GUI.DEFAULT_WIDTH = 245; GUI.TEXT_CLOSED = 'Close Controls'; GUI.TEXT_OPEN = 'Open Controls'; GUI._keydownHandler = function(e) { if (document.activeElement.type !== 'text' && (e.which === HIDE_KEY_CODE || e.keyCode === HIDE_KEY_CODE)) { GUI.toggleHide(); } }; dom.bind(window, 'keydown', GUI._keydownHandler, false); common.extend( GUI.prototype, /** @lends GUI.prototype */ { /** * Adds a new {@link Controller} to the GUI. The type of controller created * is inferred from the initial value of <code>object[property]</code>. For * color properties, see {@link addColor}. * * @param {Object} object The object to be manipulated * @param {String} property The name of the property to be manipulated * @param {Number} [min] Minimum allowed value * @param {Number} [max] Maximum allowed value * @param {Number} [step] Increment by which to change value * @returns {Controller} The controller that was added to the GUI. * @instance * * @example * // Add a string controller. * var person = {name: 'Sam'}; * gui.add(person, 'name'); * * @example * // Add a number controller slider. * var person = {age: 45}; * gui.add(person, 'age', 0, 100); */ add: function(object, property) { return add( this, object, property, { factoryArgs: Array.prototype.slice.call(arguments, 2) } ); }, /** * Adds a new color controller to the GUI. * * @param object * @param property * @returns {Controller} The controller that was added to the GUI. * @instance * * @example * var palette = { * color1: '#FF0000', // CSS string * color2: [ 0, 128, 255 ], // RGB array * color3: [ 0, 128, 255, 0.3 ], // RGB with alpha * color4: { h: 350, s: 0.9, v: 0.3 } // Hue, saturation, value * }; * gui.addColor(palette, 'color1'); * gui.addColor(palette, 'color2'); * gui.addColor(palette, 'color3'); * gui.addColor(palette, 'color4'); */ addColor: function(object, property) { return add( this, object, property, { color: true } ); }, /** * Removes the given controller from the GUI. * @param {Controller} controller * @instance */ remove: function(controller) { // TODO listening? this.__ul.removeChild(controller.__li); this.__controllers.splice(this.__controllers.indexOf(controller), 1); const _this = this; common.defer(function() { _this.onResize(); }); }, /** * Removes the root GUI from the document and unbinds all event listeners. * For subfolders, use `gui.removeFolder(folder)` instead. * @instance */ destroy: function() { if (this.parent) { throw new Error( 'Only the root GUI should be removed with .destroy(). ' + 'For subfolders, use gui.removeFolder(folder) instead.' ); } if (this.autoPlace) { autoPlaceContainer.removeChild(this.domElement); } const _this = this; common.each(this.__folders, function(subfolder) { _this.removeFolder(subfolder); }); dom.unbind(window, 'keydown', GUI._keydownHandler, false); removeListeners(this); }, /** * Creates a new subfolder GUI instance. * @param name * @returns {dat.gui.GUI} The new folder. * @throws {Error} if this GUI already has a folder by the specified * name * @instance */ addFolder: function(name) { // We have to prevent collisions on names in order to have a key // by which to remember saved values if (this.__folders[name] !== undefined) { throw new Error('You already have a folder in this GUI by the' + ' name "' + name + '"'); } const newGuiParams = { name: name, parent: this }; // We need to pass down the autoPlace trait so that we can // attach event listeners to open/close folder actions to // ensure that a scrollbar appears if the window is too short. newGuiParams.autoPlace = this.autoPlace; // Do we have saved appearance data for this folder? if (this.load && // Anything loaded? this.load.folders && // Was my parent a dead-end? this.load.folders[name]) { // Did daddy remember me? // Start me closed if I was closed newGuiParams.closed = this.load.folders[name].closed; // Pass down the loaded data newGuiParams.load = this.load.folders[name]; } const gui = new GUI(newGuiParams); this.__folders[name] = gui; const li = addRow(this, gui.domElement); dom.addClass(li, 'folder'); return gui; }, /** * Removes a subfolder GUI instance. * @param {dat.gui.GUI} folder The folder to remove. * @instance */ removeFolder: function(folder) { this.__ul.removeChild(folder.domElement.parentElement); delete this.__folders[folder.name]; // Do we have saved appearance data for this folder? if (this.load && // Anything loaded? this.load.folders && // Was my parent a dead-end? this.load.folders[folder.name]) { delete this.load.folders[folder.name]; } removeListeners(folder); const _this = this; common.each(folder.__folders, function(subfolder) { folder.removeFolder(subfolder); }); common.defer(function() { _this.onResize(); }); }, /** * Opens the GUI. */ open: function() { this.closed = false; }, /** * Closes the GUI. */ close: function() { this.closed = true; }, /** * Hides the GUI. */ hide: function() { this.domElement.style.display = 'none'; }, /** * Shows the GUI. */ show: function() { this.domElement.style.display = ''; }, onResize: function() { // we debounce this function to prevent performance issues when rotating on tablet/mobile const root = this.getRoot(); if (root.scrollable) { const top = dom.getOffset(root.__ul).top; let h = 0; common.each(root.__ul.childNodes, function(node) { if (!(root.autoPlace && node === root.__save_row)) { h += dom.getHeight(node); } }); if (window.innerHeight - top - CLOSE_BUTTON_HEIGHT < h) { dom.addClass(root.domElement, GUI.CLASS_TOO_TALL); root.__ul.style.height = window.innerHeight - top - CLOSE_BUTTON_HEIGHT + 'px'; } else { dom.removeClass(root.domElement, GUI.CLASS_TOO_TALL); root.__ul.style.height = 'auto'; } } if (root.__resize_handle) { common.defer(function() { root.__resize_handle.style.height = root.__ul.offsetHeight + 'px'; }); } if (root.__closeButton) { root.__closeButton.style.width = root.width + 'px'; } }, onResizeDebounced: common.debounce(function() { this.onResize(); }, 50), /** * Mark objects for saving. The order of these objects cannot change as * the GUI grows. When remembering new objects, append them to the end * of the list. * * @param {...Object} objects * @throws {Error} if not called on a top level GUI. * @instance * @ignore */ remember: function() { if (common.isUndefined(SAVE_DIALOGUE)) { SAVE_DIALOGUE = new CenteredDiv(); SAVE_DIALOGUE.domElement.innerHTML = saveDialogueContents; } if (this.parent) { throw new Error('You can only call remember on a top level GUI.'); } const _this = this; common.each(Array.prototype.slice.call(arguments), function(object) { if (_this.__rememberedObjects.length === 0) { addSaveMenu(_this); } if (_this.__rememberedObjects.indexOf(object) === -1) { _this.__rememberedObjects.push(object); } }); if (this.autoPlace) { // Set save row width setWidth(this, this.width); } }, /** * @returns {dat.gui.GUI} the topmost parent GUI of a nested GUI. * @instance */ getRoot: function() { let gui = this; while (gui.parent) { gui = gui.parent; } return gui; }, /** * @returns {Object} a JSON object representing the current state of * this GUI as well as its remembered properties. * @instance */ getSaveObject: function() { const toReturn = this.load; toReturn.closed = this.closed; // Am I remembering any values? if (this.__rememberedObjects.length > 0) { toReturn.preset = this.preset; if (!toReturn.remembered) { toReturn.remembered = {}; } toReturn.remembered[this.preset] = getCurrentPreset(this); } toReturn.folders = {}; common.each(this.__folders, function(element, key) { toReturn.folders[key] = element.getSaveObject(); }); return toReturn; }, save: function() { if (!this.load.remembered) { this.load.remembered = {}; } this.load.remembered[this.preset] = getCurrentPreset(this); markPresetModified(this, false); this.saveToLocalStorageIfPossible(); }, saveAs: function(presetName) { if (!this.load.remembered) { // Retain default values upon first save this.load.remembered = {}; this.load.remembered[DEFAULT_DEFAULT_PRESET_NAME] = getCurrentPreset(this, true); } this.load.remembered[presetName] = getCurrentPreset(this); this.preset = presetName; addPresetOption(this, presetName, true); this.saveToLocalStorageIfPossible(); }, revert: function(gui) { common.each(this.__controllers, function(controller) { // Make revert work on Default. if (!this.getRoot().load.remembered) { controller.setValue(controller.initialValue); } else { recallSavedValue(gui || this.getRoot(), controller); } // fire onFinishChange callback if (controller.__onFinishChange) { controller.__onFinishChange.call(controller, controller.getValue()); } }, this); common.each(this.__folders, function(folder) { folder.revert(folder); }); if (!gui) { markPresetModified(this.getRoot(), false); } }, listen: function(controller) { const init = this.__listening.length === 0; this.__listening.push(controller); if (init) { updateDisplays(this.__listening); } }, updateDisplay: function() { common.each(this.__controllers, function(controller) { controller.updateDisplay(); }); common.each(this.__folders, function(folder) { folder.updateDisplay(); }); } } ); /** * Add a row to the end of the GUI or before another row. * * @param gui * @param [newDom] If specified, inserts the dom content in the new row * @param [liBefore] If specified, places the new row before another row * * @ignore */ function addRow(gui, newDom, liBefore) { const li = document.createElement('li'); if (newDom) { li.appendChild(newDom); } if (liBefore) { gui.__ul.insertBefore(li, liBefore); } else { gui.__ul.appendChild(li); } gui.onResize(); return li; } function removeListeners(gui) { dom.unbind(window, 'resize', gui.__resizeHandler); if (gui.saveToLocalStorageIfPossible) { dom.unbind(window, 'unload', gui.saveToLocalStorageIfPossible); } } function markPresetModified(gui, modified) { const opt = gui.__preset_select[gui.__preset_select.selectedIndex]; if (modified) { opt.innerHTML = opt.value + '*'; } else { opt.innerHTML = opt.value; } } function augmentController(gui, li, controller) { controller.__li = li; controller.__gui = gui; common.extend(controller, /** @lends Controller.prototype */ { /** * @param {Array|Object} options * @return {Controller} */ options: function(options) { if (arguments.length > 1) { const nextSibling = controller.__li.nextElementSibling; controller.remove(); return add( gui, controller.object, controller.property, { before: nextSibling, factoryArgs: [common.toArray(arguments)] } ); } if (common.isArray(options) || common.isObject(options)) { const nextSibling = controller.__li.nextElementSibling; controller.remove(); return add( gui, controller.object, controller.property, { before: nextSibling, factoryArgs: [options] } ); } }, /** * Sets the name of the controller. * @param {string} name * @return {Controller} */ name: function(name) { controller.__li.firstElementChild.firstElementChild.innerHTML = name; return controller; }, /** * Sets controller to listen for changes on its underlying object. * @return {Controller} */ listen: function() { controller.__gui.listen(controller); return controller; }, /** * Removes the controller from its parent GUI. * @return {Controller} */ remove: function() { controller.__gui.remove(controller); return controller; } }); // All sliders should be accompanied by a box. if (controller instanceof NumberControllerSlider) { const box = new NumberControllerBox(controller.object, controller.property, { min: controller.__min, max: controller.__max, step: controller.__step }); common.each(['updateDisplay', 'onChange', 'onFinishChange', 'step', 'min', 'max'], function(method) { const pc = controller[method]; const pb = box[method]; controller[method] = box[method] = function() { const args = Array.prototype.slice.call(arguments); pb.apply(box, args); return pc.apply(controller, args); }; }); dom.addClass(li, 'has-slider'); controller.domElement.insertBefore(box.domElement, controller.domElement.firstElementChild); } else if (controller instanceof NumberControllerBox) { const r = function(returned) { // Have we defined both boundaries? if (common.isNumber(controller.__min) && common.isNumber(controller.__max)) { // Well, then lets just replace this with a slider. // lets remember if the old controller had a specific name or was listening const oldName = controller.__li.firstElementChild.firstElementChild.innerHTML; const wasListening = controller.__gui.__listening.indexOf(controller) > -1; controller.remove(); const newController = add( gui, controller.object, controller.property, { before: controller.__li.nextElementSibling, factoryArgs: [controller.__min, controller.__max, controller.__step] } ); newController.name(oldName); if (wasListening) newController.listen(); return newController; } return returned; }; controller.min = common.compose(r, controller.min); controller.max = common.compose(r, controller.max); } else if (controller instanceof BooleanController) { dom.bind(li, 'click', function() { dom.fakeEvent(controller.__checkbox, 'click'); }); dom.bind(controller.__checkbox, 'click', function(e) { e.stopPropagation(); // Prevents double-toggle }); } else if (controller instanceof FunctionController) { dom.bind(li, 'click', function() { dom.fakeEvent(controller.__button, 'click'); }); dom.bind(li, 'mouseover', function() { dom.addClass(controller.__button, 'hover'); }); dom.bind(li, 'mouseout', function() { dom.removeClass(controller.__button, 'hover'); }); } else if (controller instanceof ColorController) { dom.addClass(li, 'color'); controller.updateDisplay = common.compose(function(val) { li.style.borderLeftColor = controller.__color.toString(); return val; }, controller.updateDisplay); controller.updateDisplay(); } controller.setValue = common.compose(function(val) { if (gui.getRoot().__preset_select && controller.isModified()) { markPresetModified(gui.getRoot(), true); } return val; }, controller.setValue); } function recallSavedValue(gui, controller) { // Find the topmost GUI, that's where remembered objects live. const root = gui.getRoot(); // Does the object we're controlling match anything we've been told to // remember? const matchedIndex = root.__rememberedObjects.indexOf(controller.object); // Why yes, it does! if (matchedIndex !== -1) { // Let me fetch a map of controllers for thcommon.isObject. let controllerMap = root.__rememberedObjectIndecesToControllers[matchedIndex]; // Ohp, I believe this is the first controller we've created for this // object. Lets make the map fresh. if (controllerMap === undefined) { controllerMap = {}; root.__rememberedObjectIndecesToControllers[matchedIndex] = controllerMap; } // Keep track of this controller controllerMap[controller.property] = controller; // Okay, now have we saved any values for this controller? if (root.load && root.load.remembered) { const presetMap = root.load.remembered; // Which preset are we trying to load? let preset; if (presetMap[gui.preset]) { preset = presetMap[gui.preset]; } else if (presetMap[DEFAULT_DEFAULT_PRESET_NAME]) { // Uhh, you can have the default instead? preset = presetMap[DEFAULT_DEFAULT_PRESET_NAME]; } else { // Nada. return; } // Did the loaded object remember thcommon.isObject? && Did we remember this particular property? if (preset[matchedIndex] && preset[matchedIndex][controller.property] !== undefined) { // We did remember something for this guy ... const value = preset[matchedIndex][controller.property]; // And that's what it is. controller.initialValue = value; controller.setValue(value); } } } } function add(gui, object, property, params) { if (object[property] === undefined) { throw new Error(`Object "${object}" has no property "${property}"`); } let controller; if (params.color) { controller = new ColorController(object, property); } else { const factoryArgs = [object, property].concat(params.factoryArgs); controller = ControllerFactory.apply(gui, factoryArgs); } if (params.before instanceof Controller) { params.before = params.before.__li; } recallSavedValue(gui, controller); dom.addClass(controller.domElement, 'c'); const name = document.createElement('span'); dom.addClass(name, 'property-name'); name.innerHTML = controller.property; const container = document.createElement('div'); container.appendChild(name); container.appendChild(controller.domElement); const li = addRow(gui, container, params.before); dom.addClass(li, GUI.CLASS_CONTROLLER_ROW); if (controller instanceof ColorController) { dom.addClass(li, 'color'); } else { dom.addClass(li, typeof controller.getValue()); } augmentController(gui, li, controller); gui.__controllers.push(controller); return controller; } function getLocalStorageHash(gui, key) { // TODO how does this deal with multiple GUI's? return document.location.href + '.' + key; } function addPresetOption(gui, name, setSelected) { const opt = document.createElement('option'); opt.innerHTML = name; opt.value = name; gui.__preset_select.appendChild(opt); if (setSelected) { gui.__preset_select.selectedIndex = gui.__preset_select.length - 1; } } function showHideExplain(gui, explain) { explain.style.display = gui.useLocalStorage ? 'block' : 'none'; } function addSaveMenu(gui) { const div = gui.__save_row = document.createElement('li'); dom.addClass(gui.domElement, 'has-save'); gui.__ul.insertBefore(div, gui.__ul.firstChild); dom.addClass(div, 'save-row'); const gears = document.createElement('span'); gears.innerHTML = '&nbsp;'; dom.addClass(gears, 'button gears'); // TODO replace with FunctionController const button = document.createElement('span'); button.innerHTML = 'Save'; dom.addClass(button, 'button'); dom.addClass(button, 'save'); const button2 = document.createElement('span'); button2.innerHTML = 'New'; dom.addClass(button2, 'button'); dom.addClass(button2, 'save-as'); const button3 = document.createElement('span'); button3.innerHTML = 'Revert'; dom.addClass(button3, 'button'); dom.addClass(button3, 'revert'); const select = gui.__preset_select = document.createElement('select'); if (gui.load && gui.load.remembered) { common.each(gui.load.remembered, function(value, key) { addPresetOption(gui, key, key === gui.preset); }); } else { addPresetOption(gui, DEFAULT_DEFAULT_PRESET_NAME, false); } dom.bind(select, 'change', function() { for (let index = 0; index < gui.__preset_select.length; index++) { gui.__preset_select[index].innerHTML = gui.__preset_select[index].value; } gui.preset = this.value; }); div.appendChild(select); div.appendChild(gears); div.appendChild(button); div.appendChild(button2); div.appendChild(button3); if (SUPPORTS_LOCAL_STORAGE) { const explain = document.getElementById('dg-local-explain'); const localStorageCheckBox = document.getElementById('dg-local-storage'); const saveLocally = document.getElementById('dg-save-locally'); saveLocally.style.display = 'block'; if (localStorage.getItem(getLocalStorageHash(gui, 'isLocal')) === 'true') { localStorageCheckBox.setAttribute('checked', 'checked'); } showHideExplain(gui, explain); // TODO: Use a boolean controller, fool! dom.bind(localStorageCheckBox, 'change', function() { gui.useLocalStorage = !gui.useLocalStorage; showHideExplain(gui, explain); }); } const newConstructorTextArea = document.getElementById('dg-new-constructor'); dom.bind(newConstructorTextArea, 'keydown', function(e) { if (e.metaKey && (e.which === 67 || e.keyCode === 67)) { SAVE_DIALOGUE.hide(); } }); dom.bind(gears, 'click', function() { newConstructorTextArea.innerHTML = JSON.stringify(gui.getSaveObject(), undefined, 2); SAVE_DIALOGUE.show(); newConstructorTextArea.focus(); newConstructorTextArea.select(); }); dom.bind(button, 'click', function() { gui.save(); }); dom.bind(button2, 'click', function() { const presetName = prompt('Enter a new preset name.'); if (presetName) { gui.saveAs(presetName); } }); dom.bind(button3, 'click', function() { gui.revert(); }); // div.appendChild(button2); } function addResizeHandle(gui) { let pmouseX; gui.__resize_handle = document.createElement('div'); common.extend(gui.__resize_handle.style, { width: '6px', marginLeft: '-3px', height: '200px', cursor: 'ew-resize', position: 'absolute' // border: '1px solid blue' }); function drag(e) { e.preventDefault(); gui.width += pmouseX - e.clientX; gui.onResize(); pmouseX = e.clientX; return false; } function dragStop() { dom.removeClass(gui.__closeButton, GUI.CLASS_DRAG); dom.unbind(window, 'mousemove', drag); dom.unbind(window, 'mouseup', dragStop); } function dragStart(e) { e.preventDefault(); pmouseX = e.clientX; dom.addClass(gui.__closeButton, GUI.CLASS_DRAG); dom.bind(window, 'mousemove', drag); dom.bind(window, 'mouseup', dragStop); return false; } dom.bind(gui.__resize_handle, 'mousedown', dragStart); dom.bind(gui.__closeButton, 'mousedown', dragStart); gui.domElement.insertBefore(gui.__resize_handle, gui.domElement.firstElementChild); } function setWidth(gui, w) { gui.domElement.style.width = w + 'px'; // Auto placed save-rows are position fixed, so we have to // set the width manually if we want it to bleed to the edge if (gui.__save_row && gui.autoPlace) { gui.__save_row.style.width = w + 'px'; } if (gui.__closeButton) { gui.__closeButton.style.width = w + 'px'; } } function getCurrentPreset(gui, useInitialValues) { const toReturn = {}; // For each object I'm remembering common.each(gui.__rememberedObjects, function(val, index) { const savedValues = {}; // The controllers I've made for thcommon.isObject by property const controllerMap = gui.__rememberedObjectIndecesToControllers[index]; // Remember each value for each property common.each(controllerMap, function(controller, property) { savedValues[property] = useInitialValues ? controller.initialValue : controller.getValue(); }); // Save the values for thcommon.isObject toReturn[index] = savedValues; }); return toReturn; } function setPresetSelectIndex(gui) { for (let index = 0; index < gui.__preset_select.length; index++) { if (gui.__preset_select[index].value === gui.preset) { gui.__preset_select.selectedIndex = index; } } } function updateDisplays(controllerArray) { if (controllerArray.length !== 0) { requestAnimationFrame.call(window, function() { updateDisplays(controllerArray); }); } common.each(controllerArray, function(c) { c.updateDisplay(); }); } export default GUI;