vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

740 lines
21 KiB

9 years ago
9 years ago
  1. var util = require('../util');
  2. var ColorPicker = require('./ColorPicker').default;
  3. /**
  4. * 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.
  5. * Boolean options are recognised as Boolean
  6. * Number options should be written as array: [default value, min value, max value, stepsize]
  7. * Colors should be written as array: ['color', '#ffffff']
  8. * Strings with should be written as array: [option1, option2, option3, ..]
  9. *
  10. * The options are matched with their counterparts in each of the modules and the values used in the configuration are
  11. *
  12. * @class Configurator
  13. */
  14. class Configurator {
  15. /**
  16. *
  17. * @param {Object} parentModule | the location where parentModule.setOptions() can be called
  18. * @param {Object} defaultContainer | the default container of the module
  19. * @param {Object} configureOptions | the fully configured and predefined options set found in allOptions.js
  20. * @param {Number} pixelRatio | canvas pixel ratio
  21. * @constructor Configurator
  22. */
  23. constructor(parentModule, defaultContainer, configureOptions, pixelRatio = 1) {
  24. this.parent = parentModule;
  25. this.changedOptions = [];
  26. this.container = defaultContainer;
  27. this.allowCreation = false;
  28. this.options = {};
  29. this.initialized = false;
  30. this.popupCounter = 0;
  31. this.defaultOptions = {
  32. enabled: false,
  33. filter: true,
  34. container: undefined,
  35. showButton: true
  36. };
  37. util.extend(this.options, this.defaultOptions);
  38. this.configureOptions = configureOptions;
  39. this.moduleOptions = {};
  40. this.domElements = [];
  41. this.popupDiv = {};
  42. this.popupLimit = 5;
  43. this.popupHistory = {};
  44. this.colorPicker = new ColorPicker(pixelRatio);
  45. this.wrapper = undefined;
  46. }
  47. /**
  48. * refresh all options.
  49. * Because all modules parse their options by themselves, we just use their options. We copy them here.
  50. *
  51. * @param {Object} options
  52. */
  53. setOptions(options) {
  54. if (options !== undefined) {
  55. // reset the popup history because the indices may have been changed.
  56. this.popupHistory = {};
  57. this._removePopup();
  58. let enabled = true;
  59. if (typeof options === 'string') {
  60. this.options.filter = options;
  61. }
  62. else if (options instanceof Array) {
  63. this.options.filter = options.join();
  64. }
  65. else if (typeof options === 'object') {
  66. if (options.container !== undefined) {
  67. this.options.container = options.container;
  68. }
  69. if (options.filter !== undefined) {
  70. this.options.filter = options.filter;
  71. }
  72. if (options.showButton !== undefined) {
  73. this.options.showButton = options.showButton;
  74. }
  75. if (options.enabled !== undefined) {
  76. enabled = options.enabled;
  77. }
  78. }
  79. else if (typeof options === 'boolean') {
  80. this.options.filter = true;
  81. enabled = options;
  82. }
  83. else if (typeof options === 'function') {
  84. this.options.filter = options;
  85. enabled = true;
  86. }
  87. if (this.options.filter === false) {
  88. enabled = false;
  89. }
  90. this.options.enabled = enabled;
  91. }
  92. this._clean();
  93. }
  94. /**
  95. *
  96. * @param {Object} moduleOptions
  97. */
  98. setModuleOptions(moduleOptions) {
  99. this.moduleOptions = moduleOptions;
  100. if (this.options.enabled === true) {
  101. this._clean();
  102. if (this.options.container !== undefined) {
  103. this.container = this.options.container;
  104. }
  105. this._create();
  106. }
  107. }
  108. /**
  109. * Create all DOM elements
  110. * @private
  111. */
  112. _create() {
  113. this._clean();
  114. this.changedOptions = [];
  115. let filter = this.options.filter;
  116. let counter = 0;
  117. let show = false;
  118. for (let option in this.configureOptions) {
  119. if (this.configureOptions.hasOwnProperty(option)) {
  120. this.allowCreation = false;
  121. show = false;
  122. if (typeof filter === 'function') {
  123. show = filter(option,[]);
  124. show = show || this._handleObject(this.configureOptions[option], [option], true);
  125. }
  126. else if (filter === true || filter.indexOf(option) !== -1) {
  127. show = true;
  128. }
  129. if (show !== false) {
  130. this.allowCreation = true;
  131. // linebreak between categories
  132. if (counter > 0) {
  133. this._makeItem([]);
  134. }
  135. // a header for the category
  136. this._makeHeader(option);
  137. // get the sub options
  138. this._handleObject(this.configureOptions[option], [option]);
  139. }
  140. counter++;
  141. }
  142. }
  143. if (this.options.showButton === true) {
  144. let generateButton = document.createElement('div');
  145. generateButton.className = 'vis-configuration vis-config-button';
  146. generateButton.innerHTML = 'generate options';
  147. generateButton.onclick = () => {this._printOptions();};
  148. generateButton.onmouseover = () => {generateButton.className = 'vis-configuration vis-config-button hover';};
  149. generateButton.onmouseout = () => {generateButton.className = 'vis-configuration vis-config-button';};
  150. this.optionsContainer = document.createElement('div');
  151. this.optionsContainer.className = 'vis-configuration vis-config-option-container';
  152. this.domElements.push(this.optionsContainer);
  153. this.domElements.push(generateButton);
  154. }
  155. this._push();
  156. //~ this.colorPicker.insertTo(this.container);
  157. }
  158. /**
  159. * draw all DOM elements on the screen
  160. * @private
  161. */
  162. _push() {
  163. this.wrapper = document.createElement('div');
  164. this.wrapper.className = 'vis-configuration-wrapper';
  165. this.container.appendChild(this.wrapper);
  166. for (var i = 0; i < this.domElements.length; i++) {
  167. this.wrapper.appendChild(this.domElements[i]);
  168. }
  169. this._showPopupIfNeeded()
  170. }
  171. /**
  172. * delete all DOM elements
  173. * @private
  174. */
  175. _clean() {
  176. for (var i = 0; i < this.domElements.length; i++) {
  177. this.wrapper.removeChild(this.domElements[i]);
  178. }
  179. if (this.wrapper !== undefined) {
  180. this.container.removeChild(this.wrapper);
  181. this.wrapper = undefined;
  182. }
  183. this.domElements = [];
  184. this._removePopup();
  185. }
  186. /**
  187. * get the value from the actualOptions if it exists
  188. * @param {array} path | where to look for the actual option
  189. * @returns {*}
  190. * @private
  191. */
  192. _getValue(path) {
  193. let base = this.moduleOptions;
  194. for (let i = 0; i < path.length; i++) {
  195. if (base[path[i]] !== undefined) {
  196. base = base[path[i]];
  197. }
  198. else {
  199. base = undefined;
  200. break;
  201. }
  202. }
  203. return base;
  204. }
  205. /**
  206. * all option elements are wrapped in an item
  207. * @param {Array} path | where to look for the actual option
  208. * @param {Array<Element>} domElements
  209. * @returns {Number}
  210. * @private
  211. */
  212. _makeItem(path, ...domElements) {
  213. if (this.allowCreation === true) {
  214. let item = document.createElement('div');
  215. item.className = 'vis-configuration vis-config-item vis-config-s' + path.length;
  216. domElements.forEach((element) => {
  217. item.appendChild(element);
  218. });
  219. this.domElements.push(item);
  220. return this.domElements.length;
  221. }
  222. return 0;
  223. }
  224. /**
  225. * header for major subjects
  226. * @param {string} name
  227. * @private
  228. */
  229. _makeHeader(name) {
  230. let div = document.createElement('div');
  231. div.className = 'vis-configuration vis-config-header';
  232. div.innerHTML = name;
  233. this._makeItem([],div);
  234. }
  235. /**
  236. * make a label, if it is an object label, it gets different styling.
  237. * @param {string} name
  238. * @param {array} path | where to look for the actual option
  239. * @param {string} objectLabel
  240. * @returns {HTMLElement}
  241. * @private
  242. */
  243. _makeLabel(name, path, objectLabel = false) {
  244. let div = document.createElement('div');
  245. div.className = 'vis-configuration vis-config-label vis-config-s' + path.length;
  246. if (objectLabel === true) {
  247. div.innerHTML = '<i><b>' + name + ':</b></i>';
  248. }
  249. else {
  250. div.innerHTML = name + ':';
  251. }
  252. return div;
  253. }
  254. /**
  255. * make a dropdown list for multiple possible string optoins
  256. * @param {Array<number>} arr
  257. * @param {number} value
  258. * @param {array} path | where to look for the actual option
  259. * @private
  260. */
  261. _makeDropdown(arr, value, path) {
  262. let select = document.createElement('select');
  263. select.className = 'vis-configuration vis-config-select';
  264. let selectedValue = 0;
  265. if (value !== undefined) {
  266. if (arr.indexOf(value) !== -1) {
  267. selectedValue = arr.indexOf(value);
  268. }
  269. }
  270. for (let i = 0; i < arr.length; i++) {
  271. let option = document.createElement('option');
  272. option.value = arr[i];
  273. if (i === selectedValue) {
  274. option.selected = 'selected';
  275. }
  276. option.innerHTML = arr[i];
  277. select.appendChild(option);
  278. }
  279. let me = this;
  280. select.onchange = function () {me._update(this.value, path);};
  281. let label = this._makeLabel(path[path.length-1], path);
  282. this._makeItem(path, label, select);
  283. }
  284. /**
  285. * make a range object for numeric options
  286. * @param {Array<number>} arr
  287. * @param {number} value
  288. * @param {array} path | where to look for the actual option
  289. * @private
  290. */
  291. _makeRange(arr, value, path) {
  292. let defaultValue = arr[0];
  293. let min = arr[1];
  294. let max = arr[2];
  295. let step = arr[3];
  296. let range = document.createElement('input');
  297. range.className = 'vis-configuration vis-config-range';
  298. try {
  299. range.type = 'range'; // not supported on IE9
  300. range.min = min;
  301. range.max = max;
  302. }
  303. // TODO: Add some error handling and remove this lint exception
  304. catch (err) {} // eslint-disable-line no-empty
  305. range.step = step;
  306. // set up the popup settings in case they are needed.
  307. let popupString = '';
  308. let popupValue = 0;
  309. if (value !== undefined) {
  310. let factor = 1.20;
  311. if (value < 0 && value * factor < min) {
  312. range.min = Math.ceil(value * factor);
  313. popupValue = range.min;
  314. popupString = 'range increased';
  315. }
  316. else if (value / factor < min) {
  317. range.min = Math.ceil(value / factor);
  318. popupValue = range.min;
  319. popupString = 'range increased';
  320. }
  321. if (value * factor > max && max !== 1) {
  322. range.max = Math.ceil(value * factor);
  323. popupValue = range.max;
  324. popupString = 'range increased';
  325. }
  326. range.value = value;
  327. }
  328. else {
  329. range.value = defaultValue;
  330. }
  331. let input = document.createElement('input');
  332. input.className = 'vis-configuration vis-config-rangeinput';
  333. input.value = range.value;
  334. var me = this;
  335. range.onchange = function () {input.value = this.value; me._update(Number(this.value), path);};
  336. range.oninput = function () {input.value = this.value; };
  337. let label = this._makeLabel(path[path.length-1], path);
  338. let itemIndex = this._makeItem(path, label, range, input);
  339. // if a popup is needed AND it has not been shown for this value, show it.
  340. if (popupString !== '' && this.popupHistory[itemIndex] !== popupValue) {
  341. this.popupHistory[itemIndex] = popupValue;
  342. this._setupPopup(popupString, itemIndex);
  343. }
  344. }
  345. /**
  346. * prepare the popup
  347. * @param {string} string
  348. * @param {Number} index
  349. * @private
  350. */
  351. _setupPopup(string, index) {
  352. if (this.initialized === true && this.allowCreation === true && this.popupCounter < this.popupLimit) {
  353. let div = document.createElement("div");
  354. div.id = "vis-configuration-popup";
  355. div.className = "vis-configuration-popup";
  356. div.innerHTML = string;
  357. div.onclick = () => {this._removePopup()};
  358. this.popupCounter += 1;
  359. this.popupDiv = {html:div, index:index};
  360. }
  361. }
  362. /**
  363. * remove the popup from the dom
  364. * @private
  365. */
  366. _removePopup() {
  367. if (this.popupDiv.html !== undefined) {
  368. this.popupDiv.html.parentNode.removeChild(this.popupDiv.html);
  369. clearTimeout(this.popupDiv.hideTimeout);
  370. clearTimeout(this.popupDiv.deleteTimeout);
  371. this.popupDiv = {};
  372. }
  373. }
  374. /**
  375. * Show the popup if it is needed.
  376. * @private
  377. */
  378. _showPopupIfNeeded() {
  379. if (this.popupDiv.html !== undefined) {
  380. let correspondingElement = this.domElements[this.popupDiv.index];
  381. let rect = correspondingElement.getBoundingClientRect();
  382. this.popupDiv.html.style.left = rect.left + "px";
  383. this.popupDiv.html.style.top = rect.top - 30 + "px"; // 30 is the height;
  384. document.body.appendChild(this.popupDiv.html)
  385. this.popupDiv.hideTimeout = setTimeout(() => {
  386. this.popupDiv.html.style.opacity = 0;
  387. },1500);
  388. this.popupDiv.deleteTimeout = setTimeout(() => {
  389. this._removePopup();
  390. },1800)
  391. }
  392. }
  393. /**
  394. * make a checkbox for boolean options.
  395. * @param {number} defaultValue
  396. * @param {number} value
  397. * @param {array} path | where to look for the actual option
  398. * @private
  399. */
  400. _makeCheckbox(defaultValue, value, path) {
  401. var checkbox = document.createElement('input');
  402. checkbox.type = 'checkbox';
  403. checkbox.className = 'vis-configuration vis-config-checkbox';
  404. checkbox.checked = defaultValue;
  405. if (value !== undefined) {
  406. checkbox.checked = value;
  407. if (value !== defaultValue) {
  408. if (typeof defaultValue === 'object') {
  409. if (value !== defaultValue.enabled) {
  410. this.changedOptions.push({path:path, value:value});
  411. }
  412. }
  413. else {
  414. this.changedOptions.push({path:path, value:value});
  415. }
  416. }
  417. }
  418. let me = this;
  419. checkbox.onchange = function() {me._update(this.checked, path)};
  420. let label = this._makeLabel(path[path.length-1], path);
  421. this._makeItem(path, label, checkbox);
  422. }
  423. /**
  424. * make a text input field for string options.
  425. * @param {number} defaultValue
  426. * @param {number} value
  427. * @param {array} path | where to look for the actual option
  428. * @private
  429. */
  430. _makeTextInput(defaultValue, value, path) {
  431. var checkbox = document.createElement('input');
  432. checkbox.type = 'text';
  433. checkbox.className = 'vis-configuration vis-config-text';
  434. checkbox.value = value;
  435. if (value !== defaultValue) {
  436. this.changedOptions.push({path:path, value:value});
  437. }
  438. let me = this;
  439. checkbox.onchange = function() {me._update(this.value, path)};
  440. let label = this._makeLabel(path[path.length-1], path);
  441. this._makeItem(path, label, checkbox);
  442. }
  443. /**
  444. * make a color field with a color picker for color fields
  445. * @param {Array<number>} arr
  446. * @param {number} value
  447. * @param {array} path | where to look for the actual option
  448. * @private
  449. */
  450. _makeColorField(arr, value, path) {
  451. let defaultColor = arr[1];
  452. let div = document.createElement('div');
  453. value = value === undefined ? defaultColor : value;
  454. if (value !== 'none') {
  455. div.className = 'vis-configuration vis-config-colorBlock';
  456. div.style.backgroundColor = value;
  457. }
  458. else {
  459. div.className = 'vis-configuration vis-config-colorBlock none';
  460. }
  461. value = value === undefined ? defaultColor : value;
  462. div.onclick = () => {
  463. this._showColorPicker(value,div,path);
  464. };
  465. let label = this._makeLabel(path[path.length-1], path);
  466. this._makeItem(path,label, div);
  467. }
  468. /**
  469. * used by the color buttons to call the color picker.
  470. * @param {number} value
  471. * @param {HTMLElement} div
  472. * @param {array} path | where to look for the actual option
  473. * @private
  474. */
  475. _showColorPicker(value, div, path) {
  476. // clear the callback from this div
  477. div.onclick = function() {};
  478. this.colorPicker.insertTo(div);
  479. this.colorPicker.show();
  480. this.colorPicker.setColor(value);
  481. this.colorPicker.setUpdateCallback((color) => {
  482. let colorString = 'rgba(' + color.r + ',' + color.g + ',' + color.b + ',' + color.a + ')';
  483. div.style.backgroundColor = colorString;
  484. this._update(colorString,path);
  485. });
  486. // on close of the colorpicker, restore the callback.
  487. this.colorPicker.setCloseCallback(() => {
  488. div.onclick = () => {
  489. this._showColorPicker(value,div,path);
  490. };
  491. });
  492. }
  493. /**
  494. * parse an object and draw the correct items
  495. * @param {Object} obj
  496. * @param {array} [path=[]] | where to look for the actual option
  497. * @param {boolean} [checkOnly=false]
  498. * @returns {boolean}
  499. * @private
  500. */
  501. _handleObject(obj, path = [], checkOnly = false) {
  502. let show = false;
  503. let filter = this.options.filter;
  504. let visibleInSet = false;
  505. for (let subObj in obj) {
  506. if (obj.hasOwnProperty(subObj)) {
  507. show = true;
  508. let item = obj[subObj];
  509. let newPath = util.copyAndExtendArray(path, subObj);
  510. if (typeof filter === 'function') {
  511. show = filter(subObj,path);
  512. // if needed we must go deeper into the object.
  513. if (show === false) {
  514. if (!(item instanceof Array) && typeof item !== 'string' && typeof item !== 'boolean' && item instanceof Object) {
  515. this.allowCreation = false;
  516. show = this._handleObject(item, newPath, true);
  517. this.allowCreation = checkOnly === false;
  518. }
  519. }
  520. }
  521. if (show !== false) {
  522. visibleInSet = true;
  523. let value = this._getValue(newPath);
  524. if (item instanceof Array) {
  525. this._handleArray(item, value, newPath);
  526. }
  527. else if (typeof item === 'string') {
  528. this._makeTextInput(item, value, newPath);
  529. }
  530. else if (typeof item === 'boolean') {
  531. this._makeCheckbox(item, value, newPath);
  532. }
  533. else if (item instanceof Object) {
  534. // collapse the physics options that are not enabled
  535. let draw = true;
  536. if (path.indexOf('physics') !== -1) {
  537. if (this.moduleOptions.physics.solver !== subObj) {
  538. draw = false;
  539. }
  540. }
  541. if (draw === true) {
  542. // initially collapse options with an disabled enabled option.
  543. if (item.enabled !== undefined) {
  544. let enabledPath = util.copyAndExtendArray(newPath, 'enabled');
  545. let enabledValue = this._getValue(enabledPath);
  546. if (enabledValue === true) {
  547. let label = this._makeLabel(subObj, newPath, true);
  548. this._makeItem(newPath, label);
  549. visibleInSet = this._handleObject(item, newPath) || visibleInSet;
  550. }
  551. else {
  552. this._makeCheckbox(item, enabledValue, newPath);
  553. }
  554. }
  555. else {
  556. let label = this._makeLabel(subObj, newPath, true);
  557. this._makeItem(newPath, label);
  558. visibleInSet = this._handleObject(item, newPath) || visibleInSet;
  559. }
  560. }
  561. }
  562. else {
  563. console.error('dont know how to handle', item, subObj, newPath);
  564. }
  565. }
  566. }
  567. }
  568. return visibleInSet;
  569. }
  570. /**
  571. * handle the array type of option
  572. * @param {Array<number>} arr
  573. * @param {number} value
  574. * @param {array} path | where to look for the actual option
  575. * @private
  576. */
  577. _handleArray(arr, value, path) {
  578. if (typeof arr[0] === 'string' && arr[0] === 'color') {
  579. this._makeColorField(arr, value, path);
  580. if (arr[1] !== value) {this.changedOptions.push({path:path, value:value});}
  581. }
  582. else if (typeof arr[0] === 'string') {
  583. this._makeDropdown(arr, value, path);
  584. if (arr[0] !== value) {this.changedOptions.push({path:path, value:value});}
  585. }
  586. else if (typeof arr[0] === 'number') {
  587. this._makeRange(arr, value, path);
  588. if (arr[0] !== value) {this.changedOptions.push({path:path, value:Number(value)});}
  589. }
  590. }
  591. /**
  592. * called to update the network with the new settings.
  593. * @param {number} value
  594. * @param {array} path | where to look for the actual option
  595. * @private
  596. */
  597. _update(value, path) {
  598. let options = this._constructOptions(value,path);
  599. if (this.parent.body && this.parent.body.emitter && this.parent.body.emitter.emit) {
  600. this.parent.body.emitter.emit("configChange", options);
  601. }
  602. this.initialized = true;
  603. this.parent.setOptions(options);
  604. }
  605. /**
  606. *
  607. * @param {String|Boolean} value
  608. * @param {Array<String>} path
  609. * @param {{}} optionsObj
  610. * @returns {{}}
  611. * @private
  612. */
  613. _constructOptions(value, path, optionsObj = {}) {
  614. let pointer = optionsObj;
  615. // when dropdown boxes can be string or boolean, we typecast it into correct types
  616. value = value === 'true' ? true : value;
  617. value = value === 'false' ? false : value;
  618. for (let i = 0; i < path.length; i++) {
  619. if (path[i] !== 'global') {
  620. if (pointer[path[i]] === undefined) {
  621. pointer[path[i]] = {};
  622. }
  623. if (i !== path.length - 1) {
  624. pointer = pointer[path[i]];
  625. }
  626. else {
  627. pointer[path[i]] = value;
  628. }
  629. }
  630. }
  631. return optionsObj;
  632. }
  633. /**
  634. * @private
  635. */
  636. _printOptions() {
  637. let options = this.getOptions();
  638. this.optionsContainer.innerHTML = '<pre>var options = ' + JSON.stringify(options, null, 2) + '</pre>';
  639. }
  640. /**
  641. *
  642. * @returns {{}} options
  643. */
  644. getOptions() {
  645. let options = {};
  646. for (var i = 0; i < this.changedOptions.length; i++) {
  647. this._constructOptions(this.changedOptions[i].value, this.changedOptions[i].path, options)
  648. }
  649. return options;
  650. }
  651. }
  652. export default Configurator;