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.

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