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.

1605 lines
43 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
9 years ago
9 years ago
9 years ago
11 years ago
11 years ago
11 years ago
10 years ago
10 years ago
11 years ago
11 years ago
10 years ago
10 years ago
10 years ago
10 years ago
Network: Fix handling of multi-fonts (#3486) * The next fix on Travis unit test failure This is the next escalation on the war against the Travis unit tests failing (which came into being by yours truly). By accident, I could recreate the unit test failure on my development machine. This led to a more directed effort to squash the bug. The insight here is that test `(window === undefined)` fails, but `(typeof window === 'undefined`)` succeeds. This undoubtedly has to do with the special status `window` has as a global object. Changes: - Added check on presence of `window` in `Canvas._requestNextFrame()`, fixed local source errors. - Added catch clause in `CanvasRendered._determinePixelRatio()` - small fix: raised timeout for the network `worldCup2014` unit test * Preliminary refactoring in utils.js * Added unit tests for extend routines, commenting and small fixes * More unit tests for extend routines * - Completed unit tests for extend routines in - Small fixes and cleanup in `util.js` - Removed `util.protoExtend()`, not used anywhere * Added unit tests for known font options * Interim save before trying out another proto chain strategy * Fixed problem in first example #3408 * Removed silly file that shouldn't be there * Added unit test for multi-fonts * Comment edits * Verufy unit tests, small adjustments for groups * Further work on getting unit tests to work. PARTS NEED TO BE CLEANED UP! * Further tweaks to get unit tests working * All unit tests passing * Fixes due to linting * Small edits * Removed prototype handling from font pile * Fixes during testing examples of #3408 * Added unit test for edge labels, small fixes * Added unit tests for shorthand string fonts; some tests still failing * All unit tests pass * Removed Label.parseOptions() * Completed shorthand font tests, code cleanup, fixed choosify for edges * Addressed review comments * Addressed review comments, cleanup
7 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
9 years ago
9 years ago
Network: Fix handling of multi-fonts (#3486) * The next fix on Travis unit test failure This is the next escalation on the war against the Travis unit tests failing (which came into being by yours truly). By accident, I could recreate the unit test failure on my development machine. This led to a more directed effort to squash the bug. The insight here is that test `(window === undefined)` fails, but `(typeof window === 'undefined`)` succeeds. This undoubtedly has to do with the special status `window` has as a global object. Changes: - Added check on presence of `window` in `Canvas._requestNextFrame()`, fixed local source errors. - Added catch clause in `CanvasRendered._determinePixelRatio()` - small fix: raised timeout for the network `worldCup2014` unit test * Preliminary refactoring in utils.js * Added unit tests for extend routines, commenting and small fixes * More unit tests for extend routines * - Completed unit tests for extend routines in - Small fixes and cleanup in `util.js` - Removed `util.protoExtend()`, not used anywhere * Added unit tests for known font options * Interim save before trying out another proto chain strategy * Fixed problem in first example #3408 * Removed silly file that shouldn't be there * Added unit test for multi-fonts * Comment edits * Verufy unit tests, small adjustments for groups * Further work on getting unit tests to work. PARTS NEED TO BE CLEANED UP! * Further tweaks to get unit tests working * All unit tests passing * Fixes due to linting * Small edits * Removed prototype handling from font pile * Fixes during testing examples of #3408 * Added unit test for edge labels, small fixes * Added unit tests for shorthand string fonts; some tests still failing * All unit tests pass * Removed Label.parseOptions() * Completed shorthand font tests, code cleanup, fixed choosify for edges * Addressed review comments * Addressed review comments, cleanup
7 years ago
Network: Fix handling of multi-fonts (#3486) * The next fix on Travis unit test failure This is the next escalation on the war against the Travis unit tests failing (which came into being by yours truly). By accident, I could recreate the unit test failure on my development machine. This led to a more directed effort to squash the bug. The insight here is that test `(window === undefined)` fails, but `(typeof window === 'undefined`)` succeeds. This undoubtedly has to do with the special status `window` has as a global object. Changes: - Added check on presence of `window` in `Canvas._requestNextFrame()`, fixed local source errors. - Added catch clause in `CanvasRendered._determinePixelRatio()` - small fix: raised timeout for the network `worldCup2014` unit test * Preliminary refactoring in utils.js * Added unit tests for extend routines, commenting and small fixes * More unit tests for extend routines * - Completed unit tests for extend routines in - Small fixes and cleanup in `util.js` - Removed `util.protoExtend()`, not used anywhere * Added unit tests for known font options * Interim save before trying out another proto chain strategy * Fixed problem in first example #3408 * Removed silly file that shouldn't be there * Added unit test for multi-fonts * Comment edits * Verufy unit tests, small adjustments for groups * Further work on getting unit tests to work. PARTS NEED TO BE CLEANED UP! * Further tweaks to get unit tests working * All unit tests passing * Fixes due to linting * Small edits * Removed prototype handling from font pile * Fixes during testing examples of #3408 * Added unit test for edge labels, small fixes * Added unit tests for shorthand string fonts; some tests still failing * All unit tests pass * Removed Label.parseOptions() * Completed shorthand font tests, code cleanup, fixed choosify for edges * Addressed review comments * Addressed review comments, cleanup
7 years ago
  1. // utility functions
  2. // first check if moment.js is already loaded in the browser window, if so,
  3. // use this instance. Else, load via commonjs.
  4. var moment = require('./module/moment');
  5. var uuid = require('./module/uuid');
  6. /**
  7. * Test whether given object is a number
  8. * @param {*} object
  9. * @return {Boolean} isNumber
  10. */
  11. exports.isNumber = function (object) {
  12. return (object instanceof Number || typeof object == 'number');
  13. };
  14. /**
  15. * Remove everything in the DOM object
  16. * @param {Element} DOMobject
  17. */
  18. exports.recursiveDOMDelete = function (DOMobject) {
  19. if (DOMobject) {
  20. while (DOMobject.hasChildNodes() === true) {
  21. exports.recursiveDOMDelete(DOMobject.firstChild);
  22. DOMobject.removeChild(DOMobject.firstChild);
  23. }
  24. }
  25. };
  26. /**
  27. * this function gives you a range between 0 and 1 based on the min and max values in the set, the total sum of all values and the current value.
  28. *
  29. * @param {number} min
  30. * @param {number} max
  31. * @param {number} total
  32. * @param {number} value
  33. * @returns {number}
  34. */
  35. exports.giveRange = function (min, max, total, value) {
  36. if (max == min) {
  37. return 0.5;
  38. }
  39. else {
  40. var scale = 1 / (max - min);
  41. return Math.max(0, (value - min) * scale);
  42. }
  43. };
  44. /**
  45. * Test whether given object is a string
  46. * @param {*} object
  47. * @return {Boolean} isString
  48. */
  49. exports.isString = function (object) {
  50. return (object instanceof String || typeof object == 'string');
  51. };
  52. /**
  53. * Test whether given object is a Date, or a String containing a Date
  54. * @param {Date | String} object
  55. * @return {Boolean} isDate
  56. */
  57. exports.isDate = function (object) {
  58. if (object instanceof Date) {
  59. return true;
  60. }
  61. else if (exports.isString(object)) {
  62. // test whether this string contains a date
  63. var match = ASPDateRegex.exec(object);
  64. if (match) {
  65. return true;
  66. }
  67. else if (!isNaN(Date.parse(object))) {
  68. return true;
  69. }
  70. }
  71. return false;
  72. };
  73. /**
  74. * Create a semi UUID
  75. * source: http://stackoverflow.com/a/105074/1262753
  76. * @return {string} uuid
  77. */
  78. exports.randomUUID = function () {
  79. return uuid.v4();
  80. };
  81. /**
  82. * assign all keys of an object that are not nested objects to a certain value (used for color objects).
  83. * @param {object} obj
  84. * @param {number} value
  85. */
  86. exports.assignAllKeys = function (obj, value) {
  87. for (var prop in obj) {
  88. if (obj.hasOwnProperty(prop)) {
  89. if (typeof obj[prop] !== 'object') {
  90. obj[prop] = value;
  91. }
  92. }
  93. }
  94. };
  95. /**
  96. * Copy property from b to a if property present in a.
  97. * If property in b explicitly set to null, delete it if `allowDeletion` set.
  98. *
  99. * Internal helper routine, should not be exported. Not added to `exports` for that reason.
  100. *
  101. * @param {object} a target object
  102. * @param {object} b source object
  103. * @param {string} prop name of property to copy to a
  104. * @param {boolean} allowDeletion if true, delete property in a if explicitly set to null in b
  105. * @private
  106. */
  107. function copyOrDelete(a, b, prop, allowDeletion) {
  108. var doDeletion = false;
  109. if (allowDeletion === true) {
  110. doDeletion = (b[prop] === null && a[prop] !== undefined);
  111. }
  112. if (doDeletion) {
  113. delete a[prop];
  114. } else {
  115. a[prop] = b[prop]; // Remember, this is a reference copy!
  116. }
  117. }
  118. /**
  119. * Fill an object with a possibly partially defined other object.
  120. *
  121. * Only copies values for the properties already present in a.
  122. * That means an object is not created on a property if only the b object has it.
  123. *
  124. * @param {object} a
  125. * @param {object} b
  126. * @param {boolean} [allowDeletion=false] if true, delete properties in a that are explicitly set to null in b
  127. */
  128. exports.fillIfDefined = function (a, b, allowDeletion = false) {
  129. // NOTE: iteration of properties of a
  130. // NOTE: prototype properties iterated over as well
  131. for (var prop in a) {
  132. if (b[prop] !== undefined) {
  133. if (b[prop] === null || typeof b[prop] !== 'object') { // Note: typeof null === 'object'
  134. copyOrDelete(a, b, prop, allowDeletion);
  135. } else {
  136. if (typeof a[prop] === 'object') {
  137. exports.fillIfDefined(a[prop], b[prop], allowDeletion);
  138. }
  139. }
  140. }
  141. }
  142. };
  143. /**
  144. * Extend object a with the properties of object b or a series of objects
  145. * Only properties with defined values are copied
  146. * @param {Object} a
  147. * @param {...Object} b
  148. * @return {Object} a
  149. */
  150. exports.extend = function (a, b) { // eslint-disable-line no-unused-vars
  151. for (var i = 1; i < arguments.length; i++) {
  152. var other = arguments[i];
  153. for (var prop in other) {
  154. if (other.hasOwnProperty(prop)) {
  155. a[prop] = other[prop];
  156. }
  157. }
  158. }
  159. return a;
  160. };
  161. /**
  162. * Extend object a with selected properties of object b or a series of objects
  163. * Only properties with defined values are copied
  164. * @param {Array.<string>} props
  165. * @param {Object} a
  166. * @param {Object} b
  167. * @return {Object} a
  168. */
  169. exports.selectiveExtend = function (props, a, b) { // eslint-disable-line no-unused-vars
  170. if (!Array.isArray(props)) {
  171. throw new Error('Array with property names expected as first argument');
  172. }
  173. for (var i = 2; i < arguments.length; i++) {
  174. var other = arguments[i];
  175. for (var p = 0; p < props.length; p++) {
  176. var prop = props[p];
  177. if (other && other.hasOwnProperty(prop)) {
  178. a[prop] = other[prop];
  179. }
  180. }
  181. }
  182. return a;
  183. };
  184. /**
  185. * Extend object a with selected properties of object b.
  186. * Only properties with defined values are copied.
  187. *
  188. * **Note:** Previous version of this routine implied that multiple source objects
  189. * could be used; however, the implementation was **wrong**.
  190. * Since multiple (>1) sources weren't used anywhere in the `vis.js` code,
  191. * this has been removed
  192. *
  193. * @param {Array.<string>} props names of first-level properties to copy over
  194. * @param {object} a target object
  195. * @param {object} b source object
  196. * @param {boolean} [allowDeletion=false] if true, delete property in a if explicitly set to null in b
  197. * @returns {Object} a
  198. */
  199. exports.selectiveDeepExtend = function (props, a, b, allowDeletion = false) {
  200. // TODO: add support for Arrays to deepExtend
  201. if (Array.isArray(b)) {
  202. throw new TypeError('Arrays are not supported by deepExtend');
  203. }
  204. for (var p = 0; p < props.length; p++) {
  205. var prop = props[p];
  206. if (b.hasOwnProperty(prop)) {
  207. if (b[prop] && b[prop].constructor === Object) {
  208. if (a[prop] === undefined) {
  209. a[prop] = {};
  210. }
  211. if (a[prop].constructor === Object) {
  212. exports.deepExtend(a[prop], b[prop], false, allowDeletion);
  213. }
  214. else {
  215. copyOrDelete(a, b, prop, allowDeletion);
  216. }
  217. } else if (Array.isArray(b[prop])) {
  218. throw new TypeError('Arrays are not supported by deepExtend');
  219. } else {
  220. copyOrDelete(a, b, prop, allowDeletion);
  221. }
  222. }
  223. }
  224. return a;
  225. };
  226. /**
  227. * Extend object `a` with properties of object `b`, ignoring properties which are explicitly
  228. * specified to be excluded.
  229. *
  230. * The properties of `b` are considered for copying.
  231. * Properties which are themselves objects are are also extended.
  232. * Only properties with defined values are copied
  233. *
  234. * @param {Array.<string>} propsToExclude names of properties which should *not* be copied
  235. * @param {Object} a object to extend
  236. * @param {Object} b object to take properties from for extension
  237. * @param {boolean} [allowDeletion=false] if true, delete properties in a that are explicitly set to null in b
  238. * @return {Object} a
  239. */
  240. exports.selectiveNotDeepExtend = function (propsToExclude, a, b, allowDeletion = false) {
  241. // TODO: add support for Arrays to deepExtend
  242. // NOTE: array properties have an else-below; apparently, there is a problem here.
  243. if (Array.isArray(b)) {
  244. throw new TypeError('Arrays are not supported by deepExtend');
  245. }
  246. for (var prop in b) {
  247. if (!b.hasOwnProperty(prop)) continue; // Handle local properties only
  248. if (propsToExclude.indexOf(prop) !== -1) continue; // In exclusion list, skip
  249. if (b[prop] && b[prop].constructor === Object) {
  250. if (a[prop] === undefined) {
  251. a[prop] = {};
  252. }
  253. if (a[prop].constructor === Object) {
  254. exports.deepExtend(a[prop], b[prop]); // NOTE: allowDeletion not propagated!
  255. }
  256. else {
  257. copyOrDelete(a, b, prop, allowDeletion);
  258. }
  259. } else if (Array.isArray(b[prop])) {
  260. a[prop] = [];
  261. for (let i = 0; i < b[prop].length; i++) {
  262. a[prop].push(b[prop][i]);
  263. }
  264. } else {
  265. copyOrDelete(a, b, prop, allowDeletion);
  266. }
  267. }
  268. return a;
  269. };
  270. /**
  271. * Deep extend an object a with the properties of object b
  272. *
  273. * @param {Object} a
  274. * @param {Object} b
  275. * @param {boolean} [protoExtend=false] If true, the prototype values will also be extended.
  276. * (ie. the options objects that inherit from others will also get the inherited options)
  277. * @param {boolean} [allowDeletion=false] If true, the values of fields that are null will be deleted
  278. * @returns {Object}
  279. */
  280. exports.deepExtend = function (a, b, protoExtend=false, allowDeletion=false) {
  281. for (var prop in b) {
  282. if (b.hasOwnProperty(prop) || protoExtend === true) {
  283. if (b[prop] && b[prop].constructor === Object) {
  284. if (a[prop] === undefined) {
  285. a[prop] = {};
  286. }
  287. if (a[prop].constructor === Object) {
  288. exports.deepExtend(a[prop], b[prop], protoExtend); // NOTE: allowDeletion not propagated!
  289. }
  290. else {
  291. copyOrDelete(a, b, prop, allowDeletion);
  292. }
  293. } else if (Array.isArray(b[prop])) {
  294. a[prop] = [];
  295. for (let i = 0; i < b[prop].length; i++) {
  296. a[prop].push(b[prop][i]);
  297. }
  298. } else {
  299. copyOrDelete(a, b, prop, allowDeletion);
  300. }
  301. }
  302. }
  303. return a;
  304. };
  305. /**
  306. * Test whether all elements in two arrays are equal.
  307. * @param {Array} a
  308. * @param {Array} b
  309. * @return {boolean} Returns true if both arrays have the same length and same
  310. * elements.
  311. */
  312. exports.equalArray = function (a, b) {
  313. if (a.length != b.length) return false;
  314. for (var i = 0, len = a.length; i < len; i++) {
  315. if (a[i] != b[i]) return false;
  316. }
  317. return true;
  318. };
  319. /**
  320. * Convert an object to another type
  321. * @param {boolean | number | string | Date | Moment | Null | undefined} object
  322. * @param {string | undefined} type Name of the type. Available types:
  323. * 'Boolean', 'Number', 'String',
  324. * 'Date', 'Moment', ISODate', 'ASPDate'.
  325. * @return {*} object
  326. * @throws Error
  327. */
  328. exports.convert = function (object, type) {
  329. var match;
  330. if (object === undefined) {
  331. return undefined;
  332. }
  333. if (object === null) {
  334. return null;
  335. }
  336. if (!type) {
  337. return object;
  338. }
  339. if (!(typeof type === 'string') && !(type instanceof String)) {
  340. throw new Error('Type must be a string');
  341. }
  342. //noinspection FallthroughInSwitchStatementJS
  343. switch (type) {
  344. case 'boolean':
  345. case 'Boolean':
  346. return Boolean(object);
  347. case 'number':
  348. case 'Number':
  349. if (exports.isString(object) && !isNaN(Date.parse(object))) {
  350. return moment(object).valueOf();
  351. } else {
  352. return Number(object.valueOf());
  353. }
  354. case 'string':
  355. case 'String':
  356. return String(object);
  357. case 'Date':
  358. if (exports.isNumber(object)) {
  359. return new Date(object);
  360. }
  361. if (object instanceof Date) {
  362. return new Date(object.valueOf());
  363. }
  364. else if (moment.isMoment(object)) {
  365. return new Date(object.valueOf());
  366. }
  367. if (exports.isString(object)) {
  368. match = ASPDateRegex.exec(object);
  369. if (match) {
  370. // object is an ASP date
  371. return new Date(Number(match[1])); // parse number
  372. }
  373. else {
  374. return moment(new Date(object)).toDate(); // parse string
  375. }
  376. }
  377. else {
  378. throw new Error(
  379. 'Cannot convert object of type ' + exports.getType(object) +
  380. ' to type Date');
  381. }
  382. case 'Moment':
  383. if (exports.isNumber(object)) {
  384. return moment(object);
  385. }
  386. if (object instanceof Date) {
  387. return moment(object.valueOf());
  388. }
  389. else if (moment.isMoment(object)) {
  390. return moment(object);
  391. }
  392. if (exports.isString(object)) {
  393. match = ASPDateRegex.exec(object);
  394. if (match) {
  395. // object is an ASP date
  396. return moment(Number(match[1])); // parse number
  397. }
  398. else {
  399. return moment(object); // parse string
  400. }
  401. }
  402. else {
  403. throw new Error(
  404. 'Cannot convert object of type ' + exports.getType(object) +
  405. ' to type Date');
  406. }
  407. case 'ISODate':
  408. if (exports.isNumber(object)) {
  409. return new Date(object);
  410. }
  411. else if (object instanceof Date) {
  412. return object.toISOString();
  413. }
  414. else if (moment.isMoment(object)) {
  415. return object.toDate().toISOString();
  416. }
  417. else if (exports.isString(object)) {
  418. match = ASPDateRegex.exec(object);
  419. if (match) {
  420. // object is an ASP date
  421. return new Date(Number(match[1])).toISOString(); // parse number
  422. }
  423. else {
  424. return moment(object).format(); // ISO 8601
  425. }
  426. }
  427. else {
  428. throw new Error(
  429. 'Cannot convert object of type ' + exports.getType(object) +
  430. ' to type ISODate');
  431. }
  432. case 'ASPDate':
  433. if (exports.isNumber(object)) {
  434. return '/Date(' + object + ')/';
  435. }
  436. else if (object instanceof Date) {
  437. return '/Date(' + object.valueOf() + ')/';
  438. }
  439. else if (exports.isString(object)) {
  440. match = ASPDateRegex.exec(object);
  441. var value;
  442. if (match) {
  443. // object is an ASP date
  444. value = new Date(Number(match[1])).valueOf(); // parse number
  445. }
  446. else {
  447. value = new Date(object).valueOf(); // parse string
  448. }
  449. return '/Date(' + value + ')/';
  450. }
  451. else {
  452. throw new Error(
  453. 'Cannot convert object of type ' + exports.getType(object) +
  454. ' to type ASPDate');
  455. }
  456. default:
  457. throw new Error('Unknown type "' + type + '"');
  458. }
  459. };
  460. // parse ASP.Net Date pattern,
  461. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  462. // code from http://momentjs.com/
  463. var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
  464. /**
  465. * Get the type of an object, for example exports.getType([]) returns 'Array'
  466. * @param {*} object
  467. * @return {string} type
  468. */
  469. exports.getType = function (object) {
  470. var type = typeof object;
  471. if (type == 'object') {
  472. if (object === null) {
  473. return 'null';
  474. }
  475. if (object instanceof Boolean) {
  476. return 'Boolean';
  477. }
  478. if (object instanceof Number) {
  479. return 'Number';
  480. }
  481. if (object instanceof String) {
  482. return 'String';
  483. }
  484. if (Array.isArray(object)) {
  485. return 'Array';
  486. }
  487. if (object instanceof Date) {
  488. return 'Date';
  489. }
  490. return 'Object';
  491. }
  492. else if (type == 'number') {
  493. return 'Number';
  494. }
  495. else if (type == 'boolean') {
  496. return 'Boolean';
  497. }
  498. else if (type == 'string') {
  499. return 'String';
  500. }
  501. else if (type === undefined) {
  502. return 'undefined';
  503. }
  504. return type;
  505. };
  506. /**
  507. * Used to extend an array and copy it. This is used to propagate paths recursively.
  508. *
  509. * @param {Array} arr
  510. * @param {*} newValue
  511. * @returns {Array}
  512. */
  513. exports.copyAndExtendArray = function (arr, newValue) {
  514. let newArr = [];
  515. for (let i = 0; i < arr.length; i++) {
  516. newArr.push(arr[i]);
  517. }
  518. newArr.push(newValue);
  519. return newArr;
  520. };
  521. /**
  522. * Used to extend an array and copy it. This is used to propagate paths recursively.
  523. *
  524. * @param {Array} arr
  525. * @returns {Array}
  526. */
  527. exports.copyArray = function (arr) {
  528. let newArr = [];
  529. for (let i = 0; i < arr.length; i++) {
  530. newArr.push(arr[i]);
  531. }
  532. return newArr;
  533. };
  534. /**
  535. * Retrieve the absolute left value of a DOM element
  536. * @param {Element} elem A dom element, for example a div
  537. * @return {number} left The absolute left position of this element
  538. * in the browser page.
  539. */
  540. exports.getAbsoluteLeft = function (elem) {
  541. return elem.getBoundingClientRect().left;
  542. };
  543. exports.getAbsoluteRight = function (elem) {
  544. return elem.getBoundingClientRect().right;
  545. };
  546. /**
  547. * Retrieve the absolute top value of a DOM element
  548. * @param {Element} elem A dom element, for example a div
  549. * @return {number} top The absolute top position of this element
  550. * in the browser page.
  551. */
  552. exports.getAbsoluteTop = function (elem) {
  553. return elem.getBoundingClientRect().top;
  554. };
  555. /**
  556. * add a className to the given elements style
  557. * @param {Element} elem
  558. * @param {string} classNames
  559. */
  560. exports.addClassName = function (elem, classNames) {
  561. var classes = elem.className.split(' ');
  562. var newClasses = classNames.split(' ');
  563. classes = classes.concat(newClasses.filter(function(className) {
  564. return classes.indexOf(className) < 0;
  565. }));
  566. elem.className = classes.join(' ');
  567. };
  568. /**
  569. * add a className to the given elements style
  570. * @param {Element} elem
  571. * @param {string} classNames
  572. */
  573. exports.removeClassName = function (elem, classNames) {
  574. var classes = elem.className.split(' ');
  575. var oldClasses = classNames.split(' ');
  576. classes = classes.filter(function(className) {
  577. return oldClasses.indexOf(className) < 0;
  578. });
  579. elem.className = classes.join(' ');
  580. };
  581. /**
  582. * For each method for both arrays and objects.
  583. * In case of an array, the built-in Array.forEach() is applied. (**No, it's not!**)
  584. * In case of an Object, the method loops over all properties of the object.
  585. * @param {Object | Array} object An Object or Array
  586. * @param {function} callback Callback method, called for each item in
  587. * the object or array with three parameters:
  588. * callback(value, index, object)
  589. */
  590. exports.forEach = function (object, callback) {
  591. var i,
  592. len;
  593. if (Array.isArray(object)) {
  594. // array
  595. for (i = 0, len = object.length; i < len; i++) {
  596. callback(object[i], i, object);
  597. }
  598. }
  599. else {
  600. // object
  601. for (i in object) {
  602. if (object.hasOwnProperty(i)) {
  603. callback(object[i], i, object);
  604. }
  605. }
  606. }
  607. };
  608. /**
  609. * Convert an object into an array: all objects properties are put into the
  610. * array. The resulting array is unordered.
  611. * @param {Object} object
  612. * @returns {Array} array
  613. */
  614. exports.toArray = function (object) {
  615. var array = [];
  616. for (var prop in object) {
  617. if (object.hasOwnProperty(prop)) array.push(object[prop]);
  618. }
  619. return array;
  620. };
  621. /**
  622. * Update a property in an object
  623. * @param {Object} object
  624. * @param {string} key
  625. * @param {*} value
  626. * @return {Boolean} changed
  627. */
  628. exports.updateProperty = function (object, key, value) {
  629. if (object[key] !== value) {
  630. object[key] = value;
  631. return true;
  632. }
  633. else {
  634. return false;
  635. }
  636. };
  637. /**
  638. * Throttle the given function to be only executed once per animation frame
  639. * @param {function} fn
  640. * @returns {function} Returns the throttled function
  641. */
  642. exports.throttle = function (fn) {
  643. var scheduled = false;
  644. return function throttled () {
  645. if (!scheduled) {
  646. scheduled = true;
  647. requestAnimationFrame(function () {
  648. scheduled = false;
  649. fn();
  650. });
  651. }
  652. }
  653. };
  654. /**
  655. * Add and event listener. Works for all browsers
  656. * @param {Element} element An html element
  657. * @param {string} action The action, for example "click",
  658. * without the prefix "on"
  659. * @param {function} listener The callback function to be executed
  660. * @param {boolean} [useCapture]
  661. */
  662. exports.addEventListener = function (element, action, listener, useCapture) {
  663. if (element.addEventListener) {
  664. if (useCapture === undefined)
  665. useCapture = false;
  666. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  667. action = "DOMMouseScroll"; // For Firefox
  668. }
  669. element.addEventListener(action, listener, useCapture);
  670. } else {
  671. element.attachEvent("on" + action, listener); // IE browsers
  672. }
  673. };
  674. /**
  675. * Remove an event listener from an element
  676. * @param {Element} element An html dom element
  677. * @param {string} action The name of the event, for example "mousedown"
  678. * @param {function} listener The listener function
  679. * @param {boolean} [useCapture]
  680. */
  681. exports.removeEventListener = function (element, action, listener, useCapture) {
  682. if (element.removeEventListener) {
  683. // non-IE browsers
  684. if (useCapture === undefined)
  685. useCapture = false;
  686. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  687. action = "DOMMouseScroll"; // For Firefox
  688. }
  689. element.removeEventListener(action, listener, useCapture);
  690. } else {
  691. // IE browsers
  692. element.detachEvent("on" + action, listener);
  693. }
  694. };
  695. /**
  696. * Cancels the event if it is cancelable, without stopping further propagation of the event.
  697. * @param {Event} event
  698. */
  699. exports.preventDefault = function (event) {
  700. if (!event)
  701. event = window.event;
  702. if (event.preventDefault) {
  703. event.preventDefault(); // non-IE browsers
  704. }
  705. else {
  706. event.returnValue = false; // IE browsers
  707. }
  708. };
  709. /**
  710. * Get HTML element which is the target of the event
  711. * @param {Event} event
  712. * @return {Element} target element
  713. */
  714. exports.getTarget = function (event) {
  715. // code from http://www.quirksmode.org/js/events_properties.html
  716. if (!event) {
  717. event = window.event;
  718. }
  719. var target;
  720. if (event.target) {
  721. target = event.target;
  722. }
  723. else if (event.srcElement) {
  724. target = event.srcElement;
  725. }
  726. if (target.nodeType != undefined && target.nodeType == 3) {
  727. // defeat Safari bug
  728. target = target.parentNode;
  729. }
  730. return target;
  731. };
  732. /**
  733. * Check if given element contains given parent somewhere in the DOM tree
  734. * @param {Element} element
  735. * @param {Element} parent
  736. * @returns {boolean}
  737. */
  738. exports.hasParent = function (element, parent) {
  739. var e = element;
  740. while (e) {
  741. if (e === parent) {
  742. return true;
  743. }
  744. e = e.parentNode;
  745. }
  746. return false;
  747. };
  748. exports.option = {};
  749. /**
  750. * Convert a value into a boolean
  751. * @param {Boolean | function | undefined} value
  752. * @param {boolean} [defaultValue]
  753. * @returns {Boolean} bool
  754. */
  755. exports.option.asBoolean = function (value, defaultValue) {
  756. if (typeof value == 'function') {
  757. value = value();
  758. }
  759. if (value != null) {
  760. return (value != false);
  761. }
  762. return defaultValue || null;
  763. };
  764. /**
  765. * Convert a value into a number
  766. * @param {Boolean | function | undefined} value
  767. * @param {number} [defaultValue]
  768. * @returns {number} number
  769. */
  770. exports.option.asNumber = function (value, defaultValue) {
  771. if (typeof value == 'function') {
  772. value = value();
  773. }
  774. if (value != null) {
  775. return Number(value) || defaultValue || null;
  776. }
  777. return defaultValue || null;
  778. };
  779. /**
  780. * Convert a value into a string
  781. * @param {string | function | undefined} value
  782. * @param {string} [defaultValue]
  783. * @returns {String} str
  784. */
  785. exports.option.asString = function (value, defaultValue) {
  786. if (typeof value == 'function') {
  787. value = value();
  788. }
  789. if (value != null) {
  790. return String(value);
  791. }
  792. return defaultValue || null;
  793. };
  794. /**
  795. * Convert a size or location into a string with pixels or a percentage
  796. * @param {string | number | function | undefined} value
  797. * @param {string} [defaultValue]
  798. * @returns {String} size
  799. */
  800. exports.option.asSize = function (value, defaultValue) {
  801. if (typeof value == 'function') {
  802. value = value();
  803. }
  804. if (exports.isString(value)) {
  805. return value;
  806. }
  807. else if (exports.isNumber(value)) {
  808. return value + 'px';
  809. }
  810. else {
  811. return defaultValue || null;
  812. }
  813. };
  814. /**
  815. * Convert a value into a DOM element
  816. * @param {HTMLElement | function | undefined} value
  817. * @param {HTMLElement} [defaultValue]
  818. * @returns {HTMLElement | null} dom
  819. */
  820. exports.option.asElement = function (value, defaultValue) {
  821. if (typeof value == 'function') {
  822. value = value();
  823. }
  824. return value || defaultValue || null;
  825. };
  826. /**
  827. * http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
  828. *
  829. * @param {string} hex
  830. * @returns {{r: *, g: *, b: *}} | 255 range
  831. */
  832. exports.hexToRGB = function (hex) {
  833. // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
  834. var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
  835. hex = hex.replace(shorthandRegex, function (m, r, g, b) {
  836. return r + r + g + g + b + b;
  837. });
  838. var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  839. return result ? {
  840. r: parseInt(result[1], 16),
  841. g: parseInt(result[2], 16),
  842. b: parseInt(result[3], 16)
  843. } : null;
  844. };
  845. /**
  846. * This function takes color in hex format or rgb() or rgba() format and overrides the opacity. Returns rgba() string.
  847. * @param {string} color
  848. * @param {number} opacity
  849. * @returns {String}
  850. */
  851. exports.overrideOpacity = function (color, opacity) {
  852. var rgb;
  853. if (color.indexOf("rgba") != -1) {
  854. return color;
  855. }
  856. else if (color.indexOf("rgb") != -1) {
  857. rgb = color.substr(color.indexOf("(") + 1).replace(")", "").split(",");
  858. return "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + "," + opacity + ")"
  859. }
  860. else {
  861. rgb = exports.hexToRGB(color);
  862. if (rgb == null) {
  863. return color;
  864. }
  865. else {
  866. return "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + "," + opacity + ")"
  867. }
  868. }
  869. };
  870. /**
  871. *
  872. * @param {number} red 0 -- 255
  873. * @param {number} green 0 -- 255
  874. * @param {number} blue 0 -- 255
  875. * @returns {String}
  876. * @constructor
  877. */
  878. exports.RGBToHex = function (red, green, blue) {
  879. return "#" + ((1 << 24) + (red << 16) + (green << 8) + blue).toString(16).slice(1);
  880. };
  881. /**
  882. * Parse a color property into an object with border, background, and
  883. * highlight colors
  884. * @param {Object | String} color
  885. * @return {Object} colorObject
  886. */
  887. exports.parseColor = function (color) {
  888. var c;
  889. if (exports.isString(color) === true) {
  890. if (exports.isValidRGB(color) === true) {
  891. var rgb = color.substr(4).substr(0, color.length - 5).split(',').map(function (value) { return parseInt(value) });
  892. color = exports.RGBToHex(rgb[0], rgb[1], rgb[2]);
  893. }
  894. if (exports.isValidHex(color) === true) {
  895. var hsv = exports.hexToHSV(color);
  896. var lighterColorHSV = { h: hsv.h, s: hsv.s * 0.8, v: Math.min(1, hsv.v * 1.02) };
  897. var darkerColorHSV = { h: hsv.h, s: Math.min(1, hsv.s * 1.25), v: hsv.v * 0.8 };
  898. var darkerColorHex = exports.HSVToHex(darkerColorHSV.h, darkerColorHSV.s, darkerColorHSV.v);
  899. var lighterColorHex = exports.HSVToHex(lighterColorHSV.h, lighterColorHSV.s, lighterColorHSV.v);
  900. c = {
  901. background: color,
  902. border: darkerColorHex,
  903. highlight: {
  904. background: lighterColorHex,
  905. border: darkerColorHex
  906. },
  907. hover: {
  908. background: lighterColorHex,
  909. border: darkerColorHex
  910. }
  911. };
  912. }
  913. else {
  914. c = {
  915. background: color,
  916. border: color,
  917. highlight: {
  918. background: color,
  919. border: color
  920. },
  921. hover: {
  922. background: color,
  923. border: color
  924. }
  925. };
  926. }
  927. }
  928. else {
  929. c = {};
  930. c.background = color.background || undefined;
  931. c.border = color.border || undefined;
  932. if (exports.isString(color.highlight)) {
  933. c.highlight = {
  934. border: color.highlight,
  935. background: color.highlight
  936. }
  937. }
  938. else {
  939. c.highlight = {};
  940. c.highlight.background = color.highlight && color.highlight.background || undefined;
  941. c.highlight.border = color.highlight && color.highlight.border || undefined;
  942. }
  943. if (exports.isString(color.hover)) {
  944. c.hover = {
  945. border: color.hover,
  946. background: color.hover
  947. }
  948. }
  949. else {
  950. c.hover = {};
  951. c.hover.background = color.hover && color.hover.background || undefined;
  952. c.hover.border = color.hover && color.hover.border || undefined;
  953. }
  954. }
  955. return c;
  956. };
  957. /**
  958. * http://www.javascripter.net/faq/rgb2hsv.htm
  959. *
  960. * @param {number} red
  961. * @param {number} green
  962. * @param {number} blue
  963. * @returns {{h: number, s: number, v: number}}
  964. * @constructor
  965. */
  966. exports.RGBToHSV = function (red, green, blue) {
  967. red = red / 255; green = green / 255; blue = blue / 255;
  968. var minRGB = Math.min(red, Math.min(green, blue));
  969. var maxRGB = Math.max(red, Math.max(green, blue));
  970. // Black-gray-white
  971. if (minRGB == maxRGB) {
  972. return { h: 0, s: 0, v: minRGB };
  973. }
  974. // Colors other than black-gray-white:
  975. var d = (red == minRGB) ? green - blue : ((blue == minRGB) ? red - green : blue - red);
  976. var h = (red == minRGB) ? 3 : ((blue == minRGB) ? 1 : 5);
  977. var hue = 60 * (h - d / (maxRGB - minRGB)) / 360;
  978. var saturation = (maxRGB - minRGB) / maxRGB;
  979. var value = maxRGB;
  980. return { h: hue, s: saturation, v: value };
  981. };
  982. var cssUtil = {
  983. // split a string with css styles into an object with key/values
  984. split: function (cssText) {
  985. var styles = {};
  986. cssText.split(';').forEach(function (style) {
  987. if (style.trim() != '') {
  988. var parts = style.split(':');
  989. var key = parts[0].trim();
  990. var value = parts[1].trim();
  991. styles[key] = value;
  992. }
  993. });
  994. return styles;
  995. },
  996. // build a css text string from an object with key/values
  997. join: function (styles) {
  998. return Object.keys(styles)
  999. .map(function (key) {
  1000. return key + ': ' + styles[key];
  1001. })
  1002. .join('; ');
  1003. }
  1004. };
  1005. /**
  1006. * Append a string with css styles to an element
  1007. * @param {Element} element
  1008. * @param {string} cssText
  1009. */
  1010. exports.addCssText = function (element, cssText) {
  1011. var currentStyles = cssUtil.split(element.style.cssText);
  1012. var newStyles = cssUtil.split(cssText);
  1013. var styles = exports.extend(currentStyles, newStyles);
  1014. element.style.cssText = cssUtil.join(styles);
  1015. };
  1016. /**
  1017. * Remove a string with css styles from an element
  1018. * @param {Element} element
  1019. * @param {string} cssText
  1020. */
  1021. exports.removeCssText = function (element, cssText) {
  1022. var styles = cssUtil.split(element.style.cssText);
  1023. var removeStyles = cssUtil.split(cssText);
  1024. for (var key in removeStyles) {
  1025. if (removeStyles.hasOwnProperty(key)) {
  1026. delete styles[key];
  1027. }
  1028. }
  1029. element.style.cssText = cssUtil.join(styles);
  1030. };
  1031. /**
  1032. * https://gist.github.com/mjijackson/5311256
  1033. * @param {number} h
  1034. * @param {number} s
  1035. * @param {number} v
  1036. * @returns {{r: number, g: number, b: number}}
  1037. * @constructor
  1038. */
  1039. exports.HSVToRGB = function (h, s, v) {
  1040. var r, g, b;
  1041. var i = Math.floor(h * 6);
  1042. var f = h * 6 - i;
  1043. var p = v * (1 - s);
  1044. var q = v * (1 - f * s);
  1045. var t = v * (1 - (1 - f) * s);
  1046. switch (i % 6) {
  1047. case 0: r = v, g = t, b = p; break;
  1048. case 1: r = q, g = v, b = p; break;
  1049. case 2: r = p, g = v, b = t; break;
  1050. case 3: r = p, g = q, b = v; break;
  1051. case 4: r = t, g = p, b = v; break;
  1052. case 5: r = v, g = p, b = q; break;
  1053. }
  1054. return { r: Math.floor(r * 255), g: Math.floor(g * 255), b: Math.floor(b * 255) };
  1055. };
  1056. exports.HSVToHex = function (h, s, v) {
  1057. var rgb = exports.HSVToRGB(h, s, v);
  1058. return exports.RGBToHex(rgb.r, rgb.g, rgb.b);
  1059. };
  1060. exports.hexToHSV = function (hex) {
  1061. var rgb = exports.hexToRGB(hex);
  1062. return exports.RGBToHSV(rgb.r, rgb.g, rgb.b);
  1063. };
  1064. exports.isValidHex = function (hex) {
  1065. var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
  1066. return isOk;
  1067. };
  1068. exports.isValidRGB = function (rgb) {
  1069. rgb = rgb.replace(" ", "");
  1070. var isOk = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/i.test(rgb);
  1071. return isOk;
  1072. };
  1073. exports.isValidRGBA = function (rgba) {
  1074. rgba = rgba.replace(" ", "");
  1075. var isOk = /rgba\((\d{1,3}),(\d{1,3}),(\d{1,3}),(.{1,3})\)/i.test(rgba);
  1076. return isOk;
  1077. };
  1078. /**
  1079. * This recursively redirects the prototype of JSON objects to the referenceObject
  1080. * This is used for default options.
  1081. *
  1082. * @param {Array.<string>} fields
  1083. * @param {Object} referenceObject
  1084. * @returns {*}
  1085. */
  1086. exports.selectiveBridgeObject = function (fields, referenceObject) {
  1087. if (referenceObject !== null && typeof referenceObject === "object") { // !!! typeof null === 'object'
  1088. var objectTo = Object.create(referenceObject);
  1089. for (var i = 0; i < fields.length; i++) {
  1090. if (referenceObject.hasOwnProperty(fields[i])) {
  1091. if (typeof referenceObject[fields[i]] == "object") {
  1092. objectTo[fields[i]] = exports.bridgeObject(referenceObject[fields[i]]);
  1093. }
  1094. }
  1095. }
  1096. return objectTo;
  1097. }
  1098. else {
  1099. return null;
  1100. }
  1101. };
  1102. /**
  1103. * This recursively redirects the prototype of JSON objects to the referenceObject
  1104. * This is used for default options.
  1105. *
  1106. * @param {Object} referenceObject
  1107. * @returns {*}
  1108. */
  1109. exports.bridgeObject = function (referenceObject) {
  1110. if (referenceObject !== null && typeof referenceObject === "object") { // !!! typeof null === 'object'
  1111. var objectTo = Object.create(referenceObject);
  1112. if (referenceObject instanceof Element) {
  1113. // Avoid bridging DOM objects
  1114. objectTo = referenceObject;
  1115. } else {
  1116. objectTo = Object.create(referenceObject);
  1117. for (var i in referenceObject) {
  1118. if (referenceObject.hasOwnProperty(i)) {
  1119. if (typeof referenceObject[i] == "object") {
  1120. objectTo[i] = exports.bridgeObject(referenceObject[i]);
  1121. }
  1122. }
  1123. }
  1124. }
  1125. return objectTo;
  1126. }
  1127. else {
  1128. return null;
  1129. }
  1130. };
  1131. /**
  1132. * This method provides a stable sort implementation, very fast for presorted data
  1133. *
  1134. * @param {Array} a the array
  1135. * @param {function} compare an order comparator
  1136. * @returns {Array}
  1137. */
  1138. exports.insertSort = function (a,compare) {
  1139. for (var i = 0; i < a.length; i++) {
  1140. var k = a[i];
  1141. for (var j = i; j > 0 && compare(k,a[j - 1])<0; j--) {
  1142. a[j] = a[j - 1];
  1143. }
  1144. a[j] = k;
  1145. }
  1146. return a;
  1147. }
  1148. /**
  1149. * This is used to set the options of subobjects in the options object.
  1150. *
  1151. * A requirement of these subobjects is that they have an 'enabled' element
  1152. * which is optional for the user but mandatory for the program.
  1153. *
  1154. * The added value here of the merge is that option 'enabled' is set as required.
  1155. *
  1156. *
  1157. * @param {object} mergeTarget | either this.options or the options used for the groups.
  1158. * @param {object} options | options
  1159. * @param {string} option | option key in the options argument
  1160. * @param {object} globalOptions | global options, passed in to determine value of option 'enabled'
  1161. */
  1162. exports.mergeOptions = function (mergeTarget, options, option, globalOptions = {}) {
  1163. // Local helpers
  1164. var isPresent = function(obj) {
  1165. return obj !== null && obj !== undefined;
  1166. }
  1167. var isObject = function(obj) {
  1168. return obj !== null && typeof obj === 'object';
  1169. }
  1170. // https://stackoverflow.com/a/34491287/1223531
  1171. var isEmpty = function(obj) {
  1172. for (var x in obj) { if (obj.hasOwnProperty(x)) return false; }
  1173. return true;
  1174. };
  1175. // Guards
  1176. if (!isObject(mergeTarget)) {
  1177. throw new Error('Parameter mergeTarget must be an object');
  1178. }
  1179. if (!isObject(options)) {
  1180. throw new Error('Parameter options must be an object');
  1181. }
  1182. if (!isPresent(option)) {
  1183. throw new Error('Parameter option must have a value');
  1184. }
  1185. if (!isObject(globalOptions)) {
  1186. throw new Error('Parameter globalOptions must be an object');
  1187. }
  1188. //
  1189. // Actual merge routine, separated from main logic
  1190. // Only a single level of options is merged. Deeper levels are ref'd. This may actually be an issue.
  1191. //
  1192. var doMerge = function(target, options, option) {
  1193. if (!isObject(target[option])) {
  1194. target[option] = {};
  1195. }
  1196. let src = options[option];
  1197. let dst = target[option];
  1198. for (var prop in src) {
  1199. if (src.hasOwnProperty(prop)) {
  1200. dst[prop] = src[prop];
  1201. }
  1202. }
  1203. };
  1204. // Local initialization
  1205. var srcOption = options[option];
  1206. var globalPassed = isObject(globalOptions) && !isEmpty(globalOptions);
  1207. var globalOption = globalPassed? globalOptions[option]: undefined;
  1208. var globalEnabled = globalOption? globalOption.enabled: undefined;
  1209. /////////////////////////////////////////
  1210. // Main routine
  1211. /////////////////////////////////////////
  1212. if (srcOption === undefined) {
  1213. return; // Nothing to do
  1214. }
  1215. if ((typeof srcOption) === 'boolean') {
  1216. if (!isObject(mergeTarget[option])) {
  1217. mergeTarget[option] = {};
  1218. }
  1219. mergeTarget[option].enabled = srcOption;
  1220. return;
  1221. }
  1222. if (srcOption === null && !isObject(mergeTarget[option])) {
  1223. // If possible, explicit copy from globals
  1224. if (isPresent(globalOption)) {
  1225. mergeTarget[option] = Object.create(globalOption);
  1226. } else {
  1227. return; // Nothing to do
  1228. }
  1229. }
  1230. if (!isObject(srcOption)) {
  1231. return;
  1232. }
  1233. //
  1234. // Ensure that 'enabled' is properly set. It is required internally
  1235. // Note that the value from options will always overwrite the existing value
  1236. //
  1237. let enabled = true; // default value
  1238. if (srcOption.enabled !== undefined) {
  1239. enabled = srcOption.enabled;
  1240. } else {
  1241. // Take from globals, if present
  1242. if (globalEnabled !== undefined) {
  1243. enabled = globalOption.enabled;
  1244. }
  1245. }
  1246. doMerge(mergeTarget, options, option);
  1247. mergeTarget[option].enabled = enabled;
  1248. }
  1249. /**
  1250. * This function does a binary search for a visible item in a sorted list. If we find a visible item, the code that uses
  1251. * this function will then iterate in both directions over this sorted list to find all visible items.
  1252. *
  1253. * @param {Item[]} orderedItems | Items ordered by start
  1254. * @param {function} comparator | -1 is lower, 0 is equal, 1 is higher
  1255. * @param {string} field
  1256. * @param {string} field2
  1257. * @returns {number}
  1258. * @private
  1259. */
  1260. exports.binarySearchCustom = function (orderedItems, comparator, field, field2) {
  1261. var maxIterations = 10000;
  1262. var iteration = 0;
  1263. var low = 0;
  1264. var high = orderedItems.length - 1;
  1265. while (low <= high && iteration < maxIterations) {
  1266. var middle = Math.floor((low + high) / 2);
  1267. var item = orderedItems[middle];
  1268. var value = (field2 === undefined) ? item[field] : item[field][field2];
  1269. var searchResult = comparator(value);
  1270. if (searchResult == 0) { // jihaa, found a visible item!
  1271. return middle;
  1272. }
  1273. else if (searchResult == -1) { // it is too small --> increase low
  1274. low = middle + 1;
  1275. }
  1276. else { // it is too big --> decrease high
  1277. high = middle - 1;
  1278. }
  1279. iteration++;
  1280. }
  1281. return -1;
  1282. };
  1283. /**
  1284. * This function does a binary search for a specific value in a sorted array. If it does not exist but is in between of
  1285. * two values, we return either the one before or the one after, depending on user input
  1286. * If it is found, we return the index, else -1.
  1287. *
  1288. * @param {Array} orderedItems
  1289. * @param {{start: number, end: number}} target
  1290. * @param {string} field
  1291. * @param {string} sidePreference 'before' or 'after'
  1292. * @param {function} comparator an optional comparator, returning -1,0,1 for <,==,>.
  1293. * @returns {number}
  1294. * @private
  1295. */
  1296. exports.binarySearchValue = function (orderedItems, target, field, sidePreference, comparator) {
  1297. var maxIterations = 10000;
  1298. var iteration = 0;
  1299. var low = 0;
  1300. var high = orderedItems.length - 1;
  1301. var prevValue, value, nextValue, middle;
  1302. comparator = comparator != undefined ? comparator : function (a, b) {
  1303. return a == b ? 0 : a < b ? -1 : 1
  1304. };
  1305. while (low <= high && iteration < maxIterations) {
  1306. // get a new guess
  1307. middle = Math.floor(0.5 * (high + low));
  1308. prevValue = orderedItems[Math.max(0, middle - 1)][field];
  1309. value = orderedItems[middle][field];
  1310. nextValue = orderedItems[Math.min(orderedItems.length - 1, middle + 1)][field];
  1311. if (comparator(value, target) == 0) { // we found the target
  1312. return middle;
  1313. }
  1314. else if (comparator(prevValue, target) < 0 && comparator(value, target) > 0) { // target is in between of the previous and the current
  1315. return sidePreference == 'before' ? Math.max(0, middle - 1) : middle;
  1316. }
  1317. else if (comparator(value, target) < 0 && comparator(nextValue, target) > 0) { // target is in between of the current and the next
  1318. return sidePreference == 'before' ? middle : Math.min(orderedItems.length - 1, middle + 1);
  1319. }
  1320. else { // didnt find the target, we need to change our boundaries.
  1321. if (comparator(value, target) < 0) { // it is too small --> increase low
  1322. low = middle + 1;
  1323. }
  1324. else { // it is too big --> decrease high
  1325. high = middle - 1;
  1326. }
  1327. }
  1328. iteration++;
  1329. }
  1330. // didnt find anything. Return -1.
  1331. return -1;
  1332. };
  1333. /*
  1334. * Easing Functions - inspired from http://gizma.com/easing/
  1335. * only considering the t value for the range [0, 1] => [0, 1]
  1336. * https://gist.github.com/gre/1650294
  1337. */
  1338. exports.easingFunctions = {
  1339. // no easing, no acceleration
  1340. linear: function (t) {
  1341. return t
  1342. },
  1343. // accelerating from zero velocity
  1344. easeInQuad: function (t) {
  1345. return t * t
  1346. },
  1347. // decelerating to zero velocity
  1348. easeOutQuad: function (t) {
  1349. return t * (2 - t)
  1350. },
  1351. // acceleration until halfway, then deceleration
  1352. easeInOutQuad: function (t) {
  1353. return t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t
  1354. },
  1355. // accelerating from zero velocity
  1356. easeInCubic: function (t) {
  1357. return t * t * t
  1358. },
  1359. // decelerating to zero velocity
  1360. easeOutCubic: function (t) {
  1361. return (--t) * t * t + 1
  1362. },
  1363. // acceleration until halfway, then deceleration
  1364. easeInOutCubic: function (t) {
  1365. return t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
  1366. },
  1367. // accelerating from zero velocity
  1368. easeInQuart: function (t) {
  1369. return t * t * t * t
  1370. },
  1371. // decelerating to zero velocity
  1372. easeOutQuart: function (t) {
  1373. return 1 - (--t) * t * t * t
  1374. },
  1375. // acceleration until halfway, then deceleration
  1376. easeInOutQuart: function (t) {
  1377. return t < .5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t
  1378. },
  1379. // accelerating from zero velocity
  1380. easeInQuint: function (t) {
  1381. return t * t * t * t * t
  1382. },
  1383. // decelerating to zero velocity
  1384. easeOutQuint: function (t) {
  1385. return 1 + (--t) * t * t * t * t
  1386. },
  1387. // acceleration until halfway, then deceleration
  1388. easeInOutQuint: function (t) {
  1389. return t < .5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t
  1390. }
  1391. };
  1392. exports.getScrollBarWidth = function () {
  1393. var inner = document.createElement('p');
  1394. inner.style.width = "100%";
  1395. inner.style.height = "200px";
  1396. var outer = document.createElement('div');
  1397. outer.style.position = "absolute";
  1398. outer.style.top = "0px";
  1399. outer.style.left = "0px";
  1400. outer.style.visibility = "hidden";
  1401. outer.style.width = "200px";
  1402. outer.style.height = "150px";
  1403. outer.style.overflow = "hidden";
  1404. outer.appendChild (inner);
  1405. document.body.appendChild (outer);
  1406. var w1 = inner.offsetWidth;
  1407. outer.style.overflow = 'scroll';
  1408. var w2 = inner.offsetWidth;
  1409. if (w1 == w2) w2 = outer.clientWidth;
  1410. document.body.removeChild (outer);
  1411. return (w1 - w2);
  1412. };
  1413. exports.topMost = function (pile, accessors) {
  1414. let candidate;
  1415. if (!Array.isArray(accessors)) {
  1416. accessors = [accessors];
  1417. }
  1418. for (const member of pile) {
  1419. if (member) {
  1420. candidate = member[accessors[0]];
  1421. for (let i = 1; i < accessors.length; i++){
  1422. if (candidate) {
  1423. candidate = candidate[accessors[i]]
  1424. } else {
  1425. continue;
  1426. }
  1427. }
  1428. if (typeof candidate != 'undefined') {
  1429. break;
  1430. }
  1431. }
  1432. }
  1433. return candidate;
  1434. };