import morphdom from "morphdom";

var name = "cable_ready";

var version = "5.0.6";

var description = "CableReady helps you create great real-time user experiences by making it simple to trigger client-side DOM changes from server-side Ruby.";

var keywords = [ "ruby", "rails", "websockets", "actioncable", "cable", "ssr", "stimulus_reflex", "client-side", "dom" ];

var homepage = "https://cableready.stimulusreflex.com";

var bugs = "https://github.com/stimulusreflex/cable_ready/issues";

var repository = "https://github.com/stimulusreflex/cable_ready";

var license = "MIT";

var author = "Nathan Hopkins <natehop@gmail.com>";

var contributors = [ "Andrew Mason <andrewmcodes@protonmail.com>", "Julian Rubisch <julian@julianrubisch.at>", "Marco Roth <marco.roth@intergga.ch>", "Nathan Hopkins <natehop@gmail.com>" ];

var main = "./dist/cable_ready.js";

var module = "./dist/cable_ready.js";

var browser = "./dist/cable_ready.js";

var unpkg = "./dist/cable_ready.umd.js";

var umd = "./dist/cable_ready.umd.js";

var files = [ "dist/*", "javascript/*" ];

var scripts = {
  lint: "yarn run format --check",
  format: "yarn run prettier-standard ./javascript/**/*.js rollup.config.mjs",
  build: "yarn rollup -c",
  watch: "yarn rollup -wc",
  test: "web-test-runner javascript/test/**/*.test.js",
  "docs:dev": "vitepress dev docs",
  "docs:build": "vitepress build docs && cp ./docs/_redirects ./docs/.vitepress/dist",
  "docs:preview": "vitepress preview docs"
};

var dependencies = {
  morphdom: "2.6.1"
};

var devDependencies = {
  "@open-wc/testing": "^4.0.0",
  "@rollup/plugin-json": "^6.1.0",
  "@rollup/plugin-node-resolve": "^15.3.0",
  "@rollup/plugin-terser": "^0.4.4",
  "@web/dev-server-esbuild": "^1.0.3",
  "@web/dev-server-rollup": "^0.6.4",
  "@web/test-runner": "^0.19.0",
  "prettier-standard": "^16.4.1",
  rollup: "^4.25.0",
  sinon: "^19.0.2",
  vite: "^5.4.10",
  vitepress: "^1.5.0",
  "vitepress-plugin-search": "^1.0.4-alpha.22"
};

var packageInfo = {
  name: name,
  version: version,
  description: description,
  keywords: keywords,
  homepage: homepage,
  bugs: bugs,
  repository: repository,
  license: license,
  author: author,
  contributors: contributors,
  main: main,
  module: module,
  browser: browser,
  import: "./dist/cable_ready.js",
  unpkg: unpkg,
  umd: umd,
  files: files,
  scripts: scripts,
  dependencies: dependencies,
  devDependencies: devDependencies
};

const inputTags = {
  INPUT: true,
  TEXTAREA: true,
  SELECT: true
};

const mutableTags = {
  INPUT: true,
  TEXTAREA: true,
  OPTION: true
};

const textInputTypes = {
  "datetime-local": true,
  "select-multiple": true,
  "select-one": true,
  color: true,
  date: true,
  datetime: true,
  email: true,
  month: true,
  number: true,
  password: true,
  range: true,
  search: true,
  tel: true,
  text: true,
  textarea: true,
  time: true,
  url: true,
  week: true
};

let activeElement;

var ActiveElement = {
  get element() {
    return activeElement;
  },
  set(element) {
    activeElement = element;
  }
};

// Indicates if the passed element is considered a text input.

const isTextInput = element => inputTags[element.tagName] && textInputTypes[element.type];

// Assigns focus to the appropriate element... preferring the explicitly passed selector

// * selector - a CSS selector for the element that should have focus

const assignFocus = selector => {
  const element = selector && selector.nodeType === Node.ELEMENT_NODE ? selector : document.querySelector(selector);
  const focusElement = element || ActiveElement.element;
  if (focusElement && focusElement.focus) focusElement.focus();
};

// Dispatches an event on the passed element

// * element - the element
// * name - the name of the event
// * detail - the event detail

const dispatch = (element, name, detail = {}) => {
  const init = {
    bubbles: true,
    cancelable: true,
    detail: detail
  };
  const event = new CustomEvent(name, init);
  element.dispatchEvent(event);
  if (window.jQuery) window.jQuery(element).trigger(name, detail);
};

// Accepts an xPath query and returns the element found at that position in the DOM

const xpathToElement = xpath => document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;

// Accepts an xPath query and returns all matching elements in the DOM

const xpathToElementArray = (xpath, reverse = false) => {
  const snapshotList = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
  const snapshots = [];
  for (let i = 0; i < snapshotList.snapshotLength; i++) {
    snapshots.push(snapshotList.snapshotItem(i));
  }
  return reverse ? snapshots.reverse() : snapshots;
};

// Return an array with the class names to be used

// * names - could be a string or an array of strings for multiple classes.

const getClassNames = names => Array.from(names).flat()
// Perform operation for either the first or all of the elements returned by CSS selector

// * operation - the instruction payload from perform
// * callback - the operation function to run for each element

;

const processElements = (operation, callback) => {
  Array.from(operation.selectAll ? operation.element : [ operation.element ]).forEach(callback);
};

