visjs-network
Version:
A dynamic, browser-based network visualization library.
761 lines (693 loc) • 21.9 kB
JavaScript
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