diff --git a/src/core/session.js b/src/core/session.js index 44edc7856..eccca04d3 100644 --- a/src/core/session.js +++ b/src/core/session.js @@ -17,6 +17,10 @@ import { PageView } from "./drive/page_view" import { FrameElement } from "../elements/frame_element" import { Preloader } from "./drive/preloader" +/** + * The Session class represents a user session, managing the various observers, navigators, + * and controllers to provide a seamless user experience in a Turbo Drive enabled application. + */ export class Session { navigator = new Navigator(this) history = new History(this) @@ -40,6 +44,9 @@ export class Session { started = false formMode = "on" + /** + * Starts all observers and services if not already started. + */ start() { if (!this.started) { this.pageObserver.start() @@ -57,10 +64,16 @@ export class Session { } } + /** + * Disables the session. + */ disable() { this.enabled = false } + /** + * Stops all observers and services if they are running. + */ stop() { if (this.started) { this.pageObserver.stop() @@ -76,10 +89,22 @@ export class Session { } } + /** + * Registers a new browser adapter for the session. + * + * @param {BrowserAdapter} adapter - The new browser adapter to register. + */ registerAdapter(adapter) { this.adapter = adapter } + /** + * Initiates a visit to a specified location, with optional parameters. + * + * @param {string} location - The location to visit. + * @param {Object} options - Optional parameters for the visit. + * @param {string} [options.frame] - The ID of the frame element to use for the visit. + */ visit(location, options = {}) { const frameElement = options.frame ? document.getElementById(options.frame) : null @@ -91,26 +116,54 @@ export class Session { } } + /** + * Connects a new stream source. + * + * @param {any} source - The new stream source to connect. + */ connectStreamSource(source) { this.streamObserver.connectStreamSource(source) } + /** + * Disconnects an existing stream source. + * + * @param {any} source - The stream source to disconnect. + */ disconnectStreamSource(source) { this.streamObserver.disconnectStreamSource(source) } + /** + * Renders a stream message. + * + * @param {Object} message - The stream message to render. + */ renderStreamMessage(message) { this.streamMessageRenderer.render(StreamMessage.wrap(message)) } + /** + * Clears the cache. + */ clearCache() { this.view.clearSnapshotCache() } + /** + * Sets the delay for the progress bar. + * + * @param {number} delay - The delay in milliseconds. + */ setProgressBarDelay(delay) { this.progressBarDelay = delay } + /** + * Sets the form mode. + * + * @param {string} mode - The form mode to set, either "on" or "off". + */ setFormMode(mode) { this.formMode = mode } @@ -361,6 +414,14 @@ export class Session { // Helpers + /** + * Helper function to check if a submission is navigatable based on the current form mode and element attributes. + * + * @private + * @param {HTMLFormElement} form - The form element being submitted. + * @param {HTMLElement} submitter - The element that triggered the form submission. + * @return {boolean} - True if the submission is navigatable, false otherwise. + */ submissionIsNavigatable(form, submitter) { if (this.formMode == "off") { return false @@ -375,6 +436,13 @@ export class Session { } } + /** + * Helper function to check if an element is navigatable based on data attributes and the current drive mode. + * + * @private + * @param {HTMLElement} element - The element to check. + * @return {boolean} - True if the element is navigatable, false otherwise. + */ elementIsNavigatable(element) { const container = findClosestRecursively(element, "[data-turbo]") const withinFrame = findClosestRecursively(element, "turbo-frame") @@ -399,10 +467,24 @@ export class Session { // Private + /** + * Helper function to get the appropriate action for a link click. + * + * @private + * @param {HTMLAnchorElement} link - The link being clicked. + * @return {string} - The action to perform for the link click. + */ getActionForLink(link) { return getVisitAction(link) || "advance" } + /** + * Helper function to get the current page snapshot. + * + * @private + * @readonly + * @return {Snapshot} - The current page snapshot. + */ get snapshot() { return this.view.snapshot } @@ -419,6 +501,11 @@ export class Session { // older adapters which do not expect URL objects. We should // consider removing this support at some point in the future. +/** + * Extends a URL with deprecated properties for compatibility with older Turbo Native adapters. + * + * @param {URL} url - The URL to extend. + */ function extendURLWithDeprecatedProperties(url) { Object.defineProperties(url, deprecatedLocationPropertyDescriptors) } diff --git a/src/core/snapshot.js b/src/core/snapshot.js index 2a5c1dbd6..f85734a8e 100644 --- a/src/core/snapshot.js +++ b/src/core/snapshot.js @@ -1,28 +1,68 @@ +/** + * Represents a snapshot of an HTML element along with various properties and methods to interact with it. + */ export class Snapshot { + /** + * Creates a new snapshot instance. + * + * @param {Element} element - The HTML element to create a snapshot of. + */ constructor(element) { this.element = element } + /** + * Gets the active element in the document that owns the snapshot element. + * + * @returns {Element | null} - The active element in the document, or null if there is no active element. + */ get activeElement() { return this.element.ownerDocument.activeElement } + /** + * Gets an array of the child elements of the snapshot element. + * + * @returns {Element[]} - An array of the child elements. + */ get children() { return [...this.element.children] } + /** + * Checks if the snapshot element contains a specific anchor. + * + * @param {string} anchor - The anchor to check for. + * @returns {boolean} - True if the anchor exists in the snapshot element, false otherwise. + */ hasAnchor(anchor) { return this.getElementForAnchor(anchor) != null } + /** + * Gets an element associated with a specific anchor in the snapshot element. + * + * @param {string} anchor - The anchor to get the element for. + * @returns {Element | null} - The element associated with the anchor, or null if no such element exists. + */ getElementForAnchor(anchor) { return anchor ? this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`) : null } + /** + * Checks if the snapshot element is connected to the document. + * + * @returns {boolean} - True if the snapshot element is connected to the document, false otherwise. + */ get isConnected() { return this.element.isConnected } + /** + * Gets the first element in the snapshot that can be auto-focused, if any. + * + * @returns {Element | null} - The first auto-focusable element, or null if no such element exists. + */ get firstAutofocusableElement() { const inertDisabledOrHidden = "[inert], :disabled, [hidden], details:not([open]), dialog:not([open])" @@ -34,14 +74,31 @@ export class Snapshot { return null } + /** + * Gets all permanent elements in the snapshot. + * + * @returns {NodeListOf} - A node list of all permanent elements in the snapshot. + */ get permanentElements() { return queryPermanentElementsAll(this.element) } + /** + * Gets a permanent element by its ID. + * + * @param {string} id - The ID of the permanent element to get. + * @returns {Element | null} - The permanent element with the specified ID, or null if no such element exists. + */ getPermanentElementById(id) { return getPermanentElementById(this.element, id) } + /** + * Creates a map of permanent elements for a given snapshot. + * + * @param {Snapshot} snapshot - The snapshot to create the permanent element map for. + * @returns {Object} - A map of permanent elements. + */ getPermanentElementMapForSnapshot(snapshot) { const permanentElementMap = {} @@ -57,10 +114,23 @@ export class Snapshot { } } +/** + * Gets a permanent element by its ID within a specific node. + * + * @param {Node} node - The node to query for the permanent element. + * @param {string} id - The ID of the permanent element to get. + * @returns {Element | null} - The permanent element with the specified ID, or null if no such element exists. + */ export function getPermanentElementById(node, id) { return node.querySelector(`#${id}[data-turbo-permanent]`) } +/** + * Queries all permanent elements within a specific node. + * + * @param {Node} node - The node to query for permanent elements. + * @returns {NodeListOf} - A node list of all permanent elements in the node. + */ export function queryPermanentElementsAll(node) { return node.querySelectorAll("[id][data-turbo-permanent]") } diff --git a/src/core/url.js b/src/core/url.js index dcd50cf26..cbfdf7c59 100644 --- a/src/core/url.js +++ b/src/core/url.js @@ -1,7 +1,19 @@ +/** + * Expands a locatable object to a full URL, resolving it relative to the document's base URI. + * + * @param {string | URL} locatable - The locatable object (a string or URL instance) to expand. + * @returns {URL} - The expanded URL. + */ export function expandURL(locatable) { return new URL(locatable.toString(), document.baseURI) } +/** + * Retrieves the anchor part from a URL. + * + * @param {URL} url - The URL from which to get the anchor part. + * @returns {string} - The anchor part of the URL, or undefined if no anchor is found. + */ export function getAnchor(url) { let anchorMatch if (url.hash) { @@ -12,54 +24,130 @@ export function getAnchor(url) { } } +/** + * Retrieves the action URL from a form and an optional submitter element. + * + * @param {HTMLFormElement} form - The form element to get the action URL from. + * @param {HTMLElement} [submitter] - The submitter element which initiated the form submission. + * @returns {URL} - The action URL. + */ export function getAction(form, submitter) { const action = submitter?.getAttribute("formaction") || form.getAttribute("action") || form.action return expandURL(action) } +/** + * Retrieves the extension part from a URL. + * + * @param {URL} url - The URL from which to get the extension part. + * @returns {string} - The extension part of the URL, including the leading dot, or an empty string if no extension is found. + */ export function getExtension(url) { return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "" } +/** + * Determines if the URL points to an HTML document based on its extension. + * + * @param {URL} url - The URL to check. + * @returns {boolean} - True if the URL points to an HTML document, false otherwise. + */ export function isHTML(url) { return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml|php))$/) } +/** + * Checks if a URL is prefixed by a base URL. + * + * @param {URL} baseURL - The base URL to check against. + * @param {URL} url - The URL to check. + * @returns {boolean} - True if the URL is prefixed by the base URL, false otherwise. + */ export function isPrefixedBy(baseURL, url) { const prefix = getPrefix(url) return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix) } +/** + * Determines if a location is visitable based on its prefix and whether it points to an HTML document. + * + * @param {URL} location - The location URL to check. + * @param {URL} rootLocation - The root location URL to check against. + * @returns {boolean} - True if the location is visitable, false otherwise. + */ export function locationIsVisitable(location, rootLocation) { return isPrefixedBy(location, rootLocation) && isHTML(location) } +/** + * Retrieves the request URL from a URL by removing the anchor part. + * + * @param {URL} url - The URL to get the request URL from. + * @returns {string} - The request URL. + */ export function getRequestURL(url) { const anchor = getAnchor(url) return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href } +/** + * Converts a URL to a cache key by using the request URL part of the URL. + * + * @param {URL} url - The URL to convert to a cache key. + * @returns {string} - The cache key. + */ export function toCacheKey(url) { return getRequestURL(url) } +/** + * Checks if two URLs are equal by comparing their expanded href attributes. + * + * @param {URL|string} left - The first URL to compare. + * @param {URL|string} right - The second URL to compare. + * @returns {boolean} - True if the URLs are equal, false otherwise. + */ export function urlsAreEqual(left, right) { return expandURL(left).href == expandURL(right).href } +/** + * Retrieves the path components of a URL. + * + * @param {URL} url - The URL to get the path components from. + * @returns {string[]} - The path components of the URL. + */ function getPathComponents(url) { return url.pathname.split("/").slice(1) } +/** + * Retrieves the last path component of a URL. + * + * @param {URL} url - The URL to get the last path component from. + * @returns {string} - The last path component of the URL. + */ function getLastPathComponent(url) { return getPathComponents(url).slice(-1)[0] } +/** + * Retrieves the prefix of a URL, which consists of the origin and the pathname. + * + * @param {URL} url - The URL to get the prefix from. + * @returns {string} - The prefix of the URL. + */ function getPrefix(url) { return addTrailingSlash(url.origin + url.pathname) } +/** + * Adds a trailing slash to a string if it doesn't already have one. + * + * @param {string} value - The string to add a trailing slash to. + * @returns {string} - The string with a trailing slash. + */ function addTrailingSlash(value) { return value.endsWith("/") ? value : value + "/" } diff --git a/src/core/view.js b/src/core/view.js index ca81e8bdb..2ec18e7f0 100644 --- a/src/core/view.js +++ b/src/core/view.js @@ -1,9 +1,18 @@ import { getAnchor } from "./url" +/** + * The View class manages the rendering and scrolling behaviors for a specific element and its delegate. + */ export class View { #resolveRenderPromise = (_value) => {} #resolveInterceptionPromise = (_value) => {} + /** + * Creates a new View instance. + * + * @param {Object} delegate - The delegate that manages the view. + * @param {HTMLElement} element - The element that the view manages. + */ constructor(delegate, element) { this.delegate = delegate this.element = element @@ -11,6 +20,11 @@ export class View { // Scrolling + /** + * Scrolls to the specified anchor in the view's snapshot. + * + * @param {string} anchor - The anchor to scroll to. + */ scrollToAnchor(anchor) { const element = this.snapshot.getElementForAnchor(anchor) if (element) { @@ -21,14 +35,29 @@ export class View { } } + /** + * Scrolls to the anchor derived from the specified location. + * + * @param {Location} location - The location to get the anchor from. + */ scrollToAnchorFromLocation(location) { this.scrollToAnchor(getAnchor(location)) } + /** + * Scrolls to the specified element. + * + * @param {HTMLElement} element - The element to scroll to. + */ scrollToElement(element) { element.scrollIntoView() } + /** + * Focuses the specified element, adding a tabindex attribute if necessary. + * + * @param {HTMLElement} element - The element to focus. + */ focusElement(element) { if (element instanceof HTMLElement) { if (element.hasAttribute("tabindex")) { @@ -41,20 +70,41 @@ export class View { } } + /** + * Scrolls to the specified position. + * + * @param {Object} position - The position to scroll to, defined by x and y coordinates. + * @param {number} position.x - The x-coordinate to scroll to. + * @param {number} position.y - The y-coordinate to scroll to. + */ scrollToPosition({ x, y }) { this.scrollRoot.scrollTo(x, y) } + /** + * Scrolls to the top of the view. + */ scrollToTop() { this.scrollToPosition({ x: 0, y: 0 }) } + /** + * Gets the root element for scrolling operations, which is the window object. + * + * @returns {Window} - The root element for scrolling. + */ get scrollRoot() { return window } // Rendering + /** + * Renders the view using the specified renderer. + * + * @param {Object} renderer - The renderer to use for rendering the view. + * @returns {Promise} - A promise that resolves when rendering is complete. + */ async render(renderer) { const { isPreview, shouldRender, newSnapshot: snapshot } = renderer if (shouldRender) { @@ -82,15 +132,31 @@ export class View { } } + /** + * Invalidates the view for the specified reason. + * + * @param {string} reason - The reason for invalidating the view. + */ invalidate(reason) { this.delegate.viewInvalidated(reason) } + /** + * Prepares the view to render a snapshot using the specified renderer. + * + * @param {Object} renderer - The renderer to use for preparing to render the snapshot. + * @returns {Promise} - A promise that resolves when the preparation is complete. + */ async prepareToRenderSnapshot(renderer) { this.markAsPreview(renderer.isPreview) await renderer.prepareToRender() } + /** + * Marks the view as a preview if the specified value is true. + * + * @param {boolean} isPreview - Whether to mark the view as a preview. + */ markAsPreview(isPreview) { if (isPreview) { this.element.setAttribute("data-turbo-preview", "") @@ -99,10 +165,21 @@ export class View { } } + /** + * Renders a snapshot using the specified renderer. + * + * @param {Object} renderer - The renderer to use for rendering the snapshot. + * @returns {Promise} - A promise that resolves when rendering is complete. + */ async renderSnapshot(renderer) { await renderer.render() } + /** + * Finishes rendering a snapshot using the specified renderer. + * + * @param {Object} renderer - The renderer to use for finishing rendering the snapshot. + */ finishRenderingSnapshot(renderer) { renderer.finishRendering() } diff --git a/src/util.js b/src/util.js index 9710deb86..99ac5a7ab 100644 --- a/src/util.js +++ b/src/util.js @@ -1,3 +1,13 @@ +/** + * Activates a script element, potentially modifying it based on its "data-turbo-eval" attribute and other settings. + * + * If the "data-turbo-eval" attribute is set to "false", the function returns the element unmodified. + * Otherwise, it creates a new script element, copies the original element's attributes and content to it, and sets + * additional attributes such as "async" and potentially a "nonce" based on the content of a "csp-nonce" meta tag. + * + * @param {HTMLElement} element - The script element to activate. + * @returns {HTMLElement} The activated script element, which may be a new element or the original element, depending on its attributes. + */ export function activateScriptElement(element) { if (element.getAttribute("data-turbo-eval") == "false") { return element @@ -14,24 +24,46 @@ export function activateScriptElement(element) { } } +/** + * Copies all attributes from the source element to the destination element. + * + * @param {HTMLElement} destinationElement - The element to which attributes should be copied. + * @param {HTMLElement} sourceElement - The element from which attributes should be copied. + */ function copyElementAttributes(destinationElement, sourceElement) { for (const { name, value } of sourceElement.attributes) { destinationElement.setAttribute(name, value) } } +/** + * Creates a document fragment from a string of HTML. + * + * @param {string} html - The HTML string to convert into a document fragment. + * @returns {DocumentFragment} A document fragment containing the nodes represented by the HTML string. + */ export function createDocumentFragment(html) { const template = document.createElement("template") template.innerHTML = html return template.content } +/** + * Dispatches a custom event with the specified name and options. + * + * @param {string} eventName - The name of the custom event to dispatch. + * @param {object} [options={}] - An object containing the options for the event. + * @param {HTMLElement} [options.target] - The target for the event. If not specified or the target is not connected, the event will be dispatched on the document.documentElement. + * @param {boolean} [options.cancelable] - Whether the event is cancelable. + * @param {*} [options.detail] - Any details to pass along with the event. + * @returns {CustomEvent} The custom event that was dispatched. + */ export function dispatch(eventName, { target, cancelable, detail } = {}) { const event = new CustomEvent(eventName, { cancelable, bubbles: true, composed: true, - detail + detail, }) if (target && target.isConnected) { @@ -43,22 +75,50 @@ export function dispatch(eventName, { target, cancelable, detail } = {}) { return event } +/** + * Returns a promise that resolves on the next animation frame. + * + * @returns {Promise} A promise that resolves on the next animation frame. + */ export function nextAnimationFrame() { return new Promise((resolve) => requestAnimationFrame(() => resolve())) } +/** + * Returns a promise that resolves on the next event loop tick. + * + * @returns {Promise} A promise that resolves on the next event loop tick. + */ export function nextEventLoopTick() { return new Promise((resolve) => setTimeout(() => resolve(), 0)) } +/** + * Returns a promise that resolves on the next microtask. + * + * @returns {Promise} A promise that resolves on the next microtask. + */ export function nextMicrotask() { return Promise.resolve() } +/** + * Parses an HTML string into a DOM Document. + * + * @param {string} [html=""] - The HTML string to parse. + * @returns {Document} A DOM Document representing the parsed HTML string. + */ export function parseHTMLDocument(html = "") { return new DOMParser().parseFromString(html, "text/html") } +/** + * Removes the common leading whitespace from every line in a template literal string. + * + * @param {TemplateStringsArray} strings - An array of strings from the template literal. + * @param {...*} values - The values to interpolate into the template literal. + * @returns {string} The unindented template literal string. + */ export function unindent(strings, ...values) { const lines = interpolate(strings, values).replace(/^\n/, "").split("\n") const match = lines[0].match(/^\s+/) @@ -66,6 +126,13 @@ export function unindent(strings, ...values) { return lines.map((line) => line.slice(indent)).join("\n") } +/** + * Interpolates the values into the template literal strings. + * + * @param {TemplateStringsArray} strings - An array of strings from the template literal. + * @param {Array} values - The values to interpolate into the template literal. + * @returns {string} The interpolated template literal string. + */ function interpolate(strings, values) { return strings.reduce((result, string, i) => { const value = values[i] == undefined ? "" : values[i] @@ -73,6 +140,11 @@ function interpolate(strings, values) { }, "") } +/** + * Generates a random UUID (Universally Unique Identifier) according to the UUID v4 specification. + * + * @returns {string} A random UUID. + */ export function uuid() { return Array.from({ length: 36 }) .map((_, i) => { @@ -89,6 +161,13 @@ export function uuid() { .join("") } +/** + * Gets the value of the specified attribute from the first element in the list of elements that has the attribute set. + * + * @param {string} attributeName - The name of the attribute to get. + * @param {...HTMLElement} elements - The elements to search for the attribute. + * @returns {string|null} The value of the attribute from the first element that has it set, or null if none of the elements have the attribute set. + */ export function getAttribute(attributeName, ...elements) { for (const value of elements.map((element) => element?.getAttribute(attributeName))) { if (typeof value == "string") return value @@ -97,10 +176,23 @@ export function getAttribute(attributeName, ...elements) { return null } +/** + * Checks if any of the specified elements has the specified attribute set. + * + * @param {string} attributeName - The name of the attribute to check for. + * @param {...HTMLElement} elements - The elements to check for the attribute. + * @returns {boolean} True if any of the elements has the attribute set, false otherwise. + */ export function hasAttribute(attributeName, ...elements) { return elements.some((element) => element && element.hasAttribute(attributeName)) } +/** + * Marks the specified elements as "busy" by setting the `busy` attribute (for elements with local name "turbo-frame") + * and the `aria-busy` attribute (for all elements) to true. + * + * @param {...HTMLElement} elements - The elements to mark as "busy". + */ export function markAsBusy(...elements) { for (const element of elements) { if (element.localName == "turbo-frame") { @@ -110,6 +202,12 @@ export function markAsBusy(...elements) { } } +/** + * Clears the "busy" state of the specified elements by removing the `busy` + * and `aria-busy` attributes. + * + * @param {...HTMLElement} elements - The elements to clear the "busy" state from. + */ export function clearBusyState(...elements) { for (const element of elements) { if (element.localName == "turbo-frame") { @@ -120,6 +218,13 @@ export function clearBusyState(...elements) { } } +/** + * Waits for the specified element to load or an error to occur, up to the specified timeout. + * + * @param {HTMLElement} element - The element to wait for. + * @param {number} timeoutInMilliseconds - The maximum time to wait, in milliseconds. + * @returns {Promise} A promise that resolves when the element has loaded, an error occurs, or the timeout is reached. + */ export function waitForLoad(element, timeoutInMilliseconds = 2000) { return new Promise((resolve) => { const onComplete = () => { @@ -134,6 +239,12 @@ export function waitForLoad(element, timeoutInMilliseconds = 2000) { }) } +/** + * Gets the appropriate history method for the specified action. + * + * @param {"replace"|"advance"|"restore"} action - The action to get the history method for. + * @returns {("replaceState"|"pushState")|undefined} The history method for the action, or undefined if the action is not recognized. + */ export function getHistoryMethodForAction(action) { switch (action) { case "replace": @@ -144,25 +255,56 @@ export function getHistoryMethodForAction(action) { } } +/** + * Checks if the specified action is a valid action. + * + * @param {string} action - The action to check. + * @returns {boolean} True if the action is a valid action, false otherwise. + */ export function isAction(action) { return action == "advance" || action == "replace" || action == "restore" } +/** + * Gets the visit action from the `data-turbo-action` attribute of the first element in the list that has the attribute set. + * + * @param {...HTMLElement} elements - The elements to get the visit action from. + * @returns {string|null} The visit action, or null if none of the elements has the `data-turbo-action` attribute set. + */ export function getVisitAction(...elements) { const action = getAttribute("data-turbo-action", ...elements) return isAction(action) ? action : null } +/** + * Gets the meta element with the specified name. + * + * @param {string} name - The name of the meta element to get. + * @returns {HTMLMetaElement|null} The meta element, or null if no meta element with the specified name is found. + */ export function getMetaElement(name) { return document.querySelector(`meta[name="${name}"]`) } +/** + * Gets the content of the meta element with the specified name. + * + * @param {string} name - The name of the meta element to get the content from. + * @returns {string|null} The content of the meta element, or null if no meta element with the specified name is found. + */ export function getMetaContent(name) { const element = getMetaElement(name) return element && element.content } +/** + * Sets the content of the meta element with the specified name, creating the element if necessary. + * + * @param {string} name - The name of the meta element to set the content for. + * @param {string} content - The content to set. + * @returns {HTMLMetaElement} The meta element with the content set. + */ export function setMetaContent(name, content) { let element = getMetaElement(name) @@ -178,6 +320,13 @@ export function setMetaContent(name, content) { return element } +/** + * Finds the closest ancestor of the specified element that matches the specified selector, including ancestors in shadow DOM trees. + * + * @param {HTMLElement} element - The element to start searching from. + * @param {string} selector - The selector to match against. + * @returns {HTMLElement|null} The closest matching ancestor, or null if no matching ancestor is found. + */ export function findClosestRecursively(element, selector) { if (element instanceof Element) { return (