// convert string to kebab-case
// most other implementations (lodash) are focused on camelCase to kebab-case
// instead, this uses word token boundaries to produce readable URL slugs and keys
// this implementation will not support Emoji or other non-ASCII characters

const kebabize = createCompounder((function(result, word, index) {
  return result + (index ? "-" : "") + word.toLowerCase();
}));

function createCompounder(callback) {
  return function(str) {
    return words(str).reduce(callback, "");
  };
}

const words = str => {
  str = str == null ? "" : str;
  return str.match(/([A-Z]{2,}|[0-9]+|[A-Z]?[a-z]+|[A-Z])/g) || [];
};

// Provide a standardized pipeline of checks and modifications to all operations based on provided options
// Currently skips execution if cancelled and implements an optional delay

const operate = (operation, callback) => {
  if (!operation.cancel) {
    operation.delay ? setTimeout(callback, operation.delay) : callback();
    return true;
  }
  return false;
};

// Dispatch life-cycle events with standardized naming
const before = (target, operation) => dispatch(target, `cable-ready:before-${kebabize(operation.operation)}`, operation);

const after = (target, operation) => dispatch(target, `cable-ready:after-${kebabize(operation.operation)}`, operation);

function debounce(fn, delay = 250) {
  let timer;
  return (...args) => {
    const callback = () => fn.apply(this, args);
    if (timer) clearTimeout(timer);
    timer = setTimeout(callback, delay);
  };
}

function handleErrors(response) {
  if (!response.ok) throw Error(response.statusText);
  return response;
}

function safeScalar(val) {
  if (val !== undefined && ![ "string", "number", "boolean" ].includes(typeof val)) console.warn(`Operation expects a string, number or boolean, but got ${val} (${typeof val})`);
  return val != null ? val : "";
}

function safeString(str) {
  if (str !== undefined && typeof str !== "string") console.warn(`Operation expects a string, but got ${str} (${typeof str})`);
  return str != null ? String(str) : "";
}

function safeArray(arr) {
  if (arr !== undefined && !Array.isArray(arr)) console.warn(`Operation expects an array, but got ${arr} (${typeof arr})`);
  return arr != null ? Array.from(arr) : [];
}

function safeObject(obj) {
  if (obj !== undefined && typeof obj !== "object") console.warn(`Operation expects an object, but got ${obj} (${typeof obj})`);
  return obj != null ? Object(obj) : {};
}

function safeStringOrArray(elem) {
  if (elem !== undefined && !Array.isArray(elem) && typeof elem !== "string") console.warn(`Operation expects an Array or a String, but got ${elem} (${typeof elem})`);
  return elem == null ? "" : Array.isArray(elem) ? Array.from(elem) : String(elem);
}

function fragmentToString(fragment) {
  return (new XMLSerializer).serializeToString(fragment);
}

// A proxy method to wrap a fetch call in error handling

// * url - the URL to fetch
// * additionalHeaders - an object of additional headers passed to fetch

async function graciouslyFetch(url, additionalHeaders) {
  try {
    const response = await fetch(url, {
      headers: {
        "X-REQUESTED-WITH": "XmlHttpRequest",
        ...additionalHeaders
      }
    });
    if (response == undefined) return;
    handleErrors(response);
    return response;
  } catch (e) {
    console.error(`Could not fetch ${url}`);
  }
}

class BoundedQueue {
  constructor(maxSize) {
    this.maxSize = maxSize;
    this.queue = [];
  }
  push(item) {
    if (this.isFull()) {
      // Remove the oldest item to make space for the new one
      this.shift();
    }
    this.queue.push(item);
  }
  shift() {
    return this.queue.shift();
  }
  isFull() {
    return this.queue.length === this.maxSize;
  }
}

var utils =  Object.freeze({
  __proto__: null,
  BoundedQueue: BoundedQueue,
  after: after,
  assignFocus: assignFocus,
  before: before,
  debounce: debounce,
  dispatch: dispatch,
  fragmentToString: fragmentToString,
  getClassNames: getClassNames,
  graciouslyFetch: graciouslyFetch,
  handleErrors: handleErrors,
  isTextInput: isTextInput,
  kebabize: kebabize,
  operate: operate,
  processElements: processElements,
  safeArray: safeArray,
  safeObject: safeObject,
  safeScalar: safeScalar,
  safeString: safeString,
  safeStringOrArray: safeStringOrArray,
  xpathToElement: xpathToElement,
  xpathToElementArray: xpathToElementArray
});

// Indicates whether or not we should morph an element via onBeforeElUpdated callback
// SEE: https://github.com/patrick-steele-idem/morphdom#morphdomfromnode-tonode-options--node

const shouldMorph = operation => (fromEl, toEl) => !shouldMorphCallbacks.map((callback => typeof callback === "function" ? callback(operation, fromEl, toEl) : true)).includes(false)
// Execute any pluggable functions that modify elements after morphing via onElUpdated callback

;

const didMorph = operation => el => {
  didMorphCallbacks.forEach((callback => {
    if (typeof callback === "function") callback(operation, el);
  }));
};

const verifyNotMutable = (detail, fromEl, toEl) => {
  // Skip nodes that are equal:
  // https://github.com/patrick-steele-idem/morphdom#can-i-make-morphdom-blaze-through-the-dom-tree-even-faster-yes
  if (!mutableTags[fromEl.tagName] && fromEl.isEqualNode(toEl)) return false;
  return true;
};

