Skip to content

Commit 0e7c792

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.
1 parent 0470cf7 commit 0e7c792

File tree

1 file changed

+114
-47
lines changed

1 file changed

+114
-47
lines changed

src/lib/polyfill.js

Lines changed: 114 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,56 @@
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 docFrag = document.createDocumentFragment();
1528

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

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

3046
// polyfill for closest
3147

3248
(function (arr) {
3349
arr.forEach(function (item) {
34-
if (item.hasOwnProperty("closest")) {
35-
return;
36-
}
50+
if (Object.hasOwn(item, "closest")) return;
3751
Object.defineProperty(item, "closest", {
3852
configurable: true,
39-
enumerable: true,
53+
enumerable: false,
4054
writable: true,
4155
value: function closest(s) {
4256
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
@@ -49,68 +63,93 @@
4963
return el;
5064
},
5165
});
66+
item.closest.prototype = null;
5267
});
5368
})([Element.prototype]);
5469

5570
// polyfill for replaceWith
5671

5772
(function (arr) {
5873
arr.forEach(function (item) {
59-
if (item.hasOwnProperty("replaceWith")) {
60-
return;
61-
}
74+
var className = item.name;
75+
item = item.prototype;
76+
if (Object.hasOwn(item, "replaceWith")) return;
6277
Object.defineProperty(item, "replaceWith", {
6378
configurable: true,
64-
enumerable: true,
79+
enumerable: false,
6580
writable: true,
6681
value: function replaceWith() {
67-
var parent = this.parentNode,
68-
i = arguments.length,
69-
currentNode;
82+
var parent = this.parentNode;
7083
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);
84+
var viableNextSibling = this.nextSibling;
85+
var argLength = arguments.length;
86+
while (viableNextSibling) {
87+
var inArgs = false;
88+
for (var j = 0; j < argLength; j++) {
89+
if (arguments[j] === viableNextSibling) {
90+
inArgs = true;
91+
break;
92+
}
93+
}
94+
if (!inArgs) break;
95+
viableNextSibling = viableNextSibling.nextSibling;
96+
}
97+
var ownerDocument = this.ownerDocument;
98+
this.remove();
99+
var docFrag = document.createDocumentFragment();
100+
try {
101+
for (var i = 0; i < argLength; i++) {
102+
var currentNode = arguments[i];
103+
if (currentNode instanceof Node) {
104+
var ancestor = parent;
105+
do {
106+
if (ancestor !== currentNode) continue;
107+
throw new DOMException(
108+
"Failed to execute 'replaceWith' on '" +
109+
className +
110+
"': The new child element contains the parent.",
111+
);
112+
} while ((ancestor = ancestor.parentNode));
113+
} else {
114+
currentNode = ownerDocument.createTextNode(currentNode);
115+
}
116+
docFrag.appendChild(currentNode);
117+
}
118+
} finally {
119+
if (argLength >= 1) {
120+
parent.insertBefore(docFrag, viableNextSibling);
81121
}
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);
88122
}
89123
},
90124
});
125+
item.replaceWith.prototype = null;
91126
});
92-
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
127+
})([Element, CharacterData, DocumentType]);
93128

94129
// polyfill for toggleAttribute
95130

96131
(function (arr) {
97132
arr.forEach(function (item) {
98-
if (item.hasOwnProperty("toggleAttribute")) {
99-
return;
100-
}
133+
if (Object.hasOwn(item, "toggleAttribute")) return;
101134
Object.defineProperty(item, "toggleAttribute", {
102135
configurable: true,
103-
enumerable: true,
136+
enumerable: false,
104137
writable: true,
105-
value: function toggleAttribute() {
106-
var attr = arguments[0];
138+
value: function toggleAttribute(attr, force) {
107139
if (this.hasAttribute(attr)) {
108-
this.removeAttribute(attr);
140+
if (!force && force !== undefined) {
141+
this.removeAttribute(attr);
142+
}
143+
return false;
109144
} else {
110-
this.setAttribute(attr, arguments[1] || "");
145+
if (force || force === undefined) {
146+
this.setAttribute(attr, "");
147+
}
148+
return true;
111149
}
112150
},
113151
});
152+
item.toggleAttribute.prototype = null;
114153
});
115154
})([Element.prototype]);
116155

@@ -140,3 +179,31 @@
140179
};
141180
}
142181
})();
182+
183+
// polyfill for Promise.withResolvers
184+
185+
if (!Object.hasOwn(Promise, "withResolvers")) {
186+
Object.defineProperty(Promise, "withResolvers", {
187+
configurable: true,
188+
enumerable: false,
189+
writable: true,
190+
value: function withResolvers() {
191+
var resolve, reject;
192+
var promise = new this(function (_resolve, _reject) {
193+
resolve = _resolve;
194+
reject = _reject;
195+
});
196+
if (typeof resolve !== "function" || typeof reject !== "function") {
197+
throw new TypeError(
198+
"Promise resolve or reject function is not callable",
199+
);
200+
}
201+
return {
202+
promise: promise,
203+
resolve: resolve,
204+
reject: reject,
205+
};
206+
},
207+
});
208+
Promise.withResolvers.prototype = null;
209+
}

0 commit comments

Comments
 (0)