Skip to content

Commit 1b03ccc

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 1b03ccc

File tree

1 file changed

+116
-46
lines changed

1 file changed

+116
-46
lines changed

src/lib/polyfill.js

Lines changed: 116 additions & 46 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,95 @@
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-
}
75+
var className = item.name;
76+
item = item.prototype;
77+
if (Object.hasOwn(item, "replaceWith")) return;
6278
Object.defineProperty(item, "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 error;
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) break;
107+
} while ((ancestor = ancestor.parentNode));
108+
if (ancestor === currentNode) {
109+
error = true;
110+
continue;
111+
}
112+
} else {
113+
currentNode = ownerDocument.createTextNode(currentNode);
114+
}
115+
docFrag.appendChild(currentNode);
116+
}
117+
if (error) {
118+
throw new DOMException(
119+
"Failed to execute 'replaceWith' on '" +
120+
className +
121+
"': The new child element contains the parent.",
122+
);
123+
}
124+
this.remove();
125+
if (argLength >= 1) {
126+
parent.insertBefore(docFrag, viableNextSibling);
88127
}
89128
},
90129
});
130+
item.replaceWith.prototype = null;
91131
});
92-
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
132+
})([Element, CharacterData, DocumentType]);
93133

94134
// polyfill for toggleAttribute
95135

96136
(function (arr) {
97137
arr.forEach(function (item) {
98-
if (item.hasOwnProperty("toggleAttribute")) {
99-
return;
100-
}
138+
if (Object.hasOwn(item, "toggleAttribute")) return;
101139
Object.defineProperty(item, "toggleAttribute", {
102140
configurable: true,
103-
enumerable: true,
141+
enumerable: false,
104142
writable: true,
105-
value: function toggleAttribute() {
106-
var attr = arguments[0];
143+
value: function toggleAttribute(attr, force) {
107144
if (this.hasAttribute(attr)) {
145+
if (force && force !== undefined) return true;
108146
this.removeAttribute(attr);
147+
return false;
109148
} else {
110-
this.setAttribute(attr, arguments[1] || "");
149+
if (!force && force !== undefined) return false;
150+
this.setAttribute(attr, "");
151+
return true;
111152
}
112153
},
113154
});
155+
item.toggleAttribute.prototype = null;
114156
});
115157
})([Element.prototype]);
116158

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

0 commit comments

Comments
 (0)