UNPKG

visjs-network

Version:

A dynamic, browser-based network visualization library.

761 lines (693 loc) 21.9 kB
var util = require('../util') var ColorPicker = require('./ColorPicker').default /** * The way this works is for all properties of this.possible options, you can supply the property name in any form to list the options. * Boolean options are recognised as Boolean * Number options should be written as array: [default value, min value, max value, stepsize] * Colors should be written as array: ['color', '#ffffff'] * Strings with should be written as array: [option1, option2, option3, ..] * * The options are matched with their counterparts in each of the modules and the values used in the configuration are */ class Configurator { /** * @param {Object} parentModule | the location where parentModule.setOptions() can be called * @param {Object} defaultContainer | the default container of the module * @param {Object} configureOptions | the fully configured and predefined options set found in allOptions.js * @param {number} pixelRatio | canvas pixel ratio */ constructor( parentModule, defaultContainer, configureOptions, pixelRatio = 1 ) { this.parent = parentModule this.changedOptions = [] this.container = defaultContainer this.allowCreation = false this.options = {} this.initialized = false this.popupCounter = 0 this.defaultOptions = { enabled: false, filter: true, container: undefined, showButton: true } util.extend(this.options, this.defaultOptions) this.configureOptions = configureOptions this.moduleOptions = {} this.domElements = [] this.popupDiv = {} this.popupLimit = 5 this.popupHistory = {} this.colorPicker = new ColorPicker(pixelRatio) this.wrapper = undefined } /** * refresh all options. * Because all modules parse their options by themselves, we just use their options. We copy them here. * * @param {Object} options */ setOptions(options) { if (options !== undefined) { // reset the popup history because the indices may have been changed. this.popupHistory = {} this._removePopup() let enabled = true if (typeof options === 'string') { this.options.filter = options } else if (options instanceof Array) { this.options.filter = options.join() } else if (typeof options === 'object') { if (options == null) { throw new TypeError('options cannot be null') } if (options.container !== undefined) { this.options.container = options.container } if (options.filter !== undefined) { this.options.filter = options.filter } if (options.showButton !== undefined) { this.options.showButton = options.showButton } if (options.enabled !== undefined) { enabled = options.enabled } } else if (typeof options === 'boolean') { this.options.filter = true enabled = options } else if (typeof options === 'function') { this.options.filter = options enabled = true } if (this.options.filter === false) { enabled = false } this.options.enabled = enabled } this._clean() } /** * * @param {Object} moduleOptions */ setModuleOptions(moduleOptions) { this.moduleOptions = moduleOptions if (this.options.enabled === true) { this._clean() if (this.options.container !== undefined) { this.container = this.options.container } this._create() } } /** * Create all DOM elements * @private */ _create() { this._clean() this.changedOptions = [] let filter = this.options.filter let counter = 0 let show = false for (let option in this.configureOptions) { if (this.configureOptions.hasOwnProperty(option)) { this.allowCreation = false show = false if (typeof filter === 'function') { show = filter(option, []) show = show || this._handleObject(this.configureOptions[option], [option], true) } else if (filter === true || filter.indexOf(option) !== -1) { show = true } if (show !== false) { this.allowCreation = true // linebreak between categories if (counter > 0) { this._makeItem([]) } // a header for the category this._makeHeader(option) // get the sub options this._handleObject(this.configureOptions[option], [option]) } counter++ } } this._makeButton() this._push() //~ this.colorPicker.insertTo(this.container); } /** * draw all DOM elements on the screen * @private */ _push() { this.wrapper = document.createElement('div') this.wrapper.className = 'vis-configuration-wrapper' this.container.appendChild(this.wrapper) for (var i = 0; i < this.domElements.length; i++) { this.wrapper.appendChild(this.domElements[i]) } this._showPopupIfNeeded() } /** * delete all DOM elements * @private */ _clean() { for (var i = 0; i < this.domElements.length; i++) { this.wrapper.removeChild(this.domElements[i]) } if (this.wrapper !== undefined) { this.container.removeChild(this.wrapper) this.wrapper = undefined } this.domElements = [] this._removePopup() } /** * get the value from the actualOptions if it exists * @param {array} path | where to look for the actual option * @returns {*} * @private */ _getValue(path) { let base = this.moduleOptions for (let i = 0; i < path.length; i++) { if (base[path[i]] !== undefined) { base = base[path[i]] } else { base = undefined break } } return base } /** * all option elements are wrapped in an item * @param {Array} path | where to look for the actual option * @param {Array.<Element>} domElements * @returns {number} * @private */ _makeItem(path, ...domElements) { if (this.allowCreation === true) { let item = document.createElement('div') item.className = 'vis-configuration vis-config-item vis-config-s' + path.length domElements.forEach(element => { item.appendChild(element) }) this.domElements.push(item) return this.domElements.length } return 0 } /** * header for major subjects * @param {string} name * @private */ _makeHeader(name) { let div = document.createElement('div') div.className = 'vis-configuration vis-config-header' div.innerHTML = name this._makeItem([], div) } /** * make a label, if it is an object label, it gets different styling. * @param {string} name * @param {array} path | where to look for the actual option * @param {string} objectLabel * @returns {HTMLElement} * @private */ _makeLabel(name, path, objectLabel = false) { let div = document.createElement('div') div.className = 'vis-configuration vis-config-label vis-config-s' + path.length if (objectLabel === true) { div.innerHTML = '<i><b>' + name + ':</b></i>' } else { div.innerHTML = name + ':' } return div } /** * make a dropdown list for multiple possible string optoins * @param {Array.<number>} arr * @param {number} value * @param {array} path | where to look for the actual option * @private */ _makeDropdown(arr, value, path) { let select = document.createElement('select') select.className = 'vis-configuration vis-config-select' let selectedValue = 0 if (value !== undefined) { if (arr.indexOf(value) !== -1) { selectedValue = arr.indexOf(value) } } for (let i = 0; i < arr.length; i++) { let option = document.createElement('option') option.value = arr[i] if (i === selectedValue) { option.selected = 'selected' } option.innerHTML = arr[i] select.appendChild(option) } let me = this select.onchange = function() { me._update(this.value, path) } let label = this._makeLabel(path[path.length - 1], path) this._makeItem(path, label, select) } /** * make a range object for numeric options * @param {Array.<number>} arr * @param {number} value * @param {array} path | where to look for the actual option * @private */ _makeRange(arr, value, path) { let defaultValue = arr[0] let min = arr[1] let max = arr[2] let step = arr[3] let range = document.createElement('input') range.className = 'vis-configuration vis-config-range' try { range.type = 'range' // not supported on IE9 range.min = min range.max = max } catch (err) { // TODO: Add some error handling and remove this lint exception } // eslint-disable-line no-empty range.step = step // set up the popup settings in case they are needed. let popupString = '' let popupValue = 0 if (value !== undefined) { let factor = 1.2 if (value < 0 && value * factor < min) { range.min = Math.ceil(value * factor) popupValue = range.min popupString = 'range increased' } else if (value / factor < min) { range.min = Math.ceil(value / factor) popupValue = range.min popupString = 'range increased' } if (value * factor > max && max !== 1) { range.max = Math.ceil(value * factor) popupValue = range.max popupString = 'range increased' } range.value = value } else { range.value = defaultValue } let input = document.createElement('input') input.className = 'vis-configuration vis-config-rangeinput' input.value = range.value var me = this range.onchange = function() { input.value = this.value me._update(Number(this.value), path) } range.oninput = function() { input.value = this.value } let label = this._makeLabel(path[path.length - 1], path) let itemIndex = this._makeItem(path, label, range, input) // if a popup is needed AND it has not been shown for this value, show it. if (popupString !== '' && this.popupHistory[itemIndex] !== popupValue) { this.popupHistory[itemIndex] = popupValue this._setupPopup(popupString, itemIndex) } } /** * make a button object * @private */ _makeButton() { if (this.options.showButton === true) { let generateButton = document.createElement('div') generateButton.className = 'vis-configuration vis-config-button' generateButton.innerHTML = 'generate options' generateButton.onclick = () => { this._printOptions() } generateButton.onmouseover = () => { generateButton.className = 'vis-configuration vis-config-button hover' } generateButton.onmouseout = () => { generateButton.className = 'vis-configuration vis-config-button' } this.optionsContainer = document.createElement('div') this.optionsContainer.className = 'vis-configuration vis-config-option-container' this.domElements.push(this.optionsContainer) this.domElements.push(generateButton) } } /** * prepare the popup * @param {string} string * @param {number} index * @private */ _setupPopup(string, index) { if ( this.initialized === true && this.allowCreation === true && this.popupCounter < this.popupLimit ) { let div = document.createElement('div') div.id = 'vis-configuration-popup' div.className = 'vis-configuration-popup' div.innerHTML = string div.onclick = () => { this._removePopup() } this.popupCounter += 1 this.popupDiv = { html: div, index: index } } } /** * remove the popup from the dom * @private */ _removePopup() { if (this.popupDiv.html !== undefined) { this.popupDiv.html.parentNode.removeChild(this.popupDiv.html) clearTimeout(this.popupDiv.hideTimeout) clearTimeout(this.popupDiv.deleteTimeout) this.popupDiv = {} } } /** * Show the popup if it is needed. * @private */ _showPopupIfNeeded() { if (this.popupDiv.html !== undefined) { let correspondingElement = this.domElements[this.popupDiv.index] let rect = correspondingElement.getBoundingClientRect() this.popupDiv.html.style.left = rect.left + 'px' this.popupDiv.html.style.top = rect.top - 30 + 'px' // 30 is the height; document.body.appendChild(this.popupDiv.html) this.popupDiv.hideTimeout = setTimeout(() => { this.popupDiv.html.style.opacity = 0 }, 1500) this.popupDiv.deleteTimeout = setTimeout(() => { this._removePopup() }, 1800) } } /** * make a checkbox for boolean options. * @param {number} defaultValue * @param {number} value * @param {array} path | where to look for the actual option * @private */ _makeCheckbox(defaultValue, value, path) { var checkbox = document.createElement('input') checkbox.type = 'checkbox' checkbox.className = 'vis-configuration vis-config-checkbox' checkbox.checked = defaultValue if (value !== undefined) { checkbox.checked = value if (value !== defaultValue) { if (typeof defaultValue === 'object') { if (value !== defaultValue.enabled) { this.changedOptions.push({ path: path, value: value }) } } else { this.changedOptions.push({ path: path, value: value }) } } } let me = this checkbox.onchange = function() { me._update(this.checked, path) } let label = this._makeLabel(path[path.length - 1], path) this._makeItem(path, label, checkbox) } /** * make a text input field for string options. * @param {number} defaultValue * @param {number} value * @param {array} path | where to look for the actual option * @private */ _makeTextInput(defaultValue, value, path) { var checkbox = document.createElement('input') checkbox.type = 'text' checkbox.className = 'vis-configuration vis-config-text' checkbox.value = value if (value !== defaultValue) { this.changedOptions.push({ path: path, value: value }) } let me = this checkbox.onchange = function() { me._update(this.value, path) } let label = this._makeLabel(path[path.length - 1], path) this._makeItem(path, label, checkbox) } /** * make a color field with a color picker for color fields * @param {Array.<number>} arr * @param {number} value * @param {array} path | where to look for the actual option * @private */ _makeColorField(arr, value, path) { let defaultColor = arr[1] let div = document.createElement('div') value = value === undefined ? defaultColor : value if (value !== 'none') { div.className = 'vis-configuration vis-config-colorBlock' div.style.backgroundColor = value } else { div.className = 'vis-configuration vis-config-colorBlock none' } value = value === undefined ? defaultColor : value div.onclick = () => { this._showColorPicker(value, div, path) } let label = this._makeLabel(path[path.length - 1], path) this._makeItem(path, label, div) } /** * used by the color buttons to call the color picker. * @param {number} value * @param {HTMLElement} div * @param {array} path | where to look for the actual option * @private */ _showColorPicker(value, div, path) { // clear the callback from this div div.onclick = function() {} this.colorPicker.insertTo(div) this.colorPicker.show() this.colorPicker.setColor(value) this.colorPicker.setUpdateCallback(color => { let colorString = 'rgba(' + color.r + ',' + color.g + ',' + color.b + ',' + color.a + ')' div.style.backgroundColor = colorString this._update(colorString, path) }) // on close of the colorpicker, restore the callback. this.colorPicker.setCloseCallback(() => { div.onclick = () => { this._showColorPicker(value, div, path) } }) } /** * parse an object and draw the correct items * @param {Object} obj * @param {array} [path=[]] | where to look for the actual option * @param {boolean} [checkOnly=false] * @returns {boolean} * @private */ _handleObject(obj, path = [], checkOnly = false) { let show = false let filter = this.options.filter let visibleInSet = false for (let subObj in obj) { if (obj.hasOwnProperty(subObj)) { show = true let item = obj[subObj] let newPath = util.copyAndExtendArray(path, subObj) if (typeof filter === 'function') { show = filter(subObj, path) // if needed we must go deeper into the object. if (show === false) { if ( !(item instanceof Array) && typeof item !== 'string' && typeof item !== 'boolean' && item instanceof Object ) { this.allowCreation = false show = this._handleObject(item, newPath, true) this.allowCreation = checkOnly === false } } } if (show !== false) { visibleInSet = true let value = this._getValue(newPath) if (item instanceof Array) { this._handleArray(item, value, newPath) } else if (typeof item === 'string') { this._makeTextInput(item, value, newPath) } else if (typeof item === 'boolean') { this._makeCheckbox(item, value, newPath) } else if (item instanceof Object) { // collapse the physics options that are not enabled let draw = true if (path.indexOf('physics') !== -1) { if (this.moduleOptions.physics.solver !== subObj) { draw = false } } if (draw === true) { // initially collapse options with an disabled enabled option. if (item.enabled !== undefined) { let enabledPath = util.copyAndExtendArray(newPath, 'enabled') let enabledValue = this._getValue(enabledPath) if (enabledValue === true) { let label = this._makeLabel(subObj, newPath, true) this._makeItem(newPath, label) visibleInSet = this._handleObject(item, newPath) || visibleInSet } else { this._makeCheckbox(item, enabledValue, newPath) } } else { let label = this._makeLabel(subObj, newPath, true) this._makeItem(newPath, label) visibleInSet = this._handleObject(item, newPath) || visibleInSet } } } else { console.error('dont know how to handle', item, subObj, newPath) } } } } return visibleInSet } /** * handle the array type of option * @param {Array.<number>} arr * @param {number} value * @param {array} path | where to look for the actual option * @private */ _handleArray(arr, value, path) { if (typeof arr[0] === 'string' && arr[0] === 'color') { this._makeColorField(arr, value, path) if (arr[1] !== value) { this.changedOptions.push({ path: path, value: value }) } } else if (typeof arr[0] === 'string') { this._makeDropdown(arr, value, path) if (arr[0] !== value) { this.changedOptions.push({ path: path, value: value }) } } else if (typeof arr[0] === 'number') { this._makeRange(arr, value, path) if (arr[0] !== value) { this.changedOptions.push({ path: path, value: Number(value) }) } } } /** * called to update the network with the new settings. * @param {number} value * @param {array} path | where to look for the actual option * @private */ _update(value, path) { let options = this._constructOptions(value, path) if ( this.parent.body && this.parent.body.emitter && this.parent.body.emitter.emit ) { this.parent.body.emitter.emit('configChange', options) } this.initialized = true this.parent.setOptions(options) } /** * * @param {string|Boolean} value * @param {Array.<string>} path * @param {{}} optionsObj * @returns {{}} * @private */ _constructOptions(value, path, optionsObj = {}) { let pointer = optionsObj // when dropdown boxes can be string or boolean, we typecast it into correct types value = value === 'true' ? true : value value = value === 'false' ? false : value for (let i = 0; i < path.length; i++) { if (path[i] !== 'global') { if (pointer[path[i]] === undefined) { pointer[path[i]] = {} } if (i !== path.length - 1) { pointer = pointer[path[i]] } else { pointer[path[i]] = value } } } return optionsObj } /** * @private */ _printOptions() { let options = this.getOptions() this.optionsContainer.innerHTML = '<pre>var options = ' + JSON.stringify(options, null, 2) + '</pre>' } /** * * @returns {{}} options */ getOptions() { let options = {} for (var i = 0; i < this.changedOptions.length; i++) { this._constructOptions( this.changedOptions[i].value, this.changedOptions[i].path, options ) } return options } } export default Configurator