const verifyNotContentEditable = (detail, fromEl, toEl) => {
  if (fromEl === ActiveElement.element && fromEl.isContentEditable) return false;
  return true;
};

const verifyNotPermanent = (detail, fromEl, toEl) => {
  const {permanentAttributeName: permanentAttributeName} = detail;
  if (!permanentAttributeName) return true;
  const permanent = fromEl.closest(`[${permanentAttributeName}]`);
  // only morph attributes on the active non-permanent text input
    if (!permanent && fromEl === ActiveElement.element && isTextInput(fromEl)) {
    const ignore = {
      value: true
    };
    Array.from(toEl.attributes).forEach((attribute => {
      if (!ignore[attribute.name]) fromEl.setAttribute(attribute.name, attribute.value);
    }));
    return false;
  }
  return !permanent;
};

const shouldMorphCallbacks = [ verifyNotMutable, verifyNotPermanent, verifyNotContentEditable ];

const didMorphCallbacks = [];

var morph_callbacks =  Object.freeze({
  __proto__: null,
  didMorph: didMorph,
  didMorphCallbacks: didMorphCallbacks,
  shouldMorph: shouldMorph,
  shouldMorphCallbacks: shouldMorphCallbacks,
  verifyNotContentEditable: verifyNotContentEditable,
  verifyNotMutable: verifyNotMutable,
  verifyNotPermanent: verifyNotPermanent
});

