Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 122 additions & 47 deletions src/lib/polyfill.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,57 @@
// polyfill for Object.hasOwn

(function () {
var oldHasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty);
if (oldHasOwn(Object, "hasOwn")) return;
Object.defineProperty(Object, "hasOwn", {
configurable: true,
enumerable: false,
writable: true,
value: function hasOwn(obj, prop) {
return oldHasOwn(obj, prop);
},
});
Object.hasOwn.prototype = null;
})();

// polyfill for prepend

(function (arr) {
arr.forEach(function (item) {
if (item.hasOwnProperty("prepend")) {
return;
}
if (Object.hasOwn(item, "prepend")) return;
Object.defineProperty(item, "prepend", {
configurable: true,
enumerable: true,
enumerable: false,
writable: true,
value: function prepend() {
var argArr = Array.prototype.slice.call(arguments),
docFrag = document.createDocumentFragment();
var ownerDocument = this.ownerDocument || this;
var docFrag = ownerDocument.createDocumentFragment();

argArr.forEach(function (argItem) {
var node =
var argLength = arguments.length;
for (var i = 0; i < argLength; i++) {
var argItem = arguments[i];
docFrag.appendChild(
argItem instanceof Node
? argItem
: document.createTextNode(String(argItem));
docFrag.appendChild(node);
});
: ownerDocument.createTextNode(argItem),
);
}

this.insertBefore(docFrag, this.firstChild);
},
});
item.prepend.prototype = null;
});
})([Element.prototype, Document.prototype, DocumentFragment.prototype]);

// polyfill for closest

(function (arr) {
arr.forEach(function (item) {
if (item.hasOwnProperty("closest")) {
return;
}
if (Object.hasOwn(item, "closest")) return;
Object.defineProperty(item, "closest", {
configurable: true,
enumerable: true,
enumerable: false,
writable: true,
value: function closest(s) {
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
Expand All @@ -49,68 +64,100 @@
return el;
},
});
item.closest.prototype = null;
});
})([Element.prototype]);

// polyfill for replaceWith

