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.

530 lines
15 KiB

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