var Operations = {
  // DOM Mutations
  append: operation => {
    processElements(operation, (element => {
      before(element, operation);
      operate(operation, (() => {
        const {html: html, focusSelector: focusSelector} = operation;
        element.insertAdjacentHTML("beforeend", safeScalar(html));
        assignFocus(focusSelector);
      }));
      after(element, operation);
    }));
  },
  graft: operation => {
    processElements(operation, (element => {
      before(element, operation);
      operate(operation, (() => {
        const {parent: parent, focusSelector: focusSelector} = operation;
        const parentElement = document.querySelector(parent);
        if (parentElement) {
          parentElement.appendChild(element);
          assignFocus(focusSelector);
        }
      }));
      after(element, operation);
    }));
  },
  innerHtml: operation => {
    processElements(operation, (element => {
      before(element, operation);
      operate(operation, (() => {
        const {html: html, focusSelector: focusSelector} = operation;
        element.innerHTML = safeScalar(html);
        assignFocus(focusSelector);
      }));
      after(element, operation);
    }));
  },
  insertAdjacentHtml: operation => {
    processElements(operation, (element => {
      before(element, operation);
      operate(operation, (() => {
        const {html: html, position: position, focusSelector: focusSelector} = operation;
        element.insertAdjacentHTML(position || "beforeend", safeScalar(html));
        assignFocus(focusSelector);
      }));
      after(element, operation);
    }));
  },
  insertAdjacentText: operation => {
    processElements(operation, (element => {
      before(element, operation);
      operate(operation, (() => {
        const {text: text, position: position, focusSelector: focusSelector} = operation;
        element.insertAdjacentText(position || "beforeend", safeScalar(text));
        assignFocus(focusSelector);
      }));
      after(element, operation);
    }));
  },
  outerHtml: operation => {
    processElements(operation, (element => {
      const parent = element.parentElement;
      const idx = parent && Array.from(parent.children).indexOf(element);
      before(element, operation);
      operate(operation, (() => {
        const {html: html, focusSelector: focusSelector} = operation;
        element.outerHTML = safeScalar(html);
        assignFocus(focusSelector);
      }));
      after(parent ? parent.children[idx] : document.documentElement, operation);
    }));
  },
  prepend: operation => {
    processElements(operation, (element => {
      before(element, operation);
      operate(operation, (() => {
        const {html: html, focusSelector: focusSelector} = operation;
        element.insertAdjacentHTML("afterbegin", safeScalar(html));
        assignFocus(focusSelector);
      }));
      after(element, operation);
    }));
  },
  remove: operation => {
    processElements(operation, (element => {
      before(element, operation);
      operate(operation, (() => {
        const {focusSelector: focusSelector} = operation;
        element.remove();
        assignFocus(focusSelector);
      }));
      after(document, operation);
    }));
  },
  replace: operation => {
    processElements(operation, (element => {
      const parent = element.parentElement;
      const idx = parent && Array.from(parent.children).indexOf(element);
      before(element, operation);
      operate(operation, (() => {
        const {html: html, focusSelector: focusSelector} = operation;
        element.outerHTML = safeScalar(html);
        assignFocus(focusSelector);
      }));
      after(parent ? parent.children[idx] : document.documentElement, operation);
    }));
  },
  textContent: operation => {
    processElements(operation, (element => {
      before(element, operation);
      operate(operation, (() => {
        const {text: text, focusSelector: focusSelector} = operation;
        element.textContent = safeScalar(text);
        assignFocus(focusSelector);
      }));
      after(element, operation);
    }));
  },
  // Element Property Mutations
  addCssClass: operation => {
    processElements(operation, (element => {
      before(element, operation);
      operate(operation, (() => {
        const {name: name} = operation;
        element.classList.add(...getClassNames([ safeStringOrArray(name) ]));
      }));
      after(element, operation);
    }));
  },
  removeAttribute: operation => {
    processElements(operation, (element => {
      before(element, operation);
      operate(operation, (() => {
        const {name: name} = operation;
        element.removeAttribute(safeString(name));
      }));
      after(element, operation);
    }));
  },
  removeCssClass: operation => {
    processElements(operation, (element => {
      before(element, operation);
      operate(operation, (() => {
        const {name: name} = operation;
        element.classList.remove(...getClassNames([ safeStringOrArray(name) ]));
        if (element.classList.length === 0) element.removeAttribute("class");
      }));
      after(element, operation);
    }));
  },
  setAttribute: operation => {
    processElements(operation, (element => {
      before(element, operation);
      operate(operation, (() => {
        const {name: name, value: value} = operation;
        element.setAttribute(safeString(name), safeScalar(value));
      }));
      after(element, operation);
    }));
  },
  setDatasetProperty: operation => {
    processElements(operation, (element => {
      before(element, operation);
      operate(operation, (() => {
        const {name: name, value: value} = operation;
        element.dataset[safeString(name)] = safeScalar(value);
      }));
      after(element, operation);
    }));
  },
  setProperty: operation => {
    processElements(operation, (element => {
      before(element, operation);
      operate(operation, (() => {
        const {name: name, value: value} = operation;
        if (name in element) element[safeString(name)] = safeScalar(value);
      }));
      after(element, operation);
    }));
  },
  setStyle: operation => {
    processElements(operation, (element => {
      before(element, operation);
      operate(operation, (() => {
        const {name: name, value: value} = operation;
        element.style[safeString(name)] = safeScalar(value);
      }));
      after(element, operation);
    }));
  },
  setStyles: operation => {
    processElements(operation, (element => {
      before(element, operation);
      operate(operation, (() => {
        const {styles: styles} = operation;
        for (let [name, value] of Object.entries(styles)) element.style[safeString(name)] = safeScalar(value);
      }));
      after(element, operation);
    }));
  },
  setValue: operation => {
    processElements(operation, (element => {
      before(element, operation);
      operate(operation, (() => {
        const {value: value} = operation;
        element.value = safeScalar(value);
      }));
      after(element, operation);
    }));
  },
  // DOM Events and Meta-Operations
  dispatchEvent: operation => {
    processElements(operation, (element => {
      before(element, operation);
      operate(operation, (() => {
        const {name: name, detail: detail} = operation;
        dispatch(element, safeString(name), safeObject(detail));
      }));
      after(element, operation);
    }));
  },
  setMeta: operation => {
    before(document, operation);
    operate(operation, (() => {
      const {name: name, content: content} = operation;
      let meta = document.head.querySelector(`meta[name='${name}']`);
      if (!meta) {
        meta = document.createElement("meta");
        meta.name = safeString(name);
        document.head.appendChild(meta);
      }
      meta.content = safeScalar(content);
    }));
    after(document, operation);
  },
  setTitle: operation => {
    before(document, operation);
    operate(operation, (() => {
      const {title: title} = operation;
      document.title = safeScalar(title);
    }));
    after(document, operation);
  },
  // Browser Manipulations
  clearStorage: operation => {
    before(document, operation);
    operate(operation, (() => {
      const {type: type} = operation;
      const storage = type === "session" ? sessionStorage : localStorage;
      storage.clear();
    }));
    after(document, operation);
  },
  go: operation => {
    before(window, operation);
    operate(operation, (() => {
      const {delta: delta} = operation;
      history.go(delta);
    }));
    after(window, operation);
  },
  pushState: operation => {
    before(window, operation);
    operate(operation, (() => {
      const {state: state, title: title, url: url} = operation;
      history.pushState(safeObject(state), safeString(title), safeString(url));
    }));
    after(window, operation);
  },
  redirectTo: operation => {
    before(window, operation);
    operate(operation, (() => {
      let {url: url, action: action, turbo: turbo} = operation;
      action = action || "advance";
      url = safeString(url);
      if (turbo === undefined) turbo = true;
      if (turbo) {
        if (window.Turbo) window.Turbo.visit(url, {
          action: action
        });
        if (window.Turbolinks) window.Turbolinks.visit(url, {
          action: action
        });
        if (!window.Turbo && !window.Turbolinks) window.location.href = url;
      } else {
        window.location.href = url;
      }
    }));
    after(window, operation);
  },
  reload: operation => {
    before(window, operation);
    operate(operation, (() => {
      window.location.reload();
    }));
    after(window, operation);
  },
  removeStorageItem: operation => {
    before(document, operation);
    operate(operation, (() => {
      const {key: key, type: type} = operation;
      const storage = type === "session" ? sessionStorage : localStorage;
      storage.removeItem(safeString(key));
    }));
    after(document, operation);
  },
  replaceState: operation => {
    before(window, operation);
    operate(operation, (() => {
      const {state: state, title: title, url: url} = operation;
      history.replaceState(safeObject(state), safeString(title), safeString(url));
    }));
    after(window, operation);
  },
  scrollIntoView: operation => {
    const {element: element} = operation;
    before(element, operation);
    operate(operation, (() => {
      element.scrollIntoView(operation);
    }));
    after(element, operation);
  },
  setCookie: operation => {
    before(document, operation);
    operate(operation, (() => {
      const {cookie: cookie} = operation;
      document.cookie = safeScalar(cookie);
    }));
    after(document, operation);
  },
  setFocus: operation => {
    const {element: element} = operation;
    before(element, operation);
    operate(operation, (() => {
      assignFocus(element);
    }));
    after(element, operation);
  },
  setStorageItem: operation => {
    before(document, operation);
    operate(operation, (() => {
      const {key: key, value: value, type: type} = operation;
      const storage = type === "session" ? sessionStorage : localStorage;
      storage.setItem(safeString(key), safeScalar(value));
    }));
    after(document, operation);
  },
  // Notifications
  consoleLog: operation => {
    before(document, operation);
    operate(operation, (() => {
      const {message: message, level: level} = operation;
      level && [ "warn", "info", "error" ].includes(level) ? console[level](message) : console.log(message);
    }));
    after(document, operation);
  },
  consoleTable: operation => {
    before(document, operation);
    operate(operation, (() => {
      const {data: data, columns: columns} = operation;
      console.table(data, safeArray(columns));
    }));
    after(document, operation);
  },
  notification: operation => {
    before(document, operation);
    operate(operation, (() => {
      const {title: title, options: options} = operation;
      Notification.requestPermission().then((result => {
        operation.permission = result;
        if (result === "granted") new Notification(safeString(title), safeObject(options));
      }));
    }));
    after(document, operation);
  },
  // Morph operations
  morph: operation => {
    processElements(operation, (element => {
      const {html: html} = operation;
      const template = document.createElement("template");
      template.innerHTML = String(safeScalar(html)).trim();
      operation.content = template.content;
      const parent = element.parentElement;
      const idx = parent && Array.from(parent.children).indexOf(element);
      before(element, operation);
      operate(operation, (() => {
        const {childrenOnly: childrenOnly, focusSelector: focusSelector} = operation;
        morphdom(element, childrenOnly ? template.content : template.innerHTML, {
          childrenOnly: !!childrenOnly,
          onBeforeElUpdated: shouldMorph(operation),
          onElUpdated: didMorph(operation)
        });
        assignFocus(focusSelector);
      }));
      after(parent ? parent.children[idx] : document.documentElement, operation);
    }));
  }
};

