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.

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