Skip to content

Commit 006e5cb

Browse files
committed
refactor: Modernise polyfills and add Promise.withResolvers
- Implement spec-compliant `replaceWith` with hierarchy checks. - Add `Object.hasOwn` and [`Promise.withResolvers`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers) polyfills. - Update `toggleAttribute` to support the `force` parameter. - Optimise `prepend` performance by using standard loops. - Ensure all polyfills are non-enumerable and have null prototypes. (AI generated commit message)
1 parent 0470cf7 commit 006e5cb

File tree

1 file changed

+122
-47
lines changed

1 file changed

+122
-47
lines changed

src/lib/polyfill.js

Lines changed: 122 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,57 @@
1+
// polyfill for Object.hasOwn
2+
3+
(function () {
4+
var oldHasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty);
5+
if (oldHasOwn(Object, "hasOwn")) return;
6+
Object.defineProperty(Object, "hasOwn", {
7+
configurable: true,
8+
enumerable: false,
9+
writable: true,
10+
value: function hasOwn(obj, prop) {
11+
return oldHasOwn(obj, prop);
12+
},
13+
});
14+
Object.hasOwn.prototype = null;
15+
})();
16+
117
// polyfill for prepend
218

319
(function (arr) {
420
arr.forEach(function (item) {
5-
if (item.hasOwnProperty("prepend")) {
6-
return;
7-
}
21+
if (Object.hasOwn(item, "prepend")) return;
822
Object.defineProperty(item, "prepend", {
923
configurable: true,
10-
enumerable: true,
24+
enumerable: false,
1125
writable: true,
1226
value: function prepend() {
13-
var argArr = Array.prototype.slice.call(arguments),
14-
docFrag = document.createDocumentFragment();
27+
var ownerDocument = this.ownerDocument || this;
28+
var docFrag = ownerDocument.createDocumentFragment();
1529

16-
argArr.forEach(function (argItem) {
17-
var node =
30+
var argLength = arguments.length;
31+
for (var i = 0; i < argLength; i++) {
32+
var argItem = arguments[i];
33+
docFrag.appendChild(
1834
argItem instanceof Node
1935
? argItem
20-
: document.createTextNode(String(argItem));
21-
docFrag.appendChild(node);
22-
});
36+
: ownerDocument.createTextNode(argItem),
37+
);
38+
}
2339

2440
this.insertBefore(docFrag, this.firstChild);
2541
},
2642
});
43+
item.prepend.prototype = null;
2744
});
2845
})([Element.prototype, Document.prototype, DocumentFragment.prototype]);
2946

3047
// polyfill for closest
3148

3249
(function (arr) {
3350
arr.forEach(function (item) {
34-
if (item.hasOwnProperty("closest")) {
35-
return;
36-
}
51+
if (Object.hasOwn(item, "closest")) return;
3752
Object.defineProperty(item, "closest", {
3853
configurable: true,
39-
enumerable: true,
54+
enumerable: false,
4055
writable: true,
4156
value: function closest(s) {
4257
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
@@ -49,68 +64,100 @@
4964
return el;
5065
},
5166
});
67+
item.closest.prototype = null;
5268
});
5369
})([Element.prototype]);
5470

5571
// polyfill for replaceWith
5672

