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.

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