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.

707 lines
20 KiB

9 years ago
9 years ago
  1. var util = require('../util');
  2. import ColorPicker from './ColorPicker'
  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 suboptions
  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(document.body);
  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. catch (err) {}
  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. * prepare the popup
  336. * @param string
  337. * @param index
  338. * @private
  339. */
  340. _setupPopup(string, index) {
  341. if (this.initialized === true && this.allowCreation === true && this.popupCounter < this.popupLimit) {
  342. let div = document.createElement("div");
  343. div.id = "vis-configuration-popup";
  344. div.className = "vis-configuration-popup";
  345. div.innerHTML = string;
  346. div.onclick = () => {this._removePopup()};
  347. this.popupCounter += 1;
  348. this.popupDiv = {html:div, index:index};
  349. }
  350. }
  351. /**
  352. * remove the popup from the dom
  353. * @private
  354. */
  355. _removePopup() {
  356. if (this.popupDiv.html !== undefined) {
  357. this.popupDiv.html.parentNode.removeChild(this.popupDiv.html);
  358. clearTimeout(this.popupDiv.hideTimeout);
  359. clearTimeout(this.popupDiv.deleteTimeout);
  360. this.popupDiv = {};
  361. }
  362. }
  363. /**
  364. * Show the popup if it is needed.
  365. * @private
  366. */
  367. _showPopupIfNeeded() {
  368. if (this.popupDiv.html !== undefined) {
  369. let correspondingElement = this.domElements[this.popupDiv.index];
  370. let rect = correspondingElement.getBoundingClientRect();
  371. this.popupDiv.html.style.left = rect.left + "px";
  372. this.popupDiv.html.style.top = rect.top - 30 + "px"; // 30 is the height;
  373. document.body.appendChild(this.popupDiv.html)
  374. this.popupDiv.hideTimeout = setTimeout(() => {
  375. this.popupDiv.html.style.opacity = 0;
  376. },1500);
  377. this.popupDiv.deleteTimeout = setTimeout(() => {
  378. this._removePopup();
  379. },1800)
  380. }
  381. }
  382. /**
  383. * make a checkbox for boolean options.
  384. * @param defaultValue
  385. * @param value
  386. * @param path
  387. * @private
  388. */
  389. _makeCheckbox(defaultValue, value, path) {
  390. var checkbox = document.createElement('input');
  391. checkbox.type = 'checkbox';
  392. checkbox.className = 'vis-configuration vis-config-checkbox';
  393. checkbox.checked = defaultValue;
  394. if (value !== undefined) {
  395. checkbox.checked = value;
  396. if (value !== defaultValue) {
  397. if (typeof defaultValue === 'object') {
  398. if (value !== defaultValue.enabled) {
  399. this.changedOptions.push({path:path, value:value});
  400. }
  401. }
  402. else {
  403. this.changedOptions.push({path:path, value:value});
  404. }
  405. }
  406. }
  407. let me = this;
  408. checkbox.onchange = function() {me._update(this.checked, path)};
  409. let label = this._makeLabel(path[path.length-1], path);
  410. this._makeItem(path, label, checkbox);
  411. }
  412. /**
  413. * make a text input field for string options.
  414. * @param defaultValue
  415. * @param value
  416. * @param path
  417. * @private
  418. */
  419. _makeTextInput(defaultValue, value, path) {
  420. var checkbox = document.createElement('input');
  421. checkbox.type = 'text';
  422. checkbox.className = 'vis-configuration vis-config-text';
  423. checkbox.value = value;
  424. if (value !== defaultValue) {
  425. this.changedOptions.push({path:path, value:value});
  426. }
  427. let me = this;
  428. checkbox.onchange = function() {me._update(this.value, path)};
  429. let label = this._makeLabel(path[path.length-1], path);
  430. this._makeItem(path, label, checkbox);
  431. }
  432. /**
  433. * make a color field with a color picker for color fields
  434. * @param arr
  435. * @param value
  436. * @param path
  437. * @private
  438. */
  439. _makeColorField(arr, value, path) {
  440. let defaultColor = arr[1];
  441. let div = document.createElement('div');
  442. value = value === undefined ? defaultColor : value;
  443. if (value !== 'none') {
  444. div.className = 'vis-configuration vis-config-colorBlock';
  445. div.style.backgroundColor = value;
  446. }
  447. else {
  448. div.className = 'vis-configuration vis-config-colorBlock none';
  449. }
  450. value = value === undefined ? defaultColor : value;
  451. div.onclick = () => {
  452. this._showColorPicker(value,div,path);
  453. };
  454. let label = this._makeLabel(path[path.length-1], path);
  455. this._makeItem(path,label, div);
  456. }
  457. /**
  458. * used by the color buttons to call the color picker.
  459. * @param event
  460. * @param value
  461. * @param div
  462. * @param path
  463. * @private
  464. */
  465. _showColorPicker(value, div, path) {
  466. let rect = div.getBoundingClientRect();
  467. let bodyRect = document.body.getBoundingClientRect();
  468. let pickerX = rect.left + rect.width + 5;
  469. let pickerY = rect.top - bodyRect.top + rect.height +2;
  470. this.colorPicker.show(pickerX,pickerY);
  471. this.colorPicker.setColor(value);
  472. this.colorPicker.setCallback((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. }
  478. /**
  479. * parse an object and draw the correct items
  480. * @param obj
  481. * @param path
  482. * @private
  483. */
  484. _handleObject(obj, path = [], checkOnly = false) {
  485. let show = false;
  486. let filter = this.options.filter;
  487. let visibleInSet = false;
  488. for (let subObj in obj) {
  489. if (obj.hasOwnProperty(subObj)) {
  490. show = true;
  491. let item = obj[subObj];
  492. let newPath = util.copyAndExtendArray(path, subObj);
  493. if (typeof filter === 'function') {
  494. show = filter(subObj,path);
  495. // if needed we must go deeper into the object.
  496. if (show === false) {
  497. if (!(item instanceof Array) && typeof item !== 'string' && typeof item !== 'boolean' && item instanceof Object) {
  498. this.allowCreation = false;
  499. show = this._handleObject(item, newPath, true);
  500. this.allowCreation = checkOnly === false;
  501. }
  502. }
  503. }
  504. if (show !== false) {
  505. visibleInSet = true;
  506. let value = this._getValue(newPath);
  507. if (item instanceof Array) {
  508. this._handleArray(item, value, newPath);
  509. }
  510. else if (typeof item === 'string') {
  511. this._makeTextInput(item, value, newPath);
  512. }
  513. else if (typeof item === 'boolean') {
  514. this._makeCheckbox(item, value, newPath);
  515. }
  516. else if (item instanceof Object) {
  517. // collapse the physics options that are not enabled
  518. let draw = true;
  519. if (path.indexOf('physics') !== -1) {
  520. if (this.moduleOptions.physics.solver !== subObj) {
  521. draw = false;
  522. }
  523. }
  524. if (draw === true) {
  525. // initially collapse options with an disabled enabled option.
  526. if (item.enabled !== undefined) {
  527. let enabledPath = util.copyAndExtendArray(newPath, 'enabled');
  528. let enabledValue = this._getValue(enabledPath);
  529. if (enabledValue === true) {
  530. let label = this._makeLabel(subObj, newPath, true);
  531. this._makeItem(newPath, label);
  532. visibleInSet = this._handleObject(item, newPath) || visibleInSet;
  533. }
  534. else {
  535. this._makeCheckbox(item, enabledValue, newPath);
  536. }
  537. }
  538. else {
  539. let label = this._makeLabel(subObj, newPath, true);
  540. this._makeItem(newPath, label);
  541. visibleInSet = this._handleObject(item, newPath) || visibleInSet;
  542. }
  543. }
  544. }
  545. else {
  546. console.error('dont know how to handle', item, subObj, newPath);
  547. }
  548. }
  549. }
  550. }
  551. return visibleInSet;
  552. }
  553. /**
  554. * handle the array type of option
  555. * @param optionName
  556. * @param arr
  557. * @param value
  558. * @param path
  559. * @private
  560. */
  561. _handleArray(arr, value, path) {
  562. if (typeof arr[0] === 'string' && arr[0] === 'color') {
  563. this._makeColorField(arr, value, path);
  564. if (arr[1] !== value) {this.changedOptions.push({path:path, value:value});}
  565. }
  566. else if (typeof arr[0] === 'string') {
  567. this._makeDropdown(arr, value, path);
  568. if (arr[0] !== value) {this.changedOptions.push({path:path, value:value});}
  569. }
  570. else if (typeof arr[0] === 'number') {
  571. this._makeRange(arr, value, path);
  572. if (arr[0] !== value) {this.changedOptions.push({path:path, value:Number(value)});}
  573. }
  574. }
  575. /**
  576. * called to update the network with the new settings.
  577. * @param value
  578. * @param path
  579. * @private
  580. */
  581. _update(value, path) {
  582. let options = this._constructOptions(value,path);
  583. if (this.parent.body && this.parent.body.emitter && this.parent.body.emitter.emit) {
  584. this.parent.body.emitter.emit("configChange", options);
  585. }
  586. this.initialized = true;
  587. this.parent.setOptions(options);
  588. }
  589. _constructOptions(value, path, optionsObj = {}) {
  590. let pointer = optionsObj;
  591. // when dropdown boxes can be string or boolean, we typecast it into correct types
  592. value = value === 'true' ? true : value;
  593. value = value === 'false' ? false : value;
  594. for (let i = 0; i < path.length; i++) {
  595. if (path[i] !== 'global') {
  596. if (pointer[path[i]] === undefined) {
  597. pointer[path[i]] = {};
  598. }
  599. if (i !== path.length - 1) {
  600. pointer = pointer[path[i]];
  601. }
  602. else {
  603. pointer[path[i]] = value;
  604. }
  605. }
  606. }
  607. return optionsObj;
  608. }
  609. _printOptions() {
  610. let options = this.getOptions();
  611. this.optionsContainer.innerHTML = '<pre>var options = ' + JSON.stringify(options, null, 2) + '</pre>';
  612. }
  613. getOptions() {
  614. let options = {};
  615. for (var i = 0; i < this.changedOptions.length; i++) {
  616. this._constructOptions(this.changedOptions[i].value, this.changedOptions[i].path, options)
  617. }
  618. return options;
  619. }
  620. }
  621. export default Configurator;