Skip to content

Commit cf37d3f

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 cf37d3f

File tree

1 file changed

+119
-46
lines changed

1 file changed

+119
-46
lines changed

src/lib/polyfill.js

Lines changed: 119 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,98 @@
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+
for (var i = 0; i < argLength; i++) {
101+
var currentNode = arguments[i];
102+
if (!(currentNode instanceof Node)) {
103+
arguments[i] = currentNode + "";
104+
continue;
105+
}
106+
var ancestor = parent;
107+
do {
108+
if (ancestor !== currentNode) continue;
109+
throw new DOMException(
110+
"Failed to execute 'replaceWith' on '" +
111+
className +
112+
"': The new child element contains the parent.",
113+
"HierarchyRequestError",
114+
);
115+
} while ((ancestor = ancestor.parentNode));
116+
}
117+
var isItselfInFragment;
118+
for (var i = 0; i < argLength; i++) {
119+
var currentNode = arguments[i];
120+
if (typeof currentNode === "string") {
121+
currentNode = ownerDocument.createTextNode(currentNode);
122+
} else if (currentNode === this) {
123+
isItselfInFragment = true;
124+
}
125+
docFrag.appendChild(currentNode);
126+
}
127+
if (!isItselfInFragment) this.remove();
128+
if (argLength >= 1) {
129+
parent.insertBefore(docFrag, viableNextSibling);
88130
}
89131
},
90132
});
133+
item.replaceWith.prototype = null;
91134
});
92-
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
135+
})([Element, CharacterData, DocumentType]);
93136

94137
// polyfill for toggleAttribute
95138

96139
(function (arr) {
97140
arr.forEach(function (item) {
98-
if (item.hasOwnProperty("toggleAttribute")) {
99-
return;
100-
}
141+
if (Object.hasOwn(item, "toggleAttribute")) return;
101142
Object.defineProperty(item, "toggleAttribute", {
102143
configurable: true,
103-
enumerable: true,
144+
enumerable: false,
104145
writable: true,
105-
value: function toggleAttribute() {
106-
var attr = arguments[0];
146+
value: function toggleAttribute(attr, force) {
107147
if (this.hasAttribute(attr)) {
148+
if (force && force !== undefined) return true;
108149
this.removeAttribute(attr);
150+
return false;
109151
} else {
110-
this.setAttribute(attr, arguments[1] || "");
152+
if (!force && force !== undefined) return false;
153+
this.setAttribute(attr, "");
154+
return true;
111155
}
112156
},
113157
});
158+
item.toggleAttribute.prototype = null;
114159
});
115160
})([Element.prototype]);
116161

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

0 commit comments

Comments
 (0)