(function (arr) {
arr.forEach(function (item) {
if (item.hasOwnProperty("replaceWith")) {
return;
}
Object.defineProperty(item, "replaceWith", {
var className = item.name;
var proto = item.prototype;
if (Object.hasOwn(proto, "replaceWith")) return;
Object.defineProperty(proto, "replaceWith", {
configurable: true,
enumerable: true,
enumerable: false,
writable: true,
value: function replaceWith() {
var parent = this.parentNode,
i = arguments.length,
currentNode;
var parent = this.parentNode;
if (!parent) return;
if (!i)
// if there are no arguments
parent.removeChild(this);
while (i--) {
// i-- decrements i and returns the value of i before the decrement
currentNode = arguments[i];
if (typeof currentNode !== "object") {
currentNode = this.ownerDocument.createTextNode(currentNode);
} else if (currentNode.parentNode) {
currentNode.parentNode.removeChild(currentNode);
var viableNextSibling = this.nextSibling;
var argLength = arguments.length;
while (viableNextSibling) {
var inArgs = false;
for (var j = 0; j < argLength; j++) {
if (arguments[j] === viableNextSibling) {
inArgs = true;
break;
}
}
// the value of "i" below is after the decrement
if (!i)
// if currentNode is the first argument (currentNode === arguments[0])
parent.replaceChild(currentNode, this);
// if currentNode isn't the first
else parent.insertBefore(this.previousSibling, currentNode);
if (!inArgs) break;
viableNextSibling = viableNextSibling.nextSibling;
}
var ownerDocument = this.ownerDocument || this;
var docFrag = ownerDocument.createDocumentFragment();
var nodes = [];
for (var i = 0; i < argLength; i++) {
var currentNode = arguments[i];
if (!(currentNode instanceof Node)) {
nodes[i] = currentNode + "";
continue;
}
var ancestor = parent;
do {
if (ancestor !== currentNode) continue;
throw new DOMException(
"Failed to execute 'replaceWith' on '" +
className +
"': The new child element contains the parent.",
"HierarchyRequestError",
);
} while ((ancestor = ancestor.parentNode));
Comment on lines +108 to +116
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continue in do-while skips to the condition, not the loop body

Using continue inside a do-while loop is correct but subtle: it jumps directly to the while-condition (ancestor = ancestor.parentNode), which is what advances the walk up the tree. This is easy to misread as a regular continue-to-top-of-loop.

More importantly, there is a logical gap: currentNode === parent will correctly throw (since parent is checked in the first iteration), but currentNode === this will not throw — the walk starts from parent and traverses upward through parent.parentNode etc., never visiting descendants of parent. While this being in the argument list is handled separately via isItselfInFragment, no check prevents the user from passing currentNode that is a sibling or another descendant of parent, yet is itself the node hosting this — the ancestor check exclusively guards against cycle-creating insertions, not against passing this's own children. This is actually spec-correct behaviour, but the continue-based idiom is worth documenting with a comment to avoid future regressions:

// Walk UP from parent; if we reach currentNode, it is
// an ancestor of parent — inserting it would create a cycle.
var ancestor = parent;
do {
    if (ancestor === currentNode) {
        throw new DOMException(
            "Failed to execute 'replaceWith' on '" +
                className +
                "': The new child element contains the parent.",
            "HierarchyRequestError",
        );
    }
} while ((ancestor = ancestor.parentNode));

The continue-based inversion makes the throw condition less obvious than a positive equality check.

nodes[i] = currentNode;
}
var isItselfInFragment;
for (var i = 0; i < argLength; i++) {
var currentNode = nodes[i];
if (typeof currentNode === "string") {
currentNode = ownerDocument.createTextNode(currentNode);
} else if (currentNode === this) {
isItselfInFragment = true;
}
docFrag.appendChild(currentNode);
}
if (!isItselfInFragment) this.remove();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.remove() is unsupported in IE 11 and earlier. This polyfill targets legacy environments that lack native replaceWith, which includes IE 11. The universally-supported alternative is parent.removeChild(this):

if (!isItselfInFragment) parent.removeChild(this);

if (argLength >= 1) {
parent.insertBefore(docFrag, viableNextSibling);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In IE 8 and earlier, insertBefore with null as the second argument throws TypeError, even though the DOM spec treats it as equivalent to appendChild. For full legacy compatibility, branch explicitly when viableNextSibling is null:

if (viableNextSibling) {
    parent.insertBefore(docFrag, viableNextSibling);
} else {
    parent.appendChild(docFrag);
}

}
Comment on lines +130 to 132
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

insertBefore(docFrag, null) compatibility with older browsers

When viableNextSibling is null, parent.insertBefore(docFrag, viableNextSibling) is called. Per the DOM spec, insertBefore(node, null) is equivalent to appendChild(node), and modern browsers implement this correctly. However, some older browsers (notably IE ≤ 8) throw a TypeError when the second argument of insertBefore is null rather than a Node.

For full compatibility with legacy environments, the code should branch explicitly:

Suggested change
if (argLength >= 1) {
parent.insertBefore(docFrag, viableNextSibling);
}
if (argLength >= 1) {
if (viableNextSibling) {
parent.insertBefore(docFrag, viableNextSibling);
} else {
parent.appendChild(docFrag);
}
}

},
});
proto.replaceWith.prototype = null;
});
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
})([Element, CharacterData, DocumentType]);

// polyfill for toggleAttribute

(function (arr) {
arr.forEach(function (item) {
if (item.hasOwnProperty("toggleAttribute")) {
return;
}
if (Object.hasOwn(item, "toggleAttribute")) return;
Object.defineProperty(item, "toggleAttribute", {
configurable: true,
enumerable: true,
enumerable: false,
writable: true,
value: function toggleAttribute() {
var attr = arguments[0];
value: function toggleAttribute(attr, force) {
if (this.hasAttribute(attr)) {
if (force && force !== undefined) return true;
this.removeAttribute(attr);
return false;
} else {
this.setAttribute(attr, arguments[1] || "");
if (!force && force !== undefined) return false;
this.setAttribute(attr, "");
return true;
}
},
});
item.toggleAttribute.prototype = null;
});
})([Element.prototype]);

Expand Down Expand Up @@ -140,3 +187,31 @@
};
}
})();

// polyfill for Promise.withResolvers

if (!Object.hasOwn(Promise, "withResolvers")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object.hasOwn(Promise, "withResolvers") is evaluated unconditionally at module scope. In very old environments where the global Promise is not defined, this throws ReferenceError before the polyfill can run. Add a guard:

if (typeof Promise !== "undefined" && !Object.hasOwn(Promise, "withResolvers")) {

Object.defineProperty(Promise, "withResolvers", {
configurable: true,
enumerable: false,
writable: true,
value: function withResolvers() {
var resolve, reject;
var promise = new this(function (_resolve, _reject) {
resolve = _resolve;
reject = _reject;
});
if (typeof resolve !== "function" || typeof reject !== "function") {
throw new TypeError(
"Promise resolve or reject function is not callable",
);
}
return {
promise: promise,
resolve: resolve,
reject: reject,
};
},
});
Promise.withResolvers.prototype = null;
}
Comment on lines +193 to +217
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing guard for Promise in very old environments

Line 191 evaluates Object.hasOwn(Promise, "withResolvers") without first checking whether Promise itself is defined. In very old environments where Promise does not exist as a global, this will throw a ReferenceError: Promise is not defined before the polyfill can even install.

Add a typeof Promise !== "undefined" guard:

Suggested change
if (!Object.hasOwn(Promise, "withResolvers")) {
Object.defineProperty(Promise, "withResolvers", {
configurable: true,
enumerable: false,
writable: true,
value: function withResolvers() {
var resolve, reject;
var promise = new this(function (_resolve, _reject) {
resolve = _resolve;
reject = _reject;
});
if (typeof resolve !== "function" || typeof reject !== "function") {
throw new TypeError(
"Promise resolve or reject function is not callable",
);
}
return {
promise: promise,
resolve: resolve,
reject: reject,
};
},
});
Promise.withResolvers.prototype = null;
}
if (typeof Promise !== "undefined" && !Object.hasOwn(Promise, "withResolvers")) {
Object.defineProperty(Promise, "withResolvers", {
configurable: true,
enumerable: false,
writable: true,
value: function withResolvers() {
var resolve, reject;
var promise = new this(function (_resolve, _reject) {
resolve = _resolve;
reject = _reject;
});
if (typeof resolve !== "function" || typeof reject !== "function") {
throw new TypeError(
"Promise resolve or reject function is not callable",
);
}
return {
promise: promise,
resolve: resolve,
reject: reject,
};
},
});
Promise.withResolvers.prototype = null;
}