5773
(function (arr) {
5874
arr.forEach(function (item) {
59-
if (item.hasOwnProperty("replaceWith")) {
60-
return;
61-
}
62-
Object.defineProperty(item, "replaceWith", {
75+
var className = item.name;
76+
var proto = item.prototype;
77+
if (Object.hasOwn(proto, "replaceWith")) return;
78+
Object.defineProperty(proto, "replaceWith", {
6379
configurable: true,
64-
enumerable: true,
80+
enumerable: false,
6581
writable: true,
6682
value: function replaceWith() {
67-
var parent = this.parentNode,
68-
i = arguments.length,
69-
currentNode;
83+
var parent = this.parentNode;
7084
if (!parent) return;
71-
if (!i)
72-
// if there are no arguments
73-
parent.removeChild(this);
74-
while (i--) {
75-
// i-- decrements i and returns the value of i before the decrement
76-
currentNode = arguments[i];
77-
if (typeof currentNode !== "object") {
78-
currentNode = this.ownerDocument.createTextNode(currentNode);
79-
} else if (currentNode.parentNode) {
80-
currentNode.parentNode.removeChild(currentNode);
85+
var viableNextSibling = this.nextSibling;
86+
var argLength = arguments.length;
87+
while (viableNextSibling) {
88+
var inArgs = false;
89+
for (var j = 0; j < argLength; j++) {
90+
if (arguments[j] === viableNextSibling) {
91+
inArgs = true;
92+
break;
93+
}
8194
}
82-
// the value of "i" below is after the decrement
83-
if (!i)
84-
// if currentNode is the first argument (currentNode === arguments[0])
85-
parent.replaceChild(currentNode, this);
86-
// if currentNode isn't the first
87-
else parent.insertBefore(this.previousSibling, currentNode);
95+
if (!inArgs) break;
96+
viableNextSibling = viableNextSibling.nextSibling;
97+
}
98+
var ownerDocument = this.ownerDocument || this;
99+
var docFrag = ownerDocument.createDocumentFragment();
100+
var nodes = [];
101+
for (var i = 0; i < argLength; i++) {
102+
var currentNode = arguments[i];
103+
if (!(currentNode instanceof Node)) {
104+
nodes[i] = currentNode + "";
105+
continue;
106+
}
107+
var ancestor = parent;
108+
do {
109+
if (ancestor !== currentNode) continue;
110+
throw new DOMException(
111+
"Failed to execute 'replaceWith' on '" +
112+
className +
113+
"': The new child element contains the parent.",
114+
"HierarchyRequestError",
115+
);
116+
} while ((ancestor = ancestor.parentNode));
117+
nodes[i] = currentNode;
118+
}
119+
var isItselfInFragment;
120+
for (var i = 0; i < argLength; i++) {
121+
var currentNode = nodes[i];
122+
if (typeof currentNode === "string") {
123+
currentNode = ownerDocument.createTextNode(currentNode);
124+
} else if (currentNode === this) {
125+
isItselfInFragment = true;
126+
}
127+
docFrag.appendChild(currentNode);
128+
}
129+
if (!isItselfInFragment) this.remove();
130+
if (argLength >= 1) {
131+
parent.insertBefore(docFrag, viableNextSibling);
88132
}
89133
},
90134
});
135+
proto.replaceWith.prototype = null;
91136
});
92-
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
137+
})([Element, CharacterData, DocumentType]);
93138

94139
// polyfill for toggleAttribute
95140

96141
(function (arr) {
97142
arr.forEach(function (item) {
98-
if (item.hasOwnProperty("toggleAttribute")) {
99-
return;
100-
}
143+
if (Object.hasOwn(item, "toggleAttribute")) return;
101144
Object.defineProperty(item, "toggleAttribute", {
102145
configurable: true,
103-
enumerable: true,
146+
enumerable: false,
104147
writable: true,
105-
value: function toggleAttribute() {
106-
var attr = arguments[0];
148+
value: function toggleAttribute(attr, force) {
107149
if (this.hasAttribute(attr)) {
150+
if (force && force !== undefined) return true;
108151
this.removeAttribute(attr);
152+
return false;
109153
} else {
110-
this.setAttribute(attr, arguments[1] || "");
154+
if (!force && force !== undefined) return false;
155+
this.setAttribute(attr, "");
156+
return true;
111157
}
112158
},
113159
});
160+
item.toggleAttribute.prototype = null;
114161
});
115162
})([Element.prototype]);
116163

@@ -140,3 +187,31 @@
140187
};
141188
}
142189
})();
190+
191+
// polyfill for Promise.withResolvers
192+
193+
if (!Object.hasOwn(Promise, "withResolvers")) {
194+
Object.defineProperty(Promise, "withResolvers", {
195+
configurable: true,
196+
enumerable: false,
197+
writable: true,
198+
value: function withResolvers() {
199+
var resolve, reject;
200+
var promise = new this(function (_resolve, _reject) {
201+
resolve = _resolve;
202+
reject = _reject;
203+
});
204+
if (typeof resolve !== "function" || typeof reject !== "function") {
205+
throw new TypeError(
206+
"Promise resolve or reject function is not callable",
207+
);
208+
}
209+
return {
210+
promise: promise,
211+
resolve: resolve,
212+
reject: reject,
213+
};
214+
},
215+
});
216+
Promise.withResolvers.prototype = null;
217+
}

0 commit comments

Comments
 (0)