let operations = Operations;

const add = newOperations => {
  operations = {
    ...operations,
    ...newOperations
  };
};

const addOperations = operations => {
  add(operations);
};

const addOperation = (name, operation) => {
  const operations = {};
  operations[name] = operation;
  add(operations);
};

var OperationStore = {
  get all() {
    return operations;
  }
};

let missingElement = "warn";

var MissingElement = {
  get behavior() {
    return missingElement;
  },
  set(value) {
    if ([ "warn", "ignore", "event", "exception" ].includes(value)) missingElement = value; else console.warn("Invalid 'onMissingElement' option. Defaulting to 'warn'.");
  }
};

const perform = (operations, options = {
  onMissingElement: MissingElement.behavior
}) => {
  const batches = {};
  operations.forEach((operation => {
    if (!!operation.batch) batches[operation.batch] = batches[operation.batch] ? ++batches[operation.batch] : 1;
  }));
  operations.forEach((operation => {
    const name = operation.operation;
    try {
      if (operation.selector) {
        if (operation.xpath) {
          operation.element = operation.selectAll ? xpathToElementArray(operation.selector) : xpathToElement(operation.selector);
        } else {
          operation.element = operation.selectAll ? document.querySelectorAll(operation.selector) : document.querySelector(operation.selector);
        }
      } else {
        operation.element = document;
      }
      if (operation.element || options.onMissingElement !== "ignore") {
        ActiveElement.set(document.activeElement);
        const cableReadyOperation = OperationStore.all[name];
        if (cableReadyOperation) {
          cableReadyOperation(operation);
          if (!!operation.batch && --batches[operation.batch] === 0) dispatch(document, "cable-ready:batch-complete", {
            batch: operation.batch
          });
        } else {
          console.error(`CableReady couldn't find the "${name}" operation. Make sure you use the camelized form when calling an operation method.`);
        }
      }
    } catch (e) {
      if (operation.element) {
        console.error(`CableReady detected an error in ${name || "operation"}: ${e.message}. If you need to support older browsers make sure you've included the corresponding polyfills. https://docs.stimulusreflex.com/setup#polyfills-for-ie11.`);
        console.error(e);
      } else {
        const warning = `CableReady ${name || ""} operation failed due to missing DOM element for selector: '${operation.selector}'`;
        switch (options.onMissingElement) {
         case "ignore":
          break;

         case "event":
          dispatch(document, "cable-ready:missing-element", {
            warning: warning,
            operation: operation
          });
          break;

         case "exception":
          throw warning;

         default:
          console.warn(warning);
        }
      }
    }
  }));
};

const performAsync = (operations, options = {
  onMissingElement: MissingElement.behavior
}) => new Promise(((resolve, reject) => {
  try {
    resolve(perform(operations, options));
  } catch (err) {
    reject(err);
  }
}));

