Skip to content

Commit 1983d3c

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 1983d3c

File tree

1 file changed

+112
-46
lines changed

1 file changed

+112
-46
lines changed

src/lib/polyfill.js

Lines changed: 112 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;
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,91 @@
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+
}
94+
}
95+
if (!inArgs) break;
96+
viableNextSibling = viableNextSibling.nextSibling;
97+
}
98+
var ownerDocument = this.ownerDocument;
99+
this.remove();
100+
var docFrag = ownerDocument.createDocumentFragment();
101+
try {
102+
for (var i = 0; i < argLength; i++) {
103+
var currentNode = arguments[i];
104+
if (currentNode instanceof Node) {
105+
var ancestor = parent;
106+
do {
107+
if (ancestor !== currentNode) continue;
108+
throw new DOMException(
109+
"Failed to execute 'replaceWith' on '" +
110+
className +
111+
"': The new child element contains the parent.",
112+
);
113+
} while ((ancestor = ancestor.parentNode));
114+
} else {
115+
currentNode = ownerDocument.createTextNode(currentNode);
116+
}
117+
docFrag.appendChild(currentNode);
118+
}
119+
} finally {
120+
if (argLength >= 1) {
121+
parent.insertBefore(docFrag, viableNextSibling);
81122
}
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);
88123
}
89124
},
90125
});
126+
item.replaceWith.prototype = null;
91127
});
92-
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
128+
})([Element, CharacterData, DocumentType]);
93129

94130
// polyfill for toggleAttribute
95131

96132
(function (arr) {
97133
arr.forEach(function (item) {
98-
if (item.hasOwnProperty("toggleAttribute")) {
99-
return;
100-
}
134+
if (Object.hasOwn(item, "toggleAttribute")) return;
101135
Object.defineProperty(item, "toggleAttribute", {
102136
configurable: true,
103-
enumerable: true,
137+
enumerable: false,
104138
writable: true,
105-
value: function toggleAttribute() {
106-
var attr = arguments[0];
139+
value: function toggleAttribute(attr, force) {
107140
if (this.hasAttribute(attr)) {
141+
if (force || force === undefined) return true;
108142
this.removeAttribute(attr);
143+
return false;
109144
} else {
110-
this.setAttribute(attr, arguments[1] || "");
145+
if (!force && force !== undefined) return false;
146+
this.setAttribute(attr, "");
147+
return true;
111148
}
112149
},
113150
});
151+
item.toggleAttribute.prototype = null;
114152
});
115153
})([Element.prototype]);
116154

@@ -140,3 +178,31 @@
140178
};
141179
}
142180
})();
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+
}

0 commit comments

Comments
 (0)