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.

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