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.

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