Skip to content

Commit 90d5ebc

Browse files
fix: custom collection util to handle nodelist and htmlcollection (salesforce#1540)
* fix: custom collection util to handle nodelist and htmlcollection * fix: address pr feedback accept collection as argument do not invoke callback with a thisArg * fix: minor optimization * fix: document.querySelector path to use collectionFind
1 parent 99f8ceb commit 90d5ebc

File tree

5 files changed

+140
-42
lines changed

5 files changed

+140
-42
lines changed

packages/@lwc/synthetic-shadow/src/faux-shadow/focus.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import {
2626
ArrayFind,
2727
ArrayIndexOf,
2828
ArrayReverse,
29-
ArraySlice,
3029
isNull,
3130
isUndefined,
3231
toString,
@@ -43,6 +42,7 @@ import {
4342
import { isDelegatingFocus } from './shadow-root';
4443
import { getOwnerDocument, getOwnerWindow } from '../shared/utils';
4544
import { patchedGetRootNode } from './traverse';
45+
import { collectionSlice, collectionIndexOf } from '../shared/node-collection-util';
4646

4747
const TabbableElementsQuery = `
4848
button:not([tabindex="-1"]):not([disabled]),
@@ -83,7 +83,7 @@ interface QuerySegments {
8383
function getTabbableSegments(host: HTMLElement): QuerySegments {
8484
const doc = getOwnerDocument(host);
8585
const all = documentQuerySelectorAll.call(doc, TabbableElementsQuery);
86-
const inner = ArraySlice.call(querySelectorAll.call(host, TabbableElementsQuery));
86+
const inner = collectionSlice(querySelectorAll.call(host, TabbableElementsQuery));
8787
if (process.env.NODE_ENV !== 'production') {
8888
assert.invariant(
8989
getAttribute.call(host, 'tabindex') === '-1' || isDelegatingFocus(host),
@@ -92,22 +92,22 @@ function getTabbableSegments(host: HTMLElement): QuerySegments {
9292
}
9393
const firstChild = inner[0];
9494
const lastChild = inner[inner.length - 1];
95-
const hostIndex = ArrayIndexOf.call(all, host);
95+
const hostIndex = collectionIndexOf(all, host);
9696

9797
// Host element can show up in our "previous" section if its tabindex is 0
9898
// We want to filter that out here
99-
const firstChildIndex = hostIndex > -1 ? hostIndex : ArrayIndexOf.call(all, firstChild);
99+
const firstChildIndex = hostIndex > -1 ? hostIndex : collectionIndexOf(all, firstChild);
100100

101101
// Account for an empty inner list
102102
const lastChildIndex =
103-
inner.length === 0 ? firstChildIndex + 1 : ArrayIndexOf.call(all, lastChild) + 1;
104-
const prev = ArraySlice.call(all, 0, firstChildIndex);
105-
const next = ArraySlice.call(all, lastChildIndex);
103+
inner.length === 0 ? firstChildIndex + 1 : collectionIndexOf(all, lastChild) + 1;
104+
const prev = collectionSlice(all, 0, firstChildIndex);
105+
const next = collectionSlice(all, lastChildIndex);
106106
return {
107107
prev,
108108
inner,
109109
next,
110-
};
110+
} as QuerySegments;
111111
}
112112

113113
export function getActiveElement(host: HTMLElement): Element | null {

packages/@lwc/synthetic-shadow/src/faux-shadow/slot.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
isUndefined,
1616
isTrue,
1717
ArrayFilter,
18-
ArraySlice,
1918
isNull,
2019
ArrayReduce,
2120
} from '../shared/language';
@@ -31,6 +30,7 @@ import { childNodesGetter as nativeChildNodesGetter } from '../env/node';
3130
import { createStaticNodeList } from '../shared/static-node-list';
3231
import { createStaticHTMLCollection } from '../shared/static-html-collection';
3332
import { PatchedElement } from './element';
33+
import { collectionSlice } from '../shared/node-collection-util';
3434

3535
interface HTMLSlotElementConstructor {
3636
prototype: HTMLSlotElement;
@@ -72,7 +72,7 @@ export function getFilteredSlotAssignedNodes(slot: HTMLElement): Node[] {
7272
if (isNull(owner)) {
7373
return [];
7474
}
75-
const childNodes = ArraySlice.call(nativeChildNodesGetter.call(slot)) as Node[];
75+
const childNodes = collectionSlice(nativeChildNodesGetter.call(slot)) as Node[];
7676
// Typescript is inferring the wrong function type for this particular
7777
// overloaded method: https://github.com/Microsoft/TypeScript/issues/27972
7878
// @ts-ignore type-mismatch
@@ -89,7 +89,7 @@ export function getFilteredSlotAssignedNodes(slot: HTMLElement): Node[] {
8989
}
9090

9191
function getFilteredSlotFlattenNodes(slot: HTMLElement): Node[] {
92-
const childNodes = ArraySlice.call(nativeChildNodesGetter.call(slot)) as Node[];
92+
const childNodes = collectionSlice(nativeChildNodesGetter.call(slot)) as Node[];
9393
// Typescript is inferring the wrong function type for this particular
9494
// overloaded method: https://github.com/Microsoft/TypeScript/issues/27972
9595
// @ts-ignore type-mismatch

packages/@lwc/synthetic-shadow/src/polyfills/document-body-shadow/polyfill.ts

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,8 @@ import {
1010
getElementsByTagNameNS as elementGetElementsByTagNameNS,
1111
querySelectorAll as elementQuerySelectorAll,
1212
} from '../../env/element';
13-
import {
14-
ArrayFilter,
15-
ArrayFind,
16-
ArraySlice,
17-
defineProperty,
18-
isTrue,
19-
isUndefined,
20-
} from '../../shared/language';
13+
import { ArraySlice, defineProperty, isTrue, isUndefined } from '../../shared/language';
14+
import { collectionFilter, collectionFind } from '../../shared/node-collection-util';
2115
import { getNodeOwnerKey } from '../../faux-shadow/node';
2216
import { createStaticNodeList } from '../../shared/static-node-list';
2317
import { createStaticHTMLCollection } from '../../shared/static-html-collection';
@@ -52,11 +46,11 @@ export default function apply() {
5246
]);
5347
const ownerKey = getNodeOwnerKey(this);
5448
// Return the first non shadow element
55-
const filtered = ArrayFind.call(
49+
const filtered = collectionFind(
5650
elements,
5751
elm => getNodeOwnerKey(elm) === ownerKey || isGlobalPatchingSkipped(elm)
5852
);
59-
return !isUndefined(filtered) ? filtered : null;
53+
return !isUndefined(filtered) ? (filtered as Element) : null;
6054
},
6155
writable: true,
6256
enumerable: true,
@@ -69,11 +63,11 @@ export default function apply() {
6963
string
7064
]);
7165
const ownerKey = getNodeOwnerKey(this);
72-
const filtered = ArrayFilter.call(
66+
const filtered = collectionFilter(
7367
elements,
7468
elm => getNodeOwnerKey(elm) === ownerKey || isGlobalPatchingSkipped(elm)
7569
);
76-
return createStaticNodeList(filtered);
70+
return createStaticNodeList(filtered as Array<Element>);
7771
},
7872
writable: true,
7973
enumerable: true,
@@ -86,11 +80,11 @@ export default function apply() {
8680
arguments
8781
) as [string]);
8882
const ownerKey = getNodeOwnerKey(this);
89-
const filtered = ArrayFilter.call(
83+
const filtered = collectionFilter(
9084
elements,
9185
elm => getNodeOwnerKey(elm) === ownerKey || isGlobalPatchingSkipped(elm)
9286
);
93-
return createStaticHTMLCollection(filtered);
87+
return createStaticHTMLCollection(filtered as Array<Element>);
9488
},
9589
writable: true,
9690
enumerable: true,
@@ -103,11 +97,11 @@ export default function apply() {
10397
string
10498
]);
10599
const ownerKey = getNodeOwnerKey(this);
106-
const filtered = ArrayFilter.call(
100+
const filtered = collectionFilter(
107101
elements,
108102
elm => getNodeOwnerKey(elm) === ownerKey || isGlobalPatchingSkipped(elm)
109103
);
110-
return createStaticHTMLCollection(filtered);
104+
return createStaticHTMLCollection(filtered as Array<Element>);
111105
},
112106
writable: true,
113107
enumerable: true,
@@ -120,11 +114,11 @@ export default function apply() {
120114
arguments
121115
) as [string, string]);
122116
const ownerKey = getNodeOwnerKey(this);
123-
const filtered = ArrayFilter.call(
117+
const filtered = collectionFilter(
124118
elements,
125119
elm => getNodeOwnerKey(elm) === ownerKey || isGlobalPatchingSkipped(elm)
126120
);
127-
return createStaticHTMLCollection(filtered);
121+
return createStaticHTMLCollection(filtered as Array<Element>);
128122
},
129123
writable: true,
130124
enumerable: true,

packages/@lwc/synthetic-shadow/src/polyfills/document-shadow/polyfill.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ import {
1515
querySelectorAll as documentQuerySelectorAll,
1616
} from '../../env/document';
1717
import {
18-
ArrayFilter,
19-
ArrayFind,
2018
ArraySlice,
2119
defineProperty,
2220
isNull,
@@ -31,6 +29,7 @@ import { pathComposer } from '../../3rdparty/polymer/path-composer';
3129
import { createStaticNodeList } from '../../shared/static-node-list';
3230
import { createStaticHTMLCollection } from '../../shared/static-html-collection';
3331
import { getOwnerDocument } from '../../shared/utils';
32+
import { collectionFilter, collectionFind } from '../../shared/node-collection-util';
3433

3534
let skipGlobalPatching: boolean;
3635
function isGlobalPatchingSkipped(node: Node) {
@@ -109,7 +108,7 @@ export default function apply() {
109108
const elements = documentQuerySelectorAll.apply(this, ArraySlice.call(arguments) as [
110109
string
111110
]);
112-
const filtered = ArrayFind.call(
111+
const filtered = collectionFind(
113112
elements,
114113
elm => isUndefined(getNodeOwnerKey(elm)) || isGlobalPatchingSkipped(elm)
115114
);
@@ -125,11 +124,11 @@ export default function apply() {
125124
const elements = documentQuerySelectorAll.apply(this, ArraySlice.call(arguments) as [
126125
string
127126
]);
128-
const filtered = ArrayFilter.call(
127+
const filtered = collectionFilter(
129128
elements,
130129
elm => isUndefined(getNodeOwnerKey(elm)) || isGlobalPatchingSkipped(elm)
131130
);
132-
return createStaticNodeList(filtered);
131+
return createStaticNodeList(filtered as Array<Element>);
133132
},
134133
writable: true,
135134
enumerable: true,
@@ -141,11 +140,11 @@ export default function apply() {
141140
const elements = documentGetElementsByClassName.apply(this, ArraySlice.call(
142141
arguments
143142
) as [string]);
144-
const filtered = ArrayFilter.call(
143+
const filtered = collectionFilter(
145144
elements,
146145
elm => isUndefined(getNodeOwnerKey(elm)) || isGlobalPatchingSkipped(elm)
147146
);
148-
return createStaticHTMLCollection(filtered);
147+
return createStaticHTMLCollection(filtered as Array<Element>);
149148
},
150149
writable: true,
151150
enumerable: true,
@@ -157,11 +156,11 @@ export default function apply() {
157156
const elements = documentGetElementsByTagName.apply(this, ArraySlice.call(
158157
arguments
159158
) as [string]);
160-
const filtered = ArrayFilter.call(
159+
const filtered = collectionFilter(
161160
elements,
162161
elm => isUndefined(getNodeOwnerKey(elm)) || isGlobalPatchingSkipped(elm)
163162
);
164-
return createStaticHTMLCollection(filtered);
163+
return createStaticHTMLCollection(filtered as Array<Element>);
165164
},
166165
writable: true,
167166
enumerable: true,
@@ -173,11 +172,11 @@ export default function apply() {
173172
const elements = documentGetElementsByTagNameNS.apply(this, ArraySlice.call(
174173
arguments
175174
) as [string, string]);
176-
const filtered = ArrayFilter.call(
175+
const filtered = collectionFilter(
177176
elements,
178177
elm => isUndefined(getNodeOwnerKey(elm)) || isGlobalPatchingSkipped(elm)
179178
);
180-
return createStaticHTMLCollection(filtered);
179+
return createStaticHTMLCollection(filtered as Array<Element>);
181180
},
182181
writable: true,
183182
enumerable: true,
@@ -195,11 +194,11 @@ export default function apply() {
195194
const elements = getElementsByName.apply(this, ArraySlice.call(arguments) as [
196195
string
197196
]);
198-
const filtered = ArrayFilter.call(
197+
const filtered = collectionFilter(
199198
elements,
200199
elm => isUndefined(getNodeOwnerKey(elm)) || isGlobalPatchingSkipped(elm)
201200
);
202-
return createStaticNodeList(filtered);
201+
return createStaticNodeList(filtered as Array<Element>);
203202
},
204203
writable: true,
205204
enumerable: true,
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright (c) 2018, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: MIT
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6+
*/
7+
import { isUndefined, isTrue, ArrayPush } from './language';
8+
/**
9+
* Writing our own utils to handle NodeList and HTMLCollection. This is to not conflict with
10+
* some legacy third party libraries like prototype.js that patch Array.prototype.
11+
*/
12+
13+
/**
14+
* Custom implementation of filter since using Array.prototype.filter conflicts with other
15+
* legacy libraries like prototype.js
16+
* Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter#Polyfill
17+
*/
18+
export function collectionFilter<T extends Node, K extends Element>(
19+
collection: NodeListOf<T> | HTMLCollectionOf<K>,
20+
fn: (value: T | K, index?: number, collection?: NodeListOf<T> | HTMLCollectionOf<K>) => boolean
21+
): Array<T | K> {
22+
const res: Array<T | K> = [];
23+
const length = collection.length;
24+
for (let i = 0; i < length; i++) {
25+
const curr = collection[i];
26+
if (isTrue(fn(curr, i, collection))) {
27+
ArrayPush.call(res, curr);
28+
}
29+
}
30+
return res;
31+
}
32+
33+
/**
34+
* Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find#Polyfill
35+
*/
36+
export function collectionFind<T extends Node>(
37+
collection: NodeListOf<T>,
38+
fn: (value: T, index?: number, nodelist?: NodeListOf<T>) => boolean
39+
): T | undefined {
40+
const length = collection.length;
41+
for (let i = 0; i < length; i++) {
42+
const curr = collection[i];
43+
if (isTrue(fn(curr, i, collection))) {
44+
return curr;
45+
}
46+
}
47+
return undefined;
48+
}
49+
50+
/**
51+
* Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice#Streamlining_cross-browser_behavior
52+
*/
53+
export function collectionSlice<T extends Node>(
54+
collection: NodeListOf<T>,
55+
begin?: number,
56+
end?: number
57+
): Array<T> {
58+
end = !isUndefined(end) ? end : collection.length;
59+
const cloned: T[] = [];
60+
const len = collection.length;
61+
62+
// Handle negative value for "begin"
63+
let start = !isUndefined(begin) ? begin : 0;
64+
start = start >= 0 ? start : Math.max(0, len + start);
65+
66+
// Handle negative value for "end"
67+
let upTo = !isUndefined(end) ? Math.min(end, len) : len;
68+
if (end < 0) {
69+
upTo = len + end;
70+
}
71+
72+
// Actual expected size of the slice
73+
const size = upTo - start;
74+
75+
if (size > 0) {
76+
for (let i = 0; i < size; i++) {
77+
ArrayPush.call(cloned, collection[start + i]);
78+
}
79+
}
80+
return cloned;
81+
}
82+
83+
/**
84+
* Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf#Polyfill
85+
*/
86+
export function collectionIndexOf<T extends Node>(
87+
collection: NodeListOf<T>,
88+
searchItem: T,
89+
fromIndex: number = 0
90+
): number {
91+
const len = collection.length;
92+
let i = Math.min(fromIndex, len);
93+
if (i < 0) {
94+
i = Math.max(0, len + i);
95+
} else if (i >= len) {
96+
return -1;
97+
}
98+
99+
for (; i !== len; ++i) {
100+
if (collection[i] === searchItem) {
101+
return i;
102+
}
103+
}
104+
return -1;
105+
}

0 commit comments

Comments
 (0)