Skip to content

Commit f7bc166

Browse files
committed
refactor: Modernise polyfills with Object.hasOwn and ES2024 features
- Implement `Object.hasOwn` polyfill and use it for internal property checks. - Add [`Promise.withResolvers`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers) (ES2024) to the standard library. - Optimise `prepend`, `replaceWith`, and `toggleAttribute` for performance and spec compliance. - Set polyfilled methods to non-enumerable to match native behavior. - Introduce robust `createTextNode` and `createDocumentFragment` utility helpers. - Improve performance by replacing `Array.slice` on arguments with manual loops. (AI generated commit message)
1 parent d8bb199 commit f7bc166

File tree

1 file changed

+129
-47
lines changed

1 file changed

+129
-47
lines changed

src/lib/polyfill.js

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

422
(function (arr) {
523
arr.forEach(function (item) {
6-
if (item.hasOwnProperty("prepend")) {
7-
return;
8-
}
24+
if (Object.hasOwn(item, "prepend")) return;
925
Object.defineProperty(item, "prepend", {
1026
configurable: true,
11-
enumerable: true,
27+
enumerable: false,
1228
writable: true,
1329
value: function prepend() {
14-
var argArr = Array.prototype.slice.call(arguments),
15-
docFrag = document.createDocumentFragment();
30+
var docFrag = createDocumentFragment();
1631

17-
argArr.forEach(function (argItem) {
18-
var node =
19-
argItem instanceof Node
20-
? argItem
21-
: document.createTextNode(String(argItem));
22-
docFrag.appendChild(node);
23-
});
32+
var argLength = arguments.length;
33+
for (var i = 0; i < argLength; i++) {
34+
var argItem = arguments[i];
35+
docFrag.appendChild(
36+
argItem instanceof Node ? argItem : createTextNode(argItem + ""),
37+
);
38+
}
2439

2540
this.insertBefore(docFrag, this.firstChild);
2641
},
2742
});
43+
item.prepend.prototype = null;
2844
});
2945
})([Element.prototype, Document.prototype, DocumentFragment.prototype]);
3046

3147
// polyfill for closest
3248

