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.

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