UNPKG

67.3 kBPlain TextView Raw
1import {GridOptionsWrapper} from "./gridOptionsWrapper";
2import {Column} from "./entities/column";
3import {RowNode} from "./entities/rowNode";
4import {Constants} from "./constants";
5
6let FUNCTION_STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
7let FUNCTION_ARGUMENT_NAMES = /([^\s,]+)/g;
8
9let AG_GRID_STOP_PROPAGATION = '__ag_Grid_Stop_Propagation';
10
11// util class, only used when debugging, for printing time to console
12export class Timer {
13
14 private timestamp = new Date().getTime();
15
16 public print(msg: string) {
17 let duration = (new Date().getTime()) - this.timestamp;
18 console.log(`${msg} = ${duration}`);
19 this.timestamp = new Date().getTime();
20 }
21
22}
23
24/** HTML Escapes. */
25const HTML_ESCAPES: { [id: string]: string } = {
26 '&': '&',
27 '<': '&lt;',
28 '>': '&gt;',
29 '"': '&quot;',
30 "'": '&#39;'
31};
32
33const reUnescapedHtml = /[&<>"']/g;
34
35export class Utils {
36
37 // taken from:
38 // http://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
39 // both of these variables are lazy loaded, as otherwise they try and get initialised when we are loading
40 // unit tests and we don't have references to window or document in the unit tests
41 private static isSafari: boolean;
42 private static isIE: boolean;
43 private static isEdge: boolean;
44 private static isChrome: boolean;
45 private static isFirefox: boolean;
46
47 private static isIPad: boolean;
48
49 private static PRINTABLE_CHARACTERS = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890!"£$%^&*()_+-=[];\'#,./\\|<>?:@~{}';
50
51 private static NUMPAD_DEL_NUMLOCK_ON_KEY = 'Del';
52 private static NUMPAD_DEL_NUMLOCK_ON_CHARCODE = 46;
53
54 private static doOnceFlags: {[key: string]: boolean} = {};
55
56 // if the key was passed before, then doesn't execute the func
57 static doOnce(func: ()=>void, key: string ) {
58 if (this.doOnceFlags[key]) { return; }
59 func();
60 this.doOnceFlags[key] = true;
61 }
62
63 // returns true if the event is close to the original event by X pixels either vertically or horizontally.
64 // we only start dragging after X pixels so this allows us to know if we should start dragging yet.
65 static areEventsNear(e1: MouseEvent | Touch, e2: MouseEvent | Touch, pixelCount: number): boolean {
66 // by default, we wait 4 pixels before starting the drag
67 if (pixelCount === 0) {
68 return false;
69 }
70 let diffX = Math.abs(e1.clientX - e2.clientX);
71 let diffY = Math.abs(e1.clientY - e2.clientY);
72
73 return Math.max(diffX, diffY) <= pixelCount;
74 }
75
76 static shallowCompare(arr1: any[], arr2: any[]): boolean {
77 // if both are missing, then they are the same
78 if (this.missing(arr1) && this.missing(arr2)) {
79 return true;
80 }
81 // if one is present, but other is missing, then then are different
82 if (this.missing(arr1) || this.missing(arr2)) {
83 return false;
84 }
85
86 if (arr1.length !== arr2.length) {
87 return false;
88 }
89
90 for (let i = 0; i < arr1.length; i++) {
91 if (arr1[i] !== arr2[i]) {
92 return false;
93 }
94 }
95
96 return true;
97 }
98
99 static getNameOfClass(TheClass: any) {
100 let funcNameRegex = /function (.{1,})\(/;
101 let funcAsString = TheClass.toString();
102 let results = (funcNameRegex).exec(funcAsString);
103 return (results && results.length > 1) ? results[1] : "";
104 }
105
106 static values<T>(object: { [key: string]: T }): T[] {
107 let result: T[] = [];
108 this.iterateObject(object, (key: string, value: T) => {
109 result.push(value);
110 });
111 return result;
112 }
113
114 static getValueUsingField(data: any, field: string, fieldContainsDots: boolean): any {
115 if (!field || !data) {
116 return;
117 }
118 // if no '.', then it's not a deep value
119 if (!fieldContainsDots) {
120 return data[field];
121 } else {
122 // otherwise it is a deep value, so need to dig for it
123 let fields = field.split('.');
124 let currentObject = data;
125 for (let i = 0; i < fields.length; i++) {
126 currentObject = currentObject[fields[i]];
127 if (this.missing(currentObject)) {
128 return null;
129 }
130 }
131 return currentObject;
132 }
133 }
134
135 static getScrollLeft(element: HTMLElement, rtl: boolean): number {
136 let scrollLeft = element.scrollLeft;
137 if (rtl) {
138 // Absolute value - for FF that reports RTL scrolls in negative numbers
139 scrollLeft = Math.abs(scrollLeft);
140
141 // Get Chrome and Safari to return the same value as well
142 if (this.isBrowserSafari() || this.isBrowserChrome()) {
143 scrollLeft = element.scrollWidth - element.clientWidth - scrollLeft;
144 }
145 }
146 return scrollLeft;
147 }
148
149 static cleanNumber(value: any): number {
150 if (typeof value === 'string') {
151 value = parseInt(value);
152 }
153 if (typeof value === 'number') {
154 value = Math.floor(value);
155 } else {
156 value = null;
157 }
158 return value;
159 }
160
161 static setScrollLeft(element: HTMLElement, value: number, rtl: boolean): void {
162 if (rtl) {
163 // Chrome and Safari when doing RTL have the END position of the scroll as zero, not the start
164 if (this.isBrowserSafari() || this.isBrowserChrome()) {
165 value = element.scrollWidth - element.clientWidth - value;
166 }
167 // Firefox uses negative numbers when doing RTL scrolling
168 if (this.isBrowserFirefox()) {
169 value *= -1;
170 }
171 }
172 element.scrollLeft = value;
173 }
174
175 static iterateNamedNodeMap(map: NamedNodeMap, callback: (key: string, value: string)=>void): void {
176 if (!map) { return; }
177 for (let i = 0; i < map.length; i++) {
178 let attr = map[i];
179 callback(attr.name, attr.value);
180 }
181 }
182
183 static iterateObject<T>(object: {[p:string]:T} | T[], callback: (key: string, value: T) => void) {
184 if (this.missing(object)) {
185 return;
186 }
187
188 if (Array.isArray(object)){
189 object.forEach((value, index)=>{
190 callback(index + '', value);
191 })
192 } else {
193 let keys = Object.keys(object);
194 for (let i = 0; i < keys.length; i++) {
195 let key = keys[i];
196 let value = object[key];
197 callback(key, value);
198 }
199 }
200
201 }
202
203 static cloneObject<T>(object: T): T {
204 let copy = <T>{};
205 let keys = Object.keys(object);
206 for (let i = 0; i < keys.length; i++) {
207 let key = keys[i];
208 let value = (<any>object)[key];
209 (<any>copy)[key] = value;
210 }
211 return copy;
212 }
213
214 static map<TItem, TResult>(array: TItem[], callback: (item: TItem) => TResult) {
215 let result: TResult[] = [];
216 for (let i = 0; i < array.length; i++) {
217 let item = array[i];
218 let mappedItem = callback(item);
219 result.push(mappedItem);
220 }
221 return result;
222 }
223
224 static mapObject<TResult>(object: any, callback: (item: any) => TResult) {
225 let result: TResult[] = [];
226 Utils.iterateObject(object, (key: string, value: any) => {
227 result.push(callback(value));
228 });
229 return result;
230 }
231
232 static forEach<T>(array: T[], callback: (item: T, index: number) => void) {
233 if (!array) {
234 return;
235 }
236
237 for (let i = 0; i < array.length; i++) {
238 let value = array[i];
239 callback(value, i);
240 }
241 }
242
243 static filter<T>(array: T[], callback: (item: T) => boolean): T[] {
244 let result: T[] = [];
245 array.forEach(function (item: T) {
246 if (callback(item)) {
247 result.push(item);
248 }
249 });
250 return result;
251 }
252
253 static getAllKeysInObjects(objects: any[]): string[] {
254 let allValues: any = {};
255 objects.forEach(obj => {
256 if (obj) {
257 Object.keys(obj).forEach(key => allValues[key] = null);
258 }
259 });
260 return Object.keys(allValues);
261 }
262
263 static mergeDeep(dest: any, source: any): void {
264
265 if (this.exists(source)) {
266 this.iterateObject(source, (key: string, newValue: any) => {
267
268 let oldValue: any = dest[key];
269
270 if (oldValue === newValue) { return; }
271
272 if (typeof oldValue === 'object' && typeof newValue === 'object') {
273 Utils.mergeDeep(oldValue, newValue);
274 } else {
275 dest[key] = newValue;
276 }
277 });
278 }
279 }
280
281 static assign(object: any, ...sources: any[]): any {
282 sources.forEach(source => {
283 if (this.exists(source)) {
284 this.iterateObject(source, function (key: string, value: any) {
285 object[key] = value;
286 });
287 }
288 });
289
290 return object;
291 }
292
293 static parseYyyyMmDdToDate(yyyyMmDd: string, separator: string): Date {
294 try {
295 if (!yyyyMmDd) return null;
296 if (yyyyMmDd.indexOf(separator) === -1) return null;
297
298 let fields: string[] = yyyyMmDd.split(separator);
299 if (fields.length != 3) return null;
300 return new Date(Number(fields[0]), Number(fields[1]) - 1, Number(fields[2]));
301 } catch (e) {
302 return null;
303 }
304 }
305
306 static serializeDateToYyyyMmDd(date: Date, separator: string): string {
307 if (!date) return null;
308 return date.getFullYear() + separator + Utils.pad(date.getMonth() + 1, 2) + separator + Utils.pad(date.getDate(), 2)
309 }
310
311 static pad(num: number, totalStringSize: number): string {
312 let asString: string = num + "";
313 while (asString.length < totalStringSize) asString = "0" + asString;
314 return asString;
315 }
316
317 static pushAll(target: any[], source: any[]): void {
318 if (this.missing(source) || this.missing(target)) {
319 return;
320 }
321 source.forEach(func => target.push(func));
322 }
323
324 static createArrayOfNumbers(first: number, last: number): number[] {
325 let result: number[] = [];
326 for (let i = first; i <= last; i++) {
327 result.push(i);
328 }
329 return result;
330 }
331
332 static getFunctionParameters(func: any) {
333 let fnStr = func.toString().replace(FUNCTION_STRIP_COMMENTS, '');
334 let result = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')).match(FUNCTION_ARGUMENT_NAMES);
335 if (result === null) {
336 return [];
337 } else {
338 return result;
339 }
340 }
341
342 static find<T>(collection: T[] | { [id: string]: T }, predicate: string | boolean | ((item: T) => void), value?: any): T {
343 if (collection === null || collection === undefined) {
344 return null;
345 }
346
347 if (!Array.isArray(collection)) {
348 let objToArray = this.values(collection);
349 return this.find(objToArray, predicate, value);
350 }
351
352 let collectionAsArray = <T[]> collection;
353
354 let firstMatchingItem: T;
355 for (let i = 0; i < collectionAsArray.length; i++) {
356 let item: T = collectionAsArray[i];
357 if (typeof predicate === 'string') {
358 if ((<any>item)[predicate] === value) {
359 firstMatchingItem = item;
360 break;
361 }
362 } else {
363 let callback = <(item: T) => void> predicate;
364 if (callback(item)) {
365 firstMatchingItem = item;
366 break;
367 }
368 }
369 }
370 return firstMatchingItem;
371 }
372
373 static toStrings<T>(array: T[]): string[] {
374 return this.map(array, function (item) {
375 if (item === undefined || item === null || !item.toString) {
376 return null;
377 } else {
378 return item.toString();
379 }
380 });
381 }
382
383 static iterateArray<T>(array: T[], callback: (item: T, index: number) => void) {
384 for (let index = 0; index < array.length; index++) {
385 let value = array[index];
386 callback(value, index);
387 }
388 }
389
390 //Returns true if it is a DOM node
391 //taken from: http://stackoverflow.com/questions/384286/javascript-isdom-how-do-you-check-if-a-javascript-object-is-a-dom-object
392 static isNode(o: any) {
393 return (
394 typeof Node === "function" ? o instanceof Node :
395 o && typeof o === "object" && typeof o.nodeType === "number" && typeof o.nodeName === "string"
396 );
397 }
398
399 //Returns true if it is a DOM element
400 //taken from: http://stackoverflow.com/questions/384286/javascript-isdom-how-do-you-check-if-a-javascript-object-is-a-dom-object
401 static isElement(o: any) {
402 return (
403 typeof HTMLElement === "function" ? o instanceof HTMLElement : //DOM2
404 o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName === "string"
405 );
406 }
407
408 static isNodeOrElement(o: any) {
409 return this.isNode(o) || this.isElement(o);
410 }
411
412 // makes a copy of a node list into a list
413 static copyNodeList(nodeList: NodeList) {
414 let childCount = nodeList ? nodeList.length : 0;
415 let res: Node[] = [];
416 for (let i = 0; i < childCount; i++) {
417 res.push(nodeList[i]);
418 }
419 return res;
420 }
421
422 static isEventFromPrintableCharacter(event: KeyboardEvent): boolean {
423 let pressedChar = String.fromCharCode(event.charCode);
424
425 // newline is an exception, as it counts as a printable character, but we don't
426 // want to start editing when it is pressed. without this check, if user is in chrome
427 // and editing a cell, and they press ctrl+enter, the cell stops editing, and then
428 // starts editing again with a blank value (two 'key down' events are fired). to
429 // test this, remove the line below, edit a cell in chrome and hit ctrl+enter while editing.
430 // https://ag-grid.atlassian.net/browse/AG-605
431 if (this.isKeyPressed(event, Constants.KEY_NEW_LINE)) { return false; }
432
433 if (_.exists(event.key)) {
434 // modern browser will implement key, so we return if key is length 1, eg if it is 'a' for the
435 // a key, or '2' for the '2' key. non-printable characters have names, eg 'Enter' or 'Backspace'.
436 const printableCharacter = event.key.length === 1;
437
438 // IE11 & Edge treat the numpad del key differently - with numlock on we get "Del" for key,
439 // so this addition checks if its IE11/Edge and handles that specific case the same was as all other browers
440 const numpadDelWithNumlockOnForEdgeOrIe = Utils.isNumpadDelWithNumlockOnForEdgeOrIe(event);
441
442 return printableCharacter || numpadDelWithNumlockOnForEdgeOrIe;
443 } else {
444 // otherwise, for older browsers, we test against a list of characters, which doesn't include
445 // accents for non-English, but don't care much, as most users are on modern browsers
446 return Utils.PRINTABLE_CHARACTERS.indexOf(pressedChar) >= 0
447 }
448 }
449
450 //adds all type of change listeners to an element, intended to be a text field
451 static addChangeListener(element: HTMLElement, listener: EventListener) {
452 element.addEventListener("changed", listener);
453 element.addEventListener("paste", listener);
454 element.addEventListener("input", listener);
455 // IE doesn't fire changed for special keys (eg delete, backspace), so need to
456 // listen for this further ones
457 element.addEventListener("keydown", listener);
458 element.addEventListener("keyup", listener);
459 }
460
461 //if value is undefined, null or blank, returns null, otherwise returns the value
462 static makeNull<T>(value: T): T {
463 let valueNoType = <any> value;
464 if (value === null || value === undefined || valueNoType === "") {
465 return null;
466 } else {
467 return value;
468 }
469 }
470
471 static missing(value: any): boolean {
472 return !this.exists(value);
473 }
474
475 static missingOrEmpty(value: any[] | string): boolean {
476 return this.missing(value) || value.length === 0;
477 }
478
479 static missingOrEmptyObject(value: any): boolean {
480 return this.missing(value) || Object.keys(value).length === 0;
481 }
482
483 static exists(value: any): boolean {
484 if (value === null || value === undefined || value === '') {
485 return false;
486 } else {
487 return true;
488 }
489 }
490
491 static firstExistingValue<A>(...values: A[]): A {
492 for (let i = 0; i < values.length; i++) {
493 let value: A = values[i];
494 if (_.exists(value)) return value;
495 }
496
497 return null;
498 }
499
500 static anyExists(values: any[]): boolean {
501 if (values) {
502 for (let i = 0; i < values.length; i++) {
503 if (this.exists(values[i])) {
504 return true;
505 }
506 }
507 }
508 return false;
509 }
510
511 static existsAndNotEmpty(value: any[]): boolean {
512 return this.exists(value) && value.length > 0;
513 }
514
515 static removeAllChildren(node: HTMLElement) {
516 if (node) {
517 while (node.hasChildNodes()) {
518 node.removeChild(node.lastChild);
519 }
520 }
521 }
522
523 static removeElement(parent: HTMLElement, cssSelector: string) {
524 this.removeFromParent(parent.querySelector(cssSelector));
525 }
526
527 static removeFromParent(node: Element) {
528 if (node && node.parentNode) {
529 node.parentNode.removeChild(node);
530 }
531 }
532
533 static isVisible(element: HTMLElement) {
534 return (element.offsetParent !== null);
535 }
536
537 /**
538 * loads the template and returns it as an element. makes up for no simple way in
539 * the dom api to load html directly, eg we cannot do this: document.createElement(template)
540 */
541 static loadTemplate(template: string): HTMLElement {
542 let tempDiv = document.createElement("div");
543 tempDiv.innerHTML = template;
544 return <HTMLElement> tempDiv.firstChild;
545 }
546
547 static appendHtml(eContainer: HTMLElement, htmlTemplate: string) {
548 if (eContainer.lastChild) {
549 // https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
550 // we put the items at the start, so new items appear underneath old items,
551 // so when expanding/collapsing groups, the new rows don't go on top of the
552 // rows below that are moving our of the way
553 eContainer.insertAdjacentHTML('afterbegin', htmlTemplate);
554 } else {
555 eContainer.innerHTML = htmlTemplate;
556 }
557 }
558
559 static addOrRemoveCssClass(element: HTMLElement, className: string, addOrRemove: boolean) {
560 if (addOrRemove) {
561 this.addCssClass(element, className);
562 } else {
563 this.removeCssClass(element, className);
564 }
565 }
566
567 static callIfPresent(func: Function): void {
568 if (func) {
569 func();
570 }
571 }
572
573 static addCssClass(element: HTMLElement, className: string) {
574 if (!className || className.length === 0) {
575 return;
576 }
577 if (className.indexOf(' ') >= 0) {
578 className.split(' ').forEach(value => this.addCssClass(element, value));
579 return;
580 }
581 if (element.classList) {
582 if (!element.classList.contains(className)) {
583 element.classList.add(className);
584 }
585 } else {
586 if (element.className && element.className.length > 0) {
587 let cssClasses = element.className.split(' ');
588 if (cssClasses.indexOf(className) < 0) {
589 cssClasses.push(className);
590 element.className = cssClasses.join(' ');
591 }
592 } else {
593 element.className = className;
594 }
595 }
596 }
597
598 static containsClass(element: any, className: string): boolean {
599 if (element.classList) {
600 // for modern browsers
601 return element.classList.contains(className);
602 } else if (element.className) {
603 // for older browsers, check against the string of class names
604 // if only one class, can check for exact match
605 let onlyClass = element.className === className;
606 // if many classes, check for class name, we have to pad with ' ' to stop other
607 // class names that are a substring of this class
608 let contains = element.className.indexOf(' ' + className + ' ') >= 0;
609 // the padding above then breaks when it's the first or last class names
610 let startsWithClass = element.className.indexOf(className + ' ') === 0;
611 let endsWithClass = element.className.lastIndexOf(' ' + className) === (element.className.length - className.length - 1);
612 return onlyClass || contains || startsWithClass || endsWithClass;
613 } else {
614 // if item is not a node
615 return false;
616 }
617 }
618
619 static getElementAttribute(element: any, attributeName: string): string {
620 if (element.attributes) {
621 if (element.attributes[attributeName]) {
622 let attribute = element.attributes[attributeName];
623 return attribute.value;
624 } else {
625 return null;
626 }
627 } else {
628 return null;
629 }
630 }
631
632 static offsetHeight(element: HTMLElement) {
633 return element && element.clientHeight ? element.clientHeight : 0;
634 }
635
636 static offsetWidth(element: HTMLElement) {
637 return element && element.clientWidth ? element.clientWidth : 0;
638 }
639
640 static sortNumberArray(numberArray: number[]): void {
641 numberArray.sort((a: number, b: number) => a - b);
642 }
643
644 static removeCssClass(element: HTMLElement, className: string) {
645 if (element.classList) {
646 if (element.classList.contains(className)) {
647 element.classList.remove(className);
648 }
649 } else {
650 if (element.className && element.className.length > 0) {
651 let cssClasses = element.className.split(' ');
652 if (cssClasses.indexOf(className) >= 0) {
653 // remove all instances of the item, not just the first, in case it's in more than once
654 while (cssClasses.indexOf(className) >= 0) {
655 cssClasses.splice(cssClasses.indexOf(className), 1);
656 }
657 element.className = cssClasses.join(' ');
658 }
659 }
660 }
661 }
662
663 static removeRepeatsFromArray<T>(array: T[], object: T) {
664 if (!array) {
665 return;
666 }
667 for (let index = array.length - 2; index >= 0; index--) {
668 let thisOneMatches = array[index] === object;
669 let nextOneMatches = array[index + 1] === object;
670 if (thisOneMatches && nextOneMatches) {
671 array.splice(index + 1, 1);
672 }
673 }
674
675 }
676
677 static removeFromArray<T>(array: T[], object: T) {
678 if (array.indexOf(object) >= 0) {
679 array.splice(array.indexOf(object), 1);
680 }
681 }
682
683 static removeAllFromArray<T>(array: T[], toRemove: T[]) {
684 toRemove.forEach(item => {
685 if (array.indexOf(item) >= 0) {
686 array.splice(array.indexOf(item), 1);
687 }
688 });
689 }
690
691 static insertIntoArray<T>(array: T[], object: T, toIndex: number) {
692 array.splice(toIndex, 0, object);
693 }
694
695 static insertArrayIntoArray<T>(dest: T[], src: T[], toIndex: number) {
696 if (this.missing(dest) || this.missing(src)) {
697 return;
698 }
699 // put items in backwards, otherwise inserted items end up in reverse order
700 for (let i = src.length - 1; i >= 0; i--) {
701 let item = src[i];
702 this.insertIntoArray(dest, item, toIndex);
703 }
704 }
705
706 static moveInArray<T>(array: T[], objectsToMove: T[], toIndex: number) {
707 // first take out it items from the array
708 objectsToMove.forEach((obj) => {
709 this.removeFromArray(array, obj);
710 });
711
712 // now add the objects, in same order as provided to us, that means we start at the end
713 // as the objects will be pushed to the right as they are inserted
714 objectsToMove.slice().reverse().forEach((obj) => {
715 this.insertIntoArray(array, obj, toIndex);
716 });
717 }
718
719 static defaultComparator(valueA: any, valueB: any, accentedCompare: boolean = false): number {
720 let valueAMissing = valueA === null || valueA === undefined;
721 let valueBMissing = valueB === null || valueB === undefined;
722
723 // this is for aggregations sum and avg, where the result can be a number that is wrapped.
724 // if we didn't do this, then the toString() value would be used, which would result in
725 // the strings getting used instead of the numbers.
726 if (valueA && valueA.toNumber) {
727 valueA = valueA.toNumber();
728 }
729 if (valueB && valueB.toNumber) {
730 valueB = valueB.toNumber();
731 }
732
733 if (valueAMissing && valueBMissing) {
734 return 0;
735 }
736 if (valueAMissing) {
737 return -1;
738 }
739 if (valueBMissing) {
740 return 1;
741 }
742
743 if (typeof valueA === "string") {
744 if (!accentedCompare) {
745 return doQuickCompare(valueA, valueB);
746 } else {
747 try {
748 // using local compare also allows chinese comparisons
749 return valueA.localeCompare(valueB);
750 } catch (e) {
751 // if something wrong with localeCompare, eg not supported
752 // by browser, then just continue with the quick one
753 return doQuickCompare(valueA, valueB);
754 }
755 }
756
757 }
758
759 if (valueA < valueB) {
760 return -1;
761 } else if (valueA > valueB) {
762 return 1;
763 } else {
764 return 0;
765 }
766
767 function doQuickCompare(a: string, b: string): number {
768 return (a > b ? 1 : (a < b ? -1 : 0));
769 }
770 }
771
772 static compareArrays(array1: any[], array2: any[]): boolean {
773 if (this.missing(array1) && this.missing(array2)) {
774 return true;
775 }
776 if (this.missing(array1) || this.missing(array2)) {
777 return false;
778 }
779 if (array1.length !== array2.length) {
780 return false;
781 }
782 for (let i = 0; i < array1.length; i++) {
783 if (array1[i] !== array2[i]) {
784 return false;
785 }
786 }
787 return true;
788 }
789
790 static ensureDomOrder(eContainer: HTMLElement, eChild: HTMLElement, eChildBefore: HTMLElement): void {
791
792 // if already in right order, do nothing
793 if (eChildBefore && eChildBefore.nextSibling === eChild) {
794 return;
795 }
796
797 if (eChildBefore) {
798 if (eChildBefore.nextSibling) {
799 // insert between the eRowBefore and the row after it
800 eContainer.insertBefore(eChild, eChildBefore.nextSibling);
801 } else {
802 // if nextSibling is missing, means other row is at end, so just append new row at the end
803 eContainer.appendChild(eChild);
804 }
805 } else {
806 // otherwise put at start
807 if (eContainer.firstChild) {
808 // insert it at the first location
809 eContainer.insertBefore(eChild, eContainer.firstChild);
810 }
811 }
812 }
813
814 static insertWithDomOrder(eContainer: HTMLElement, eChild: HTMLElement, eChildBefore: HTMLElement): void {
815 if (eChildBefore) {
816 if (eChildBefore.nextSibling) {
817 // insert between the eRowBefore and the row after it
818 eContainer.insertBefore(eChild, eChildBefore.nextSibling);
819 } else {
820 // if nextSibling is missing, means other row is at end, so just append new row at the end
821 eContainer.appendChild(eChild);
822 }
823 } else {
824 if (eContainer.firstChild) {
825 // insert it at the first location
826 eContainer.insertBefore(eChild, eContainer.firstChild);
827 } else {
828 // otherwise eContainer is empty, so just append it
829 eContainer.appendChild(eChild);
830 }
831 }
832 }
833
834 static insertTemplateWithDomOrder(eContainer: HTMLElement,
835 htmlTemplate: string,
836 eChildBefore: HTMLElement): HTMLElement {
837 let res: HTMLElement;
838 if (eChildBefore) {
839 // if previous element exists, just slot in after the previous element
840 eChildBefore.insertAdjacentHTML('afterend', htmlTemplate);
841 res = <HTMLElement> eChildBefore.nextSibling;
842 } else {
843 if (eContainer.firstChild) {
844 // insert it at the first location
845 eContainer.insertAdjacentHTML('afterbegin', htmlTemplate);
846 } else {
847 // otherwise eContainer is empty, so just append it
848 eContainer.innerHTML = htmlTemplate;
849 }
850 res = <HTMLElement> eContainer.firstChild;
851 }
852 return res;
853 }
854
855 static every<T>(items: T[], callback: (item: T)=>boolean): boolean {
856 if (!items || items.length===0) { return true; }
857
858 for (let i = 0; i<items.length; i++) {
859 if (!callback(items[i])) {
860 return false;
861 }
862 }
863
864 return true;
865 }
866
867 static toStringOrNull(value: any): string {
868 if (this.exists(value) && value.toString) {
869 return value.toString();
870 } else {
871 return null;
872 }
873 }
874
875 static formatWidth(width: number | string) {
876 if (typeof width === "number") {
877 return width + "px";
878 } else {
879 return width;
880 }
881 }
882
883 static formatNumberTwoDecimalPlacesAndCommas(value: number): string {
884 if (typeof value !== 'number') {
885 return '';
886 }
887
888 // took this from: http://blog.tompawlak.org/number-currency-formatting-javascript
889 return (Math.round(value * 100) / 100).toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,");
890 }
891
892 // the native method number.toLocaleString(undefined, {minimumFractionDigits: 0}) puts in decimal places in IE,
893 // so we use this method instead
894 static formatNumberCommas(value: number): string {
895 if (typeof value !== 'number') {
896 return '';
897 }
898
899 // took this from: http://blog.tompawlak.org/number-currency-formatting-javascript
900 return value.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,");
901 }
902
903 static prependDC(parent: HTMLElement, documentFragment: DocumentFragment): void {
904 if (this.exists(parent.firstChild)) {
905 parent.insertBefore(documentFragment, parent.firstChild);
906 } else {
907 parent.appendChild(documentFragment);
908 }
909 }
910
911 // static prepend(parent: HTMLElement, child: HTMLElement): void {
912 // if (this.exists(parent.firstChild)) {
913 // parent.insertBefore(child, parent.firstChild);
914 // } else {
915 // parent.appendChild(child);
916 // }
917 // }
918
919
920 static iconNameClassMap: { [key: string]: string } = {
921 'columnMovePin': 'pin',
922 'columnMoveAdd': 'plus',
923 'columnMoveHide': 'eye-slash',
924 'columnMoveMove': 'arrows',
925 'columnMoveLeft': 'left',
926 'columnMoveRight': 'right',
927 'columnMoveGroup': 'group',
928 'columnMoveValue': 'aggregation',
929 'columnMovePivot': 'pivot',
930 'dropNotAllowed': 'not-allowed',
931 'groupContracted': 'expanded',
932 'groupExpanded': 'contracted',
933 'checkboxChecked': 'checkbox-checked',
934 'checkboxUnchecked': 'checkbox-unchecked',
935 'checkboxIndeterminate': 'checkbox-indeterminate',
936 'checkboxCheckedReadOnly': 'checkbox-checked-readonly',
937 'checkboxUncheckedReadOnly': 'checkbox-unchecked-readonly',
938 'checkboxIndeterminateReadOnly': 'checkbox-indeterminate-readonly',
939 'groupLoading': 'loading',
940 'menu': 'menu',
941 'filter': 'filter',
942 'columns': 'columns',
943 'menuPin': 'pin',
944 'menuValue': 'aggregation',
945 'menuAddRowGroup': 'group',
946 'menuRemoveRowGroup': 'group',
947 'clipboardCopy': 'copy',
948 'clipboardCut': 'cut',
949 'clipboardPaste': 'paste',
950 'pivotPanel': 'pivot',
951 'rowGroupPanel': 'group',
952 'valuePanel': 'aggregation',
953 'columnGroupOpened': 'expanded',
954 'columnGroupClosed': 'contracted',
955 'columnSelectClosed': 'tree-closed',
956 'columnSelectOpen': 'tree-open',
957 // from deprecated header, remove at some point
958 'sortAscending': 'asc',
959 'sortDescending': 'desc',
960 'sortUnSort': 'none'
961 }
962
963 /**
964 * If icon provided, use this (either a string, or a function callback).
965 * if not, then use the default icon from the theme
966 */
967 static createIcon(iconName: string, gridOptionsWrapper: GridOptionsWrapper, column: Column): HTMLElement {
968 const iconContents = this.createIconNoSpan(iconName, gridOptionsWrapper, column)
969 if (iconContents.className.indexOf('ag-icon') > -1) {
970 return iconContents;
971 } else {
972 let eResult = document.createElement('span');
973 eResult.appendChild(iconContents);
974 return eResult;
975 }
976 }
977
978 static createIconNoSpan(iconName: string, gridOptionsWrapper: GridOptionsWrapper, column: Column): HTMLElement {
979 let userProvidedIcon: Function | string;
980 // check col for icon first
981 if (column && column.getColDef().icons) {
982 userProvidedIcon = column.getColDef().icons[iconName];
983 }
984 // it not in col, try grid options
985 if (!userProvidedIcon && gridOptionsWrapper.getIcons()) {
986 userProvidedIcon = gridOptionsWrapper.getIcons()[iconName];
987 }
988 // now if user provided, use it
989 if (userProvidedIcon) {
990 let rendererResult: any;
991 if (typeof userProvidedIcon === 'function') {
992 rendererResult = userProvidedIcon();
993 } else if (typeof userProvidedIcon === 'string') {
994 rendererResult = userProvidedIcon;
995 } else {
996 throw 'icon from grid options needs to be a string or a function';
997 }
998 if (typeof rendererResult === 'string') {
999 return this.loadTemplate(rendererResult);
1000 } else if (this.isNodeOrElement(rendererResult)) {
1001 return rendererResult;
1002 } else {
1003 throw 'iconRenderer should return back a string or a dom object';
1004 }
1005 } else {
1006 const span = document.createElement('span');
1007 const cssClass = this.iconNameClassMap[iconName];
1008 if (!cssClass) {
1009 throw new Error(`${iconName} did not find class`)
1010 }
1011 span.setAttribute("class", "ag-icon ag-icon-" + cssClass);
1012 return span;
1013 }
1014 }
1015
1016 static addStylesToElement(eElement: any, styles: any) {
1017 if (!styles) {
1018 return;
1019 }
1020 Object.keys(styles).forEach((key) => {
1021 let keyCamelCase = this.hyphenToCamelCase(key);
1022 eElement.style[keyCamelCase] = styles[key];
1023 });
1024 }
1025
1026 static isHorizontalScrollShowing(element: HTMLElement): boolean {
1027 return element.clientWidth < element.scrollWidth;
1028 }
1029
1030 static isVerticalScrollShowing(element: HTMLElement): boolean {
1031 return element.clientHeight < element.scrollHeight;
1032 }
1033
1034 static getMaxDivHeight(): number {
1035 if (!document.body) {
1036 return -1;
1037 }
1038
1039 let res = 1000000;
1040 // FF reports the height back but still renders blank after ~6M px
1041 let testUpTo = navigator.userAgent.toLowerCase().match(/firefox/) ? 6000000 : 1000000000;
1042 let div = this.loadTemplate("<div/>");
1043 document.body.appendChild(div);
1044 while (true) {
1045 let test = res * 2;
1046 div.style.height = test + 'px';
1047
1048 if (test > testUpTo || div.clientHeight !== test) {
1049 break;
1050 } else {
1051 res = test;
1052 }
1053 }
1054
1055 document.body.removeChild(div);
1056
1057 return res;
1058 }
1059
1060 static getScrollbarWidth() {
1061 let outer = document.createElement("div");
1062 outer.style.visibility = "hidden";
1063 outer.style.width = "100px";
1064 outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps
1065
1066 document.body.appendChild(outer);
1067
1068 let widthNoScroll = outer.offsetWidth;
1069 // force scrollbars
1070 outer.style.overflow = "scroll";
1071
1072 // add inner div
1073 let inner = document.createElement("div");
1074 inner.style.width = "100%";
1075 outer.appendChild(inner);
1076
1077 let widthWithScroll = inner.offsetWidth;
1078
1079 // remove divs
1080 outer.parentNode.removeChild(outer);
1081
1082 return widthNoScroll - widthWithScroll;
1083 }
1084
1085 static isKeyPressed(event: KeyboardEvent, keyToCheck: number) {
1086 let pressedKey = event.which || event.keyCode;
1087 return pressedKey === keyToCheck;
1088 }
1089
1090 static setVisible(element: HTMLElement, visible: boolean) {
1091 this.addOrRemoveCssClass(element, 'ag-hidden', !visible);
1092 }
1093
1094 static setHidden(element: HTMLElement, hidden: boolean) {
1095 this.addOrRemoveCssClass(element, 'ag-visibility-hidden', hidden);
1096 }
1097
1098 static isBrowserIE(): boolean {
1099 if (this.isIE === undefined) {
1100 this.isIE = /*@cc_on!@*/false || !!(<any>document).documentMode; // At least IE6
1101 }
1102 return this.isIE;
1103 }
1104
1105 static isBrowserEdge(): boolean {
1106 if (this.isEdge === undefined) {
1107 this.isEdge = !this.isBrowserIE() && !!(<any>window).StyleMedia;
1108 }
1109 return this.isEdge;
1110 }
1111
1112 static isBrowserSafari(): boolean {
1113 if (this.isSafari === undefined) {
1114 let anyWindow = <any> window;
1115 // taken from https://github.com/ag-grid/ag-grid/issues/550
1116 this.isSafari = Object.prototype.toString.call(anyWindow.HTMLElement).indexOf('Constructor') > 0
1117 || (function (p) {
1118 return p.toString() === "[object SafariRemoteNotification]";
1119 })
1120 (!anyWindow.safari || anyWindow.safari.pushNotification);
1121 }
1122 return this.isSafari;
1123 }
1124
1125 static isBrowserChrome(): boolean {
1126 if (this.isChrome === undefined) {
1127 let anyWindow = <any> window;
1128 this.isChrome = !!anyWindow.chrome && !!anyWindow.chrome.webstore;
1129 }
1130 return this.isChrome;
1131 }
1132
1133 static isBrowserFirefox(): boolean {
1134 if (this.isFirefox === undefined) {
1135 let anyWindow = <any> window;
1136 this.isFirefox = typeof anyWindow.InstallTrigger !== 'undefined';
1137 }
1138 return this.isFirefox;
1139 }
1140
1141 static isUserAgentIPad(): boolean {
1142 if (this.isIPad === undefined) {
1143 // taken from https://davidwalsh.name/detect-ipad
1144 this.isIPad = navigator.userAgent.match(/iPad|iPhone/i) != null;
1145 }
1146 return this.isIPad;
1147 }
1148
1149 // srcElement is only available in IE. In all other browsers it is target
1150 // http://stackoverflow.com/questions/5301643/how-can-i-make-event-srcelement-work-in-firefox-and-what-does-it-mean
1151 static getTarget(event: Event): Element {
1152 let eventNoType = <any> event;
1153 return eventNoType.target || eventNoType.srcElement;
1154 }
1155
1156 static isElementInEventPath(element: HTMLElement, event: Event): boolean {
1157 if (!event || !element) {
1158 return false;
1159 }
1160 let path = _.getEventPath(event);
1161 return path.indexOf(element) >= 0;
1162 }
1163
1164 static createEventPath(event: Event): EventTarget[] {
1165 let res: EventTarget[] = [];
1166 let pointer = _.getTarget(event);
1167 while (pointer) {
1168 res.push(pointer);
1169 pointer = pointer.parentElement;
1170 }
1171 return res;
1172 }
1173
1174 // firefox doesn't have event.path set, or any alternative to it, so we hack
1175 // it in. this is needed as it's to late to work out the path when the item is
1176 // removed from the dom. used by MouseEventService, where it works out if a click
1177 // was from the current grid, or a detail grid (master / detail).
1178 static addAgGridEventPath(event: Event): void {
1179 (<any>event).__agGridEventPath = this.getEventPath(event);
1180 }
1181
1182 static getEventPath(event: Event): EventTarget[] {
1183 // https://stackoverflow.com/questions/39245488/event-path-undefined-with-firefox-and-vue-js
1184 // https://developer.mozilla.org/en-US/docs/Web/API/Event
1185
1186 let eventNoType = <any> event;
1187 if (event.deepPath) {
1188 // IE supports deep path
1189 return event.deepPath();
1190 } else if (eventNoType.path) {
1191 // Chrome supports path
1192 return eventNoType.path;
1193 } else if (eventNoType.composedPath) {
1194 // Firefox supports composePath
1195 return eventNoType.composedPath();
1196 } else if (eventNoType.__agGridEventPath) {
1197 // Firefox supports composePath
1198 return eventNoType.__agGridEventPath;
1199 } else {
1200 // and finally, if none of the above worked,
1201 // we create the path ourselves
1202 return this.createEventPath(event);
1203 }
1204 }
1205
1206 static forEachSnapshotFirst(list: any[], callback: (item: any) => void): void {
1207 if (list) {
1208 let arrayCopy = list.slice(0);
1209 arrayCopy.forEach(callback);
1210 }
1211 }
1212
1213 // taken from: http://stackoverflow.com/questions/1038727/how-to-get-browser-width-using-javascript-code
1214 static getBodyWidth(): number {
1215 if (document.body) {
1216 return document.body.clientWidth;
1217 }
1218
1219 if (window.innerHeight) {
1220 return window.innerWidth;
1221 }
1222
1223 if (document.documentElement && document.documentElement.clientWidth) {
1224 return document.documentElement.clientWidth;
1225 }
1226
1227 return -1;
1228 }
1229
1230 // taken from: http://stackoverflow.com/questions/1038727/how-to-get-browser-width-using-javascript-code
1231 static getBodyHeight(): number {
1232 if (document.body) {
1233 return document.body.clientHeight;
1234 }
1235
1236 if (window.innerHeight) {
1237 return window.innerHeight;
1238 }
1239
1240 if (document.documentElement && document.documentElement.clientHeight) {
1241 return document.documentElement.clientHeight;
1242 }
1243
1244 return -1;
1245 }
1246
1247 static setCheckboxState(eCheckbox: any, state: any) {
1248 if (typeof state === 'boolean') {
1249 eCheckbox.checked = state;
1250 eCheckbox.indeterminate = false;
1251 } else {
1252 // isNodeSelected returns back undefined if it's a group and the children
1253 // are a mix of selected and unselected
1254 eCheckbox.indeterminate = true;
1255 }
1256 }
1257
1258 static traverseNodesWithKey(nodes: RowNode[], callback: (node: RowNode, key: string) => void): void {
1259 let keyParts: any[] = [];
1260
1261 recursiveSearchNodes(nodes);
1262
1263 function recursiveSearchNodes(nodes: RowNode[]): void {
1264 nodes.forEach((node: RowNode) => {
1265
1266 // also checking for children for tree data
1267 if (node.group || node.hasChildren()) {
1268 keyParts.push(node.key);
1269 let key = keyParts.join('|');
1270 callback(node, key);
1271 recursiveSearchNodes(node.childrenAfterGroup);
1272 keyParts.pop();
1273 }
1274 });
1275 }
1276 }
1277
1278 // from https://gist.github.com/youssman/745578062609e8acac9f
1279 static camelCaseToHyphen(str: string): string {
1280 if (str === null || str === undefined) {
1281 return null;
1282 }
1283 return str.replace(/([A-Z])/g, (g) => '-' + g[0].toLowerCase());
1284 }
1285
1286 // from https://stackoverflow.com/questions/6660977/convert-hyphens-to-camel-case-camelcase
1287 static hyphenToCamelCase(str: string): string {
1288 if (str === null || str === undefined) {
1289 return null;
1290 }
1291 return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
1292 }
1293
1294 // pas in an object eg: {color: 'black', top: '25px'} and it returns "color: black; top: 25px;" for html
1295 static cssStyleObjectToMarkup(stylesToUse: any): string {
1296 if (!stylesToUse) {
1297 return '';
1298 }
1299
1300 let resParts: string[] = [];
1301 this.iterateObject(stylesToUse, (styleKey: string, styleValue: string) => {
1302 let styleKeyDashed = this.camelCaseToHyphen(styleKey);
1303 resParts.push(`${styleKeyDashed}: ${styleValue};`)
1304 });
1305
1306 return resParts.join(' ');
1307 }
1308
1309 /**
1310 * From http://stackoverflow.com/questions/9716468/is-there-any-function-like-isnumeric-in-javascript-to-validate-numbers
1311 */
1312 static isNumeric(value: any): boolean {
1313 if (value === '') return false;
1314 return !isNaN(parseFloat(value)) && isFinite(value);
1315 }
1316
1317 static escape(toEscape: string): string {
1318 if (toEscape === null || toEscape === undefined || !toEscape.replace) {
1319 return toEscape;
1320 }
1321
1322 return toEscape.replace(reUnescapedHtml, chr => HTML_ESCAPES[chr])
1323 }
1324
1325 // Taken from here: https://github.com/facebook/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js
1326 /**
1327 * Mouse wheel (and 2-finger trackpad) support on the web sucks. It is
1328 * complicated, thus this doc is long and (hopefully) detailed enough to answer
1329 * your questions.
1330 *
1331 * If you need to react to the mouse wheel in a predictable way, this code is
1332 * like your bestest friend. * hugs *
1333 *
1334 * As of today, there are 4 DOM event types you can listen to:
1335 *
1336 * 'wheel' -- Chrome(31+), FF(17+), IE(9+)
1337 * 'mousewheel' -- Chrome, IE(6+), Opera, Safari
1338 * 'MozMousePixelScroll' -- FF(3.5 only!) (2010-2013) -- don't bother!
1339 * 'DOMMouseScroll' -- FF(0.9.7+) since 2003
1340 *
1341 * So what to do? The is the best:
1342 *
1343 * normalizeWheel.getEventType();
1344 *
1345 * In your event callback, use this code to get sane interpretation of the
1346 * deltas. This code will return an object with properties:
1347 *
1348 * spinX -- normalized spin speed (use for zoom) - x plane
1349 * spinY -- " - y plane
1350 * pixelX -- normalized distance (to pixels) - x plane
1351 * pixelY -- " - y plane
1352 *
1353 * Wheel values are provided by the browser assuming you are using the wheel to
1354 * scroll a web page by a number of lines or pixels (or pages). Values can vary
1355 * significantly on different platforms and browsers, forgetting that you can
1356 * scroll at different speeds. Some devices (like trackpads) emit more events
1357 * at smaller increments with fine granularity, and some emit massive jumps with
1358 * linear speed or acceleration.
1359 *
1360 * This code does its best to normalize the deltas for you:
1361 *
1362 * - spin is trying to normalize how far the wheel was spun (or trackpad
1363 * dragged). This is super useful for zoom support where you want to
1364 * throw away the chunky scroll steps on the PC and make those equal to
1365 * the slow and smooth tiny steps on the Mac. Key data: This code tries to
1366 * resolve a single slow step on a wheel to 1.
1367 *
1368 * - pixel is normalizing the desired scroll delta in pixel units. You'll
1369 * get the crazy differences between browsers, but at least it'll be in
1370 * pixels!
1371 *
1372 * - positive value indicates scrolling DOWN/RIGHT, negative UP/LEFT. This
1373 * should translate to positive value zooming IN, negative zooming OUT.
1374 * This matches the newer 'wheel' event.
1375 *
1376 * Why are there spinX, spinY (or pixels)?
1377 *
1378 * - spinX is a 2-finger side drag on the trackpad, and a shift + wheel turn
1379 * with a mouse. It results in side-scrolling in the browser by default.
1380 *
1381 * - spinY is what you expect -- it's the classic axis of a mouse wheel.
1382 *
1383 * - I dropped spinZ/pixelZ. It is supported by the DOM 3 'wheel' event and
1384 * probably is by browsers in conjunction with fancy 3D controllers .. but
1385 * you know.
1386 *
1387 * Implementation info:
1388 *
1389 * Examples of 'wheel' event if you scroll slowly (down) by one step with an
1390 * average mouse:
1391 *
1392 * OS X + Chrome (mouse) - 4 pixel delta (wheelDelta -120)
1393 * OS X + Safari (mouse) - N/A pixel delta (wheelDelta -12)
1394 * OS X + Firefox (mouse) - 0.1 line delta (wheelDelta N/A)
1395 * Win8 + Chrome (mouse) - 100 pixel delta (wheelDelta -120)
1396 * Win8 + Firefox (mouse) - 3 line delta (wheelDelta -120)
1397 *
1398 * On the trackpad:
1399 *
1400 * OS X + Chrome (trackpad) - 2 pixel delta (wheelDelta -6)
1401 * OS X + Firefox (trackpad) - 1 pixel delta (wheelDelta N/A)
1402 *
1403 * On other/older browsers.. it's more complicated as there can be multiple and
1404 * also missing delta values.
1405 *
1406 * The 'wheel' event is more standard:
1407 *
1408 * http://www.w3.org/TR/DOM-Level-3-Events/#events-wheelevents
1409 *
1410 * The basics is that it includes a unit, deltaMode (pixels, lines, pages), and
1411 * deltaX, deltaY and deltaZ. Some browsers provide other values to maintain
1412 * backward compatibility with older events. Those other values help us
1413 * better normalize spin speed. Example of what the browsers provide:
1414 *
1415 * | event.wheelDelta | event.detail
1416 * ------------------+------------------+--------------
1417 * Safari v5/OS X | -120 | 0
1418 * Safari v5/Win7 | -120 | 0
1419 * Chrome v17/OS X | -120 | 0
1420 * Chrome v17/Win7 | -120 | 0
1421 * IE9/Win7 | -120 | undefined
1422 * Firefox v4/OS X | undefined | 1
1423 * Firefox v4/Win7 | undefined | 3
1424 *
1425 */
1426 static normalizeWheel(event: any): any {
1427 let PIXEL_STEP = 10;
1428 let LINE_HEIGHT = 40;
1429 let PAGE_HEIGHT = 800;
1430
1431 // spinX, spinY
1432 let sX = 0;
1433 let sY = 0;
1434
1435 // pixelX, pixelY
1436 let pX = 0;
1437 let pY = 0;
1438
1439 // Legacy
1440 if ('detail' in event) {
1441 sY = event.detail;
1442 }
1443 if ('wheelDelta' in event) {
1444 sY = -event.wheelDelta / 120;
1445 }
1446 if ('wheelDeltaY' in event) {
1447 sY = -event.wheelDeltaY / 120;
1448 }
1449 if ('wheelDeltaX' in event) {
1450 sX = -event.wheelDeltaX / 120;
1451 }
1452
1453 // side scrolling on FF with DOMMouseScroll
1454 if ('axis' in event && event.axis === event.HORIZONTAL_AXIS) {
1455 sX = sY;
1456 sY = 0;
1457 }
1458
1459 pX = sX * PIXEL_STEP;
1460 pY = sY * PIXEL_STEP;
1461
1462 if ('deltaY' in event) {
1463 pY = event.deltaY;
1464 }
1465 if ('deltaX' in event) {
1466 pX = event.deltaX;
1467 }
1468
1469 if ((pX || pY) && event.deltaMode) {
1470 if (event.deltaMode == 1) { // delta in LINE units
1471 pX *= LINE_HEIGHT;
1472 pY *= LINE_HEIGHT;
1473 } else { // delta in PAGE units
1474 pX *= PAGE_HEIGHT;
1475 pY *= PAGE_HEIGHT;
1476 }
1477 }
1478
1479 // Fall-back if spin cannot be determined
1480 if (pX && !sX) {
1481 sX = (pX < 1) ? -1 : 1;
1482 }
1483 if (pY && !sY) {
1484 sY = (pY < 1) ? -1 : 1;
1485 }
1486
1487 return {
1488 spinX: sX,
1489 spinY: sY,
1490 pixelX: pX,
1491 pixelY: pY
1492 };
1493 }
1494
1495 /**
1496 * https://stackoverflow.com/questions/24004791/can-someone-explain-the-debounce-function-in-javascript
1497 */
1498 static debounce(func: () => void, wait: number, immediate: boolean = false) {
1499 // 'private' variable for instance
1500 // The returned function will be able to reference this due to closure.
1501 // Each call to the returned function will share this common timer.
1502 var timeout: any;
1503
1504 // Calling debounce returns a new anonymous function
1505 return function () {
1506 // reference the context and args for the setTimeout function
1507 var context = this,
1508 args = arguments;
1509
1510 // Should the function be called now? If immediate is true
1511 // and not already in a timeout then the answer is: Yes
1512 var callNow = immediate && !timeout;
1513
1514 // This is the basic debounce behaviour where you can call this
1515 // function several times, but it will only execute once
1516 // [before or after imposing a delay].
1517 // Each time the returned function is called, the timer starts over.
1518 clearTimeout(timeout);
1519
1520 // Set the new timeout
1521 timeout = setTimeout(function () {
1522
1523 // Inside the timeout function, clear the timeout variable
1524 // which will let the next execution run when in 'immediate' mode
1525 timeout = null;
1526
1527 // Check if the function already ran with the immediate flag
1528 if (!immediate) {
1529 // Call the original function with apply
1530 // apply lets you define the 'this' object as well as the arguments
1531 // (both captured before setTimeout)
1532 func.apply(context, args);
1533 }
1534 }, wait);
1535
1536 // Immediate mode and no wait timer? Execute the function..
1537 if (callNow) func.apply(context, args);
1538 };
1539 };
1540
1541 // a user once raised an issue - they said that when you opened a popup (eg context menu)
1542 // and then clicked on a selection checkbox, the popup wasn't closed. this is because the
1543 // popup listens for clicks on the body, however ag-grid WAS stopping propagation on the
1544 // checkbox clicks (so the rows didn't pick them up as row selection selection clicks).
1545 // to get around this, we have a pattern to stop propagation for the purposes of ag-Grid,
1546 // but we still let the event pass back to teh body.
1547 static stopPropagationForAgGrid(event: Event): void {
1548 (<any>event)[AG_GRID_STOP_PROPAGATION] = true;
1549 }
1550
1551 static isStopPropagationForAgGrid(event: Event): boolean {
1552 return (<any>event)[AG_GRID_STOP_PROPAGATION] === true;
1553 }
1554
1555 static executeInAWhile(funcs: Function[]): void {
1556 this.executeAfter(funcs, 400);
1557 }
1558
1559 static executeNextVMTurn(funcs: Function[]): void {
1560 this.executeAfter(funcs, 0);
1561 }
1562
1563 static executeAfter(funcs: Function[], millis: number): void {
1564 if (funcs.length > 0) {
1565 setTimeout(() => {
1566 funcs.forEach(func => func());
1567 }, millis);
1568 }
1569 }
1570
1571 static referenceCompare(left: any, right: any): boolean {
1572 if (left == null && right == null) return true;
1573 if (left == null && right) return false;
1574 if (left && right == null) return false;
1575 return left === right;
1576 }
1577
1578 static get(source: { [p: string]: any }, expression: string, defaultValue: any): any {
1579 if (source == null) return defaultValue;
1580
1581 if (expression.indexOf('.') > -1) {
1582 let fields: string[] = expression.split('.');
1583 let thisKey: string = fields[0];
1584 let nextValue: any = source[thisKey];
1585 if (nextValue != null) {
1586 return Utils.get(nextValue, fields.slice(1, fields.length).join('.'), defaultValue);
1587 } else {
1588 return defaultValue;
1589 }
1590 } else {
1591 let nextValue: any = source[expression];
1592 return nextValue != null ? nextValue : defaultValue;
1593 }
1594 }
1595
1596 static passiveEvents: string[] = ['touchstart', 'touchend', 'touchmove', 'touchcancel'];
1597
1598 static addSafePassiveEventListener(eElement: HTMLElement, event: string, listener: (event?: any) => void) {
1599 eElement.addEventListener(event, listener, <any>(Utils.passiveEvents.indexOf(event) > -1 ? {passive: true} : undefined));
1600 }
1601
1602 static camelCaseToHumanText(camelCase: string): string {
1603 if (camelCase == null) return null;
1604
1605 // Who needs to learn how to code when you have stack overflow!
1606 // from: https://stackoverflow.com/questions/15369566/putting-space-in-camel-case-string-using-regular-expression
1607 let rex = /([A-Z])([A-Z])([a-z])|([a-z])([A-Z])/g;
1608 let words: string[] = camelCase.replace(rex, '$1$4 $2$3$5').replace('.', ' ').split(' ');
1609
1610 return words.map(word => word.substring(0, 1).toUpperCase() + ((word.length > 1) ? word.substring(1, word.length) : '')).join(' ');
1611 }
1612
1613 // displays a message to the browser. this is useful in iPad, where you can't easily see the console.
1614 // so the javascript code can use this to give feedback. this is NOT intended to be called in production.
1615 // it is intended the ag-Grid developer calls this to troubleshoot, but then takes out the calls before
1616 // checking in.
1617 static message(msg: string): void {
1618 let eMessage = document.createElement('div');
1619 eMessage.innerHTML = msg;
1620 let eBox = document.querySelector('#__ag__message');
1621 if (!eBox) {
1622 let template = `<div id="__ag__message" style="display: inline-block; position: absolute; top: 0px; left: 0px; color: white; background-color: black; z-index: 20; padding: 2px; border: 1px solid darkred; height: 200px; overflow-y: auto;"></div>`;
1623 eBox = this.loadTemplate(template);
1624 if (document.body) {
1625 document.body.appendChild(eBox);
1626 }
1627 }
1628 eBox.appendChild(eMessage);
1629 }
1630
1631 // gets called by: a) ClientSideNodeManager and b) GroupStage to do sorting.
1632 // when in ClientSideNodeManager we always have indexes (as this sorts the items the
1633 // user provided) but when in GroupStage, the nodes can contain filler nodes that
1634 // don't have order id's
1635 static sortRowNodesByOrder(rowNodes: RowNode[], rowNodeOrder: { [id: string]: number }): void {
1636 if (!rowNodes) {
1637 return;
1638 }
1639 rowNodes.sort((nodeA: RowNode, nodeB: RowNode) => {
1640 let positionA = rowNodeOrder[nodeA.id];
1641 let positionB = rowNodeOrder[nodeB.id];
1642
1643 let aHasIndex = positionA !== undefined;
1644 let bHasIndex = positionB !== undefined;
1645
1646 let bothNodesAreUserNodes = aHasIndex && bHasIndex;
1647 let bothNodesAreFillerNodes = !aHasIndex && !bHasIndex;
1648
1649 if (bothNodesAreUserNodes) {
1650 // when comparing two nodes the user has provided, they always
1651 // have indexes
1652 return positionA - positionB;
1653 } else if (bothNodesAreFillerNodes) {
1654 // when comparing two filler nodes, we have no index to compare them
1655 // against, however we want this sorting to be deterministic, so that
1656 // the rows don't jump around as the user does delta updates. so we
1657 // want the same sort result. so we use the id - which doesn't make sense
1658 // from a sorting point of view, but does give consistent behaviour between
1659 // calls. otherwise groups jump around as delta updates are done.
1660 return nodeA.id > nodeB.id ? 1 : -1;
1661 } else if (aHasIndex) {
1662 return 1;
1663 } else {
1664 return -1;
1665 }
1666 });
1667 }
1668
1669 public static fuzzyCheckStrings(
1670 inputValues: string[],
1671 validValues: string[],
1672 allSuggestions: string[]
1673 ) : {[p:string]: string[]}{
1674 let fuzzyMatches: {[p:string]: string[]} = {};
1675 let invalidInputs: string [] = inputValues.filter(inputValue =>
1676 !validValues.some(
1677 (validValue) => validValue === inputValue
1678 )
1679 );
1680
1681 if (invalidInputs.length > 0) {
1682 invalidInputs.forEach(invalidInput =>
1683 fuzzyMatches[invalidInput] = this.fuzzySuggestions(invalidInput, validValues, allSuggestions)
1684 );
1685 }
1686
1687 return fuzzyMatches;
1688 }
1689
1690 public static fuzzySuggestions(
1691 inputValue: string,
1692 validValues: string[],
1693 allSuggestions: string[]
1694 ) : string[]{
1695 let thisSuggestions: string [] = allSuggestions.slice(0);
1696 thisSuggestions.sort((suggestedValueLeft, suggestedValueRight) => {
1697 let leftDifference = _.string_similarity(suggestedValueLeft.toLowerCase(), inputValue.toLowerCase());
1698 let rightDifference = _.string_similarity(suggestedValueRight.toLowerCase(), inputValue.toLowerCase());
1699 return leftDifference > rightDifference ? -1 :
1700 leftDifference === rightDifference ? 0 :
1701 1;
1702 }
1703 );
1704
1705 return thisSuggestions;
1706 }
1707
1708
1709
1710 //Algorithm to do fuzzy search
1711 //https://stackoverflow.com/questions/23305000/javascript-fuzzy-search-that-makes-sense
1712 static get_bigrams (from:string) {
1713 var i, j, ref, s, v;
1714 s = from.toLowerCase();
1715 v = new Array(s.length - 1);
1716 for (i = j = 0, ref = v.length; j <= ref; i = j += 1) {
1717 v[i] = s.slice(i, i + 2);
1718
1719 }
1720 return v;
1721 }
1722
1723 static string_similarity = function(str1:string, str2:string) {
1724 var hit_count, j, k, len, len1, pairs1, pairs2, union, x, y;
1725 if (str1.length > 0 && str2.length > 0) {
1726 pairs1 = Utils.get_bigrams(str1);
1727 pairs2 = Utils.get_bigrams(str2);
1728 union = pairs1.length + pairs2.length;
1729 hit_count = 0;
1730 for (j = 0, len = pairs1.length; j < len; j++) {
1731 x = pairs1[j];
1732 for (k = 0, len1 = pairs2.length; k < len1; k++) {
1733 y = pairs2[k];
1734 if (x === y) {
1735 hit_count++;
1736 }
1737 }
1738 }
1739 if (hit_count > 0) {
1740 return (2.0 * hit_count) / union;
1741 }
1742 }
1743 return 0.0;
1744 };
1745
1746 private static isNumpadDelWithNumlockOnForEdgeOrIe(event: KeyboardEvent) {
1747 if(Utils.isBrowserEdge() || Utils.isBrowserIE()) {
1748 return event.key === Utils.NUMPAD_DEL_NUMLOCK_ON_KEY &&
1749 event.charCode === Utils.NUMPAD_DEL_NUMLOCK_ON_CHARCODE;
1750 }
1751
1752 return false;
1753 }
1754}
1755
1756export class NumberSequence {
1757
1758 private nextValue: number;
1759 private step: number;
1760
1761 constructor(initValue = 0, step = 1) {
1762 this.nextValue = initValue;
1763 this.step = step;
1764 }
1765
1766 public next(): number {
1767 let valToReturn = this.nextValue;
1768 this.nextValue += this.step;
1769 return valToReturn;
1770 }
1771
1772 public peek(): number {
1773 return this.nextValue;
1774 }
1775
1776 public skip(count: number): void {
1777 this.nextValue += count;
1778 }
1779}
1780
1781export let _ = Utils;
1782
1783export type ResolveAndRejectCallback<T> = (resolve:(value:T)=>void, reject:(params:any)=>void)=>void;
1784
1785export enum PromiseStatus {
1786 IN_PROGRESS, RESOLVED
1787}
1788
1789export interface ExternalPromise<T> {
1790 resolve:(value:T)=>void,
1791 promise:Promise<T>
1792}
1793
1794export class Promise<T> {
1795 private status:PromiseStatus = PromiseStatus.IN_PROGRESS;
1796 private resolution:T = null;
1797 private listOfWaiters: ((value:T)=>void)[] = [];
1798
1799
1800 static all<T> (toCombine:Promise<T>[]): Promise<T[]>{
1801 return new Promise(resolve=>{
1802 let combinedValues:T[] = [];
1803 let remainingToResolve:number = toCombine.length;
1804 toCombine.forEach((source, index)=> {
1805 source.then(sourceResolved=>{
1806 remainingToResolve --;
1807 combinedValues[index] = sourceResolved;
1808 if (remainingToResolve == 0){
1809 resolve(combinedValues);
1810 }
1811 });
1812 combinedValues.push(null);
1813 });
1814 });
1815 }
1816
1817 static resolve<T> (value:T): Promise<T>{
1818 return new Promise<T>(resolve=>resolve(value));
1819 }
1820
1821 static external<T> ():ExternalPromise<T>{
1822 let capture: (value:T)=> void;
1823 let promise:Promise<T> = new Promise<T>((resolve)=>{
1824 capture = resolve
1825 });
1826 return <ExternalPromise<T>>{
1827 promise: promise,
1828 resolve: (value:T):void => {
1829 capture(value)
1830 }
1831 };
1832 }
1833
1834 constructor (
1835 callback:ResolveAndRejectCallback<T>
1836 ){
1837 callback(this.onDone.bind(this), this.onReject.bind(this))
1838 }
1839
1840 public then(func: (result: any)=>void) {
1841 if (this.status === PromiseStatus.IN_PROGRESS){
1842 this.listOfWaiters.push(func);
1843 } else {
1844 func(this.resolution);
1845 }
1846 }
1847
1848 public firstOneOnly(func: (result: any)=>void) {
1849 if (this.status === PromiseStatus.IN_PROGRESS){
1850 if (this.listOfWaiters.length === 0) {
1851 this.listOfWaiters.push(func);
1852 }
1853 } else {
1854 func(this.resolution);
1855 }
1856 }
1857
1858 public map<Z> (adapter:(from:T)=>Z):Promise<Z>{
1859 return new Promise<Z>((resolve)=>{
1860 this.then(unmapped=>{
1861 resolve(adapter(unmapped))
1862 })
1863 });
1864 }
1865
1866 public resolveNow<Z> (ifNotResolvedValue:Z, ifResolved:(current:T)=>Z):Z{
1867 if (this.status == PromiseStatus.IN_PROGRESS) return ifNotResolvedValue;
1868
1869 return ifResolved(this.resolution);
1870 }
1871
1872 private onDone (value:T):void {
1873 this.status = PromiseStatus.RESOLVED;
1874 this.resolution = value;
1875 this.listOfWaiters.forEach(waiter=>waiter(value));
1876 }
1877
1878 private onReject (params:any):void {
1879 console.warn('TBI');
1880 }
1881}
\No newline at end of file