class SubscribingElement extends HTMLElement {
  static get tagName() {
    throw new Error("Implement the tagName() getter in the inheriting class");
  }
  static define() {
    if (!customElements.get(this.tagName)) {
      customElements.define(this.tagName, this);
    }
  }
  disconnectedCallback() {
    if (this.channel) this.channel.unsubscribe();
  }
  createSubscription(consumer, channel, receivedCallback) {
    this.channel = consumer.subscriptions.create({
      channel: channel,
      identifier: this.identifier
    }, {
      received: receivedCallback
    });
  }
  get preview() {
    return document.documentElement.hasAttribute("data-turbolinks-preview") || document.documentElement.hasAttribute("data-turbo-preview");
  }
  get identifier() {
    return this.getAttribute("identifier");
  }
}

let consumer;

const BACKOFF = [ 25, 50, 75, 100, 200, 250, 500, 800, 1e3, 2e3 ];

const wait = ms => new Promise((resolve => setTimeout(resolve, ms)));

const getConsumerWithRetry = async (retry = 0) => {
  if (consumer) return consumer;
  if (retry >= BACKOFF.length) {
    throw new Error("Couldn't obtain a Action Cable consumer within 5s");
  }
  await wait(BACKOFF[retry]);
  return await getConsumerWithRetry(retry + 1);
};

var CableConsumer = {
  setConsumer(value) {
    consumer = value;
  },
  get consumer() {
    return consumer;
  },
  async getConsumer() {
    return await getConsumerWithRetry();
  }
};

class StreamFromElement extends SubscribingElement {
  static get tagName() {
    return "cable-ready-stream-from";
  }
  async connectedCallback() {
    if (this.preview) return;
    const consumer = await CableConsumer.getConsumer();
    if (consumer) {
      this.createSubscription(consumer, "CableReady::Stream", this.performOperations.bind(this));
    } else {
      console.error("The `cable_ready_stream_from` helper cannot connect. You must initialize CableReady with an Action Cable consumer.");
    }
  }
  performOperations(data) {
    if (data.cableReady) perform(data.operations, {
      onMissingElement: this.onMissingElement
    });
  }
  get onMissingElement() {
    const value = this.getAttribute("missing") || MissingElement.behavior;
    // stream_from does not support raising exceptions on missing elements because there's no way to catch them
        if ([ "warn", "ignore", "event" ].includes(value)) return value; else {
      console.warn("Invalid 'missing' attribute. Defaulting to 'warn'.");
      return "warn";
    }
  }
}

let debugging = false;

var Debug = {
  get enabled() {
    return debugging;
  },
  get disabled() {
    return !debugging;
  },
  get value() {
    return debugging;
  },
  set(value) {
    debugging = !!value;
  },
  set debug(value) {
    debugging = !!value;
  }
};

const request = (data, blocks) => {
  if (Debug.disabled) return;
  const message = `↑ Updatable request affecting ${blocks.length} element(s): `;
  console.log(message, {
    elements: blocks.map((b => b.element)),
    identifiers: blocks.map((b => b.element.getAttribute("identifier"))),
    data: data
  });
  return message;
};

const cancel = (timestamp, reason) => {
  if (Debug.disabled) return;
  const duration = new Date - timestamp;
  const message = `❌ Updatable request canceled after ${duration}ms: ${reason}`;
  console.log(message);
  return message;
};

const response = (timestamp, element, urls) => {
  if (Debug.disabled) return;
  const duration = new Date - timestamp;
  const message = `↓ Updatable response: All URLs fetched in ${duration}ms`;
  console.log(message, {
    element: element,
    urls: urls
  });
  return message;
};

const morphStart = (timestamp, element) => {
  if (Debug.disabled) return;
  const duration = new Date - timestamp;
  const message = `↻ Updatable morph: starting after ${duration}ms`;
  console.log(message, {
    element: element
  });
  return message;
};

const morphEnd = (timestamp, element) => {
  if (Debug.disabled) return;
  const duration = new Date - timestamp;
  const message = `↺ Updatable morph: completed after ${duration}ms`;
  console.log(message, {
    element: element
  });
  return message;
};

var Log = {
  request: request,
  cancel: cancel,
  response: response,
  morphStart: morphStart,
  morphEnd: morphEnd
};

class AppearanceObserver {
  constructor(delegate, element = null) {
    this.delegate = delegate;
    this.element = element || delegate;
    this.started = false;
    this.intersecting = false;
    this.intersectionObserver = new IntersectionObserver(this.intersect);
  }
  start() {
    if (!this.started) {
      this.started = true;
      this.intersectionObserver.observe(this.element);
      this.observeVisibility();
    }
  }
  stop() {
    if (this.started) {
      this.started = false;
      this.intersectionObserver.unobserve(this.element);
      this.unobserveVisibility();
    }
  }
  observeVisibility=() => {
    document.addEventListener("visibilitychange", this.handleVisibilityChange);
  };
  unobserveVisibility=() => {
    document.removeEventListener("visibilitychange", this.handleVisibilityChange);
  };
  intersect=entries => {
    entries.forEach((entry => {
      if (entry.target === this.element) {
        if (entry.isIntersecting && document.visibilityState === "visible") {
          this.intersecting = true;
          this.delegate.appearedInViewport();
        } else {
          this.intersecting = false;
          this.delegate.disappearedFromViewport();
        }
      }
    }));
  };
  handleVisibilityChange=event => {
    if (document.visibilityState === "visible" && this.intersecting) {
      this.delegate.appearedInViewport();
    } else {
      this.delegate.disappearedFromViewport();
    }
  };
}

const template = `\n<style>\n  :host {\n    display: block;\n  }\n</style>\n<slot></slot>\n`;