3349
(function (arr) {
3450
arr.forEach(function (item) {
35-
if (item.hasOwnProperty("closest")) {
36-
return;
37-
}
51+
if (Object.hasOwn(item, "closest")) return;
3852
Object.defineProperty(item, "closest", {
3953
configurable: true,
40-
enumerable: true,
54+
enumerable: false,
4155
writable: true,
4256
value: function closest(s) {
43-
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
57+
var matches = (this.document || this.ownerDocument).querySelectorAll(
58+
s,
59+
),
4460
i,
4561
el = this;
4662
do {
@@ -50,68 +66,88 @@
5066
return el;
5167
},
5268
});
69+
item.closest.prototype = null;
5370
});
5471
})([Element.prototype]);
5572

5673
// polyfill for replaceWith
5774

5875
(function (arr) {
5976
arr.forEach(function (item) {
60-
if (item.hasOwnProperty("replaceWith")) {
61-
return;
62-
}
77+
var className = item.name;
78+
item = item.prototype;
79+
if (Object.hasOwn(item, "replaceWith")) return;
6380
Object.defineProperty(item, "replaceWith", {
6481
configurable: true,
65-
enumerable: true,
82+
enumerable: false,
6683
writable: true,
6784
value: function replaceWith() {
68-
var parent = this.parentNode,
69-
i = arguments.length,
70-
currentNode;
85+
var parent = this.parentNode;
7186
if (!parent) return;
72-
if (!i)
73-
// if there are no arguments
74-
parent.removeChild(this);
75-
while (i--) {
76-
// i-- decrements i and returns the value of i before the decrement
77-
currentNode = arguments[i];
78-
if (typeof currentNode !== "object") {
79-
currentNode = this.ownerDocument.createTextNode(currentNode);
80-
} else if (currentNode.parentNode) {
81-
currentNode.parentNode.removeChild(currentNode);
87+
var viableNextSibling = this.nextSibling;
88+
var argLength = arguments.length;
89+
while (viableNextSibling) {
90+
var inArgs = false;
91+
for (var j = 0; j < argLength; j++) {
92+
if (arguments[j] === viableNextSibling) {
93+
inArgs = true;
94+
break;
95+
}
96+
}
97+
if (!inArgs) break;
98+
viableNextSibling = viableNextSibling.nextSibling;
99+
}
100+
this.remove();
101+
var docFrag = document.createDocumentFragment();
102+
try {
103+
for (var i = 0; i < argLength; i++) {
104+
var currentNode = arguments[i];
105+
if (currentNode instanceof Node) {
106+
var parent2 = currentNode;
107+
do {
108+
if (parent2 !== parent) continue;
109+
throw new DOMException(
110+
"Failed to execute 'replaceWith' on '" + className +
111+
"': The new child element contains the parent."
112+
);
113+
} while (parent2 = parent2.parentNode);
114+
} else {
115+
currentNode = this.ownerDocument.createTextNode(currentNode);
116+
}
117+
docFrag.appendChild(currentNode);
118+
}
119+
} finally {
120+
if (argLength >= 1) {
121+
parent.insertBefore(docFrag, viableNextSibling);
82122
}
83-
// the value of "i" below is after the decrement
84-
if (!i)
85-
// if currentNode is the first argument (currentNode === arguments[0])
86-
parent.replaceChild(currentNode, this);
87-
// if currentNode isn't the first
88-
else parent.insertBefore(this.previousSibling, currentNode);
89123
}
90124
},
91125
});
126+
item.replaceWith.prototype = null;
92127
});
93-
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
128+
})([Element, CharacterData, DocumentType]);
94129

95130
// polyfill for toggleAttribute
96131

97132
(function (arr) {
98133
arr.forEach(function (item) {
99-
if (item.hasOwnProperty("toggleAttribute")) {
100-
return;
101-
}
134+
if (Object.hasOwn(item, "toggleAttribute")) return;
102135
Object.defineProperty(item, "toggleAttribute", {
103136
configurable: true,
104-
enumerable: true,
137+
enumerable: false,
105138
writable: true,
106139
value: function toggleAttribute() {
107140
var attr = arguments[0];
108141
if (this.hasAttribute(attr)) {
109142
this.removeAttribute(attr);
143+
return false;
110144
} else {
111-
this.setAttribute(attr, arguments[1] || "");
145+
this.setAttribute(attr, arguments.length >= 2 ? arguments[1] : "");
146+
return true;
112147
}
113148
},
114149
});
150+
item.toggleAttribute.prototype = null;
115151
});
116152
})([Element.prototype]);
117153

@@ -141,4 +177,50 @@
141177
};
142178
}
143179
})();
180+
181+
// polyfill for Promise.withResolvers
182+
183+
if (!Object.hasOwn(Promise, "withResolvers")) {
184+
Object.defineProperty(Promise, "withResolvers", {
185+
configurable: true,
186+
enumerable: false,
187+
writable: true,
188+
value: function withResolvers() {
189+
var resolve, reject;
190+
var promise = new this(function (_resolve, _reject) {
191+
resolve = _resolve;
192+
reject = _reject;
193+
});
194+
if (typeof resolve !== "function" || typeof reject !== "function") {
195+
throw new TypeError(
196+
"Promise resolve or reject function is not callable",
197+
);
198+
}
199+
return {
200+
promise: promise,
201+
resolve: resolve,
202+
reject: reject,
203+
};
204+
},
205+
});
206+
Promise.withResolvers.prototype = null;
207+
}
208+
209+
// utils
210+
211+
function createTextNode(text, doc) {
212+
if (doc === undefined) doc = document;
213+
if (doc !== document)
214+
try {
215+
return new Text(text);
216+
} catch (_) {}
217+
return doc.createTextNode(text);
218+
}
219+
function createDocumentFragment() {
220+
try {
221+
return new DocumentFragment();
222+
} catch (_) {
223+
return document.createDocumentFragment();
224+
}
225+
}
144226
})();

0 commit comments

Comments
 (0)