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.

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