class UpdatesForElement extends SubscribingElement {
  static get tagName() {
    return "cable-ready-updates-for";
  }
  constructor() {
    super();
    const shadowRoot = this.attachShadow({
      mode: "open"
    });
    shadowRoot.innerHTML = template;
    this.triggerElementLog = new BoundedQueue(10);
    this.targetElementLog = new BoundedQueue(10);
    this.appearanceObserver = new AppearanceObserver(this);
    this.visible = false;
    this.didTransitionToVisible = false;
  }
  async connectedCallback() {
    if (this.preview) return;
    this.update = debounce(this.update.bind(this), this.debounce);
    const consumer = await CableConsumer.getConsumer();
    if (consumer) {
      this.createSubscription(consumer, "CableReady::Stream", this.update);
    } else {
      console.error("The `cable_ready_updates_for` helper cannot connect. You must initialize CableReady with an Action Cable consumer.");
    }
    if (this.observeAppearance) {
      this.appearanceObserver.start();
    }
  }
  disconnectedCallback() {
    super.disconnectedCallback();
    if (this.observeAppearance) {
      this.appearanceObserver.stop();
    }
  }
  async update(data) {
    this.lastUpdateTimestamp = new Date;
    const blocks = Array.from(document.querySelectorAll(this.query), (element => new Block(element))).filter((block => block.shouldUpdate(data)));
    this.triggerElementLog.push(`${(new Date).toLocaleString()}: ${Log.request(data, blocks)}`);
    if (blocks.length === 0) {
      this.triggerElementLog.push(`${(new Date).toLocaleString()}: ${Log.cancel(this.lastUpdateTimestamp, "All elements filtered out")}`);
      return;
    }
    // first <cable-ready-updates-for> element in the DOM *at any given moment* updates all of the others
    // if the element becomes visible though, we have to overrule and load it
        if (blocks[0].element !== this && !this.didTransitionToVisible) {
      this.triggerElementLog.push(`${(new Date).toLocaleString()}: ${Log.cancel(this.lastUpdateTimestamp, "Update already requested")}`);
      return;
    }
    // hold a reference to the active element so that it can be restored after the morph
        ActiveElement.set(document.activeElement);
    // store all retrieved HTML in an object keyed by URL to minimize fetch calls
        this.html = {};
    const uniqueUrls = [ ...new Set(blocks.map((block => block.url))) ];
    await Promise.all(uniqueUrls.map((async url => {
      if (!this.html.hasOwnProperty(url)) {
        const response = await graciouslyFetch(url, {
          "X-Cable-Ready": "update"
        });
        this.html[url] = await response.text();
      }
    })));
    this.triggerElementLog.push(`${(new Date).toLocaleString()}: ${Log.response(this.lastUpdateTimestamp, this, uniqueUrls)}`);
    // track current block index for each URL; referred to as fragments
        this.index = {};
    blocks.forEach((block => {
      // if the block's URL is not in the index, initialize it to 0; otherwise, increment it
      this.index.hasOwnProperty(block.url) ? this.index[block.url]++ : this.index[block.url] = 0;
      block.process(data, this.html, this.index, this.lastUpdateTimestamp);
    }));
  }
  appearedInViewport() {
    if (!this.visible) {
      // transition from invisible to visible forces update
      this.didTransitionToVisible = true;
      this.update({});
    }
    this.visible = true;
  }
  disappearedFromViewport() {
    this.visible = false;
  }
  get query() {
    return `${this.tagName}[identifier="${this.identifier}"]`;
  }
  get identifier() {
    return this.getAttribute("identifier");
  }
  get debounce() {
    return this.hasAttribute("debounce") ? parseInt(this.getAttribute("debounce")) : 20;
  }
  get observeAppearance() {
    return this.hasAttribute("observe-appearance");
  }
}

