mirror of
https://github.com/hariroshan/elm-native-library.git
synced 2025-01-05 19:36:22 +03:00
added custom element functions
This commit is contained in:
parent
ff471954dc
commit
803e46f78a
4
.gitignore
vendored
4
.gitignore
vendored
@ -29,3 +29,7 @@ typings/
|
||||
|
||||
# Rescript
|
||||
lib
|
||||
*.bs.js
|
||||
|
||||
# Elm
|
||||
elm-stuff
|
||||
|
@ -13,5 +13,6 @@
|
||||
}
|
||||
],
|
||||
"suffix": ".bs.js",
|
||||
"bsc-flags": [],
|
||||
"bs-dependencies": []
|
||||
}
|
||||
|
@ -13,7 +13,9 @@
|
||||
"dependencies": {
|
||||
"@nativescript/core": "~8.4.0",
|
||||
"@nativescript/theme": "~3.0.2",
|
||||
"elm": "^0.19.1-5"
|
||||
"elm": "^0.19.1-5",
|
||||
"happy-dom": "^8.1.2",
|
||||
"vm-shim": "^0.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nativescript/types": "~8.4.0",
|
||||
|
149
src/CustomElement.res
Normal file
149
src/CustomElement.res
Normal file
@ -0,0 +1,149 @@
|
||||
type constructor = {
|
||||
observedAttributes: array<string>,
|
||||
name: string,
|
||||
}
|
||||
|
||||
type event
|
||||
|
||||
type nativeObject = {
|
||||
on: (event, event => unit) => unit,
|
||||
off: (event, event => unit) => unit,
|
||||
}
|
||||
|
||||
type super = {
|
||||
addEventListener: (event, event => unit) => unit,
|
||||
removeEventListener: (event, event => unit) => unit,
|
||||
}
|
||||
|
||||
type extendedThis = {
|
||||
getAttributes: unit => Js.Dict.t<string>,
|
||||
getProps: unit => Js.Dict.t<string>,
|
||||
init: unit => unit,
|
||||
update: (string, string) => unit,
|
||||
dispose: unit => unit,
|
||||
isConnected: bool,
|
||||
render: Js.Nullable.t<unit => unit>,
|
||||
object: nativeObject,
|
||||
}
|
||||
|
||||
type this = {
|
||||
getAttribute: string => Js.Nullable.t<string>,
|
||||
style: string,
|
||||
super: super,
|
||||
constructor: constructor,
|
||||
}
|
||||
|
||||
%%private(
|
||||
@scope("prototype") @set
|
||||
external assignGetPropsInProto: (Obj.t, unit => Js.Dict.t<string>) => unit = "getProps"
|
||||
|
||||
@scope("prototype") @set
|
||||
external assignGetAttributes: (Obj.t, unit => Js.Dict.t<string>) => unit = "getAttributes"
|
||||
|
||||
@scope("prototype") @set
|
||||
external assignConstructor: (Obj.t, unit => unit) => unit = "constructor"
|
||||
|
||||
@scope("prototype") @set
|
||||
external assignAttributeChangedCallback: (Obj.t, (string, unit, string) => unit) => unit =
|
||||
"attributeChangedCallback"
|
||||
|
||||
@scope("prototype") @set
|
||||
external assignConnectedCallback: (Obj.t, unit => unit) => unit = "connectedCallback"
|
||||
|
||||
@scope("prototype") @set
|
||||
external assignDisconnectedCallback: (Obj.t, unit => unit) => unit = "disconnectedCallback"
|
||||
|
||||
@scope("prototype") @set
|
||||
external assignAddEventListener: (Obj.t, (event, event => unit) => unit) => unit =
|
||||
"addEventListener"
|
||||
|
||||
@scope("prototype") @set
|
||||
external assignRemoveEventListener: (Obj.t, (event, event => unit) => unit) => unit =
|
||||
"removeEventListener"
|
||||
|
||||
@set
|
||||
external assignObservedAttributes: (Obj.t, unit => array<string>) => unit = "observedAttributes"
|
||||
|
||||
@val external this: this = "this"
|
||||
|
||||
external toExtendedThis: this => extendedThis = "%identity"
|
||||
|
||||
@val external thisSuper: unit => unit = "this.super"
|
||||
|
||||
@set
|
||||
external setThisProps: (this, Js.Dict.t<string>) => unit = "props"
|
||||
)
|
||||
|
||||
let withAttrs = class => {
|
||||
class->assignObservedAttributes(_ => [])
|
||||
class->assignGetAttributes(_ => {
|
||||
this.constructor.observedAttributes->Belt.Array.reduce(Js.Dict.empty(), (acc, attrName) => {
|
||||
let attrValue = switch this.getAttribute(attrName)->Js.Nullable.toOption {
|
||||
| Some(a) => Some(a)
|
||||
| None => attrName == "style" ? Some(this.style) : None
|
||||
}
|
||||
let attrNameMap = attrName == "class" ? "className" : attrName
|
||||
|
||||
attrValue
|
||||
->Belt.Option.map(
|
||||
value => {
|
||||
acc->Js.Dict.set(attrNameMap, value)
|
||||
acc
|
||||
},
|
||||
)
|
||||
->Belt.Option.getWithDefault(acc)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
let withProps = class => {
|
||||
class->assignGetPropsInProto(() => {
|
||||
(this->toExtendedThis).getAttributes()
|
||||
})
|
||||
}
|
||||
|
||||
let withCreate = class => {
|
||||
class->assignConstructor(() => {
|
||||
thisSuper()
|
||||
Js.log(this.constructor.name ++ " created")
|
||||
(this->toExtendedThis).init()
|
||||
})
|
||||
}
|
||||
|
||||
let withInitAndUpdate = class => {
|
||||
class->assignAttributeChangedCallback((name, _, value) => {
|
||||
let extendedThis = this->toExtendedThis
|
||||
this->setThisProps(extendedThis.getProps())
|
||||
extendedThis.update(name, value)
|
||||
Js.log(this.constructor.name ++ " update")
|
||||
})
|
||||
}
|
||||
|
||||
let withMountAndRender = class => {
|
||||
class->assignConnectedCallback(_ => {
|
||||
let extendedThis = this->toExtendedThis
|
||||
if extendedThis.isConnected {
|
||||
extendedThis.render->Js.Nullable.toOption->Belt.Option.forEach(fx => fx())
|
||||
}
|
||||
|
||||
Js.log(this.constructor.name ++ " connected")
|
||||
})
|
||||
}
|
||||
|
||||
let withUnmount = class => {
|
||||
class->assignDisconnectedCallback(_ => {
|
||||
(this->toExtendedThis).dispose()
|
||||
Js.log(this.constructor.name ++ " disconnected")
|
||||
})
|
||||
}
|
||||
|
||||
let withEventListener = class => {
|
||||
class->assignAddEventListener((event, callback) => {
|
||||
this.super.addEventListener(event, callback)
|
||||
(this->toExtendedThis).object.on(event, callback)
|
||||
})
|
||||
class->assignRemoveEventListener((event, callback) => {
|
||||
this.super.removeEventListener(event, callback)
|
||||
(this->toExtendedThis).object.off(event, callback)
|
||||
})
|
||||
}
|
6
src/Init.res
Normal file
6
src/Init.res
Normal file
@ -0,0 +1,6 @@
|
||||
let start = _ => {
|
||||
let mockWindow = MockWindow.newWindow()
|
||||
mockWindow->MockWindow.patchInsertBefore
|
||||
let document = mockWindow->MockWindow.document
|
||||
Js.log(document)
|
||||
}
|
8
src/MockWindow.res
Normal file
8
src/MockWindow.res
Normal file
@ -0,0 +1,8 @@
|
||||
@module("./dom-mock/window") @new
|
||||
external newWindow: unit => Dom.window = "Window"
|
||||
|
||||
@get
|
||||
external document: Dom.window => Dom.document = "document"
|
||||
|
||||
@module("./dom-mock/window") @val
|
||||
external patchInsertBefore: Dom.window => unit = "patchInsertBefore"
|
710
src/dom-mock/document.js
Normal file
710
src/dom-mock/document.js
Normal file
@ -0,0 +1,710 @@
|
||||
"use strict";
|
||||
const Element_1 = (require("happy-dom/lib/nodes/element/Element"));
|
||||
const HTMLUnknownElement_1 = (require("happy-dom/lib/nodes/html-unknown-element/HTMLUnknownElement"));
|
||||
const Text_1 = (require("happy-dom/lib/nodes/text/Text"));
|
||||
const Comment_1 = (require("happy-dom/lib/nodes/comment/Comment"));
|
||||
const Node_1 = (require("happy-dom/lib/nodes/node/Node"));
|
||||
const TreeWalker_1 = (require("happy-dom/lib/tree-walker/TreeWalker"));
|
||||
const DocumentFragment_1 = (require("happy-dom/lib/nodes/document-fragment/DocumentFragment"));
|
||||
const XMLParser_1 = (require("happy-dom/lib/xml-parser/XMLParser"));
|
||||
const Event_1 = (require("happy-dom/lib/event/Event"));
|
||||
const DOMImplementation_1 = (require("happy-dom/lib/dom-implementation/DOMImplementation"));
|
||||
const Attr_1 = (require("happy-dom/lib/attribute/Attr"));
|
||||
const NamespaceURI_1 = (require("happy-dom/lib/config/NamespaceURI"));
|
||||
const DocumentType_1 = (require("happy-dom/lib/nodes/document-type/DocumentType"));
|
||||
const ParentNodeUtility_1 = (require("happy-dom/lib/nodes/parent-node/ParentNodeUtility"));
|
||||
const QuerySelector_1 = (require("happy-dom/lib/query-selector/QuerySelector"));
|
||||
const DOMException_1 = (require("happy-dom/lib/exception/DOMException"));
|
||||
const HTMLCollectionFactory_1 = (require("happy-dom/lib/nodes/element/HTMLCollectionFactory"));
|
||||
const DocumentReadyStateEnum_1 = (require("happy-dom/lib/nodes/document/DocumentReadyStateEnum"));
|
||||
const DocumentReadyStateManager_1 = (require("happy-dom/lib/nodes/document/DocumentReadyStateManager"));
|
||||
const Selection_1 = (require("happy-dom/lib/selection/Selection"));
|
||||
/**
|
||||
* Document.
|
||||
*/
|
||||
class Document extends Node_1.default {
|
||||
/**
|
||||
* Creates an instance of Document.
|
||||
*
|
||||
* @param defaultView Default view.
|
||||
*/
|
||||
constructor(defaultView) {
|
||||
super();
|
||||
this.onreadystatechange = null;
|
||||
this.nodeType = Node_1.default.DOCUMENT_NODE;
|
||||
this.adoptedStyleSheets = [];
|
||||
this.children = HTMLCollectionFactory_1.default.create();
|
||||
this.readyState = DocumentReadyStateEnum_1.default.interactive;
|
||||
this.isConnected = true;
|
||||
this._activeElement = null;
|
||||
this._isFirstWrite = true;
|
||||
this._isFirstWriteAfterOpen = false;
|
||||
this._cookie = '';
|
||||
this._selection = null;
|
||||
this.defaultView = defaultView //this.constructor._defaultView;
|
||||
this.implementation = new DOMImplementation_1.default(this);
|
||||
this._readyStateManager = new DocumentReadyStateManager_1.default(this.defaultView);
|
||||
const doctype = this.implementation.createDocumentType('html', '', '');
|
||||
const documentElement = this.createElement('html');
|
||||
const bodyElement = this.createElement('body');
|
||||
const headElement = this.createElement('head');
|
||||
this.appendChild(doctype);
|
||||
this.appendChild(documentElement);
|
||||
documentElement.appendChild(headElement);
|
||||
documentElement.appendChild(bodyElement);
|
||||
}
|
||||
/**
|
||||
* Returns character set.
|
||||
*
|
||||
* @deprecated
|
||||
* @returns Character set.
|
||||
*/
|
||||
get charset() {
|
||||
return this.characterSet;
|
||||
}
|
||||
/**
|
||||
* Returns character set.
|
||||
*
|
||||
* @returns Character set.
|
||||
*/
|
||||
get characterSet() {
|
||||
const charset = this.querySelector('meta[charset]')?.getAttributeNS(null, 'charset');
|
||||
return charset ? charset : 'UTF-8';
|
||||
}
|
||||
/**
|
||||
* Last element child.
|
||||
*
|
||||
* @returns Element.
|
||||
*/
|
||||
get childElementCount() {
|
||||
return this.children.length;
|
||||
}
|
||||
/**
|
||||
* First element child.
|
||||
*
|
||||
* @returns Element.
|
||||
*/
|
||||
get firstElementChild() {
|
||||
return this.children ? this.children[0] || null : null;
|
||||
}
|
||||
/**
|
||||
* Last element child.
|
||||
*
|
||||
* @returns Element.
|
||||
*/
|
||||
get lastElementChild() {
|
||||
return this.children ? this.children[this.children.length - 1] || null : null;
|
||||
}
|
||||
/**
|
||||
* Returns cookie string.
|
||||
*
|
||||
* @returns Cookie.
|
||||
*/
|
||||
get cookie() {
|
||||
return this._cookie;
|
||||
}
|
||||
/**
|
||||
* Sets a cookie string.
|
||||
*
|
||||
* @param cookie Cookie string.
|
||||
*/
|
||||
set cookie(cookie) {
|
||||
this._cookie = CookieUtility_1.default.getCookieString(this.defaultView.location, this._cookie, cookie);
|
||||
}
|
||||
/**
|
||||
* Node name.
|
||||
*
|
||||
* @returns Node name.
|
||||
*/
|
||||
get nodeName() {
|
||||
return '#document';
|
||||
}
|
||||
/**
|
||||
* Returns <html> element.
|
||||
*
|
||||
* @returns Element.
|
||||
*/
|
||||
get documentElement() {
|
||||
return ParentNodeUtility_1.default.getElementByTagName(this, 'html');
|
||||
}
|
||||
/**
|
||||
* Returns document type element.
|
||||
*
|
||||
* @returns Document type.
|
||||
*/
|
||||
get doctype() {
|
||||
for (const node of this.childNodes) {
|
||||
if (node instanceof DocumentType_1.default) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Returns <body> element.
|
||||
*
|
||||
* @returns Element.
|
||||
*/
|
||||
get body() {
|
||||
return ParentNodeUtility_1.default.getElementByTagName(this, 'body');
|
||||
}
|
||||
/**
|
||||
* Returns <head> element.
|
||||
*
|
||||
* @returns Element.
|
||||
*/
|
||||
get head() {
|
||||
return ParentNodeUtility_1.default.getElementByTagName(this, 'head');
|
||||
}
|
||||
/**
|
||||
* Returns CSS style sheets.
|
||||
*
|
||||
* @returns CSS style sheets.
|
||||
*/
|
||||
get styleSheets() {
|
||||
const styles = (this.querySelectorAll('link[rel="stylesheet"][href],style'));
|
||||
const styleSheets = [];
|
||||
for (const style of styles) {
|
||||
const sheet = style.sheet;
|
||||
if (sheet) {
|
||||
styleSheets.push(sheet);
|
||||
}
|
||||
}
|
||||
return styleSheets;
|
||||
}
|
||||
/**
|
||||
* Returns active element.
|
||||
*
|
||||
* @returns Active element.
|
||||
*/
|
||||
get activeElement() {
|
||||
if (this._activeElement) {
|
||||
let rootNode = (this._activeElement.getRootNode());
|
||||
let activeElement = this._activeElement;
|
||||
while (rootNode !== this) {
|
||||
activeElement = rootNode.host;
|
||||
rootNode = activeElement ? activeElement.getRootNode() : this;
|
||||
}
|
||||
return activeElement;
|
||||
}
|
||||
return this._activeElement || this.body || this.documentElement || null;
|
||||
}
|
||||
/**
|
||||
* Returns scrolling element.
|
||||
*
|
||||
* @returns Scrolling element.
|
||||
*/
|
||||
get scrollingElement() {
|
||||
return this.documentElement;
|
||||
}
|
||||
/**
|
||||
* Returns location.
|
||||
*
|
||||
* @returns Location.
|
||||
*/
|
||||
get location() {
|
||||
return this.defaultView.location;
|
||||
}
|
||||
/**
|
||||
* Returns scripts.
|
||||
*
|
||||
* @returns Scripts.
|
||||
*/
|
||||
get scripts() {
|
||||
return this.getElementsByTagName('script');
|
||||
}
|
||||
/**
|
||||
* Returns base URI.
|
||||
*
|
||||
* @override
|
||||
* @returns Base URI.
|
||||
*/
|
||||
get baseURI() {
|
||||
const base = this.querySelector('base');
|
||||
if (base) {
|
||||
return base.href;
|
||||
}
|
||||
return this.defaultView.location.href;
|
||||
}
|
||||
/**
|
||||
* Inserts a set of Node objects or DOMString objects after the last child of the ParentNode. DOMString objects are inserted as equivalent Text nodes.
|
||||
*
|
||||
* @param nodes List of Node or DOMString.
|
||||
*/
|
||||
append(...nodes) {
|
||||
ParentNodeUtility_1.default.append(this, ...nodes);
|
||||
}
|
||||
/**
|
||||
* Inserts a set of Node objects or DOMString objects before the first child of the ParentNode. DOMString objects are inserted as equivalent Text nodes.
|
||||
*
|
||||
* @param nodes List of Node or DOMString.
|
||||
*/
|
||||
prepend(...nodes) {
|
||||
ParentNodeUtility_1.default.prepend(this, ...nodes);
|
||||
}
|
||||
/**
|
||||
* Replaces the existing children of a node with a specified new set of children.
|
||||
*
|
||||
* @param nodes List of Node or DOMString.
|
||||
*/
|
||||
replaceChildren(...nodes) {
|
||||
ParentNodeUtility_1.default.replaceChildren(this, ...nodes);
|
||||
}
|
||||
/**
|
||||
* Query CSS selector to find matching elments.
|
||||
*
|
||||
* @param selector CSS selector.
|
||||
* @returns Matching elements.
|
||||
*/
|
||||
querySelectorAll(selector) {
|
||||
return QuerySelector_1.default.querySelectorAll(this, selector);
|
||||
}
|
||||
/**
|
||||
* Query CSS Selector to find a matching element.
|
||||
*
|
||||
* @param selector CSS selector.
|
||||
* @returns Matching element.
|
||||
*/
|
||||
querySelector(selector) {
|
||||
return QuerySelector_1.default.querySelector(this, selector);
|
||||
}
|
||||
/**
|
||||
* Returns an elements by class name.
|
||||
*
|
||||
* @param className Tag name.
|
||||
* @returns Matching element.
|
||||
*/
|
||||
getElementsByClassName(className) {
|
||||
return ParentNodeUtility_1.default.getElementsByClassName(this, className);
|
||||
}
|
||||
/**
|
||||
* Returns an elements by tag name.
|
||||
*
|
||||
* @param tagName Tag name.
|
||||
* @returns Matching element.
|
||||
*/
|
||||
getElementsByTagName(tagName) {
|
||||
return ParentNodeUtility_1.default.getElementsByTagName(this, tagName);
|
||||
}
|
||||
/**
|
||||
* Returns an elements by tag name and namespace.
|
||||
*
|
||||
* @param namespaceURI Namespace URI.
|
||||
* @param tagName Tag name.
|
||||
* @returns Matching element.
|
||||
*/
|
||||
getElementsByTagNameNS(namespaceURI, tagName) {
|
||||
return ParentNodeUtility_1.default.getElementsByTagNameNS(this, namespaceURI, tagName);
|
||||
}
|
||||
/**
|
||||
* Returns an element by ID.
|
||||
*
|
||||
* @param id ID.
|
||||
* @returns Matching element.
|
||||
*/
|
||||
getElementById(id) {
|
||||
return ParentNodeUtility_1.default.getElementById(this, id);
|
||||
}
|
||||
/**
|
||||
* Returns an element by Name.
|
||||
*
|
||||
* @returns Matching element.
|
||||
* @param name
|
||||
*/
|
||||
getElementsByName(name) {
|
||||
const _getElementsByName = (_parentNode, _name) => {
|
||||
const matches = HTMLCollectionFactory_1.default.create();
|
||||
for (const child of _parentNode.children) {
|
||||
if ((child.getAttributeNS(null, 'name') || '') === _name) {
|
||||
matches.push(child);
|
||||
}
|
||||
for (const match of _getElementsByName(child, _name)) {
|
||||
matches.push(match);
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
};
|
||||
return _getElementsByName(this, name);
|
||||
}
|
||||
/**
|
||||
* Clones a node.
|
||||
*
|
||||
* @override
|
||||
* @param [deep=false] "true" to clone deep.
|
||||
* @returns Cloned node.
|
||||
*/
|
||||
cloneNode(deep = false) {
|
||||
this.constructor._defaultView = this.defaultView;
|
||||
const clone = super.cloneNode(deep);
|
||||
if (deep) {
|
||||
for (const node of clone.childNodes) {
|
||||
if (node.nodeType === Node_1.default.ELEMENT_NODE) {
|
||||
clone.children.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
/**
|
||||
* Append a child node to childNodes.
|
||||
*
|
||||
* @override
|
||||
* @param node Node to append.
|
||||
* @returns Appended node.
|
||||
*/
|
||||
appendChild(node) {
|
||||
// If the type is DocumentFragment, then the child nodes of if it should be moved instead of the actual node.
|
||||
// See: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment
|
||||
if (node.nodeType !== Node_1.default.DOCUMENT_FRAGMENT_NODE) {
|
||||
if (node.parentNode && node.parentNode['children']) {
|
||||
const index = node.parentNode['children'].indexOf(node);
|
||||
if (index !== -1) {
|
||||
node.parentNode['children'].splice(index, 1);
|
||||
}
|
||||
}
|
||||
if (node !== this && node.nodeType === Node_1.default.ELEMENT_NODE) {
|
||||
this.children.push(node);
|
||||
}
|
||||
}
|
||||
return super.appendChild(node);
|
||||
}
|
||||
/**
|
||||
* Remove Child element from childNodes array.
|
||||
*
|
||||
* @override
|
||||
* @param node Node to remove.
|
||||
*/
|
||||
removeChild(node) {
|
||||
|
||||
if (node.nodeType === Node_1.default.ELEMENT_NODE) {
|
||||
const index = this.children.indexOf(node);
|
||||
if (index !== -1) {
|
||||
this.children.splice(index, 1);
|
||||
}
|
||||
}
|
||||
return super.removeChild(node);
|
||||
}
|
||||
/**
|
||||
* Inserts a node before another.
|
||||
*
|
||||
* @override
|
||||
* @param newNode Node to insert.
|
||||
* @param [referenceNode] Node to insert before.
|
||||
* @returns Inserted node.
|
||||
*/
|
||||
insertBefore(newNode, referenceNode) {
|
||||
const returnValue = super.insertBefore(newNode, referenceNode);
|
||||
// If the type is DocumentFragment, then the child nodes of if it should be moved instead of the actual node.
|
||||
// See: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment
|
||||
if (newNode.nodeType !== Node_1.default.DOCUMENT_FRAGMENT_NODE) {
|
||||
if (newNode.parentNode && newNode.parentNode['children']) {
|
||||
const index = newNode.parentNode['children'].indexOf(newNode);
|
||||
if (index !== -1) {
|
||||
newNode.parentNode['children'].splice(index, 1);
|
||||
}
|
||||
}
|
||||
this.children.length = 0;
|
||||
for (const node of this.childNodes) {
|
||||
if (node.nodeType === Node_1.default.ELEMENT_NODE) {
|
||||
this.children.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
/**
|
||||
* Replaces the document HTML with new HTML.
|
||||
*
|
||||
* @param html HTML.
|
||||
*/
|
||||
write(html) {
|
||||
const root = XMLParser_1.default.parse(this, html, true);
|
||||
if (this._isFirstWrite || this._isFirstWriteAfterOpen) {
|
||||
if (this._isFirstWrite) {
|
||||
if (!this._isFirstWriteAfterOpen) {
|
||||
this.open();
|
||||
}
|
||||
this._isFirstWrite = false;
|
||||
}
|
||||
this._isFirstWriteAfterOpen = false;
|
||||
let documentElement = null;
|
||||
let documentTypeNode = null;
|
||||
for (const node of root.childNodes) {
|
||||
if (node['tagName'] === 'HTML') {
|
||||
documentElement = node;
|
||||
}
|
||||
else if (node.nodeType === Node_1.default.DOCUMENT_TYPE_NODE) {
|
||||
documentTypeNode = node;
|
||||
}
|
||||
if (documentElement && documentTypeNode) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (documentElement) {
|
||||
if (!this.documentElement) {
|
||||
if (documentTypeNode) {
|
||||
this.appendChild(documentTypeNode);
|
||||
}
|
||||
this.appendChild(documentElement);
|
||||
}
|
||||
else {
|
||||
const rootBody = root.querySelector('body');
|
||||
const body = this.querySelector('body');
|
||||
if (rootBody && body) {
|
||||
for (const child of rootBody.childNodes.slice()) {
|
||||
body.appendChild(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
const body = this.querySelector('body');
|
||||
if (body) {
|
||||
for (const child of root.childNodes.slice()) {
|
||||
if (child['tagName'] !== 'HTML' && child.nodeType !== Node_1.default.DOCUMENT_TYPE_NODE) {
|
||||
body.appendChild(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
const documentElement = this.createElement('html');
|
||||
const bodyElement = this.createElement('body');
|
||||
const headElement = this.createElement('head');
|
||||
for (const child of root.childNodes.slice()) {
|
||||
bodyElement.appendChild(child);
|
||||
}
|
||||
documentElement.appendChild(headElement);
|
||||
documentElement.appendChild(bodyElement);
|
||||
this.appendChild(documentElement);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const bodyNode = root.querySelector('body');
|
||||
for (const child of (bodyNode || root).childNodes.slice()) {
|
||||
this.body.appendChild(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Opens the document.
|
||||
*
|
||||
* @returns Document.
|
||||
*/
|
||||
open() {
|
||||
this._isFirstWriteAfterOpen = true;
|
||||
for (const eventType of Object.keys(this._listeners)) {
|
||||
const listeners = this._listeners[eventType];
|
||||
if (listeners) {
|
||||
for (const listener of listeners) {
|
||||
this.removeEventListener(eventType, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const child of this.childNodes.slice()) {
|
||||
this.removeChild(child);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Closes the document.
|
||||
*/
|
||||
close() { }
|
||||
/* eslint-disable jsdoc/valid-types */
|
||||
/**
|
||||
* Creates an element.
|
||||
*
|
||||
* @param qualifiedName Tag name.
|
||||
* @param [options] Options.
|
||||
* @param [options.is] Tag name of a custom element previously defined via customElements.define().
|
||||
* @returns Element.
|
||||
*/
|
||||
createElement(qualifiedName, options) {
|
||||
return this.createElementNS(NamespaceURI_1.default.html, qualifiedName, options);
|
||||
}
|
||||
/**
|
||||
* Creates an element with the specified namespace URI and qualified name.
|
||||
*
|
||||
* @param namespaceURI Namespace URI.
|
||||
* @param qualifiedName Tag name.
|
||||
* @param [options] Options.
|
||||
* @param [options.is] Tag name of a custom element previously defined via customElements.define().
|
||||
* @returns Element.
|
||||
*/
|
||||
createElementNS(namespaceURI, qualifiedName, options) {
|
||||
const tagName = String(qualifiedName).toUpperCase();
|
||||
let customElementClass;
|
||||
if (this.defaultView && options && options.is) {
|
||||
customElementClass = this.defaultView.customElements.get(String(options.is));
|
||||
}
|
||||
else if (this.defaultView) {
|
||||
customElementClass = this.defaultView.customElements.get(tagName);
|
||||
}
|
||||
const elementClass = customElementClass || HTMLUnknownElement_1.default;
|
||||
elementClass._ownerDocument = this;
|
||||
const element = new elementClass();
|
||||
element.tagName = tagName;
|
||||
element.ownerDocument = this;
|
||||
element.namespaceURI = namespaceURI;
|
||||
if (element instanceof Element_1.default && options && options.is) {
|
||||
element._isValue = String(options.is);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
/* eslint-enable jsdoc/valid-types */
|
||||
/**
|
||||
* Creates a text node.
|
||||
*
|
||||
* @param [data] Text data.
|
||||
* @returns Text node.
|
||||
*/
|
||||
createTextNode(data) {
|
||||
Text_1.default._ownerDocument = this;
|
||||
return new Text_1.default(data);
|
||||
}
|
||||
/**
|
||||
* Creates a comment node.
|
||||
*
|
||||
* @param [data] Text data.
|
||||
* @returns Text node.
|
||||
*/
|
||||
createComment(data) {
|
||||
Comment_1.default._ownerDocument = this;
|
||||
return new Comment_1.default(data);
|
||||
}
|
||||
/**
|
||||
* Creates a document fragment.
|
||||
*
|
||||
* @returns Document fragment.
|
||||
*/
|
||||
createDocumentFragment() {
|
||||
DocumentFragment_1.default._ownerDocument = this;
|
||||
return new DocumentFragment_1.default();
|
||||
}
|
||||
/**
|
||||
* Creates a Tree Walker.
|
||||
*
|
||||
* @param root Root.
|
||||
* @param [whatToShow] What to show.
|
||||
* @param [filter] Filter.
|
||||
*/
|
||||
createTreeWalker(root, whatToShow = -1, filter = null) {
|
||||
return new TreeWalker_1.default(root, whatToShow, filter);
|
||||
}
|
||||
/**
|
||||
* Creates an event.
|
||||
*
|
||||
* @deprecated
|
||||
* @param type Type.
|
||||
* @returns Event.
|
||||
*/
|
||||
createEvent(type) {
|
||||
if (typeof this.defaultView[type] === 'function') {
|
||||
return new this.defaultView[type]('init');
|
||||
}
|
||||
return new Event_1.default('init');
|
||||
}
|
||||
/**
|
||||
* Creates an Attr node.
|
||||
*
|
||||
* @param name Name.
|
||||
* @returns Attribute.
|
||||
*/
|
||||
createAttribute(name) {
|
||||
const attribute = new Attr_1.default();
|
||||
attribute.name = name.toLowerCase();
|
||||
attribute.ownerDocument = this;
|
||||
return attribute;
|
||||
}
|
||||
/**
|
||||
* Creates a namespaced Attr node.
|
||||
*
|
||||
* @param namespaceURI Namespace URI.
|
||||
* @param qualifiedName Qualified name.
|
||||
* @returns Element.
|
||||
*/
|
||||
createAttributeNS(namespaceURI, qualifiedName) {
|
||||
const attribute = new Attr_1.default();
|
||||
attribute.namespaceURI = namespaceURI;
|
||||
attribute.name = qualifiedName;
|
||||
attribute.ownerDocument = this;
|
||||
return attribute;
|
||||
}
|
||||
/**
|
||||
* Imports a node.
|
||||
*
|
||||
* @param node Node to import.
|
||||
* @param [deep=false] Set to "true" if the clone should be deep.
|
||||
* @param Imported Node.
|
||||
*/
|
||||
importNode(node, deep = false) {
|
||||
if (!(node instanceof Node_1.default)) {
|
||||
throw new DOMException_1.default('Parameter 1 was not of type Node.');
|
||||
}
|
||||
const clone = node.cloneNode(deep);
|
||||
clone.ownerDocument = this;
|
||||
return clone;
|
||||
}
|
||||
/**
|
||||
* Creates a range.
|
||||
*
|
||||
* @returns Range.
|
||||
*/
|
||||
createRange() {
|
||||
return new this.defaultView.Range();
|
||||
}
|
||||
/**
|
||||
* Adopts a node.
|
||||
*
|
||||
* @param node Node to adopt.
|
||||
* @returns Adopted node.
|
||||
*/
|
||||
adoptNode(node) {
|
||||
if (!(node instanceof Node_1.default)) {
|
||||
throw new DOMException_1.default('Parameter 1 was not of type Node.');
|
||||
}
|
||||
const adopted = node.parentNode ? node.parentNode.removeChild(node) : node;
|
||||
adopted.ownerDocument = this;
|
||||
return adopted;
|
||||
}
|
||||
/**
|
||||
* Returns selection.
|
||||
*
|
||||
* @returns Selection.
|
||||
*/
|
||||
getSelection() {
|
||||
if (!this._selection) {
|
||||
this._selection = new Selection_1.default(this);
|
||||
}
|
||||
return this._selection;
|
||||
}
|
||||
/**
|
||||
* Returns a boolean value indicating whether the document or any element inside the document has focus.
|
||||
*
|
||||
* @returns "true" if the document has focus.
|
||||
*/
|
||||
hasFocus() {
|
||||
return !!this.activeElement;
|
||||
}
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
dispatchEvent(event) {
|
||||
const returnValue = super.dispatchEvent(event);
|
||||
if (event.bubbles && !event._propagationStopped) {
|
||||
return this.defaultView.dispatchEvent(event);
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
/**
|
||||
* Triggered by window when it is ready.
|
||||
*/
|
||||
_onWindowReady() {
|
||||
this._readyStateManager.whenComplete().then(() => {
|
||||
this.readyState = DocumentReadyStateEnum_1.default.complete;
|
||||
this.dispatchEvent(new Event_1.default('readystatechange'));
|
||||
this.dispatchEvent(new Event_1.default('load', { bubbles: true }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Document;
|
||||
//# sourceMappingURL=Document.js.map
|
37
src/dom-mock/window.js
Normal file
37
src/dom-mock/window.js
Normal file
@ -0,0 +1,37 @@
|
||||
import Document from "./document";
|
||||
import Node_1 from "happy-dom/lib/nodes/node/Node";
|
||||
import CustomEvent_1 from "happy-dom/lib/event/events/CustomEvent";
|
||||
import HTMLElement_1 from "happy-dom/lib/nodes/html-element/HTMLElement";
|
||||
import CustomElementRegistry_1 from "happy-dom/lib/custom-element/CustomElementRegistry";
|
||||
import EventTarget_1 from "happy-dom/lib/event/EventTarget";
|
||||
|
||||
export class Window extends EventTarget_1 {
|
||||
constructor() {
|
||||
super();
|
||||
this.Node = Node_1;
|
||||
this.CustomEvent = CustomEvent_1;
|
||||
this.HTMLElement = HTMLElement_1;
|
||||
this.customElements = new CustomElementRegistry_1();
|
||||
const document = new Document(this)
|
||||
this.document = document;
|
||||
}
|
||||
}
|
||||
|
||||
export const patchInsertBefore = (window) => {
|
||||
/**
|
||||
* Patch `insertBefore` function to default reference node to null when passed undefined.
|
||||
* This is technically only needed for an Elm issue in version 1.0.2 of the VirtualDom
|
||||
* More context here: https://github.com/elm/virtual-dom/issues/161
|
||||
* And here: https://github.com/elm/virtual-dom/blob/1.0.2/src/Elm/Kernel/VirtualDom.js#L1409
|
||||
*/
|
||||
|
||||
const insertBefore = window.Node.prototype.insertBefore
|
||||
window.Node.prototype.insertBefore = function (...args) {
|
||||
const [newNode, refNode] = args
|
||||
const hasRefNode = args.length > 1
|
||||
const isRefNodeDefined = typeof refNode !== 'undefined'
|
||||
if (hasRefNode && !isRefNodeDefined)
|
||||
return insertBefore.call(this, newNode, null)
|
||||
return insertBefore.call(this, ...args)
|
||||
}
|
||||
}
|
78
yarn.lock
78
yarn.lock
@ -1024,6 +1024,11 @@ css-unit-converter@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21"
|
||||
integrity sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==
|
||||
|
||||
css.escape@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
|
||||
integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==
|
||||
|
||||
css@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/css/-/css-3.0.0.tgz#4447a4d58fdd03367c516ca9f64ae365cee4aa5d"
|
||||
@ -1449,6 +1454,18 @@ gzip-size@^6.0.0:
|
||||
dependencies:
|
||||
duplexer "^0.1.2"
|
||||
|
||||
happy-dom@^8.1.2:
|
||||
version "8.1.2"
|
||||
resolved "https://registry.yarnpkg.com/happy-dom/-/happy-dom-8.1.2.tgz#00b122500b6182f2bcd2d2e1c1659829aca27ff5"
|
||||
integrity sha512-A/mTzD6KiVMWZynne7R+HlZjIpz9a1Ijh99inqq51Vis1v4G1K+mQeyOo19TXHtoFwAdjx+PzXQGpcyV0yhy9Q==
|
||||
dependencies:
|
||||
css.escape "^1.5.1"
|
||||
he "^1.2.0"
|
||||
node-fetch "^2.x.x"
|
||||
webidl-conversions "^7.0.0"
|
||||
whatwg-encoding "^2.0.0"
|
||||
whatwg-mimetype "^3.0.0"
|
||||
|
||||
har-schema@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
|
||||
@ -1484,6 +1501,11 @@ hash-sum@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04"
|
||||
integrity sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==
|
||||
|
||||
he@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
highlight.js@^10.7.1:
|
||||
version "10.7.3"
|
||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
|
||||
@ -1503,6 +1525,13 @@ http-signature@~1.2.0:
|
||||
jsprim "^1.2.2"
|
||||
sshpk "^1.7.0"
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
|
||||
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3.0.0"
|
||||
|
||||
icss-utils@^5.0.0, icss-utils@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
|
||||
@ -1918,6 +1947,13 @@ node-elm-compiler@^5.0.0:
|
||||
lodash "^4.17.19"
|
||||
temp "^0.9.0"
|
||||
|
||||
node-fetch@^2.x.x:
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-releases@^2.0.6:
|
||||
version "2.0.8"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae"
|
||||
@ -2325,7 +2361,7 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2:
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
||||
"safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
@ -2605,6 +2641,11 @@ tough-cookie@~2.5.0:
|
||||
psl "^1.1.28"
|
||||
punycode "^2.1.1"
|
||||
|
||||
tr46@~0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
||||
|
||||
ts-dedent@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5"
|
||||
@ -2681,6 +2722,11 @@ verror@1.10.0:
|
||||
core-util-is "1.0.2"
|
||||
extsprintf "^1.2.0"
|
||||
|
||||
vm-shim@^0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/vm-shim/-/vm-shim-0.0.6.tgz#5ce167f067017ff4f6f4ff646e10e0a6b3dd4ea3"
|
||||
integrity sha512-KBCFEXbA/tuQN9e6D3eIGycDMaxsdJEG6+G3WkGupmJE40xVjJm1+pqizzgwSGqvwG/fghmJPUaWxYOTW1gfzw==
|
||||
|
||||
vue-hot-reload-api@^2.3.0:
|
||||
version "2.3.4"
|
||||
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"
|
||||
@ -2718,6 +2764,16 @@ watchpack@^2.4.0:
|
||||
glob-to-regexp "^0.4.1"
|
||||
graceful-fs "^4.1.2"
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
|
||||
|
||||
webidl-conversions@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
|
||||
integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
|
||||
|
||||
webpack-bundle-analyzer@^4.0.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.7.0.tgz#33c1c485a7fcae8627c547b5c3328b46de733c66"
|
||||
@ -2807,6 +2863,26 @@ webpack-virtual-modules@^0.4.0:
|
||||
watchpack "^2.4.0"
|
||||
webpack-sources "^3.2.3"
|
||||
|
||||
whatwg-encoding@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53"
|
||||
integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==
|
||||
dependencies:
|
||||
iconv-lite "0.6.3"
|
||||
|
||||
whatwg-mimetype@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7"
|
||||
integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==
|
||||
|
||||
whatwg-url@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
||||
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
|
||||
dependencies:
|
||||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
which@^1.2.9:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
||||
|
Loading…
Reference in New Issue
Block a user