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.

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