class Block {
  constructor(element) {
    this.element = element;
  }
  async process(data, html, fragmentsIndex, startTimestamp) {
    const blockIndex = fragmentsIndex[this.url];
    const template = document.createElement("template");
    this.element.setAttribute("updating", "updating");
    template.innerHTML = String(html[this.url]).trim();
    await this.resolveTurboFrames(template.content);
    const fragments = template.content.querySelectorAll(this.query);
    if (fragments.length <= blockIndex) {
      console.warn(`Update aborted due to insufficient number of elements. The offending url is ${this.url}, the offending element is:`, this.element);
      return;
    }
    const operation = {
      element: this.element,
      html: fragments[blockIndex],
      permanentAttributeName: "data-ignore-updates"
    };
    dispatch(this.element, "cable-ready:before-update", operation);
    this.element.targetElementLog.push(`${(new Date).toLocaleString()}: ${Log.morphStart(startTimestamp, this.element)}`);
    morphdom(this.element, fragments[blockIndex], {
      childrenOnly: true,
      onBeforeElUpdated: shouldMorph(operation),
      onElUpdated: _ => {
        this.element.removeAttribute("updating");
        this.element.didTransitionToVisible = false;
        dispatch(this.element, "cable-ready:after-update", operation);
        assignFocus(operation.focusSelector);
      }
    });
    this.element.targetElementLog.push(`${(new Date).toLocaleString()}: ${Log.morphEnd(startTimestamp, this.element)}`);
  }
  async resolveTurboFrames(documentFragment) {
    const reloadingTurboFrames = [ ...documentFragment.querySelectorAll('turbo-frame[src]:not([loading="lazy"])') ];
    return Promise.all(reloadingTurboFrames.map((frame => new Promise((async resolve => {
      const frameResponse = await graciouslyFetch(frame.getAttribute("src"), {
        "Turbo-Frame": frame.id,
        "X-Cable-Ready": "update"
      });
      const frameTemplate = document.createElement("template");
      frameTemplate.innerHTML = await frameResponse.text();
      // recurse here to get all nested eager loaded frames
            await this.resolveTurboFrames(frameTemplate.content);
      const selector = `turbo-frame#${frame.id}`;
      const frameContent = frameTemplate.content.querySelector(selector);
      const content = frameContent ? frameContent.innerHTML.trim() : "";
      documentFragment.querySelector(selector).innerHTML = content;
      resolve();
    })))));
  }
  shouldUpdate(data) {
    // if everything that could prevent an update is false, update this block
    return !this.ignoresInnerUpdates && this.hasChangesSelectedForUpdate(data) && (!this.observeAppearance || this.visible);
  }
  hasChangesSelectedForUpdate(data) {
    // if there's an only attribute, only update if at least one of the attributes changed is in the allow list
    const only = this.element.getAttribute("only");
    return !(only && data.changed && !only.split(" ").some((attribute => data.changed.includes(attribute))));
  }
  get ignoresInnerUpdates() {
    // don't update during a Reflex or Turbolinks redraw
    return this.element.hasAttribute("ignore-inner-updates") && this.element.hasAttribute("performing-inner-update");
  }
  get url() {
    return this.element.hasAttribute("url") ? this.element.getAttribute("url") : location.href;
  }
  get identifier() {
    return this.element.identifier;
  }
  get query() {
    return this.element.query;
  }
  get visible() {
    return this.element.visible;
  }
  get observeAppearance() {
    return this.element.observeAppearance;
  }
}

const registerInnerUpdates = () => {
  document.addEventListener("stimulus-reflex:before", (event => {
    recursiveMarkUpdatesForElements(event.detail.element);
  }));
  document.addEventListener("stimulus-reflex:after", (event => {
    setTimeout((() => {
      recursiveUnmarkUpdatesForElements(event.detail.element);
    }));
  }));
  document.addEventListener("turbo:submit-start", (event => {
    recursiveMarkUpdatesForElements(event.target);
  }));
  document.addEventListener("turbo:submit-end", (event => {
    setTimeout((() => {
      recursiveUnmarkUpdatesForElements(event.target);
    }));
  }));
  document.addEventListener("turbo-boost:command:start", (event => {
    recursiveMarkUpdatesForElements(event.target);
  }));
  document.addEventListener("turbo-boost:command:finish", (event => {
    setTimeout((() => {
      recursiveUnmarkUpdatesForElements(event.target);
    }));
  }));
  document.addEventListener("turbo-boost:command:error", (event => {
    setTimeout((() => {
      recursiveUnmarkUpdatesForElements(event.target);
    }));
  }));
};

const recursiveMarkUpdatesForElements = leaf => {
  const closestUpdatesFor = leaf && leaf.parentElement && leaf.parentElement.closest("cable-ready-updates-for");
  if (closestUpdatesFor) {
    closestUpdatesFor.setAttribute("performing-inner-update", "");
    recursiveMarkUpdatesForElements(closestUpdatesFor);
  }
};

const recursiveUnmarkUpdatesForElements = leaf => {
  const closestUpdatesFor = leaf && leaf.parentElement && leaf.parentElement.closest("cable-ready-updates-for");
  if (closestUpdatesFor) {
    closestUpdatesFor.removeAttribute("performing-inner-update");
    recursiveUnmarkUpdatesForElements(closestUpdatesFor);
  }
};

const defineElements = () => {
  registerInnerUpdates();
  StreamFromElement.define();
  UpdatesForElement.define();
};

const initialize = (initializeOptions = {}) => {
  const {consumer: consumer, onMissingElement: onMissingElement, debug: debug} = initializeOptions;
  Debug.set(!!debug);
  if (consumer) {
    CableConsumer.setConsumer(consumer);
  } else {
    console.error("CableReady requires a reference to your Action Cable `consumer` for its helpers to function.\nEnsure that you have imported the `CableReady` package as well as `consumer` from your `channels` folder, then call `CableReady.initialize({ consumer })`.");
  }
  if (onMissingElement) {
    MissingElement.set(onMissingElement);
  }
  defineElements();
};

const global = {
  perform: perform,
  performAsync: performAsync,
  shouldMorphCallbacks: shouldMorphCallbacks,
  didMorphCallbacks: didMorphCallbacks,
  initialize: initialize,
  addOperation: addOperation,
  addOperations: addOperations,
  version: packageInfo.version,
  cable: CableConsumer,
  get DOMOperations() {
    console.warn("DEPRECATED: Please use `CableReady.operations` instead of `CableReady.DOMOperations`");
    return OperationStore.all;
  },
  get operations() {
    return OperationStore.all;
  },
  get consumer() {
    return CableConsumer.consumer;
  }
};

window.CableReady = global;

export { morph_callbacks as MorphCallbacks, StreamFromElement, SubscribingElement, UpdatesForElement, utils as Utils, global as default };
