Skip to content

Commit 340b65a

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 340b65a

File tree

1 file changed

+130
-47
lines changed

1 file changed

+130
-47
lines changed

src/lib/polyfill.js

Lines changed: 130 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 : document.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,89 @@
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 '" +
111+
className +
112+
"': The new child element contains the parent.",
113+
);
114+
} while ((parent2 = parent2.parentNode));
115+
} else {
116+
currentNode = this.ownerDocument.createTextNode(currentNode);
117+
}
118+
docFrag.appendChild(currentNode);
119+
}
120+
} finally {
121+
if (argLength >= 1) {
122+
parent.insertBefore(docFrag, viableNextSibling);
82123
}
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);
89124
}
90125
},
91126
});
127+
item.replaceWith.prototype = null;
92128
});
93-
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
129+
})([Element, CharacterData, DocumentType]);
94130

95131
// polyfill for toggleAttribute
96132

97133
(function (arr) {
98134
arr.forEach(function (item) {
99-
if (item.hasOwnProperty("toggleAttribute")) {
100-
return;
101-
}
135+
if (Object.hasOwn(item, "toggleAttribute")) return;
102136
Object.defineProperty(item, "toggleAttribute", {
103137
configurable: true,
104-
enumerable: true,
138+
enumerable: false,
105139
writable: true,
106140
value: function toggleAttribute() {
107141
var attr = arguments[0];
108142
if (this.hasAttribute(attr)) {
109143
this.removeAttribute(attr);
144+
return false;
110145
} else {
111-
this.setAttribute(attr, arguments[1] || "");
146+
this.setAttribute(attr, arguments.length >= 2 ? arguments[1] : "");
147+
return true;
112148
}
113149
},
114150
});
151+
item.toggleAttribute.prototype = null;
115152
});
116153
})([Element.prototype]);
117154

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

0 commit comments

Comments
 (0)