app/assets/javascripts/stimulus_reflex.js in stimulus_reflex-3.5.0.pre10 vs app/assets/javascripts/stimulus_reflex.js in stimulus_reflex-3.5.0.rc1

- old
+ new

@@ -1,11 +1,291 @@ import { Controller } from "@hotwired/stimulus"; +import CableReady, { Utils } from "cable_ready"; + import { createConsumer } from "@rails/actioncable"; -import CableReady, { Utils } from "cable_ready"; +/*! + * Toastify js 1.12.0 + * https://github.com/apvarun/toastify-js + * @license MIT licensed + * + * Copyright (C) 2018 Varun A P + */ class Toastify { + defaults={ + oldestFirst: true, + text: "Toastify is awesome!", + node: undefined, + duration: 3e3, + selector: undefined, + callback: function() {}, + destination: undefined, + newWindow: false, + close: false, + gravity: "toastify-top", + positionLeft: false, + position: "", + backgroundColor: "", + avatar: "", + className: "", + stopOnFocus: true, + onClick: function() {}, + offset: { + x: 0, + y: 0 + }, + escapeMarkup: true, + ariaLive: "polite", + style: { + background: "" + } + }; + constructor(options) { + this.version = "1.12.0"; + this.options = {}; + this.toastElement = null; + this._rootElement = document.body; + this._init(options); + } + showToast() { + this.toastElement = this._buildToast(); + if (typeof this.options.selector === "string") { + this._rootElement = document.getElementById(this.options.selector); + } else if (this.options.selector instanceof HTMLElement || this.options.selector instanceof ShadowRoot) { + this._rootElement = this.options.selector; + } else { + this._rootElement = document.body; + } + if (!this._rootElement) { + throw "Root element is not defined"; + } + this._rootElement.insertBefore(this.toastElement, this._rootElement.firstChild); + this._reposition(); + if (this.options.duration > 0) { + this.toastElement.timeOutValue = window.setTimeout((() => { + this._removeElement(this.toastElement); + }), this.options.duration); + } + return this; + } + hideToast() { + if (this.toastElement.timeOutValue) { + clearTimeout(this.toastElement.timeOutValue); + } + this._removeElement(this.toastElement); + } + _init(options) { + this.options = Object.assign(this.defaults, options); + if (this.options.backgroundColor) { + console.warn('DEPRECATION NOTICE: "backgroundColor" is being deprecated. Please use the "style.background" property.'); + } + this.toastElement = null; + this.options.gravity = options.gravity === "bottom" ? "toastify-bottom" : "toastify-top"; + this.options.stopOnFocus = options.stopOnFocus === undefined ? true : options.stopOnFocus; + if (options.backgroundColor) { + this.options.style.background = options.backgroundColor; + } + } + _buildToast() { + if (!this.options) { + throw "Toastify is not initialized"; + } + let divElement = document.createElement("div"); + divElement.className = `toastify on ${this.options.className}`; + divElement.className += ` toastify-${this.options.position}`; + divElement.className += ` ${this.options.gravity}`; + for (const property in this.options.style) { + divElement.style[property] = this.options.style[property]; + } + if (this.options.ariaLive) { + divElement.setAttribute("aria-live", this.options.ariaLive); + } + if (this.options.node && this.options.node.nodeType === Node.ELEMENT_NODE) { + divElement.appendChild(this.options.node); + } else { + if (this.options.escapeMarkup) { + divElement.innerText = this.options.text; + } else { + divElement.innerHTML = this.options.text; + } + if (this.options.avatar !== "") { + let avatarElement = document.createElement("img"); + avatarElement.src = this.options.avatar; + avatarElement.className = "toastify-avatar"; + if (this.options.position == "left") { + divElement.appendChild(avatarElement); + } else { + divElement.insertAdjacentElement("afterbegin", avatarElement); + } + } + } + if (this.options.close === true) { + let closeElement = document.createElement("button"); + closeElement.type = "button"; + closeElement.setAttribute("aria-label", "Close"); + closeElement.className = "toast-close"; + closeElement.innerHTML = "&#10006;"; + closeElement.addEventListener("click", (event => { + event.stopPropagation(); + this._removeElement(this.toastElement); + window.clearTimeout(this.toastElement.timeOutValue); + })); + const width = window.innerWidth > 0 ? window.innerWidth : screen.width; + if (this.options.position == "left" && width > 360) { + divElement.insertAdjacentElement("afterbegin", closeElement); + } else { + divElement.appendChild(closeElement); + } + } + if (this.options.stopOnFocus && this.options.duration > 0) { + divElement.addEventListener("mouseover", (event => { + window.clearTimeout(divElement.timeOutValue); + })); + divElement.addEventListener("mouseleave", (() => { + divElement.timeOutValue = window.setTimeout((() => { + this._removeElement(divElement); + }), this.options.duration); + })); + } + if (typeof this.options.destination !== "undefined") { + divElement.addEventListener("click", (event => { + event.stopPropagation(); + if (this.options.newWindow === true) { + window.open(this.options.destination, "_blank"); + } else { + window.location = this.options.destination; + } + })); + } + if (typeof this.options.onClick === "function" && typeof this.options.destination === "undefined") { + divElement.addEventListener("click", (event => { + event.stopPropagation(); + this.options.onClick(); + })); + } + if (typeof this.options.offset === "object") { + const x = this._getAxisOffsetAValue("x", this.options); + const y = this._getAxisOffsetAValue("y", this.options); + const xOffset = this.options.position == "left" ? x : `-${x}`; + const yOffset = this.options.gravity == "toastify-top" ? y : `-${y}`; + divElement.style.transform = `translate(${xOffset},${yOffset})`; + } + return divElement; + } + _removeElement(toastElement) { + toastElement.className = toastElement.className.replace(" on", ""); + window.setTimeout((() => { + if (this.options.node && this.options.node.parentNode) { + this.options.node.parentNode.removeChild(this.options.node); + } + if (toastElement.parentNode) { + toastElement.parentNode.removeChild(toastElement); + } + this.options.callback.call(toastElement); + this._reposition(); + }), 400); + } + _reposition() { + let topLeftOffsetSize = { + top: 15, + bottom: 15 + }; + let topRightOffsetSize = { + top: 15, + bottom: 15 + }; + let offsetSize = { + top: 15, + bottom: 15 + }; + let allToasts = this._rootElement.querySelectorAll(".toastify"); + let classUsed; + for (let i = 0; i < allToasts.length; i++) { + if (allToasts[i].classList.contains("toastify-top") === true) { + classUsed = "toastify-top"; + } else { + classUsed = "toastify-bottom"; + } + let height = allToasts[i].offsetHeight; + classUsed = classUsed.substr(9, classUsed.length - 1); + let offset = 15; + let width = window.innerWidth > 0 ? window.innerWidth : screen.width; + if (width <= 360) { + allToasts[i].style[classUsed] = `${offsetSize[classUsed]}px`; + offsetSize[classUsed] += height + offset; + } else { + if (allToasts[i].classList.contains("toastify-left") === true) { + allToasts[i].style[classUsed] = `${topLeftOffsetSize[classUsed]}px`; + topLeftOffsetSize[classUsed] += height + offset; + } else { + allToasts[i].style[classUsed] = `${topRightOffsetSize[classUsed]}px`; + topRightOffsetSize[classUsed] += height + offset; + } + } + } + } + _getAxisOffsetAValue(axis, options) { + if (options.offset[axis]) { + if (isNaN(options.offset[axis])) { + return options.offset[axis]; + } else { + return `${options.offset[axis]}px`; + } + } + return "0px"; + } +} +function StartToastifyInstance(options) { + return new Toastify(options); +} + +CableReady.operations.stimulusReflexVersionMismatch = operation => { + const levels = { + info: {}, + success: { + background: "#198754", + color: "white" + }, + warn: { + background: "#ffc107", + color: "black" + }, + error: { + background: "#dc3545", + color: "white" + } + }; + const defaults = { + selector: setupToastify(), + close: true, + duration: 30 * 1e3, + gravity: "bottom", + position: "right", + newWindow: true, + style: levels[operation.level || "info"] + }; + StartToastifyInstance({ + ...defaults, + ...operation + }).showToast(); +}; + +function setupToastify() { + const id = "stimulus-reflex-toast-element"; + let element = document.querySelector(`#${id}`); + if (!element) { + element = document.createElement("div"); + element.id = id; + document.documentElement.appendChild(element); + const styles = document.createElement("style"); + styles.innerHTML = `\n #${id} .toastify {\n padding: 12px 20px;\n color: #ffffff;\n display: inline-block;\n background: -webkit-linear-gradient(315deg, #73a5ff, #5477f5);\n background: linear-gradient(135deg, #73a5ff, #5477f5);\n position: fixed;\n opacity: 0;\n transition: all 0.4s cubic-bezier(0.215, 0.61, 0.355, 1);\n border-radius: 2px;\n cursor: pointer;\n text-decoration: none;\n max-width: calc(50% - 20px);\n z-index: 2147483647;\n bottom: -150px;\n right: 15px;\n }\n\n #${id} .toastify.on {\n opacity: 1;\n }\n\n #${id} .toast-close {\n background: transparent;\n border: 0;\n color: white;\n cursor: pointer;\n font-family: inherit;\n font-size: 1em;\n opacity: 0.4;\n padding: 0 5px;\n }\n `; + document.head.appendChild(styles); + } + return element; +} + let deprecationWarnings = true; var Deprecate = { get enabled() { return deprecationWarnings; @@ -168,10 +448,12 @@ element = element.parentElement ? element.parentElement.closest(`[${Schema.reflexRoot}]`) : null; } return list; }; +const reflexNameToControllerIdentifier = reflexName => reflexName.replace(/([a-z0–9])([A-Z])/g, "$1-$2").replace(/(::)/g, "--").replace(/-reflex$/gi, "").toLowerCase(); + const stages = [ "created", "before", "delivered", "queued", "after", "finalized", "success", "error", "halted", "forbidden" ]; let lastReflex; const reflexes = new Proxy({}, { @@ -654,11 +936,11 @@ return attrs; }; var name = "stimulus_reflex"; -var version = "3.5.0-pre10"; +var version = "3.5.0-rc1"; var description = "Build reactive applications with the Rails tooling you already know and love."; var keywords = [ "ruby", "rails", "websockets", "actioncable", "turbolinks", "reactive", "cable", "ujs", "ssr", "stimulus", "reflex", "stimulus_reflex", "dom", "morphdom" ]; @@ -691,36 +973,38 @@ format: "yarn run prettier-standard ./javascript/**/*.js rollup.config.mjs", build: "yarn rollup -c", "build:watch": "yarn rollup -wc", watch: "yarn build:watch", test: "web-test-runner javascript/test/**/*.test.js", + "test:watch": "yarn test --watch", "docs:dev": "vitepress dev docs", - "docs:build": "vitepress build docs", + "docs:build": "vitepress build docs && cp docs/_redirects docs/.vitepress/dist", "docs:preview": "vitepress preview docs" }; var peerDependencies = { "@hotwired/stimulus": ">= 3.0" }; var dependencies = { - "@hotwired/stimulus": ">= 3.0, < 4", - "@rails/actioncable": ">= 6.0, < 8", - cable_ready: "5.0.0-pre10" + "@hotwired/stimulus": "^3", + "@rails/actioncable": "^6 || ^7", + cable_ready: "5.0.0-rc1" }; var devDependencies = { "@open-wc/testing": "^3.1.7", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-terser": "^0.4.0", "@web/dev-server-esbuild": "^0.3.3", "@web/dev-server-rollup": "^0.3.21", - "@web/test-runner": "^0.15.0", + "@web/test-runner": "^0.15.1", "prettier-standard": "^16.4.1", - rollup: "^3.17.1", - vitepress: "^1.0.0-alpha.47" + rollup: "^3.19.1", + "toastify-js": "^1.12.0", + vitepress: "^1.0.0-alpha.56" }; var packageInfo = { name: name, version: version, @@ -946,15 +1230,15 @@ error: reflex.error, toString: () => reflex.error }))); }; -const localReflexControllers = element => attributeValues(element.getAttribute(Schema.controller)).reduce(((memo, name) => { - const controller = App.app.getControllerForElementAndIdentifier(element, name); - if (controller && controller.StimulusReflex) memo.push(controller); - return memo; -}), []); +const localReflexControllers = element => { + const potentialIdentifiers = attributeValues(element.getAttribute(Schema.controller)); + const potentialControllers = potentialIdentifiers.map((identifier => App.app.getControllerForElementAndIdentifier(element, identifier))); + return potentialControllers.filter((controller => controller && controller.StimulusReflex)); +}; const allReflexControllers = element => { let controllers = []; while (element) { controllers = controllers.concat(localReflexControllers(element)); @@ -963,33 +1247,38 @@ return controllers; }; const findControllerByReflexName = (reflexName, controllers) => { const controller = controllers.find((controller => { - if (!controller.identifier) return; - return extractReflexName(reflexName).replace(/([a-z0–9])([A-Z])/g, "$1-$2").replace(/(::)/g, "--").toLowerCase() === controller.identifier; + if (!controller || !controller.identifier) return; + const identifier = reflexNameToControllerIdentifier(extractReflexName(reflexName)); + return identifier === controller.identifier; })); return controller || controllers[0]; }; const scanForReflexes = debounce((() => { const reflexElements = document.querySelectorAll(`[${Schema.reflex}]`); reflexElements.forEach((element => scanForReflexesOnElement(element))); }), 20); -const scanForReflexesOnElement = element => { +const scanForReflexesOnElement = (element, controller = null) => { const controllerAttribute = element.getAttribute(Schema.controller); - const controllers = attributeValues(controllerAttribute); + const controllers = attributeValues(controllerAttribute).filter((controller => controller !== "stimulus-reflex")); const reflexAttribute = element.getAttribute(Schema.reflex); const reflexAttributeNames = attributeValues(reflexAttribute); const actionAttribute = element.getAttribute(Schema.action); const actions = attributeValues(actionAttribute).filter((action => !action.includes("#__perform"))); reflexAttributeNames.forEach((reflexName => { - const controller = findControllerByReflexName(reflexName, allReflexControllers(element)); + const potentialControllers = [ controller ].concat(allReflexControllers(element)); + controller = findControllerByReflexName(reflexName, potentialControllers); const controllerName = controller ? controller.identifier : "stimulus-reflex"; actions.push(`${reflexName.split("->")[0]}->${controllerName}#__perform`); - controllers.push(controllerName); + const parentControllerElement = element.closest(`[data-controller~=${controllerName}]`); + if (!parentControllerElement) { + controllers.push(controllerName); + } })); const controllerValue = attributeValue(controllers); const actionValue = attributeValue(actions); let emitReadyEvent = false; if (controllerValue && element.getAttribute(Schema.controller) != controllerValue) { @@ -1112,10 +1401,10 @@ return Object.fromEntries(Object.entries(target[prop]).filter((([_, reflex]) => reflex.controller === this))); }.bind(this) }); } }); - scanForReflexesOnElement(controller.element); + scanForReflexesOnElement(controller.element, controller); emitEvent("stimulus-reflex:controller-registered", { detail: { controller: controller } });