diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 014da501..21939fe5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,12 +19,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [22.x, 24.x] + node-version: [22.x, 24.x, 26.x] steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: ${{ matrix.node-version }} - name: Install dependencies @@ -42,11 +42,11 @@ jobs: name: coverage-report-${{ matrix.node-version }} path: coverage - name: Upload coverage report to Coveralls - #v2.3.7 - uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e + uses: coverallsapp/github-action@8d6379e14d29928660c4ba802d8e85393440b329 with: flag-name: coverage-report-${{ matrix.node-version }} parallel: true + continue-on-error: true coverage-finished: name: Publish coverage report to Coveralls @@ -55,11 +55,50 @@ jobs: runs-on: ubuntu-latest steps: - name: Coveralls Finished - #v2.3.7 - uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e + id: coveralls + uses: coverallsapp/github-action@8d6379e14d29928660c4ba802d8e85393440b329 with: parallel-finished: true - carryforward: "coverage-report-22.x,coverage-report-24.x" + carryforward: "coverage-report-22.x,coverage-report-24.x,coverage-report-26.x" + continue-on-error: true + + - name: Check Coveralls status + id: check-status + if: always() + run: | + if [ "${{ steps.coveralls.outcome }}" == "success" ]; then + echo "✅ Coveralls report submitted successfully" + echo "status=success" >> $GITHUB_OUTPUT + elif [ "${{ steps.coveralls.outcome }}" == "failure" ]; then + echo "⚠️ Coveralls service is temporarily unavailable" + echo "status=neutral" >> $GITHUB_OUTPUT + else + echo "❓ Coveralls status unknown: ${{ steps.coveralls.outcome }}" + echo "status=neutral" >> $GITHUB_OUTPUT + fi + + - name: Set neutral status (if needed) + if: steps.check-status.outputs.status == 'neutral' + uses: actions/github-script@v9 + with: + script: | + const { context } = require('@actions/github'); + const owner = context.repo.owner; + const repo = context.repo.repo; + const sha = context.sha; + + await github.rest.checks.create({ + owner, + repo, + name: 'Coveralls / Coverage Report', + head_sha: sha, + status: 'completed', + conclusion: 'neutral', + output: { + title: 'Coveralls service temporarily unavailable', + summary: '⚠️ Coveralls is currently experiencing issues. The coverage report will be submitted when the service is available again.\n\nThis check is marked as neutral and does not block the PR.' + } + }); cypress: name: Run e2e tests with Cypress @@ -70,14 +109,13 @@ jobs: options: --user 1001 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Install dependencies run: npm install - name: Build project run: npm run build - name: Cypress run - #v6.10.8 - uses: cypress-io/github-action@948d67d3074f1bbb6379c8bdbb04e95d2f8e593f + uses: cypress-io/github-action@c541a95ea92d9239347a2355f2e895cd4b3f8060 with: browser: chrome @@ -91,7 +129,7 @@ jobs: security-events: write steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: @@ -107,18 +145,44 @@ jobs: needs: build-and-test steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 + with: + files: | + ** + files_ignore: | + package.json + .npmrc + **/*.md + .github/** + .gitignore + + - name: Check if code changes exist + id: check-changes + run: | + if [ "${{ steps.changed-files.outputs.any_changed }}" == "true" ]; then + echo "has_code_changes=true" >> $GITHUB_OUTPUT + echo "📝 Code changes detected in:" + echo "${{ steps.changed-files.outputs.all_changed_files }}" + else + echo "has_code_changes=false" >> $GITHUB_OUTPUT + echo "📦 Only dependency, documentation or CI/CD files changed" + fi + - name: Download coverage artifact + if: steps.check-changes.outputs.has_code_changes == 'true' uses: actions/download-artifact@v8 with: - name: coverage-report-22.x + name: coverage-report-26.x path: coverage - name: SonarQube Scan - # v7.0.0 - uses: SonarSource/sonarqube-scan-action@59db25f34e16620e48ab4bb9e4a5dce155cb5432 + if: steps.check-changes.outputs.has_code_changes == 'true' + uses: SonarSource/sonarqube-scan-action@d209202bc7d53ff1cc128f7f907dac145c9d6ae9 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 528d4266..a9d5537a 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -1,10 +1,8 @@ name: Deploy documentation on: - push: - branches: [ "v5" ] - pull_request: - branches: [ "v5" ] + release: + types: [published] workflow_dispatch: permissions: @@ -24,11 +22,11 @@ jobs: working-directory: docs steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Use Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: - node-version: 24.x + node-version: 26.x - name: Install dependencies run: npm install - name: Build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..1f16cffb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,44 @@ +name: Publish Package to npmJS + +on: + release: + types: [published] + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-node@v7 + with: + node-version: '26.x' + + - name: Install dependencies + run: npm install + + - name: Determine npm tag + id: npm-tag + run: | + RELEASE_NAME="${{ github.event.release.name }}" + RELEASE_TAG="${{ github.event.release.tag_name }}" + + echo "📦 Release name: $RELEASE_NAME" + echo "🏷️ Release tag: $RELEASE_TAG" + + if [ "${{ github.event.release.prerelease }}" == "true" ]; then + echo "🔬 Prerelease detected - using 'next' tag" + echo "tag=next" >> $GITHUB_OUTPUT + else + echo "🚀 Full release detected - using 'latest' tag" + echo "tag=latest" >> $GITHUB_OUTPUT + fi + + - name: Stage publish to npm + run: | + npm stage publish --provenance --access public --tag ${{ steps.npm-tag.outputs.tag }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md index bb7d17b3..f538b0f1 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,6 @@ See EOL for each version in [Security Policy Page](https://github.com/palcarazm/ - - [Installation](#installation) - [CDN](#cdn) - [ECMAS Interface](#ecmas-interface) @@ -54,6 +53,7 @@ See EOL for each version in [Security Policy Page](https://github.com/palcarazm/ + --- # Installation @@ -66,18 +66,18 @@ See EOL for each version in [Security Policy Page](https://github.com/palcarazm/ ```html - + ``` ### jQuery Interface ```html - + ``` ## Download @@ -89,13 +89,13 @@ See EOL for each version in [Security Policy Page](https://github.com/palcarazm/ [![NPM Badge](https://img.shields.io/npm/dm/bootstrap5-toggle?logo=npm)](https://www.npmjs.com/package/bootstrap5-toggle) ```ksh -npm install bootstrap5-toggle@5.4.1 +npm install bootstrap5-toggle@5.5.0 ``` ## Yarn ```ksh -yarn add bootstrap5-toggle@5.4.1 +yarn add bootstrap5-toggle@5.5.0 ``` # Usage diff --git a/cypress.config.cjs b/cypress.config.cjs index 460e0b48..6dd145bf 100644 --- a/cypress.config.cjs +++ b/cypress.config.cjs @@ -1,7 +1,6 @@ const { defineConfig } = require("cypress"); module.exports = defineConfig({ - allowCypressEnv: false, e2e: { setupNodeEvents(on, config) { // implement node event listeners here diff --git a/cypress/e2e/monitoring.spec.cy.js b/cypress/e2e/monitoring.spec.cy.js new file mode 100644 index 00000000..174dad3c --- /dev/null +++ b/cypress/e2e/monitoring.spec.cy.js @@ -0,0 +1,124 @@ +/// +import PageModel from "../support/pagemodel"; + +describe("Developer Tools: Monitoring", () => { + context("Given ECMAS bootstrap toggle interface", () => { + testCase("ecmas"); + }); + context("Given jQuery bootstrap toggle interface", () => { + testCase("jquery"); + }); +}); + +function testCase(bstInterface) { + const data_test = "api-methods"; + + beforeEach(() => { + PageModel.load(bstInterface, data_test); + }); + + afterEach(() => { + cy.window().then((win) => { + try { + win.BootstrapToggle.MONITOR.stop(); + } catch { + // ignore: monitor may already be stopped + } + }); + }); + + it("MONITOR is exposed and starts stopped", () => { + cy.window().then((win) => { + expect(win.BootstrapToggle.MONITOR).to.exist; + expect(win.BootstrapToggle.MONITOR.getLevel()).to.be.undefined; + }); + }); + + it("start('DEBUG') produces a console.debug line per fired domain event", () => { + cy.window().then((win) => { + win.BootstrapToggle.MONITOR.start("DEBUG"); + }); + + cy.get("@consoleDebug").should("not.have.been.called"); + + PageModel.getTests().each(($test) => { + cy.wrap($test).find('button[data-method="initialize"]').click(); + cy.wrap($test).find('button[data-method="on"]').click(); + }); + + cy.get("@consoleDebug").should("have.been.called"); + cy.get("@consoleDebug").should((stub) => { + const calls = stub.getCalls().map((c) => c.args[0]); + expect(calls.some((msg) => /\[toggle:DEBUG\] on/.test(msg))).to.be + .true; + }); + }); + + it("setLevel('INFO') stops domain-event logging but keeps lifecycle logging", () => { + cy.window().then((win) => { + win.BootstrapToggle.MONITOR.start("DEBUG"); + win.BootstrapToggle.MONITOR.setLevel("INFO"); + }); + + cy.get("@consoleDebug").invoke("resetHistory"); + + PageModel.getTests().each(($test) => { + cy.wrap($test).find('button[data-method="initialize"]').click(); + cy.wrap($test).find('button[data-method="on"]').click(); + cy.wrap($test).find('button[data-method="destroy"]').click(); + }); + + cy.get("@consoleDebug").should("not.have.been.called"); + }); + + it("stop() produces no new log lines on subsequent clicks", () => { + cy.window().then((win) => { + win.BootstrapToggle.MONITOR.start("DEBUG"); + win.BootstrapToggle.MONITOR.stop(); + }); + + cy.get("@consoleDebug").invoke("resetHistory"); + + PageModel.getTests().each(($test) => { + cy.wrap($test).find('button[data-method="initialize"]').click(); + cy.wrap($test).find('button[data-method="on"]').click(); + }); + + cy.get("@consoleDebug").should("not.have.been.called"); + }); + + it("start('DEBUG') after stop() restores logging", () => { + cy.window().then((win) => { + win.BootstrapToggle.MONITOR.start("DEBUG"); + win.BootstrapToggle.MONITOR.stop(); + }); + + cy.get("@consoleDebug").invoke("resetHistory"); + + cy.window().then((win) => { + win.BootstrapToggle.MONITOR.start("DEBUG"); + }); + + PageModel.getTests().each(($test) => { + cy.wrap($test).find('button[data-method="initialize"]').click(); + cy.wrap($test).find('button[data-method="on"]').click(); + }); + + cy.get("@consoleDebug").should("have.been.called"); + }); + + it("getLevel() reports the current level and undefined when stopped", () => { + cy.window().then((win) => { + expect(win.BootstrapToggle.MONITOR.getLevel()).to.be.undefined; + + win.BootstrapToggle.MONITOR.start("DEBUG"); + expect(win.BootstrapToggle.MONITOR.getLevel()).to.eq("DEBUG"); + + win.BootstrapToggle.MONITOR.setLevel("INFO"); + expect(win.BootstrapToggle.MONITOR.getLevel()).to.eq("INFO"); + + win.BootstrapToggle.MONITOR.stop(); + expect(win.BootstrapToggle.MONITOR.getLevel()).to.be.undefined; + }); + }); +} diff --git a/cypress/support/pagemodel.js b/cypress/support/pagemodel.js index 9965441e..45181926 100644 --- a/cypress/support/pagemodel.js +++ b/cypress/support/pagemodel.js @@ -9,6 +9,8 @@ class PageModel { cy.visit("./test/test-app." + bstInterface.toLowerCase() + ".html", { onBeforeLoad(win) { cy.stub(win.console, "log").as("consoleLog"); + cy.stub(win.console, "debug").as("consoleDebug"); + cy.stub(win.console, "info").as("consoleInfo"); cy.stub(win.console, "warn").as("consoleWarn"); cy.stub(win.console, "error").as("consoleError"); }, diff --git a/docs/package.json b/docs/package.json index 66c2722f..dada3a09 100644 --- a/docs/package.json +++ b/docs/package.json @@ -29,24 +29,24 @@ } }, "dependencies": { - "@fortawesome/fontawesome-free": "7.2.0", - "@highlightjs/cdn-assets": "11.11.1", + "@fortawesome/fontawesome-free": "7.3.1", + "@highlightjs/cdn-assets": "11.12.0", "bootstrap": "5.3.8", - "bootstrap5-toggle": "5.4.1", - "bs-darkmode-toggle": "1.0.0", + "bootstrap5-toggle": "5.5.0", + "bs-darkmode-toggle": "1.1.0", "highlightjs-badge": "0.1.9", "jquery": "4.0.0" }, "devDependencies": { "@rollup/plugin-terser": "^1.0.0", - "cssnano": "^8.0.2", + "cssnano": "^9.0.3", "grunt": "^1.6.1", "grunt-contrib-clean": "^2.0.1", "grunt-contrib-copy": "^1.0.0", "grunt-contrib-watch": "^1.1.0", "grunt-exec": "^3.0.0", "postcss": "^8.5.6", - "postcss-cli": "^11.0.1", + "postcss-cli": "^12.0.0", "rollup": "^4.59.0" } } diff --git a/docs/src/js/components/Aside.js b/docs/src/js/components/Aside.js index 373e3957..3aa368bc 100644 --- a/docs/src/js/components/Aside.js +++ b/docs/src/js/components/Aside.js @@ -1,122 +1,122 @@ import Badge from "./Badge"; class Aside { - static #badges = [ - { - name: "sponsor", - href: "https://github.com/sponsors/palcarazm", - imgSrc: + static #badges = [ + { + name: "sponsor", + href: "https://github.com/sponsors/palcarazm", + imgSrc: "https://img.shields.io/badge/sponsor-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#white", - }, - { - name: "GitHub repository", - href: "https://github.com/palcarazm/bootstrap5-toggle", - imgSrc: + }, + { + name: "GitHub repository", + href: "https://github.com/palcarazm/bootstrap5-toggle", + imgSrc: "https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white", - }, - { - name: "License", - href: "https://github.com/palcarazm/bootstrap5-toggle/blob/v5/LICENSE", - imgSrc: + }, + { + name: "License", + href: "https://github.com/palcarazm/bootstrap5-toggle/blob/v5/LICENSE", + imgSrc: "https://img.shields.io/github/license/palcarazm/bootstrap5-toggle.svg?style=for-the-badge", - }, - ]; - - static build() { - const aside = document.createElement("aside"); - aside.className = "col-md-3 col-lg-2"; - - const container = document.createElement("div"); - container.className = "position-sticky"; - container.style.top = "0rem"; - - container.append( - Aside.#logoContainer(), - Aside.#navBar(), - Aside.#badgeBar(), - ); - - aside.appendChild(container); - return aside; - } - - static #logoContainer() { - const container = document.createElement("div"); - container.className = + }, + ]; + + static build() { + const aside = document.createElement("aside"); + aside.className = "col-md-3 col-lg-2"; + + const container = document.createElement("div"); + container.className = "position-sticky"; + container.style.top = "0rem"; + + container.append( + Aside.#logoContainer(), + Aside.#navBar(), + Aside.#badgeBar(), + ); + + aside.appendChild(container); + return aside; + } + + static #logoContainer() { + const container = document.createElement("div"); + container.className = "d-none d-md-block position-relative pb-5 mb-3 img-toggle"; - const logoOn = document.createElement("img"); - logoOn.className = + const logoOn = document.createElement("img"); + logoOn.className = "d-block mx-auto position-absolute top-50 start-50 translate-middle w-100"; - logoOn.src = "assets/img/logo_on.png"; - logoOn.alt = "Bootstrap 5 Toggle"; - container.appendChild(logoOn); + logoOn.src = "assets/img/logo_on.png"; + logoOn.alt = "Bootstrap 5 Toggle"; + container.appendChild(logoOn); - const logoOff = document.createElement("img"); - logoOff.className = + const logoOff = document.createElement("img"); + logoOff.className = "d-block mx-auto position-absolute top-50 start-50 translate-middle w-100 invisible"; - logoOff.src = "assets/img/logo_off.png"; - logoOff.alt = "Bootstrap 5 Toggle"; - container.appendChild(logoOff); + logoOff.src = "assets/img/logo_off.png"; + logoOff.alt = "Bootstrap 5 Toggle"; + container.appendChild(logoOff); - return container; - } + return container; + } - static #navBar() { - const navBar = document.createElement("nav"); - navBar.className = + static #navBar() { + const navBar = document.createElement("nav"); + navBar.className = "navbar navbar-expand-md navbar-light flex-md-column p-0"; - navBar.setAttribute("aria-label", "Site menu"); - - const button = document.createElement("button"); - button.className = "navbar-toggler"; - button.type = "button"; - button.setAttribute("data-bs-toggle", "collapse"); - button.setAttribute("data-bs-target", "#navbarSupportedContent"); - button.setAttribute("aria-controls", "navbarSupportedContent"); - button.setAttribute("aria-expanded", "false"); - button.setAttribute("aria-label", "Toggle navigation"); - - const span = document.createElement("span"); - span.className = "navbar-toggler-icon"; - - button.appendChild(span); - navBar.appendChild(button); - - const darkModeToggle = document.createElement("div"); - darkModeToggle.dataset.plugin = "bs-darkmode-toggle"; - darkModeToggle.dataset.storage = "local"; - - navBar.appendChild(darkModeToggle); - - const collapseDiv = document.createElement("div"); - collapseDiv.className = "collapse navbar-collapse"; - collapseDiv.id = "navbarSupportedContent"; - - const navMenu = document.createElement("nav"); - navMenu.id = "toc"; - navMenu.setAttribute("data-toggle", "toc"); - navMenu.setAttribute("aria-label", "Table of contents"); - - collapseDiv.appendChild(navMenu); - navBar.appendChild(collapseDiv); - - return navBar; - } - - static #badgeBar() { - const badges = document.createElement("div"); - badges.className = "d-none d-md-flex flex-column align-items-center mt-4"; - - Aside.#badges.forEach((badge, index) => { - badges.appendChild( - Badge.build({ - ...badge, - className: index === 0 ? "mt-0" : "mt-1", - }), - ); - }); - return badges; - } + navBar.setAttribute("aria-label", "Site menu"); + + const button = document.createElement("button"); + button.className = "navbar-toggler"; + button.type = "button"; + button.setAttribute("data-bs-toggle", "collapse"); + button.setAttribute("data-bs-target", "#navbarSupportedContent"); + button.setAttribute("aria-controls", "navbarSupportedContent"); + button.setAttribute("aria-expanded", "false"); + button.setAttribute("aria-label", "Toggle navigation"); + + const span = document.createElement("span"); + span.className = "navbar-toggler-icon"; + + button.appendChild(span); + navBar.appendChild(button); + + const darkModeToggle = document.createElement("div"); + darkModeToggle.dataset.plugin = "bs-darkmode-toggle"; + darkModeToggle.dataset.storage = "local"; + + navBar.appendChild(darkModeToggle); + + const collapseDiv = document.createElement("div"); + collapseDiv.className = "collapse navbar-collapse"; + collapseDiv.id = "navbarSupportedContent"; + + const navMenu = document.createElement("nav"); + navMenu.id = "toc"; + navMenu.setAttribute("data-toggle", "toc"); + navMenu.setAttribute("aria-label", "Table of contents"); + + collapseDiv.appendChild(navMenu); + navBar.appendChild(collapseDiv); + + return navBar; + } + + static #badgeBar() { + const badges = document.createElement("div"); + badges.className = "d-none d-md-flex flex-column align-items-center mt-4"; + + Aside.#badges.forEach((badge, index) => { + badges.appendChild( + Badge.build({ + ...badge, + className: index === 0 ? "mt-0" : "mt-1", + }), + ); + }); + return badges; + } } export default Aside; diff --git a/docs/src/js/components/Badge.js b/docs/src/js/components/Badge.js index a925bf43..b43df9a8 100644 --- a/docs/src/js/components/Badge.js +++ b/docs/src/js/components/Badge.js @@ -1,23 +1,23 @@ class Badge { - static build({ name, href, imgSrc, className = "" }) { - const container = document.createElement("div"); - container.className = className; + static build({ name, href, imgSrc, className = "" }) { + const container = document.createElement("div"); + container.className = className; - const badge = { name, href, imgSrc }; - const badgeLink = document.createElement("a"); - badgeLink.href = badge.href; - badgeLink.title = badge.name; - badgeLink.target = "_blank"; - badgeLink.rel = "noopener noreferrer"; + const badge = { name, href, imgSrc }; + const badgeLink = document.createElement("a"); + badgeLink.href = badge.href; + badgeLink.title = badge.name; + badgeLink.target = "_blank"; + badgeLink.rel = "noopener noreferrer"; - const badgeImg = document.createElement("img"); - badgeImg.src = badge.imgSrc; - badgeImg.alt = badge.name; + const badgeImg = document.createElement("img"); + badgeImg.src = badge.imgSrc; + badgeImg.alt = badge.name; - badgeLink.appendChild(badgeImg); - container.appendChild(badgeLink); - return container; - } + badgeLink.appendChild(badgeImg); + container.appendChild(badgeLink); + return container; + } } export default Badge; diff --git a/docs/src/js/components/CodeBlock.js b/docs/src/js/components/CodeBlock.js index f097664e..54dcad0f 100644 --- a/docs/src/js/components/CodeBlock.js +++ b/docs/src/js/components/CodeBlock.js @@ -1,18 +1,18 @@ import Example from "./Example"; class CodeBlock { - static build({ language, code, example = [] }) { - const codeBlock = document.createElement("div"); - if (example.length > 0) codeBlock.appendChild(Example.build(example)); - const pre = document.createElement("pre"); - const codeElement = document.createElement("code"); - codeElement.className = `highlight language-${language}`; - codeElement.textContent = code; + static build({ language, code, example = [] }) { + const codeBlock = document.createElement("div"); + if (example.length > 0) codeBlock.appendChild(Example.build(example)); + const pre = document.createElement("pre"); + const codeElement = document.createElement("code"); + codeElement.className = `highlight language-${language}`; + codeElement.textContent = code; - pre.appendChild(codeElement); - codeBlock.appendChild(pre); - return codeBlock; - } + pre.appendChild(codeElement); + codeBlock.appendChild(pre); + return codeBlock; + } } export default CodeBlock; diff --git a/docs/src/js/components/CodePanel.js b/docs/src/js/components/CodePanel.js index cafeadff..05d1d863 100644 --- a/docs/src/js/components/CodePanel.js +++ b/docs/src/js/components/CodePanel.js @@ -2,72 +2,72 @@ import CodeBlock from "./CodeBlock"; import Example from "./Example"; class CodePanel { - static build({ - name, - language, - tabs = ["ECMAScript", "jQuery"], - contents, - example = [], - }) { - const codePanel = document.createElement("div"); + static build({ + name, + language, + tabs = ["ECMAScript", "jQuery"], + contents, + example = [], + }) { + const codePanel = document.createElement("div"); - if (tabs.length !== contents.length) - throw new Error("Tabs and contents arrays must have the same length."); + if (tabs.length !== contents.length) + throw new Error("Tabs and contents arrays must have the same length."); - if (tabs.length === 0 || contents.length === 0) - throw new Error("Tabs and contents arrays cannot be empty."); + if (tabs.length === 0 || contents.length === 0) + throw new Error("Tabs and contents arrays cannot be empty."); - codePanel.appendChild(CodePanel.#navTabs(name, tabs)); - if (example.length > 0) codePanel.appendChild(Example.build(example)); - codePanel.appendChild(CodePanel.#navPanes(name, language, contents)); - return codePanel; - } + codePanel.appendChild(CodePanel.#navTabs(name, tabs)); + if (example.length > 0) codePanel.appendChild(Example.build(example)); + codePanel.appendChild(CodePanel.#navPanes(name, language, contents)); + return codePanel; + } - static #navTabs(name, labels) { - const navTabs = document.createElement("ul"); - navTabs.className = "nav nav-tabs"; - navTabs.role = "tablist"; + static #navTabs(name, labels) { + const navTabs = document.createElement("ul"); + navTabs.className = "nav nav-tabs"; + navTabs.role = "tablist"; - labels.forEach((tabName, index) => { - const navItem = document.createElement("li"); - navItem.className = "nav-item"; - navItem.role = "presentation"; + labels.forEach((tabName, index) => { + const navItem = document.createElement("li"); + navItem.className = "nav-item"; + navItem.role = "presentation"; - const navLink = document.createElement("button"); - navLink.className = `nav-link ${index === 0 ? "active" : ""}`; - navLink.id = `${name}-${index}-tab`; - navLink.role = "tab"; - navLink.type = "button"; - navLink.setAttribute("data-bs-toggle", "tab"); - navLink.setAttribute("data-bs-target", `#${name}-${index}`); - navLink.setAttribute("aria-controls", `${name}-${index}`); - navLink.setAttribute("aria-selected", index === 0 ? "true" : "false"); - navLink.textContent = tabName; + const navLink = document.createElement("button"); + navLink.className = `nav-link ${index === 0 ? "active" : ""}`; + navLink.id = `${name}-${index}-tab`; + navLink.role = "tab"; + navLink.type = "button"; + navLink.setAttribute("data-bs-toggle", "tab"); + navLink.setAttribute("data-bs-target", `#${name}-${index}`); + navLink.setAttribute("aria-controls", `${name}-${index}`); + navLink.setAttribute("aria-selected", index === 0 ? "true" : "false"); + navLink.textContent = tabName; - navItem.append(navLink); - navTabs.append(navItem); - }); + navItem.append(navLink); + navTabs.append(navItem); + }); - return navTabs; - } + return navTabs; + } - static #navPanes(name, language, contents) { - const navPanes = document.createElement("div"); - navPanes.className = "tab-content"; + static #navPanes(name, language, contents) { + const navPanes = document.createElement("div"); + navPanes.className = "tab-content"; - contents.forEach((code, index) => { - const tabPane = document.createElement("div"); - tabPane.className = `tab-pane fade ${index === 0 ? "show active" : ""}`; - tabPane.id = `${name}-${index}`; - tabPane.role = "tabpanel"; - tabPane.tabindex = index === 0 ? 0 : -1; - tabPane.setAttribute("aria-labelledby", `${name}-${index}-tab`); + contents.forEach((code, index) => { + const tabPane = document.createElement("div"); + tabPane.className = `tab-pane fade ${index === 0 ? "show active" : ""}`; + tabPane.id = `${name}-${index}`; + tabPane.role = "tabpanel"; + tabPane.tabindex = index === 0 ? 0 : -1; + tabPane.setAttribute("aria-labelledby", `${name}-${index}-tab`); - tabPane.append(CodeBlock.build({ language, code })); - navPanes.append(tabPane); - }); - return navPanes; - } + tabPane.append(CodeBlock.build({ language, code })); + navPanes.append(tabPane); + }); + return navPanes; + } } export default CodePanel; diff --git a/docs/src/js/components/Console.js b/docs/src/js/components/Console.js index 796b377a..bcdd2f00 100644 --- a/docs/src/js/components/Console.js +++ b/docs/src/js/components/Console.js @@ -1,80 +1,81 @@ class Console { - #element; - #pre; - - constructor() { - this.#element = document.createElement("div"); - this.#element.classList.add("d-flex", "mt-2", "align-items-start"); - - const aside = document.createElement("div"); - aside.className = "d-flex flex-column me-2"; - - const legend = document.createElement("div"); - legend.className = "font-monospace text-muted"; - legend.textContent = "Console"; - aside.append(legend); - - const btnGroup = document.createElement("div"); - btnGroup.className = "btn-group btn-group-sm btn-group-vertical"; - btnGroup.setAttribute("role", "group"); - aside.append(btnGroup); - - const clearBtn = document.createElement("button"); - clearBtn.className = "btn btn-outline-secondary"; - clearBtn.title = "Clear"; - clearBtn.innerHTML = ``; - clearBtn.addEventListener("click", () => this.clear()); - btnGroup.append(clearBtn); - - const console = document.createElement("div"); - console.className = "flex-grow-1 me-2"; - this.#pre = document.createElement("pre"); - console.append(this.#pre); - - this.#element.append(aside, console); - } - - get htmlElement() { - return this.#element; - } - - #handleMode(mode) { - switch (mode) { - case "replace": - this.clear(); - return; - case "append": - return; - default: - throw new Error("Unsupported mode"); + #element; + #pre; + + constructor() { + this.#element = document.createElement("div"); + this.#element.classList.add("d-flex", "mt-2", "align-items-start"); + + const aside = document.createElement("div"); + aside.className = "d-flex flex-column me-2"; + + const legend = document.createElement("div"); + legend.className = "font-monospace text-muted"; + legend.textContent = "Console"; + aside.append(legend); + + const btnGroup = document.createElement("div"); + btnGroup.className = "btn-group btn-group-sm btn-group-vertical"; + btnGroup.setAttribute("role", "group"); + aside.append(btnGroup); + + const clearBtn = document.createElement("button"); + clearBtn.className = "btn btn-outline-secondary"; + clearBtn.title = "Clear"; + clearBtn.innerHTML = ""; + clearBtn.addEventListener("click", () => this.clear()); + btnGroup.append(clearBtn); + + const console = document.createElement("div"); + console.className = "flex-grow-1 me-2 overflow-x-scroll"; + console.style.scrollbarWidth = "none"; + this.#pre = document.createElement("pre"); + console.append(this.#pre); + + this.#element.append(aside, console); } - } - clear() { - this.#pre.innerHTML = ""; - } + get htmlElement() { + return this.#element; + } - json({ mode, data }) { - this.#handleMode(mode); + #handleMode(mode) { + switch (mode) { + case "replace": + this.clear(); + return; + case "append": + return; + default: + throw new Error("Unsupported mode"); + } + } - const code = document.createElement("code"); - code.className = "highlight language-json"; - code.textContent = JSON.stringify(data, null, 2); + clear() { + this.#pre.innerHTML = ""; + } - this.#pre.append(code); - hljs.highlightElement(code); - } + json({ mode, data }) { + this.#handleMode(mode); - log({ level = "info", mode, data }) { - this.#handleMode(mode); + const code = document.createElement("code"); + code.className = "highlight language-json"; + code.textContent = JSON.stringify(data, null, 2); - const code = document.createElement("code"); - code.className = "highlight language-shell"; - code.textContent = `> ${new Date().toISOString()} - ${data}`; + this.#pre.append(code); + hljs.highlightElement(code); + } - this.#pre.append(code); - hljs.highlightElement(code); - } + log({ level = "info", mode, data }) { + this.#handleMode(mode); + + const code = document.createElement("code"); + code.className = "highlight language-shell"; + code.textContent = `> ${new Date().toISOString()} - ${data}`; + + this.#pre.append(code); + hljs.highlightElement(code); + } } export default Console; diff --git a/docs/src/js/components/Documentation.js b/docs/src/js/components/Documentation.js index 2aa0dde3..bf7b4b05 100644 --- a/docs/src/js/components/Documentation.js +++ b/docs/src/js/components/Documentation.js @@ -5,26 +5,28 @@ import Features from "./documentation/Features"; import Api from "./documentation/Api"; import Events from "./documentation/Events"; import Accessibility from "./documentation/Accessibility"; +import DevTools from "./documentation/DevTools"; class Documentation { - static #sections = [ - Distribution.build(), - Installation.build(), - Usage.build(), - Features.build(), - Api.build(), - Events.build(), - Accessibility.build(), - ]; + static #sections = [ + Distribution.build(), + Installation.build(), + Usage.build(), + Features.build(), + Api.build(), + Events.build(), + DevTools.build(), + Accessibility.build(), + ]; - static build() { - const documentation = document.createElement("div"); - documentation.className = "col-md-9 col-lg-10"; + static build() { + const documentation = document.createElement("div"); + documentation.className = "col-md-9 col-lg-10"; - documentation.append(...Documentation.#sections); + documentation.append(...Documentation.#sections); - return documentation; - } + return documentation; + } } export default Documentation; diff --git a/docs/src/js/components/Example.js b/docs/src/js/components/Example.js index 0ef9eab5..9668bcc3 100644 --- a/docs/src/js/components/Example.js +++ b/docs/src/js/components/Example.js @@ -1,10 +1,10 @@ class Example { - static build(examples) { - const example = document.createElement("div"); - example.className = "example"; - example.append(...examples); - return example; - } + static build(examples) { + const example = document.createElement("div"); + example.className = "example"; + example.append(...examples); + return example; + } } export default Example; diff --git a/docs/src/js/components/Footer.js b/docs/src/js/components/Footer.js index e7ba84e7..cc11b79e 100644 --- a/docs/src/js/components/Footer.js +++ b/docs/src/js/components/Footer.js @@ -1,31 +1,31 @@ class Footer { - static #userAttributions = [ - `Original by Min Hur`, - `Bootstrap 4 by Brent Ely`, - `Bootstrap 5 by Pablo Alcaraz Martínez`, - ]; + static #userAttributions = [ + "Original by Min Hur", + "Bootstrap 4 by Brent Ely", + "Bootstrap 5 by Pablo Alcaraz Martínez", + ]; - static build() { - const footer = document.createElement("footer"); - footer.className = "bg-body-secondary border-top p-4"; + static build() { + const footer = document.createElement("footer"); + footer.className = "bg-body-secondary border-top p-4"; - const container = document.createElement("div"); - container.className = "container"; + const container = document.createElement("div"); + container.className = "container"; - const content = document.createElement("div"); - content.className = + const content = document.createElement("div"); + content.className = "d-flex font-weight-light flex-column flex-md-row justify-content-around align-items-center mb-4"; - Footer.#userAttributions.forEach((attribution) => { - const span = document.createElement("span"); - span.innerHTML = attribution; - content.appendChild(span); - }); + Footer.#userAttributions.forEach((attribution) => { + const span = document.createElement("span"); + span.innerHTML = attribution; + content.appendChild(span); + }); - container.appendChild(content); - footer.appendChild(container); - return footer; - } + container.appendChild(content); + footer.appendChild(container); + return footer; + } } export default Footer; diff --git a/docs/src/js/components/Header.js b/docs/src/js/components/Header.js index bb0f31b1..7a072087 100644 --- a/docs/src/js/components/Header.js +++ b/docs/src/js/components/Header.js @@ -1,67 +1,67 @@ class Header { - static build() { - const header = document.createElement("header"); - header.appendChild(Header.#forkMeLink()); - header.appendChild(Header.#logoContainer()); - return header; - } + static build() { + const header = document.createElement("header"); + header.appendChild(Header.#forkMeLink()); + header.appendChild(Header.#logoContainer()); + return header; + } - static #forkMeLink() { - const forkMeLink = document.createElement("a"); - forkMeLink.className = "position-absolute top-0 end-0 d-none d-md-inline"; - forkMeLink.href = "https://github.com/palcarazm/bootstrap5-toggle"; - forkMeLink.target = "_blank"; - forkMeLink.rel = "noopener noreferrer"; + static #forkMeLink() { + const forkMeLink = document.createElement("a"); + forkMeLink.className = "position-absolute top-0 end-0 d-none d-md-inline"; + forkMeLink.href = "https://github.com/palcarazm/bootstrap5-toggle"; + forkMeLink.target = "_blank"; + forkMeLink.rel = "noopener noreferrer"; - const img = document.createElement("img"); - img.loading = "lazy"; - img.width = "149"; - img.height = "149"; - img.src = "assets/img/fork-me.webp"; - img.className = "attachment-full size-full"; - img.alt = "Fork me on GitHub"; - img.setAttribute("data-recalc-dims", "1"); + const img = document.createElement("img"); + img.loading = "lazy"; + img.width = "149"; + img.height = "149"; + img.src = "assets/img/fork-me.webp"; + img.className = "attachment-full size-full"; + img.alt = "Fork me on GitHub"; + img.setAttribute("data-recalc-dims", "1"); - forkMeLink.appendChild(img); - return forkMeLink; - } + forkMeLink.appendChild(img); + return forkMeLink; + } - static #logoContainer() { - const container = document.createElement("div"); - container.className = "px-4 py-5 my-5 text-center"; + static #logoContainer() { + const container = document.createElement("div"); + container.className = "px-4 py-5 my-5 text-center"; - const logoWrapper = document.createElement("div"); - logoWrapper.className = "position-relative mx-auto mb-4 img-toggle"; + const logoWrapper = document.createElement("div"); + logoWrapper.className = "position-relative mx-auto mb-4 img-toggle"; - const logoOn = document.createElement("img"); - logoOn.className = + const logoOn = document.createElement("img"); + logoOn.className = "d-block mx-auto position-absolute top-50 start-50 translate-middle"; - logoOn.src = "assets/img/logo_on.png"; - logoOn.alt = "Bootstrap 5 Toggle"; - logoWrapper.appendChild(logoOn); + logoOn.src = "assets/img/logo_on.png"; + logoOn.alt = "Bootstrap 5 Toggle"; + logoWrapper.appendChild(logoOn); - const logoOff = document.createElement("img"); - logoOff.className = + const logoOff = document.createElement("img"); + logoOff.className = "d-block mx-auto position-absolute top-50 start-50 translate-middle invisible"; - logoOff.src = "assets/img/logo_off.png"; - logoOff.alt = "Bootstrap 5 Toggle"; - logoWrapper.appendChild(logoOff); + logoOff.src = "assets/img/logo_off.png"; + logoOff.alt = "Bootstrap 5 Toggle"; + logoWrapper.appendChild(logoOff); - container.appendChild(logoWrapper); + container.appendChild(logoWrapper); - const heading = document.createElement("div"); - heading.className = "col-lg-6 mx-auto"; + const heading = document.createElement("div"); + heading.className = "col-lg-6 mx-auto"; - const paragraph = document.createElement("p"); - paragraph.className = "lead mb-4"; - paragraph.textContent = + const paragraph = document.createElement("p"); + paragraph.className = "lead mb-4"; + paragraph.textContent = "Bootstrap 5 Toggle is a bootstrap plugin that converts checkboxes into responsive toggles."; - heading.appendChild(paragraph); - container.appendChild(heading); + heading.appendChild(paragraph); + container.appendChild(heading); - return container; - } + return container; + } } export default Header; diff --git a/docs/src/js/components/Main.js b/docs/src/js/components/Main.js index 4d54c81e..2b329b07 100644 --- a/docs/src/js/components/Main.js +++ b/docs/src/js/components/Main.js @@ -2,20 +2,20 @@ import Aside from "./Aside"; import Documentation from "./Documentation"; class Main { - static #sections = [Aside.build(), Documentation.build()]; + static #sections = [Aside.build(), Documentation.build()]; - static build() { - const main = document.createElement("main"); - main.className = "container mb-3"; + static build() { + const main = document.createElement("main"); + main.className = "container mb-3"; - const row = document.createElement("div"); - row.className = "row g5"; + const row = document.createElement("div"); + row.className = "row g5"; - row.append(...Main.#sections); - main.appendChild(row); + row.append(...Main.#sections); + main.appendChild(row); - return main; - } + return main; + } } export default Main; diff --git a/docs/src/js/components/Notice.js b/docs/src/js/components/Notice.js index 4fa980a2..7a3b4bd9 100644 --- a/docs/src/js/components/Notice.js +++ b/docs/src/js/components/Notice.js @@ -1,89 +1,89 @@ class Notice { - static #HTMLTextCols = [ - `Dark mode integration (check out Notice plugin)`, - `Includes a mini (-xs) size`, - `With jQuery and vanilla JavaScript (ECMAS) interfaces`, - `Supports three states toggle`, - ]; - - static build() { - const section = document.createElement("section"); - section.className = "container my-5"; - section.id = "notice"; - section.appendChild(Notice.#noticeAlert()); - return section; - } - - static #noticeAlert() { - const notice = document.createElement("div"); - notice.className = "alert alert-success"; - notice.setAttribute("role", "alert"); - - const h1 = document.createElement("h1"); - h1.className = "alert-heading fs-2"; - h1.textContent = "Made for Bootstrap 5!"; - notice.appendChild(h1); - - const p = document.createElement("p"); - p.className = "font-weight-light fs-5"; - p.textContent = `This page and all of the switch buttons shown are running on Bootstrap v${versions.bootstrap} and Bootstrap5-toggle v${versions.bootstrap5Toggle}.`; - notice.appendChild(p); - - const hr = document.createElement("hr"); - notice.appendChild(hr); - - notice.appendChild(Notice.#noticePanel()); - - return notice; - } - - static #noticePanel() { - const panel = document.createElement("div"); - panel.className = "row"; - Notice.#HTMLTextCols.forEach((HTMLTextCol) => { - panel.appendChild(Notice.#noticePanelItem(HTMLTextCol)); - }); - - return panel; - } - - static #noticePanelItem(HTMLTextCol) { - const panelItem = document.createElement("div"); - panelItem.className = "col-12 col-md-3 mb-1 mb-md-0"; - - const row = document.createElement("div"); - row.className = "row align-items-center h-100"; - - const textCol = document.createElement("div"); - textCol.className = "col"; - textCol.innerHTML = HTMLTextCol; - - row.append(Notice.#noticeIconCol(), textCol); - panelItem.appendChild(row); - - return panelItem; - } - - static #noticeIconCol() { - const iconCol = document.createElement("div"); - iconCol.className = "col-auto p-0 p-md-2 text-right"; - - const iconLarge = document.createElement("i"); - iconLarge.className = + static #HTMLTextCols = [ + "Dark mode integration (check out Notice plugin)", + "Includes a mini (-xs) size", + "With jQuery and vanilla JavaScript (ECMAS) interfaces", + "Supports three states toggle", + ]; + + static build() { + const section = document.createElement("section"); + section.className = "container my-5"; + section.id = "notice"; + section.appendChild(Notice.#noticeAlert()); + return section; + } + + static #noticeAlert() { + const notice = document.createElement("div"); + notice.className = "alert alert-success"; + notice.setAttribute("role", "alert"); + + const h1 = document.createElement("h1"); + h1.className = "alert-heading fs-2"; + h1.textContent = "Made for Bootstrap 5!"; + notice.appendChild(h1); + + const p = document.createElement("p"); + p.className = "font-weight-light fs-5"; + p.textContent = `This page and all of the switch buttons shown are running on Bootstrap v${versions.bootstrap} and Bootstrap5-toggle v${versions.bootstrap5Toggle}.`; + notice.appendChild(p); + + const hr = document.createElement("hr"); + notice.appendChild(hr); + + notice.appendChild(Notice.#noticePanel()); + + return notice; + } + + static #noticePanel() { + const panel = document.createElement("div"); + panel.className = "row"; + Notice.#HTMLTextCols.forEach((HTMLTextCol) => { + panel.appendChild(Notice.#noticePanelItem(HTMLTextCol)); + }); + + return panel; + } + + static #noticePanelItem(HTMLTextCol) { + const panelItem = document.createElement("div"); + panelItem.className = "col-12 col-md-3 mb-1 mb-md-0"; + + const row = document.createElement("div"); + row.className = "row align-items-center h-100"; + + const textCol = document.createElement("div"); + textCol.className = "col"; + textCol.innerHTML = HTMLTextCol; + + row.append(Notice.#noticeIconCol(), textCol); + panelItem.appendChild(row); + + return panelItem; + } + + static #noticeIconCol() { + const iconCol = document.createElement("div"); + iconCol.className = "col-auto p-0 p-md-2 text-right"; + + const iconLarge = document.createElement("i"); + iconLarge.className = "fa-solid fa-circle-check text-success d-none d-sm-none d-md-none d-lg-block"; - iconLarge.setAttribute("style", "font-size:48px"); - iconLarge.setAttribute("aria-hidden", "true"); - iconCol.appendChild(iconLarge); + iconLarge.setAttribute("style", "font-size:48px"); + iconLarge.setAttribute("aria-hidden", "true"); + iconCol.appendChild(iconLarge); - const iconSmall = document.createElement("i"); - iconSmall.className = + const iconSmall = document.createElement("i"); + iconSmall.className = "fa-solid fa-circle-check text-success d-block d-sm-block d-md-block d-lg-none "; - iconSmall.setAttribute("style", "font-size:24px"); - iconSmall.setAttribute("aria-hidden", "true"); - iconCol.appendChild(iconSmall); + iconSmall.setAttribute("style", "font-size:24px"); + iconSmall.setAttribute("aria-hidden", "true"); + iconCol.appendChild(iconSmall); - return iconCol; - } + return iconCol; + } } export default Notice; diff --git a/docs/src/js/components/VersionPill.js b/docs/src/js/components/VersionPill.js index 0a90de68..d8208dc8 100644 --- a/docs/src/js/components/VersionPill.js +++ b/docs/src/js/components/VersionPill.js @@ -1,24 +1,24 @@ class VersionPill { - static build({ version, action }) { - let color; - switch (action) { - case "SINCE": - color = "success"; - break; - case "DEPRECATED": - color = "warning"; - break; - case "REMOVED": - color = "danger"; - break; - default: - throw new Error("Unsupported action"); + static build({ version, action }) { + let color; + switch (action) { + case "SINCE": + color = "success"; + break; + case "DEPRECATED": + color = "warning"; + break; + case "REMOVED": + color = "danger"; + break; + default: + throw new Error("Unsupported action"); + } + const pill = document.createElement("span"); + pill.className = `badge rounded-pill bg-${color} px-2`; + pill.textContent = `${action} v${version}`; + return pill; } - const pill = document.createElement("span"); - pill.className = `badge rounded-pill bg-${color} px-2`; - pill.textContent = `${action} v${version}`; - return pill; - } } export default VersionPill; diff --git a/docs/src/js/components/documentation/Accessibility.js b/docs/src/js/components/documentation/Accessibility.js index 83d43183..831f1570 100644 --- a/docs/src/js/components/documentation/Accessibility.js +++ b/docs/src/js/components/documentation/Accessibility.js @@ -2,265 +2,265 @@ import DocSection from "./DocSection"; import DocArticle from "./DocArticle"; class Accessibility extends DocSection { - static build() { - return super.build("accessibility", "ARIA Accessibility", [ - DocArticle.build({ - title: "Key Accessibility Features", - description: Accessibility.#featuresCards(), - versionPill: { action: "SINCE", version: "5.3.0" }, - }), - DocArticle.build({ - title: "WCAG 2.2 Compliance", - description: Accessibility.#wcagCompliance(), - className: "mt-3", - versionPill: { action: "SINCE", version: "5.3.0" }, - }), - ]); - } + static build() { + return super.build("accessibility", "ARIA Accessibility", [ + DocArticle.build({ + title: "Key Accessibility Features", + description: Accessibility.#featuresCards(), + versionPill: { action: "SINCE", version: "5.3.0" }, + }), + DocArticle.build({ + title: "WCAG 2.2 Compliance", + description: Accessibility.#wcagCompliance(), + className: "mt-3", + versionPill: { action: "SINCE", version: "5.3.0" }, + }), + ]); + } - static #features = [ - { - title: "Semantic Role Implementation", - description: `Each toggle switch is assigned the role="switch" attribute, allowing screen readers to identify it as a toggleable control.`, - }, - { - title: "State Representation", - description: `The aria-checked attribute dynamically reflects the toggle's state (on/off), providing real-time feedback to assistive technologies.`, - }, - { - title: "Labeling", - description: `Toggles can be associated with descriptive labels using the aria-labelledby or aria-label attributes, enhancing clarity for screen reader users.`, - }, + static #features = [ + { + title: "Semantic Role Implementation", + description: "Each toggle switch is assigned the role=\"switch\" attribute, allowing screen readers to identify it as a toggleable control.", + }, + { + title: "State Representation", + description: "The aria-checked attribute dynamically reflects the toggle's state (on/off), providing real-time feedback to assistive technologies.", + }, + { + title: "Labeling", + description: "Toggles can be associated with descriptive labels using the aria-labelledby or aria-label attributes, enhancing clarity for screen reader users.", + }, - { - title: "Keyboard Navigation", - description: `The toggle supports keyboard navigation with TAB and ENTER keys, ensuring full accessibility for users who rely on keyboard input.`, - }, - { - title: "Indeterminate State Accessibility", - description: `When a toggle is in an indeterminate state, it uses the aria-checked="mixed" attribute to inform assistive technologies of its unique status.`, - }, - { - title: "Focus Management", - description: `The toggle is fully focusable and provides visual focus indicators, aiding users in tracking their navigation through the interface.`, - }, - ]; + { + title: "Keyboard Navigation", + description: "The toggle supports keyboard navigation with TAB and ENTER keys, ensuring full accessibility for users who rely on keyboard input.", + }, + { + title: "Indeterminate State Accessibility", + description: "When a toggle is in an indeterminate state, it uses the aria-checked=\"mixed\" attribute to inform assistive technologies of its unique status.", + }, + { + title: "Focus Management", + description: "The toggle is fully focusable and provides visual focus indicators, aiding users in tracking their navigation through the interface.", + }, + ]; - static #featuresCards() { - const fragment = document.createDocumentFragment(); + static #featuresCards() { + const fragment = document.createDocumentFragment(); - const description = document.createElement("p"); - description.innerHTML = `Bootstrap Toggle is built with accessibility in mind. The implementation ensures seamless compatibility with screen readers, keyboard navigation, and other assistive technologies.`; - fragment.appendChild(description); + const description = document.createElement("p"); + description.innerHTML = "Bootstrap Toggle is built with accessibility in mind. The implementation ensures seamless compatibility with screen readers, keyboard navigation, and other assistive technologies."; + fragment.appendChild(description); - const container = document.createElement("div"); - container.classList.add( - "row", - "row-cols-1", - "row-cols-md-2", - "row-cols-xl-3", - "g-4", - ); - Accessibility.#features.forEach((feature) => { - const col = document.createElement("div"); - col.classList.add("col"); + const container = document.createElement("div"); + container.classList.add( + "row", + "row-cols-1", + "row-cols-md-2", + "row-cols-xl-3", + "g-4", + ); + Accessibility.#features.forEach((feature) => { + const col = document.createElement("div"); + col.classList.add("col"); - const card = document.createElement("div"); - card.classList.add("card"); + const card = document.createElement("div"); + card.classList.add("card"); - const cardHeader = document.createElement("div"); - cardHeader.classList.add("card-header"); - cardHeader.textContent = feature.title; - card.appendChild(cardHeader); + const cardHeader = document.createElement("div"); + cardHeader.classList.add("card-header"); + cardHeader.textContent = feature.title; + card.appendChild(cardHeader); - const cardBody = document.createElement("div"); - cardBody.classList.add("card-body"); - const description = document.createElement("p"); - description.classList.add("card-text"); - description.innerHTML = feature.description; - cardBody.appendChild(description); - card.appendChild(cardBody); + const cardBody = document.createElement("div"); + cardBody.classList.add("card-body"); + const description = document.createElement("p"); + description.classList.add("card-text"); + description.innerHTML = feature.description; + cardBody.appendChild(description); + card.appendChild(cardBody); - col.appendChild(card); - container.appendChild(col); - }); + col.appendChild(card); + container.appendChild(col); + }); - fragment.appendChild(container); - return fragment; - } + fragment.appendChild(container); + return fragment; + } - static #wcagChecklist = [ - { - criterion: "1.3.1 Info and Relationships", - url: "https://www.w3.org/TR/WCAG22/#info-and-relationships", - compliance: "Compliant", - evidence: + static #wcagChecklist = [ + { + criterion: "1.3.1 Info and Relationships", + url: "https://www.w3.org/TR/WCAG22/#info-and-relationships", + compliance: "Compliant", + evidence: "role='switch', proper ARIA attributes, semantic structure", - }, - { - criterion: "1.4.4 Resize Text", - url: "https://www.w3.org/TR/WCAG22/#resize-text", - compliance: "Compliant", - evidence: "Uses relative units, responsive design", - }, - { - criterion: "2.1.1 Keyboard", - url: "https://www.w3.org/TR/WCAG22/#keyboard", - compliance: "Compliant", - evidence: + }, + { + criterion: "1.4.4 Resize Text", + url: "https://www.w3.org/TR/WCAG22/#resize-text", + compliance: "Compliant", + evidence: "Uses relative units, responsive design", + }, + { + criterion: "2.1.1 Keyboard", + url: "https://www.w3.org/TR/WCAG22/#keyboard", + compliance: "Compliant", + evidence: "Responds to Space and Enter keys, all functionality keyboard accessible", - }, - { - criterion: "2.1.2 No Keyboard Trap", - url: "https://www.w3.org/TR/WCAG22/#no-keyboard-trap", - compliance: "Compliant", - evidence: + }, + { + criterion: "2.1.2 No Keyboard Trap", + url: "https://www.w3.org/TR/WCAG22/#no-keyboard-trap", + compliance: "Compliant", + evidence: "tabindex=0 with proper focus management, users can navigate away", - }, - { - criterion: "2.1.4 Character Key Shortcuts", - url: "https://www.w3.org/TR/WCAG22/#character-key-shortcuts", - compliance: "Compliant", - evidence: "No single-character shortcuts used", - }, - { - criterion: "2.4.3 Focus Order", - url: "https://www.w3.org/TR/WCAG22/#focus-order", - compliance: "Compliant", - evidence: "tabindex respects DOM order", - notice: + }, + { + criterion: "2.1.4 Character Key Shortcuts", + url: "https://www.w3.org/TR/WCAG22/#character-key-shortcuts", + compliance: "Compliant", + evidence: "No single-character shortcuts used", + }, + { + criterion: "2.4.3 Focus Order", + url: "https://www.w3.org/TR/WCAG22/#focus-order", + compliance: "Compliant", + evidence: "tabindex respects DOM order", + notice: "Component follows logical DOM order; integration depends on page structure", - }, - { - criterion: "2.4.7 Focus Visible", - url: "https://www.w3.org/TR/WCAG22/#focus-visible", - compliance: "Partially", - evidence: "Programmatic focus management implemented", - notice: + }, + { + criterion: "2.4.7 Focus Visible", + url: "https://www.w3.org/TR/WCAG22/#focus-visible", + compliance: "Partially", + evidence: "Programmatic focus management implemented", + notice: "Visual focus indicators require CSS styling from consuming application, component defaults style meet criterion", - }, - { - criterion: "2.4.11 Focus Not Obscured (Minimum)", - url: "https://www.w3.org/TR/WCAG22/#focus-not-obscured-minimum", - compliance: "Partially", - evidence: "Programmatic focus management implemented", - notice: + }, + { + criterion: "2.4.11 Focus Not Obscured (Minimum)", + url: "https://www.w3.org/TR/WCAG22/#focus-not-obscured-minimum", + compliance: "Partially", + evidence: "Programmatic focus management implemented", + notice: "Component doesn't obscure its own focus, but page-level layout/styling may affect visibility", - }, - { - criterion: "2.4.12 Focus Not Obscured (Enhanced)", - url: "https://www.w3.org/TR/WCAG22/#focus-not-obscured-enhanced", - compliance: "Partially", - evidence: "Programmatic focus management implemented", - notice: + }, + { + criterion: "2.4.12 Focus Not Obscured (Enhanced)", + url: "https://www.w3.org/TR/WCAG22/#focus-not-obscured-enhanced", + compliance: "Partially", + evidence: "Programmatic focus management implemented", + notice: "Same as 2.4.11; component-level focus is clear but dependent on page context", - }, - { - criterion: "2.5.3 Label in Name", - url: "https://www.w3.org/TR/WCAG22/#label-in-name", - compliance: "Compliant", - evidence: "aria-label matches visual label when provided", - }, - { - criterion: "3.2.1 On Focus", - url: "https://www.w3.org/TR/WCAG22/#on-focus", - compliance: "Compliant", - evidence: "Focus does not trigger unexpected context changes", - }, - { - criterion: "3.3.2 Labels or Instructions", - url: "https://www.w3.org/TR/WCAG22/#labels-or-instructions", - compliance: "Compliant", - evidence: + }, + { + criterion: "2.5.3 Label in Name", + url: "https://www.w3.org/TR/WCAG22/#label-in-name", + compliance: "Compliant", + evidence: "aria-label matches visual label when provided", + }, + { + criterion: "3.2.1 On Focus", + url: "https://www.w3.org/TR/WCAG22/#on-focus", + compliance: "Compliant", + evidence: "Focus does not trigger unexpected context changes", + }, + { + criterion: "3.3.2 Labels or Instructions", + url: "https://www.w3.org/TR/WCAG22/#labels-or-instructions", + compliance: "Compliant", + evidence: "Supports aria-label and aria-labelledby with fallback to default label", - }, - { - criterion: "4.1.1 Parsing", - url: "https://www.w3.org/TR/WCAG22/#parsing", - compliance: "Compliant", - evidence: "Valid HTML, no duplicate IDs, proper nesting", - }, - { - criterion: "4.1.2 Name, Role, Value", - url: "https://www.w3.org/TR/WCAG22/#name-role-value", - compliance: "Compliant", - evidence: + }, + { + criterion: "4.1.1 Parsing", + url: "https://www.w3.org/TR/WCAG22/#parsing", + compliance: "Compliant", + evidence: "Valid HTML, no duplicate IDs, proper nesting", + }, + { + criterion: "4.1.2 Name, Role, Value", + url: "https://www.w3.org/TR/WCAG22/#name-role-value", + compliance: "Compliant", + evidence: "role='switch', aria-label | aria-labelledby for name, aria-checked for value", - }, - { - criterion: "4.1.3 Status Messages", - url: "https://www.w3.org/TR/WCAG22/#status-messages", - compliance: "Not Compliant", - evidence: + }, + { + criterion: "4.1.3 Status Messages", + url: "https://www.w3.org/TR/WCAG22/#status-messages", + compliance: "Not Compliant", + evidence: "There is not aria-live, aria-atomic, or aria-relevant for dynamic state announcements", - notice: + notice: "State changes (ON/OFF/INDETERMINATE) are not announced via aria-live regions. Screen readers detect changes through aria-checked updates but not proactively announced.", - }, - ]; + }, + ]; - static #wcagCompliance() { - const fragment = document.createDocumentFragment(); + static #wcagCompliance() { + const fragment = document.createDocumentFragment(); - const description = document.createElement("p"); - description.innerHTML = `Bootstrap Toggle adheres to WCAG 2.2 AA standards by implementing proper ARIA roles and attributes, ensuring that all users, including those with disabilities, can effectively interact with toggle switches.`; + const description = document.createElement("p"); + description.innerHTML = "Bootstrap Toggle adheres to WCAG 2.2 AA standards by implementing proper ARIA roles and attributes, ensuring that all users, including those with disabilities, can effectively interact with toggle switches."; - const list = document.createElement("ul"); - list.classList.add("list-group", "list-group-flush"); - Accessibility.#wcagChecklist.forEach((item) => { - const listItem = document.createElement("li"); - listItem.classList.add("list-group-item"); + const list = document.createElement("ul"); + list.classList.add("list-group", "list-group-flush"); + Accessibility.#wcagChecklist.forEach((item) => { + const listItem = document.createElement("li"); + listItem.classList.add("list-group-item"); - const header = document.createElement("div"); - header.classList.add( - "d-flex", - "justify-content-start", - "align-items-center", - "gap-2", - ); + const header = document.createElement("div"); + header.classList.add( + "d-flex", + "justify-content-start", + "align-items-center", + "gap-2", + ); - const url = document.createElement("a"); - url.classList.add("text-decoration-none"); - url.href = item.url; - url.innerHTML = ``; - url.target = "_blank"; - header.appendChild(url); + const url = document.createElement("a"); + url.classList.add("text-decoration-none"); + url.href = item.url; + url.innerHTML = ""; + url.target = "_blank"; + header.appendChild(url); - const criterionText = document.createElement("strong"); - criterionText.innerHTML = item.criterion; - header.appendChild(criterionText); + const criterionText = document.createElement("strong"); + criterionText.innerHTML = item.criterion; + header.appendChild(criterionText); - const badge = document.createElement("span"); - badge.classList.add("badge"); - if (item.compliance === "Compliant") { - badge.classList.add("bg-success"); - } else if (item.compliance === "Partially") { - badge.classList.add("bg-warning"); - } else { - badge.classList.add("bg-danger"); - } - badge.textContent = item.compliance; - header.appendChild(badge); - listItem.appendChild(header); + const badge = document.createElement("span"); + badge.classList.add("badge"); + if (item.compliance === "Compliant") { + badge.classList.add("bg-success"); + } else if (item.compliance === "Partially") { + badge.classList.add("bg-warning"); + } else { + badge.classList.add("bg-danger"); + } + badge.textContent = item.compliance; + header.appendChild(badge); + listItem.appendChild(header); - const evidence = document.createElement("p"); - evidence.classList.add("m-0"); - evidence.innerHTML = item.evidence; - listItem.appendChild(evidence); + const evidence = document.createElement("p"); + evidence.classList.add("m-0"); + evidence.innerHTML = item.evidence; + listItem.appendChild(evidence); - if (item.notice) { - const notice = document.createElement("p"); - notice.classList.add("m-0"); - notice.innerHTML = `${item.notice}`; - listItem.appendChild(notice); - } + if (item.notice) { + const notice = document.createElement("p"); + notice.classList.add("m-0"); + notice.innerHTML = `${item.notice}`; + listItem.appendChild(notice); + } - list.appendChild(listItem); - }); + list.appendChild(listItem); + }); - fragment.appendChild(description); - fragment.appendChild(list); - return fragment; - } + fragment.appendChild(description); + fragment.appendChild(list); + return fragment; + } } export default Accessibility; diff --git a/docs/src/js/components/documentation/Api.js b/docs/src/js/components/documentation/Api.js index f39757f5..c76570be 100644 --- a/docs/src/js/components/documentation/Api.js +++ b/docs/src/js/components/documentation/Api.js @@ -6,15 +6,15 @@ import State from "./api/State"; import DocSection from "./DocSection"; class Api extends DocSection { - static build() { - return super.build("api", "API", [ - Options.build(), - Methods.build(), - InputInteraction.build(), - State.build(), - Rerender.build(), - ]); - } + static build() { + return super.build("api", "API", [ + Options.build(), + Methods.build(), + InputInteraction.build(), + State.build(), + Rerender.build(), + ]); + } } export default Api; diff --git a/docs/src/js/components/documentation/DevTools.js b/docs/src/js/components/documentation/DevTools.js new file mode 100644 index 00000000..ac6de5c4 --- /dev/null +++ b/docs/src/js/components/documentation/DevTools.js @@ -0,0 +1,12 @@ +import Monitoring from "./devtools/Monitoring"; +import DocSection from "./DocSection"; + +class DevTools extends DocSection { + static build() { + return super.build("devtools", "Developer Tools", [ + Monitoring.build(), + ]); + } +} + +export default DevTools; diff --git a/docs/src/js/components/documentation/Distribution.js b/docs/src/js/components/documentation/Distribution.js index 70be81db..652c27e8 100644 --- a/docs/src/js/components/documentation/Distribution.js +++ b/docs/src/js/components/documentation/Distribution.js @@ -2,104 +2,104 @@ import DocArticle from "./DocArticle"; import DocSection from "./DocSection"; class Distribution extends DocSection { - static headers = ["Bootstrap Support", "Plugin Last Release", "End Of Life"]; - static entries = [ - { bootstrap: "5", plugin: "5", eol: "5" }, - { bootstrap: "4", plugin: "4", eol: "4" }, - { bootstrap: "3", plugin: "3", eol: "3" }, - ]; + static headers = ["Bootstrap Support", "Plugin Last Release", "End Of Life"]; + static entries = [ + { bootstrap: "5", plugin: "5", eol: "5" }, + { bootstrap: "4", plugin: "4", eol: "4" }, + { bootstrap: "3", plugin: "3", eol: "3" }, + ]; - static build() { - return super.build( - "distribution", - "Library Distributions", - [ - DocArticle.build({ - description: Distribution.#table(), - }), - ], - "mb-3" - ); - } + static build() { + return super.build( + "distribution", + "Library Distributions", + [ + DocArticle.build({ + description: Distribution.#table(), + }), + ], + "mb-3" + ); + } - static #table() { - const container = document.createElement("div"); - container.className = "table-responsive"; + static #table() { + const container = document.createElement("div"); + container.className = "table-responsive"; - const table = document.createElement("table"); - table.className = "table table-striped table-condensed mb-0"; + const table = document.createElement("table"); + table.className = "table table-striped table-condensed mb-0"; - const caption = document.createElement("caption"); - caption.textContent = "Library Distributions"; + const caption = document.createElement("caption"); + caption.textContent = "Library Distributions"; - table.append( - caption, - Distribution.#tableHeader(), - Distribution.#tableBody() - ); - container.appendChild(table); - return container; - } + table.append( + caption, + Distribution.#tableHeader(), + Distribution.#tableBody() + ); + container.appendChild(table); + return container; + } - static #tableHeader() { - const thead = document.createElement("thead"); - const row = document.createElement("tr"); + static #tableHeader() { + const thead = document.createElement("thead"); + const row = document.createElement("tr"); - Distribution.headers.forEach((headerText) => { - const th = document.createElement("th"); - th.textContent = headerText; - row.appendChild(th); - }); + Distribution.headers.forEach((headerText) => { + const th = document.createElement("th"); + th.textContent = headerText; + row.appendChild(th); + }); - thead.appendChild(row); - return thead; - } + thead.appendChild(row); + return thead; + } - static #tableBody() { - const tbody = document.createElement("tbody"); - Distribution.entries.forEach((entry) => { - const row = document.createElement("tr"); - const tdBootstrap = document.createElement("td"); - const aBootstrap = document.createElement("a"); - aBootstrap.href = `https://getbootstrap.com/docs/${entry.bootstrap}.0`; - aBootstrap.target = "_blank"; - aBootstrap.rel = "noopener noreferrer"; - aBootstrap.title = `Bootstrap ${entry.bootstrap}`; - const imgBootstrap = document.createElement("img"); - imgBootstrap.src = `https://img.shields.io/static/v1?label=bootstrap&message=%5Ev${entry.bootstrap}.0.0&color=informational&logo=bootstrap&logoColor=white`; - imgBootstrap.alt = `Bootstrap ${entry.bootstrap}`; - aBootstrap.appendChild(imgBootstrap); - tdBootstrap.appendChild(aBootstrap); + static #tableBody() { + const tbody = document.createElement("tbody"); + Distribution.entries.forEach((entry) => { + const row = document.createElement("tr"); + const tdBootstrap = document.createElement("td"); + const aBootstrap = document.createElement("a"); + aBootstrap.href = `https://getbootstrap.com/docs/${entry.bootstrap}.0`; + aBootstrap.target = "_blank"; + aBootstrap.rel = "noopener noreferrer"; + aBootstrap.title = `Bootstrap ${entry.bootstrap}`; + const imgBootstrap = document.createElement("img"); + imgBootstrap.src = `https://img.shields.io/static/v1?label=bootstrap&message=%5Ev${entry.bootstrap}.0.0&color=informational&logo=bootstrap&logoColor=white`; + imgBootstrap.alt = `Bootstrap ${entry.bootstrap}`; + aBootstrap.appendChild(imgBootstrap); + tdBootstrap.appendChild(aBootstrap); - const tdPlugin = document.createElement("td"); - const aPlugin = document.createElement("a"); - aPlugin.href = `https://github.com/palcarazm/bootstrap5-toggle/releases`; - aPlugin.target = "_blank"; - aPlugin.rel = "noopener noreferrer"; - aPlugin.title = `bs-toggle v${entry.plugin}`; - const imgPlugin = document.createElement("img"); - imgPlugin.src = `https://img.shields.io/github/package-json/v/palcarazm/bootstrap5-toggle/v${entry.plugin}?logo=github`; - imgPlugin.alt = `bs-toggle v${entry.plugin}`; - aPlugin.appendChild(imgPlugin); - tdPlugin.appendChild(aPlugin); + const tdPlugin = document.createElement("td"); + const aPlugin = document.createElement("a"); + aPlugin.href = "https://github.com/palcarazm/bootstrap5-toggle/releases"; + aPlugin.target = "_blank"; + aPlugin.rel = "noopener noreferrer"; + aPlugin.title = `bs-toggle v${entry.plugin}`; + const imgPlugin = document.createElement("img"); + imgPlugin.src = `https://img.shields.io/github/package-json/v/palcarazm/bootstrap5-toggle/v${entry.plugin}?logo=github`; + imgPlugin.alt = `bs-toggle v${entry.plugin}`; + aPlugin.appendChild(imgPlugin); + tdPlugin.appendChild(aPlugin); - const tdEOL = document.createElement("td"); - const aEOL = document.createElement("a"); - aEOL.href = `https://github.com/palcarazm/bootstrap5-toggle/security/policy`; - aEOL.target = "_blank"; - aEOL.rel = "noopener noreferrer"; - aEOL.title = `End of Life`; - const imgEOL = document.createElement("img"); - imgEOL.src = `https://img.shields.io/endpoint?url=https%3A%2F%2Fpalcarazm.github.io%2Fbootstrap5-toggle%2Fapi%2Feol%2Fv${entry.eol}`; - imgEOL.alt = `EOL v${entry.eol}`; - aEOL.appendChild(imgEOL); - tdEOL.appendChild(aEOL); + const tdEOL = document.createElement("td"); + const aEOL = document.createElement("a"); + aEOL.href = "https://github.com/palcarazm/bootstrap5-toggle/security/policy"; + aEOL.target = "_blank"; + aEOL.rel = "noopener noreferrer"; + aEOL.title = "End of Life"; + const imgEOL = document.createElement("img"); + imgEOL.src = `https://img.shields.io/endpoint?url=https%3A%2F%2Fpalcarazm.github.io%2Fbootstrap5-toggle%2Fapi%2Feol%2Fv${entry.eol}`; + imgEOL.alt = `EOL v${entry.eol}`; + aEOL.appendChild(imgEOL); + tdEOL.appendChild(aEOL); - row.append(tdBootstrap, tdPlugin, tdEOL); - tbody.appendChild(row); - }); - return tbody; - } + row.append(tdBootstrap, tdPlugin, tdEOL); + tbody.appendChild(row); + }); + return tbody; + } } export default Distribution; diff --git a/docs/src/js/components/documentation/DocAlert.js b/docs/src/js/components/documentation/DocAlert.js index c91d2ecf..5d9f3f42 100644 --- a/docs/src/js/components/documentation/DocAlert.js +++ b/docs/src/js/components/documentation/DocAlert.js @@ -1,54 +1,54 @@ import VersionPill from "../VersionPill"; class DocAlert { - static build({ type, title, content, versionPill = null }) { - const alert = document.createElement("div"); - alert.className = `alert alert-${type}`; + static build({ type, title, content, versionPill = null }) { + const alert = document.createElement("div"); + alert.className = `alert alert-${type}`; - const alertBody = document.createElement("div"); - alertBody.innerHTML = content; + const alertBody = document.createElement("div"); + alertBody.innerHTML = content; - alert.append(DocAlert.#alertHeader(type, title, versionPill), alertBody); - return alert; - } + alert.append(DocAlert.#alertHeader(type, title, versionPill), alertBody); + return alert; + } - static #alertHeader(type, title, versionPill) { - const header = document.createElement("div"); - header.className = + static #alertHeader(type, title, versionPill) { + const header = document.createElement("div"); + header.className = "d-flex justify-content-between align-items-center alert-heading"; - let icon = document.createElement("i"); - switch (type) { - case "info": - icon.className = "fa-solid fa-info-circle"; - break; - case "success": - icon.className = "fa-solid fa-circle-check"; - break; - case "warning": - icon.className = "fa-solid fa-exclamation-circle"; - break; - case "danger": - icon.className = "fa-solid fa-exclamation-triangle"; - break; - default: - icon = null; - break; + let icon = document.createElement("i"); + switch (type) { + case "info": + icon.className = "fa-solid fa-info-circle"; + break; + case "success": + icon.className = "fa-solid fa-circle-check"; + break; + case "warning": + icon.className = "fa-solid fa-exclamation-circle"; + break; + case "danger": + icon.className = "fa-solid fa-exclamation-triangle"; + break; + default: + icon = null; + break; + } + + const col = document.createElement("div"); + if (icon) col.appendChild(icon); + const titleContainer = document.createElement("span"); + titleContainer.className = "text-uppercase fw-bold ms-1"; + titleContainer.textContent = title; + col.appendChild(titleContainer); + + header.appendChild(col); + + if (versionPill) header.appendChild(VersionPill.build(versionPill)); + + return header; } - - const col = document.createElement("div"); - if (icon) col.appendChild(icon); - const titleContainer = document.createElement("span"); - titleContainer.className = "text-uppercase fw-bold ms-1"; - titleContainer.textContent = title; - col.appendChild(titleContainer); - - header.appendChild(col); - - if (versionPill) header.appendChild(VersionPill.build(versionPill)); - - return header; - } } export default DocAlert; diff --git a/docs/src/js/components/documentation/DocArticle.js b/docs/src/js/components/documentation/DocArticle.js index be1a4053..6b398135 100644 --- a/docs/src/js/components/documentation/DocArticle.js +++ b/docs/src/js/components/documentation/DocArticle.js @@ -5,53 +5,59 @@ import VersionPill from "../VersionPill"; import DocAlert from "./DocAlert"; class DocArticle { - static build({ - title = null, - className = "", - badge = null, - versionPill = null, - description = null, - example = [], - codePanel = null, - codeBlock = null, - alert = null, - }) { - const docArticle = document.createElement("article"); - docArticle.className = `ps-2 px-2 border-start border-2 ${className}`; - - if (title) - docArticle.appendChild( - DocArticle.#articleHeader(title, badge, versionPill) - ); - - if (description) docArticle.appendChild(description); - - if (codePanel && codeBlock) - throw new Error("CodePanel and CodeBlock are mutually exclusive."); - if (codePanel) - docArticle.appendChild(CodePanel.build({ example, ...codePanel })); - if (codeBlock) - docArticle.appendChild(CodeBlock.build({ example, ...codeBlock })); - - if (alert) docArticle.appendChild(DocAlert.build(alert)); - - return docArticle; - } - - static #articleHeader(title, badge, versionPill) { - const header = document.createElement("div"); - header.className = "d-flex justify-content-between align-items-center"; - const titleContainer = document.createElement("h3"); - titleContainer.className = "text-secondary"; - titleContainer.textContent = title; - header.appendChild(titleContainer); - - if (badge) header.appendChild(Badge.build(badge)); - - if (versionPill) header.appendChild(VersionPill.build(versionPill)); - - return header; - } + static build({ + title = null, + className = "", + badge = null, + versionPill = null, + description = null, + example = [], + codePanel = null, + codeBlock = null, + alert = null, + }) { + const docArticle = document.createElement("article"); + docArticle.className = `ps-2 px-2 border-start border-2 ${className}`; + + if (title) + docArticle.appendChild( + DocArticle.#articleHeader(title, badge, versionPill) + ); + + if (description) docArticle.appendChild(description); + + if (codePanel && codeBlock) + throw new Error("CodePanel and CodeBlock are mutually exclusive."); + if (codePanel) + docArticle.appendChild(CodePanel.build({ example, ...codePanel })); + if (codeBlock) + docArticle.appendChild(CodeBlock.build({ example, ...codeBlock })); + + if (alert) docArticle.appendChild(DocAlert.build(alert)); + + return docArticle; + } + + static #articleHeader(title, badge, versionPill) { + const header = document.createElement("div"); + header.className = "d-flex justify-content-between align-items-center"; + const titleContainer = document.createElement("h3"); + titleContainer.className = "text-secondary"; + titleContainer.textContent = title; + header.appendChild(titleContainer); + + if (badge) header.appendChild(Badge.build(badge)); + + if (versionPill) header.appendChild(VersionPill.build(versionPill)); + + return header; + } + + static fragment(...nodes) { + const fragment = document.createDocumentFragment(); + nodes.forEach((node) => fragment.appendChild(node)); + return fragment; + } } export default DocArticle; diff --git a/docs/src/js/components/documentation/DocSection.js b/docs/src/js/components/documentation/DocSection.js index 3bd08ee8..cbcd7426 100644 --- a/docs/src/js/components/documentation/DocSection.js +++ b/docs/src/js/components/documentation/DocSection.js @@ -1,16 +1,16 @@ class DocSection { - static build(name, title, HTMLElements, className = "") { - const section = document.createElement("section"); - section.id = name; - section.className = `container ${className}`; - section.append(DocSection.#header(title), ...HTMLElements); - return section; - } - static #header(title) { - const header = document.createElement("h2"); - header.textContent = title; - return header; - } + static build(name, title, HTMLElements, className = "") { + const section = document.createElement("section"); + section.id = name; + section.className = `container ${className}`; + section.append(DocSection.#header(title), ...HTMLElements); + return section; + } + static #header(title) { + const header = document.createElement("h2"); + header.textContent = title; + return header; + } } export default DocSection; diff --git a/docs/src/js/components/documentation/Events.js b/docs/src/js/components/documentation/Events.js index 36e16467..3d34f590 100644 --- a/docs/src/js/components/documentation/Events.js +++ b/docs/src/js/components/documentation/Events.js @@ -1,16 +1,20 @@ import DocSection from "./DocSection"; import CustomEvents from "./events/CustomEvents"; import EventPropagation from "./events/EventPropagation"; +import LifecycleEvents from "./events/LifecycleEvents"; +import ChainableSubscription from "./events/ChainableSubscription"; import SilencedActions from "./events/SilencedActions"; class Events extends DocSection { - static build() { - return super.build("events", "Events", [ - EventPropagation.build(), - CustomEvents.build(), - SilencedActions.build(), - ]); - } + static build() { + return super.build("events", "Events", [ + EventPropagation.build(), + CustomEvents.build(), + LifecycleEvents.build(), + ChainableSubscription.build(), + SilencedActions.build(), + ]); + } } export default Events; diff --git a/docs/src/js/components/documentation/Features.js b/docs/src/js/components/documentation/Features.js index a9be4660..fb34a311 100644 --- a/docs/src/js/components/documentation/Features.js +++ b/docs/src/js/components/documentation/Features.js @@ -15,24 +15,24 @@ import Tooltip from "./features/Tooltip"; import Tristate from "./features/Tristate"; class Features extends DocSection { - static build() { - return super.build("features", "Features", [ - Size.build(), - CustomSize.build(), - Tristate.build(), - Colors.build(), - OutlineColors.build(), - CustomText.build(), - CustomTitle.build(), - Tooltip.build(), - CustomStyle.build(), - AnimationSpeed.build(), - KeyboardInteraction.build(), - StateStatus.build(), - CustomFormValue.build(), - FormLayout.build(), - ]); - } + static build() { + return super.build("features", "Features", [ + Size.build(), + CustomSize.build(), + Tristate.build(), + Colors.build(), + OutlineColors.build(), + CustomText.build(), + CustomTitle.build(), + Tooltip.build(), + CustomStyle.build(), + AnimationSpeed.build(), + KeyboardInteraction.build(), + StateStatus.build(), + CustomFormValue.build(), + FormLayout.build(), + ]); + } } export default Features; diff --git a/docs/src/js/components/documentation/Installation.js b/docs/src/js/components/documentation/Installation.js index 70e07e6b..7c101d73 100644 --- a/docs/src/js/components/documentation/Installation.js +++ b/docs/src/js/components/documentation/Installation.js @@ -2,72 +2,72 @@ import DocArticle from "./DocArticle"; import DocSection from "./DocSection"; class Installation extends DocSection { - static build() { - return super.build("installation", "Installation", [ - Installation.#cdn(), - Installation.#github(), - Installation.#npm(), - Installation.#yarn(), - ]); - } + static build() { + return super.build("installation", "Installation", [ + Installation.#cdn(), + Installation.#github(), + Installation.#npm(), + Installation.#yarn(), + ]); + } - static #cdn() { - const ecmas = ` + static #cdn() { + const ecmas = ` `; - const jquery = ` + const jquery = ` `; - return DocArticle.build({ - title: "CDN", - badge: { - name: "jsDelivr", - href: "https://www.jsdelivr.com/package/npm/bootstrap5-toggle", - imgSrc: + return DocArticle.build({ + title: "CDN", + badge: { + name: "jsDelivr", + href: "https://www.jsdelivr.com/package/npm/bootstrap5-toggle", + imgSrc: "https://img.shields.io/jsdelivr/npm/hm/bootstrap5-toggle?label=hits&logo=jsdelivr&logoColor=white", - }, - codePanel: { - name: "cdn", - language: "html", - contents: [ecmas, jquery], - }, - }); - } + }, + codePanel: { + name: "cdn", + language: "html", + contents: [ecmas, jquery], + }, + }); + } - static #github() { - return DocArticle.build({ - title: "Download from GitHub", - badge: { - name: "Latest release", - href: "https://github.com/palcarazm/bootstrap5-toggle/releases", - imgSrc: + static #github() { + return DocArticle.build({ + title: "Download from GitHub", + badge: { + name: "Latest release", + href: "https://github.com/palcarazm/bootstrap5-toggle/releases", + imgSrc: "https://img.shields.io/github/package-json/v/palcarazm/bootstrap5-toggle/v5?logo=github", - }, - }); - } + }, + }); + } - static #npm() { - return DocArticle.build({ - title: "NPM", - badge: { - name: "NPM", - href: "https://www.npmjs.com/package/bootstrap5-toggle", - imgSrc: "https://img.shields.io/npm/dm/bootstrap5-toggle?logo=npm", - }, - codeBlock: { - language: "shell", - code: `npm install bootstrap5-toggle@${versions.bootstrap5Toggle}`, - }, - }); - } + static #npm() { + return DocArticle.build({ + title: "NPM", + badge: { + name: "NPM", + href: "https://www.npmjs.com/package/bootstrap5-toggle", + imgSrc: "https://img.shields.io/npm/dm/bootstrap5-toggle?logo=npm", + }, + codeBlock: { + language: "shell", + code: `npm install bootstrap5-toggle@${versions.bootstrap5Toggle}`, + }, + }); + } - static #yarn() { - return DocArticle.build({ - title: "Yarn", - codeBlock: { - language: "shell", - code: `yarn add bootstrap5-toggle@${versions.bootstrap5Toggle}`, - }, - }); - } + static #yarn() { + return DocArticle.build({ + title: "Yarn", + codeBlock: { + language: "shell", + code: `yarn add bootstrap5-toggle@${versions.bootstrap5Toggle}`, + }, + }); + } } export default Installation; diff --git a/docs/src/js/components/documentation/Usage.js b/docs/src/js/components/documentation/Usage.js index a507740d..6c6f6aa5 100644 --- a/docs/src/js/components/documentation/Usage.js +++ b/docs/src/js/components/documentation/Usage.js @@ -2,41 +2,47 @@ import DocSection from "./DocSection"; import DocArticle from "./DocArticle"; class Usage extends DocSection { - static build() { - return super.build("usage", "Usage", [Usage.#html(), Usage.#js()]); - } - - static #html() { - const description = document.createElement("p"); - description.innerHTML = `Simply add data-toggle="toggle" to convert checkboxes into toggles.`; - - return DocArticle.build({ - title: "Initialize with HTML", - description, - codeBlock: { - language: "html", - code: ``, - }, - }); - } - - static #js() { - const description = document.createElement("p"); - description.innerHTML = `Simply call the bootstrapToggle method to convert checkboxes into toggles. See Options for additional colors, etc.

`; - - const ecmas = `document.querySelector(mySelector).bootstrapToggle();`; - const jquery = `$(mySelector).bootstrapToggle();`; - - return DocArticle.build({ - title: "Initialize with JavaScript", - description, - codePanel: { - name: "usage-js", - language: "javascript", - contents: [ecmas, jquery], - }, - }); - } + static build() { + return super.build("usage", "Usage", [Usage.#html(), Usage.#js()]); + } + + static #html() { + const description = document.createElement("p"); + description.innerHTML = "Simply add data-toggle=\"toggle\" to convert checkboxes into toggles."; + + const asyncNote = document.createElement("p"); + asyncNote.innerHTML = "[data-toggle=\"toggle\"] auto-init runs on window.onload and awaits init().then(attach) internally."; + + return DocArticle.build({ + title: "Initialize with HTML", + description: DocArticle.fragment(description, asyncNote), + codeBlock: { + language: "html", + code: "", + }, + }); + } + + static #js() { + const description = document.createElement("p"); + description.innerHTML = "Simply call the bootstrapToggle method to convert checkboxes into toggles. See Options for additional colors, etc.

"; + + const asyncNote = document.createElement("p"); + asyncNote.innerHTML = "element.bootstrapToggle() returns synchronously and starts the lifecycle in a microtask. element.bsToggle is set synchronously (in the constructor) but element.bsToggle.isAttached() is false until the microtask drains."; + + const ecmas = "document.querySelector(mySelector).bootstrapToggle();"; + const jquery = "$(mySelector).bootstrapToggle();"; + + return DocArticle.build({ + title: "Initialize with JavaScript", + description: DocArticle.fragment(description, asyncNote), + codePanel: { + name: "usage-js", + language: "javascript", + contents: [ecmas, jquery], + }, + }); + } } export default Usage; diff --git a/docs/src/js/components/documentation/api/InputInteraction.js b/docs/src/js/components/documentation/api/InputInteraction.js index f33d0727..c82c3c7c 100644 --- a/docs/src/js/components/documentation/api/InputInteraction.js +++ b/docs/src/js/components/documentation/api/InputInteraction.js @@ -1,167 +1,176 @@ import DocArticle from "../DocArticle"; class InputInteraction extends DocArticle { - static #interactions = [ - { - action: "checked", - function: "myInput.checked = true", - description: "Sets the toggle to 'On' state", - demo: (input) => { - input.checked = true; - }, - }, - { - action: "unchecked", - function: "myInput.checked = false", - description: "Sets the toggle to 'Off' state", - demo: (input) => { - input.checked = false; - }, - }, - { - action: "indeterminate", - function: "myInput.indeterminate = true", - description: "Sets the toggle to 'indeterminate' state", - demo: (input) => { - input.indeterminate = true; - }, - }, - { - action: "determinate", - function: "myInput.indeterminate = false", - description: + static #interactions = [ + { + action: "checked", + function: "myInput.checked = true", + description: "Sets the toggle to 'On' state", + demo: (input) => { + input.checked = true; + }, + }, + { + action: "unchecked", + function: "myInput.checked = false", + description: "Sets the toggle to 'Off' state", + demo: (input) => { + input.checked = false; + }, + }, + { + action: "indeterminate", + function: "myInput.indeterminate = true", + description: "Sets the toggle to 'indeterminate' state", + demo: (input) => { + input.indeterminate = true; + }, + }, + { + action: "determinate", + function: "myInput.indeterminate = false", + description: "Sets the toggle to the previous 'determinate' state ('on' or 'off')", - demo: (input) => { - input.indeterminate = false; - }, - }, - { - action: "enable", - function: "myInput.disabled = false", - description: "Enables the toggle", - demo: (input) => { - input.disabled = false; - }, - }, - { - action: "disable", - function: "myInput.disabled = true", - description: "Disables the toggle", - demo: (input) => { - input.disabled = true; - }, - }, - { - action: "read-only", - function: "myInput.readOnly = true", - description: + demo: (input) => { + input.indeterminate = false; + }, + }, + { + action: "enable", + function: "myInput.disabled = false", + description: "Enables the toggle", + demo: (input) => { + input.disabled = false; + }, + }, + { + action: "disable", + function: "myInput.disabled = true", + description: "Disables the toggle", + demo: (input) => { + input.disabled = true; + }, + }, + { + action: "read-only", + function: "myInput.readOnly = true", + description: "Disables the toggle but the checkbox stay readable in form data.", - demo: (input) => { - input.readOnly = true; - }, - }, - { - action: "read-write", - function: "myInput.readOnly = false", - description: + demo: (input) => { + input.readOnly = true; + }, + }, + { + action: "read-write", + function: "myInput.readOnly = false", + description: "Removes the readonly state, enabling user interaction with the toggle.", - demo: (input) => { - input.readOnly = false; - }, - }, - ]; - - static build() { - const input = document.createElement("input"); - input.setAttribute("type", "checkbox"); - input.setAttribute("id", "api-input-interaction"); - input.setAttribute("checked", ""); - input.setAttribute("tristate", ""); - input.dataset.toggle = "toggle"; - - return super.build({ - title: "Input Interaction", - description: InputInteraction.#description(input), - codeBlock: { - language: "html", - code: ` + demo: (input) => { + input.readOnly = false; + }, + }, + ]; + + static build() { + const input = document.createElement("input"); + input.setAttribute("type", "checkbox"); + input.setAttribute("id", "api-input-interaction"); + input.setAttribute("checked", ""); + input.setAttribute("tristate", ""); + input.dataset.toggle = "toggle"; + + return super.build({ + title: "Input Interaction", + description: InputInteraction.#description(input), + codeBlock: { + language: "html", + code: ` `, - }, - example: [input], - }); - } + }, + example: [input], + }); + } - static #description(input) { - const description = document.createElement("div"); + static #description(input) { + const description = document.createElement("div"); - const paragraph = document.createElement("p"); - paragraph.innerHTML = + const paragraph = document.createElement("p"); + paragraph.innerHTML = "Interact with the input elements dispatch changes to the toggle:"; - description.append(paragraph, InputInteraction.#table(input)); - - return description; - } - - static #table(input) { - const table = document.createElement("table"); - table.className = "table table-striped table-condensed"; - const caption = document.createElement("caption"); - caption.textContent = "Input interaction demo"; - table.append( - caption, - InputInteraction.#thead(), - InputInteraction.#tbody(input), - ); - - return table; - } - - static #thead() { - const thead = document.createElement("thead"); - const tr = document.createElement("tr"); - tr.append( - ...["Action", "Example", "Description", "Demo"].map((label) => { - const th = document.createElement("th"); - th.textContent = label; - return th; - }), - ); - thead.append(tr); - return thead; - } - - static #tbody(input) { - const tbody = document.createElement("tbody"); - - const trs = InputInteraction.#interactions.map( - ({ action, function: func, description, demo }) => { - const td1 = document.createElement("td"); - td1.innerHTML = `${action}`; - - const td2 = document.createElement("td"); - td2.innerHTML = `${func}`; - - const td3 = document.createElement("td"); - td3.textContent = description; - - const td4 = document.createElement("td"); - const button = document.createElement("button"); - button.className = "btn btn-outline-dark btn-sm w-100"; - button.textContent = action; - button.onclick = () => { - demo(input); - }; - td4.append(button); + const footnote = document.createElement("p"); + footnote.innerHTML = + "These demos assume the toggle is already attached. If calling immediately after .bootstrapToggle(), await input.bsToggle.attach() first or call .update()."; + + description.append( + paragraph, + InputInteraction.#table(input), + footnote + ); + + return description; + } + + static #table(input) { + const table = document.createElement("table"); + table.className = "table table-striped table-condensed"; + const caption = document.createElement("caption"); + caption.textContent = "Input interaction demo"; + table.append( + caption, + InputInteraction.#thead(), + InputInteraction.#tbody(input), + ); + + return table; + } + + static #thead() { + const thead = document.createElement("thead"); const tr = document.createElement("tr"); - tr.append(td1, td2, td3, td4); - return tr; - }, - ); - - tbody.append(...trs); - return tbody; - } + tr.append( + ...["Action", "Example", "Description", "Demo"].map((label) => { + const th = document.createElement("th"); + th.textContent = label; + return th; + }), + ); + thead.append(tr); + return thead; + } + + static #tbody(input) { + const tbody = document.createElement("tbody"); + + const trs = InputInteraction.#interactions.map( + ({ action, function: func, description, demo }) => { + const td1 = document.createElement("td"); + td1.innerHTML = `${action}`; + + const td2 = document.createElement("td"); + td2.innerHTML = `${func}`; + + const td3 = document.createElement("td"); + td3.textContent = description; + + const td4 = document.createElement("td"); + const button = document.createElement("button"); + button.className = "btn btn-outline-dark btn-sm w-100"; + button.textContent = action; + button.onclick = () => { + demo(input); + }; + td4.append(button); + + const tr = document.createElement("tr"); + tr.append(td1, td2, td3, td4); + return tr; + }, + ); + + tbody.append(...trs); + return tbody; + } } export default InputInteraction; diff --git a/docs/src/js/components/documentation/api/Methods.js b/docs/src/js/components/documentation/api/Methods.js index cf7f0089..d4cd657d 100644 --- a/docs/src/js/components/documentation/api/Methods.js +++ b/docs/src/js/components/documentation/api/Methods.js @@ -1,151 +1,168 @@ import DocArticle from "../DocArticle"; class Methods extends DocArticle { - static #methods = [ - { - method: "initialize", - params: null, - description: "Initializes the toggle plugin with options", - }, - { - method: "destroy", - params: "destroy", - description: "Destroys the toggle", - }, - { - method: "rerender", - params: "rerender", - description: "Rerenders the toggle", - }, - { - method: "on", - params: "on", - description: "Sets the toggle to 'On' state", - }, - { - method: "off", - params: "off", - description: "Sets the toggle to 'Off' state", - }, - { - method: "toggle", - params: "toggle", - description: "Toggles the state of the toggle", - }, - { - method: "indeterminate", - params: "indeterminate", - description: "Sets the toggle to 'indeterminate' state", - }, - { - method: "determinate", - params: "determinate", - description: + static #methods = [ + { + method: "initialize", + params: null, + description: "Initializes the toggle plugin with options", + }, + { + method: "destroy", + params: "destroy", + description: "Destroys the toggle", + }, + { + method: "rerender", + params: "rerender", + description: "Rerenders the toggle", + }, + { + method: "on", + params: "on", + description: "Sets the toggle to 'On' state", + }, + { + method: "off", + params: "off", + description: "Sets the toggle to 'Off' state", + }, + { + method: "toggle", + params: "toggle", + description: "Toggles the state of the toggle", + }, + { + method: "indeterminate", + params: "indeterminate", + description: "Sets the toggle to 'indeterminate' state", + }, + { + method: "determinate", + params: "determinate", + description: "Sets the toggle to the previous 'determinate' state ('on' or 'off')", - }, - { - method: "enable", - params: "enable", - description: "Enables the toggle", - }, - { - method: "disable", - params: "disable", - description: "Disables the toggle", - }, - { - method: "readonly", - params: "readonly", - description: + }, + { + method: "enable", + params: "enable", + description: "Enables the toggle", + }, + { + method: "disable", + params: "disable", + description: "Disables the toggle", + }, + { + method: "readonly", + params: "readonly", + description: "Disables the toggle but the checkbox stay readable in form data.", - }, - ]; - - static build() { - const input = document.createElement("input"); - input.setAttribute("type", "checkbox"); - input.setAttribute("id", "api-methods-toggle"); - input.setAttribute("checked", ""); - - return super.build({ - title: "Methods", - description: Methods.#description(input), - codeBlock: { - language: "html", - code: ` + }, + ]; + + static build() { + const input = document.createElement("input"); + input.setAttribute("type", "checkbox"); + input.setAttribute("id", "api-methods-toggle"); + input.setAttribute("checked", ""); + + return super.build({ + title: "Methods", + description: Methods.#description(input), + codeBlock: { + language: "html", + code: ` `, - }, - example: [input], - }); - } + }, + example: [input], + alert: Methods.#asyncAlert(), + }); + } - static #description(input) { - const description = document.createElement("div"); + static #description(input) { + const description = document.createElement("div"); - const paragraph = document.createElement("p"); - paragraph.innerHTML = + const paragraph = document.createElement("p"); + paragraph.innerHTML = "Methods can be used to control toggles directly. The following methods are available in the following way:"; - description.append(paragraph, Methods.#table(input)); - - return description; - } - - static #table(input) { - const table = document.createElement("table"); - table.className = "table table-striped table-condensed"; - const caption = document.createElement("caption"); - caption.textContent = "API methods demo"; - table.append(caption, Methods.#thead(), Methods.#tbody(input)); - - return table; - } - - static #thead() { - const thead = document.createElement("thead"); - const tr = document.createElement("tr"); - tr.append( - ...["Method", "Example", "Description", "Demo"].map((label) => { - const th = document.createElement("th"); - th.textContent = label; - return th; - }) - ); - thead.append(tr); - return thead; - } - - static #tbody(input) { - const tbody = document.createElement("tbody"); - - const trs = Methods.#methods.map(({ method, params, description }) => { - const td1 = document.createElement("td"); - td1.innerHTML = `${method}`; - - const td2 = document.createElement("td"); - td2.innerHTML = params - ? `myToggle.bootstrapToggle("${params}")` - : `myToggle.bootstrapToggle()`; - - const td3 = document.createElement("td"); - td3.textContent = description; - - const td4 = document.createElement("td"); - const button = document.createElement("button"); - button.className = "btn btn-outline-dark btn-sm w-100"; - button.textContent = method; - button.onclick = () => { - input.bootstrapToggle(params); - }; - td4.append(button); - - const tr = document.createElement("tr"); - tr.append(td1, td2, td3, td4); - return tr; - }); - - tbody.append(...trs); - return tbody; - } + + description.append( + paragraph, + Methods.#table(input), + ); + + return description; + } + + static #table(input) { + const table = document.createElement("table"); + table.className = "table table-striped table-condensed"; + const caption = document.createElement("caption"); + caption.textContent = "API methods demo"; + table.append(caption, Methods.#thead(), Methods.#tbody(input)); + + return table; + } + + static #thead() { + const thead = document.createElement("thead"); + const tr = document.createElement("tr"); + tr.append( + ...["Method", "Example", "Description", "Demo"].map((label) => { + const th = document.createElement("th"); + th.textContent = label; + return th; + }) + ); + thead.append(tr); + return thead; + } + + static #tbody(input) { + const tbody = document.createElement("tbody"); + + const trs = Methods.#methods.map(({ method, params, description }) => { + const td1 = document.createElement("td"); + td1.innerHTML = `${method}`; + + const td2 = document.createElement("td"); + td2.innerHTML = params + ? `myToggle.bootstrapToggle("${params}")` + : "myToggle.bootstrapToggle()"; + + const td3 = document.createElement("td"); + td3.textContent = description; + + const td4 = document.createElement("td"); + const button = document.createElement("button"); + button.className = "btn btn-outline-dark btn-sm w-100"; + button.textContent = method; + button.onclick = () => { + input.bootstrapToggle(params); + }; + td4.append(button); + + const tr = document.createElement("tr"); + tr.append(td1, td2, td3, td4); + return tr; + }); + + tbody.append(...trs); + return tbody; + } + + static #asyncAlert() { + return ({ + type: "warning", + title: "Async teardown", + versionPill: { version: "5.5.0", action: "SINCE" }, + content: `

destroy() and rerender() return Promise<void>. Await before re-initializing or inspecting DOM:

+
await element.bsToggle.destroy();
+element.bootstrapToggle();
+

rerender() emits toggle:disposed and toggle:attached around the re-attach.

`, + }); + } } export default Methods; diff --git a/docs/src/js/components/documentation/api/Options.js b/docs/src/js/components/documentation/api/Options.js index 1e56a466..2dc150d2 100644 --- a/docs/src/js/components/documentation/api/Options.js +++ b/docs/src/js/components/documentation/api/Options.js @@ -1,26 +1,26 @@ import DocArticle from "../DocArticle"; class Options extends DocArticle { - static build() { - return super.build({ - title: "Options", - description: Options.#description(), - example: Options.#example(), - codePanel: Options.#codePanel(), - alert: { - type: "warning", - title: "Deprecation notice", - versionPill: { - version: "5.0.0", - action: "DEPRECATED", - }, - content: `Using on and off user options is deprecated and will throw a console warning. Use onlabel and offlabel user options instead.`, - }, - }); - } + static build() { + return super.build({ + title: "Options", + description: Options.#description(), + example: Options.#example(), + codePanel: Options.#codePanel(), + alert: { + type: "warning", + title: "Deprecation notice", + versionPill: { + version: "5.0.0", + action: "DEPRECATED", + }, + content: "Using on and off user options is deprecated and will throw a console warning. Use onlabel and offlabel user options instead.", + }, + }); + } - static #codePanel() { - const ecmas = `
+ static #codePanel() { + const ecmas = `
@@ -47,7 +47,7 @@ class Options extends DocArticle { }); }; `; - const jquery = `
+ const jquery = `
@@ -74,59 +74,59 @@ class Options extends DocArticle { }); }); `; - return { - name: "api-options", - language: "javascript", - tabs: ["ECMAScript", "jQuery"], - contents: [ecmas, jquery], - }; - } + return { + name: "api-options", + language: "javascript", + tabs: ["ECMAScript", "jQuery"], + contents: [ecmas, jquery], + }; + } - static #example() { - const form = document.createElement("form"); - form.id = "form_state_status"; - form.className = "d-flex justify-content-between align-items-center"; + static #example() { + const form = document.createElement("form"); + form.id = "form_state_status"; + form.className = "d-flex justify-content-between align-items-center"; - const input = document.createElement("input"); - input.id = "api-option-toggle"; - input.type = "checkbox"; - input.checked = true; + const input = document.createElement("input"); + input.id = "api-option-toggle"; + input.type = "checkbox"; + input.checked = true; - const button = document.createElement("button"); - button.type = "submit"; - button.className = "btn btn-outline-secondary"; - button.innerHTML = "Launch Bootstrap Toggle"; + const button = document.createElement("button"); + button.type = "submit"; + button.className = "btn btn-outline-secondary"; + button.innerHTML = "Launch Bootstrap Toggle"; - form.append(input, button); + form.append(input, button); - form.onsubmit = (e) => { - e.preventDefault(); - input.bootstrapToggle({ - onlable: "Enabled", - offlabel: "Disabled", - onstyle: "success", - offstyle: "danger", - onvalue: "1", - offvalue: "0", - size: "lg", - style: null, - width: null, - height: null, - tabindex: -1, - tristate: true, - tooltip: { - placement: "top", - title: { on: "Enabled", off: "Disabled", mixed: "Mixed" }, - }, - }); - }; + form.onsubmit = (e) => { + e.preventDefault(); + input.bootstrapToggle({ + onlable: "Enabled", + offlabel: "Disabled", + onstyle: "success", + offstyle: "danger", + onvalue: "1", + offvalue: "0", + size: "lg", + style: null, + width: null, + height: null, + tabindex: -1, + tristate: true, + tooltip: { + placement: "top", + title: { on: "Enabled", off: "Disabled", mixed: "Mixed" }, + }, + }); + }; - return [form]; - } + return [form]; + } - static #description() { - const description = document.createElement("div"); - description.innerHTML = `

Options can be passed via data attributes or JavaScript. For data attributes, append the option name to data-, as in data-on="Enabled". Data attributes will take precedence over JavaScript options.

+ static #description() { + const description = document.createElement("div"); + description.innerHTML = `

Options can be passed via data attributes or JavaScript. For data attributes, append the option name to data-, as in data-on="Enabled". Data attributes will take precedence over JavaScript options.

@@ -279,8 +279,8 @@ class Options extends DocArticle {
API constructor options
`; - return description; - } + return description; + } } export default Options; diff --git a/docs/src/js/components/documentation/api/Rerender.js b/docs/src/js/components/documentation/api/Rerender.js index 6f6960bb..52de0aec 100644 --- a/docs/src/js/components/documentation/api/Rerender.js +++ b/docs/src/js/components/documentation/api/Rerender.js @@ -2,11 +2,11 @@ import Console from "../../Console"; import DocArticle from "../DocArticle"; class Rerender extends DocArticle { - static build() { - const description = document.createElement("p"); - description.innerHTML = `Bootstrap toggle can be re-rendered with the rerender method. This can be used to update toggle when a process updates input data attributes.`; - - const code = `
+ static build() { + const description = document.createElement("p"); + description.innerHTML = "Bootstrap toggle can be re-rendered with the rerender method. This can be used to update toggle when a process updates input data attributes. rerender() returns a Promise<void> that resolves once the toggle is re-attached, and internally routes through dispose() → attach(), so it emits the toggle:disposed and toggle:attached lifecycle events around the re-render."; + + const code = `
@@ -21,41 +21,41 @@ class Rerender extends DocArticle { }; `; - return super.build({ - title: "Re-render", - description, - example: Rerender.#example(), - codeBlock: { - language: "html", - code, - }, - }); - } - - static #example() { - const form = document.createElement("form"); - form.id = "api-state-form"; - form.className = "d-flex justify-content-between align-items-center"; - form.innerHTML = ` - `; - - const console = new Console(); - - form.onsubmit = (e) => { - e.preventDefault(); - const myToggle = e.target.elements["api-rerender-toggle"]; - const onLabel = crypto.getRandomValues(new Uint32Array(1))[0]; - myToggle.dataset.onlabel = onLabel; - console.log({ - mode: "append", - data: `On label set to: ${onLabel}`, - }); - - myToggle.bootstrapToggle("rerender"); - }; - - return [form, console.htmlElement]; - } + return super.build({ + title: "Re-render", + description, + example: Rerender.#example(), + codeBlock: { + language: "html", + code, + }, + }); + } + + static #example() { + const form = document.createElement("form"); + form.id = "api-state-form"; + form.className = "d-flex justify-content-between align-items-center"; + form.innerHTML = ` + `; + + const console = new Console(); + + form.onsubmit = (e) => { + e.preventDefault(); + const myToggle = e.target.elements["api-rerender-toggle"]; + const onLabel = crypto.getRandomValues(new Uint32Array(1))[0]; + myToggle.dataset.onlabel = onLabel; + console.log({ + mode: "append", + data: `On label set to: ${onLabel}`, + }); + + myToggle.bootstrapToggle("rerender"); + }; + + return [form, console.htmlElement]; + } } export default Rerender; diff --git a/docs/src/js/components/documentation/api/State.js b/docs/src/js/components/documentation/api/State.js index 31ad0c5c..5086c57d 100644 --- a/docs/src/js/components/documentation/api/State.js +++ b/docs/src/js/components/documentation/api/State.js @@ -2,46 +2,46 @@ import Console from "../../Console"; import DocArticle from "../DocArticle"; class State extends DocArticle { - static build() { - const description = document.createElement("p"); - description.innerHTML = `Checked state of a toggle can be set by reading checked property. Use the element property for checked state myToggle.checked (returns true or false).`; + static build() { + const description = document.createElement("p"); + description.innerHTML = "Checked state of a toggle can be set by reading checked property. Use the element property for checked state myToggle.checked (returns true or false)."; - const code = `
+ const code = `
`; - return super.build({ - title: "Toggle State", - description, - example: State.#example(), - codeBlock: { - language: "html", - code, - }, - }); - } - - static #example() { - const form = document.createElement("form"); - form.id = "api-state-form"; - form.className = "d-flex justify-content-between align-items-center"; - form.innerHTML = ` + return super.build({ + title: "Toggle State", + description, + example: State.#example(), + codeBlock: { + language: "html", + code, + }, + }); + } + + static #example() { + const form = document.createElement("form"); + form.id = "api-state-form"; + form.className = "d-flex justify-content-between align-items-center"; + form.innerHTML = ` `; - const console = new Console(); + const console = new Console(); - form.onsubmit = (e) => { - e.preventDefault(); - console.log({ - mode: "append", - data: `Checked: ${e.target.elements["api-state-toggle"].checked}`, - }); - }; + form.onsubmit = (e) => { + e.preventDefault(); + console.log({ + mode: "append", + data: `Checked: ${e.target.elements["api-state-toggle"].checked}`, + }); + }; - return [form, console.htmlElement]; - } + return [form, console.htmlElement]; + } } export default State; diff --git a/docs/src/js/components/documentation/devtools/Monitoring.js b/docs/src/js/components/documentation/devtools/Monitoring.js new file mode 100644 index 00000000..2dd33eba --- /dev/null +++ b/docs/src/js/components/documentation/devtools/Monitoring.js @@ -0,0 +1,185 @@ +import Console from "../../Console"; +import DocArticle from "../DocArticle"; + +class Monitoring extends DocArticle { + /** + * Live example state. Only one instance of the demo is created per page + * load, so a module-level singleton for the console and the patched + * console.debug is acceptable. + */ + static #consoleInstance = null; + static #originalDebug = null; + static #debugPatched = false; + + static build() { + return super.build({ + title: "Monitoring", + description: Monitoring.#description(), + versionPill: { action: "SINCE", version: "5.5.0" }, + }); + } + + static #description() { + const description = document.createElement("div"); + + const p1 = document.createElement("p"); + p1.innerHTML = "Bootstrap5-Toggle is built on component-lifecycle. A Monitor singleton is exposed at window.BootstrapToggle.MONITOR and starts stopped (no listeners attached). Enable it from the browser console:"; + description.appendChild(p1); + + const code = document.createElement("pre"); + code.innerHTML = `window.BootstrapToggle.MONITOR.start("DEBUG"); // full tracing +window.BootstrapToggle.MONITOR.setLevel("INFO"); // lifecycle only +window.BootstrapToggle.MONITOR.stop(); // silence +window.BootstrapToggle.MONITOR.getLevel(); // "DEBUG" | "INFO" | "WARN" | "ERROR" | undefined`; + description.appendChild(code); + + const p2 = document.createElement("p"); + p2.innerHTML = "The Toggle component itself is unchanged. MONITOR visibility relies on Toggle being constructed with { bubbleEvents: true }, which is the current default and is not configurable via the public API."; + description.appendChild(p2); + + description.appendChild(Monitoring.#logLevelTable()); + description.appendChild(Monitoring.#liveExample()); + + return description; + } + + static #liveExample() { + const frag = document.createDocumentFragment(); + + const title = document.createElement("h4"); + title.textContent = "Live example"; + title.className = "text-secondary"; + frag.appendChild(title); + + const p = document.createElement("p"); + p.innerHTML = "The buttons below control the actual global window.BootstrapToggle.MONITOR singleton. Log output is mirrored to the panel and also appears in the browser devtools console. Click stop() to restore the global console."; + frag.appendChild(p); + + const mockConsole = new Console(); + Monitoring.#consoleInstance = mockConsole; + const buttons = [ + { + label: 'start("DEBUG")', + action: () => { + Monitoring.#patchConsoleDebug(); + window.BootstrapToggle.MONITOR.start("DEBUG"); + mockConsole.log({ + mode: "append", + data: `MONITOR.start("DEBUG") → getLevel(): ${window.BootstrapToggle.MONITOR.getLevel()}`, + }); + }, + }, + { + label: 'setLevel("INFO")', + action: () => { + Monitoring.#patchConsoleDebug(); + window.BootstrapToggle.MONITOR.setLevel("INFO"); + mockConsole.log({ + mode: "append", + data: `MONITOR.setLevel("INFO") → getLevel(): ${window.BootstrapToggle.MONITOR.getLevel()}`, + }); + }, + }, + { + label: "stop()", + action: () => { + try { + window.BootstrapToggle.MONITOR.stop(); + } catch { + // ignore: already stopped + } + Monitoring.#restoreConsoleDebug(); + mockConsole.log({ + mode: "append", + data: `MONITOR.stop() → getLevel(): ${window.BootstrapToggle.MONITOR.getLevel()}`, + }); + }, + }, + ]; + + const controls = document.createElement("div"); + controls.className = "d-flex flex-wrap gap-2 mb-2"; + buttons.forEach(({ label, action }) => { + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "btn btn-outline-secondary btn-sm"; + btn.textContent = label; + btn.addEventListener("click", action); + controls.appendChild(btn); + }); + frag.appendChild(controls); + + frag.appendChild(mockConsole.htmlElement); + return frag; + } + + static #logLevelTable() { + const frag = document.createDocumentFragment(); + + const title = document.createElement("h4"); + title.textContent = "Log-level behavior"; + title.className = "text-secondary"; + frag.appendChild(title); + + const description = document.createElement("div"); + description.innerHTML = ` +
+ + + + + + + + + + + + + + + + +
Log-level behavior
LevelLifecycle events
(initialized, attached, disposed, destroyed)
Domain events
(on, off, mixed, enabled, disabled, readonly)
Transition events
(transition-cancelled, transition-invalid)
DEBUG✔✔✔
INFO✘✘✔
WARN✘✘✔
ERROR✘✘✘
+
`; + frag.appendChild(description); + + return frag; + } + + /** + * Patches `console.debug` so DEBUG lines are mirrored into the docs + * `Console` component. Idempotent; only the first call patches. + */ + static #patchConsoleDebug() { + if (Monitoring.#debugPatched) return; + Monitoring.#originalDebug = console.debug; + console.debug = (...args) => { + Monitoring.#originalDebug.apply(console, args); + if (Monitoring.#consoleInstance) { + Monitoring.#consoleInstance.log({ + mode: "append", + data: args + .map((a) => + typeof a === "string" ? a : JSON.stringify(a) + ) + .join(" "), + }); + } + }; + Monitoring.#debugPatched = true; + } + + /** + * Restores the original `console.debug` if it was patched. + */ + static #restoreConsoleDebug() { + if (!Monitoring.#debugPatched) return; + console.debug = Monitoring.#originalDebug; + Monitoring.#originalDebug = null; + Monitoring.#debugPatched = false; + } + +} + +export default Monitoring; diff --git a/docs/src/js/components/documentation/events/ChainableSubscription.js b/docs/src/js/components/documentation/events/ChainableSubscription.js new file mode 100644 index 00000000..ee711a39 --- /dev/null +++ b/docs/src/js/components/documentation/events/ChainableSubscription.js @@ -0,0 +1,85 @@ +import Console from "../../Console"; +import DocArticle from "../DocArticle"; + +class ChainableSubscription extends DocArticle { + static build() { + const input = document.createElement("input"); + input.setAttribute("type", "checkbox"); + input.setAttribute("id", "chainable-subscription-toggle"); + input.setAttribute("checked", ""); + input.dataset.toggle = "toggle"; + const console = new Console(); + + input.addEventListener("toggle:on", () => { + console.log({ + mode: "append", + data: "[addEventListener] toggle:on received.", + }); + }); + + input.addEventListener("toggle:initialized", () => { + input.bsToggle + .on("toggle:on", () => { + console.log({ + mode: "append", + data: "[.on] toggle:on received.", + }); + }) + .once("toggle:destroyed", () => { + console.log({ + mode: "append", + data: "[.once] toggle:destroyed received (will not fire again).", + }); + }); + }); + + return super.build({ + title: "Chainable Event Subscription", + description: ChainableSubscription.#description(), + codeBlock: { + language: "html", + code: ` +`, + }, + example: [input, console.htmlElement], + versionPill: { action: "SINCE", version: "5.5.0" }, + }); + } + + static #description() { + const description = document.createElement("div"); + + const p1 = document.createElement("p"); + p1.innerHTML = + "The Component base class exposes a chainable typed event API that coexists with the standard DOM element.addEventListener pattern. Both styles receive the same CustomEvent, and both see the same e.detail.state payload on domain events."; + description.append(p1); + + const p2 = document.createElement("p"); + p2.innerHTML = + ".on(name, handler) subscribes for every occurrence; .once(name, handler) auto-unsubscribes after the first dispatch; .off(name, handler) removes a specific handler. All three return the component instance, so calls can be chained."; + description.append(p2); + + const p3 = document.createElement("p"); + p3.innerHTML = + "Domain events — toggle:on, toggle:off, toggle:mixed, toggle:enabled, toggle:disabled, toggle:readonly — carry a { state: { value, checked, status, indeterminate } } detail payload. Lifecycle events (see the previous article) carry no toggle-state payload."; + description.append(p3); + + return description; + } +} + +export default ChainableSubscription; diff --git a/docs/src/js/components/documentation/events/CustomEvents.js b/docs/src/js/components/documentation/events/CustomEvents.js index 373361b0..724d0899 100644 --- a/docs/src/js/components/documentation/events/CustomEvents.js +++ b/docs/src/js/components/documentation/events/CustomEvents.js @@ -3,134 +3,141 @@ import Console from "../../Console"; import DocArticle from "../DocArticle"; class CustomEvents extends DocArticle { - static #events = [ - { - name: "toggle:on", - action: "on", - }, - { - name: "toggle:off", - action: "off", - }, - { - name: "toggle:mixed", - action: "indeterminate", - }, - { - name: "toggle:enabled", - action: "enable", - }, - { - name: "toggle:disabled", - action: "disable", - }, - { - name: "toggle:readonly", - action: "readonly", - }, - ]; - - static build() { - const input = document.createElement("input"); - input.setAttribute("type", "checkbox"); - input.setAttribute("id", "custom-events-toggle"); - input.setAttribute("checked", ""); - input.setAttribute("tristate", ""); - input.setAttribute("data-toggle", "toggle"); - const console = new Console(); - - CustomEvents.#events.forEach((event) => { - input.addEventListener(event.name, (e) => { - console.log({ - mode: "append", - data: `Event ${event.name} fired. Toggle state: ${JSON.stringify(e.detail.state, null, 2)}`, + static #events = [ + { + name: "toggle:on", + action: "on", + }, + { + name: "toggle:off", + action: "off", + }, + { + name: "toggle:mixed", + action: "indeterminate", + }, + { + name: "toggle:enabled", + action: "enable", + }, + { + name: "toggle:disabled", + action: "disable", + }, + { + name: "toggle:readonly", + action: "readonly", + }, + ]; + + static build() { + const input = document.createElement("input"); + input.setAttribute("type", "checkbox"); + input.setAttribute("id", "custom-events-toggle"); + input.setAttribute("checked", ""); + input.setAttribute("tristate", ""); + input.setAttribute("data-toggle", "toggle"); + const console = new Console(); + + CustomEvents.#events.forEach((event) => { + input.addEventListener(event.name, (e) => { + const { value, checked, status, indeterminate } = e.detail.state; + console.log({ + mode: "append", + data: `Event ${event.name} fired. State: ${JSON.stringify( + { value, checked, status, indeterminate }, + null, + 0 + )}`, + }); + }); }); - }); - }); - return super.build({ - title: "Custom Events", - description: CustomEvents.#description(input), - codeBlock: { - language: "html", - code: ` + return super.build({ + title: "Custom Events", + description: CustomEvents.#description(input), + codeBlock: { + language: "html", + code: ` `, - }, - example: [input, console.htmlElement], - versionPill: { action: "SINCE", version: "5.3.0" }, - }); - } - - static #description(input) { - const description = document.createElement("div"); - - const paragraph = document.createElement("p"); - paragraph.innerHTML = - "Bootstrap Toggle emit custom events when its state or status is changed. The events details contain the current state of the toggle. The following custom events are available:"; - description.append(paragraph, CustomEvents.#table(input)); - - return description; - } - - static #table(input) { - const table = document.createElement("table"); - table.className = "table table-striped table-condensed"; - const caption = document.createElement("caption"); - caption.textContent = "Custom events demo"; - table.append(caption, CustomEvents.#thead(), CustomEvents.#tbody(input)); - - return table; - } - - static #thead() { - const thead = document.createElement("thead"); - const tr = document.createElement("tr"); - tr.append( - ...["Event", "Example", "Launch"].map((label) => { - const th = document.createElement("th"); - th.textContent = label; - return th; - }), - ); - thead.append(tr); - return thead; - } - - static #tbody(input) { - const tbody = document.createElement("tbody"); - - const trs = CustomEvents.#events.map(({ name, action }) => { - const td1 = document.createElement("td"); - td1.innerHTML = `${name}`; - - const td2 = document.createElement("td"); - td2.innerHTML = `myToggle.addEventListener("${name}", (e)=>{...})`; - - const td3 = document.createElement("td"); - const defaultBtn = document.createElement("button"); - defaultBtn.className = "btn btn-outline-dark btn-sm w-100"; - defaultBtn.textContent = action; - defaultBtn.onclick = () => { - input.bootstrapToggle(action, false); - }; - td3.append(defaultBtn); - - const tr = document.createElement("tr"); - tr.append(td1, td2, td3); - return tr; - }); + }, + example: [input, console.htmlElement], + versionPill: { action: "SINCE", version: "5.3.0" }, + }); + } + + static #description(input) { + const description = document.createElement("div"); + + const paragraph = document.createElement("p"); + paragraph.innerHTML = + "Bootstrap Toggle emit custom events when its state or status is changed. The events details contain the current state of the toggle. The following custom events are available:"; + description.append(paragraph, CustomEvents.#table(input)); + + return description; + } + + static #table(input) { + const table = document.createElement("table"); + table.className = "table table-striped table-condensed"; + const caption = document.createElement("caption"); + caption.textContent = "Custom events demo"; + table.append(caption, CustomEvents.#thead(), CustomEvents.#tbody(input)); + + return table; + } + + static #thead() { + const thead = document.createElement("thead"); + const tr = document.createElement("tr"); + tr.append( + ...["Event", "Example", "Launch"].map((label) => { + const th = document.createElement("th"); + th.textContent = label; + return th; + }), + ); + thead.append(tr); + return thead; + } + + static #tbody(input) { + const tbody = document.createElement("tbody"); + + const trs = CustomEvents.#events.map(({ name, action }) => { + const td1 = document.createElement("td"); + td1.innerHTML = `${name}`; + + const td2 = document.createElement("td"); + td2.innerHTML = `myToggle.addEventListener("${name}", (e)=>{...})`; + + const td3 = document.createElement("td"); + const defaultBtn = document.createElement("button"); + defaultBtn.className = "btn btn-outline-dark btn-sm w-100"; + defaultBtn.textContent = action; + defaultBtn.onclick = () => { + input.bootstrapToggle(action, false); + }; + td3.append(defaultBtn); + + const tr = document.createElement("tr"); + tr.append(td1, td2, td3); + return tr; + }); - tbody.append(...trs); - return tbody; - } + tbody.append(...trs); + return tbody; + } } export default CustomEvents; diff --git a/docs/src/js/components/documentation/events/EventPropagation.js b/docs/src/js/components/documentation/events/EventPropagation.js index 8f1ead11..26dbda06 100644 --- a/docs/src/js/components/documentation/events/EventPropagation.js +++ b/docs/src/js/components/documentation/events/EventPropagation.js @@ -2,12 +2,12 @@ import Console from "../../Console"; import DocArticle from "../DocArticle"; class EventPropagation extends DocArticle { - static build() { - const description = document.createElement("p"); - description.innerHTML = `All events are propagated to and from input element to the toggle. Listen for events on the input directly as the toggle stays synced with the + static build() { + const description = document.createElement("p"); + description.innerHTML = `All events are propagated to and from input element to the toggle. Listen for events on the input directly as the toggle stays synced with the input.`; - const code = ` + const code = ` `; - return super.build({ - title: "Event Propagation", - description, - example: EventPropagation.#example(), - codeBlock: { - language: "html", - code, - }, - }); - } - - static #example() { - const input = document.createElement("input"); - input.type = "checkbox"; - input.dataset.toggle = "toggle"; - input.checked = true; - input.id = "events-propagation-toggle"; - - const console = new Console(); - - input.addEventListener("change", (e) => { - console.log({ - mode: "append", - data: `Input change fired. Checked state: ${input.checked}`, - }); - }); - - return [input, console.htmlElement]; - } + return super.build({ + title: "Event Propagation", + description, + example: EventPropagation.#example(), + codeBlock: { + language: "html", + code, + }, + }); + } + + static #example() { + const input = document.createElement("input"); + input.type = "checkbox"; + input.dataset.toggle = "toggle"; + input.checked = true; + input.id = "events-propagation-toggle"; + + const console = new Console(); + + input.addEventListener("change", (e) => { + console.log({ + mode: "append", + data: `Input change fired. Checked state: ${input.checked}`, + }); + }); + + return [input, console.htmlElement]; + } } export default EventPropagation; diff --git a/docs/src/js/components/documentation/events/LifecycleEvents.js b/docs/src/js/components/documentation/events/LifecycleEvents.js new file mode 100644 index 00000000..cfe177f4 --- /dev/null +++ b/docs/src/js/components/documentation/events/LifecycleEvents.js @@ -0,0 +1,149 @@ +import Console from "../../Console"; +import DocArticle from "../DocArticle"; + +class LifecycleEvents extends DocArticle { + static #events = [ + { + name: "toggle:initialized", + trigger: "Fired after `init()` completes (internal state is ready, DOM not yet built).", + }, + { + name: "toggle:attached", + trigger: "Fired after `attach()` completes (toggle DOM built, listeners bound).", + }, + { + name: "toggle:disposed", + trigger: "Fired after `dispose()` completes (DOM torn down, listeners unbound). Also fires as part of `rerender()`.", + }, + { + name: "toggle:destroyed", + trigger: "Fired after `destroy()` completes (instance detached from the input).", + }, + { + name: "toggle:transition-cancelled", + trigger: "Fired when a lifecycle transition is cancelled before it completes.", + }, + { + name: "toggle:transition-invalid", + trigger: "Fired when a lifecycle transition is requested from an invalid source state.", + }, + ]; + + static build() { + const input = document.createElement("input"); + input.setAttribute("type", "checkbox"); + input.setAttribute("id", "lifecycle-events-toggle"); + input.setAttribute("checked", ""); + input.dataset.toggle = "toggle"; + const mockConsole = new Console(); + + LifecycleEvents.#events.forEach((event) => { + input.addEventListener(event.name, (_e) => { + mockConsole.log({ + mode: "append", + data: `Event ${event.name} fired.`, + }); + }); + }); + + const rerenderBtn = document.createElement("button"); + rerenderBtn.type = "button"; + rerenderBtn.className = "btn btn-outline-secondary btn-sm"; + rerenderBtn.textContent = "Trigger rerender()"; + rerenderBtn.onclick = () => { + input.bootstrapToggle("rerender"); + }; + + return super.build({ + title: "Lifecycle Events", + description: LifecycleEvents.#description(), + codeBlock: { + language: "html", + code: ` +`, + }, + example: [input, rerenderBtn, mockConsole.htmlElement], + versionPill: { action: "SINCE", version: "5.5.0" }, + }); + } + + static #description() { + const description = document.createElement("div"); + + const paragraph = document.createElement("p"); + paragraph.innerHTML = + "Bootstrap Toggle is built on component-lifecycle. Every lifecycle transition automatically emits a prefixed CustomEvent on the input element. These events carry no toggle-state payload — they signal lifecycle moments — and are emitted with bubbles: true, so you can listen for them with element.addEventListener or with the chainable .on() / .once() API (see the next article)."; + description.append(paragraph, LifecycleEvents.#table()); + + const note = document.createElement("p"); + note.innerHTML = + "Note: toggle:initialized and toggle:attached are emitted during initial wiring and cannot be observed by a listener registered after the page is ready. The demo below only observes toggle:disposed and toggle:attached when you click Trigger rerender()."; + description.append(note); + + return description; + } + + static #table() { + const table = document.createElement("table"); + table.className = "table table-striped table-condensed"; + const caption = document.createElement("caption"); + caption.textContent = "Lifecycle events emitted by the toggle"; + table.append(caption, LifecycleEvents.#thead(), LifecycleEvents.#tbody()); + return table; + } + + static #thead() { + const thead = document.createElement("thead"); + const tr = document.createElement("tr"); + tr.append( + ...["Event", "Description"].map((label) => { + const th = document.createElement("th"); + th.textContent = label; + return th; + }) + ); + thead.append(tr); + return thead; + } + + static #tbody() { + const tbody = document.createElement("tbody"); + const trs = LifecycleEvents.#events.map(({ name, trigger }) => { + const td1 = document.createElement("td"); + td1.innerHTML = `${name}`; + + const td2 = document.createElement("td"); + td2.innerHTML = trigger; + + const tr = document.createElement("tr"); + tr.append(td1, td2); + return tr; + }); + tbody.append(...trs); + return tbody; + } +} + +export default LifecycleEvents; diff --git a/docs/src/js/components/documentation/events/SilencedActions.js b/docs/src/js/components/documentation/events/SilencedActions.js index c1f382f1..af27a66b 100644 --- a/docs/src/js/components/documentation/events/SilencedActions.js +++ b/docs/src/js/components/documentation/events/SilencedActions.js @@ -2,124 +2,124 @@ import Console from "../../Console"; import DocArticle from "../DocArticle"; class SilencedActions extends DocArticle { - static #methods = [ - "on", - "off", - "toggle", - "indeterminate", - "determinate", - "enable", - "disable", - "readonly", - ]; - - static build() { - const input = document.createElement("input"); - input.setAttribute("type", "checkbox"); - input.setAttribute("id", "events-silence-toggle"); - input.setAttribute("checked", ""); - input.setAttribute("data-toggle", "toggle"); - const console = new Console(); - - input.addEventListener("change", (e) => { - console.log({ - mode: "append", - data: `Input change fired. Checked state: ${input.checked}`, - }); - }); - - return super.build({ - title: "Silenced Actions", - description: SilencedActions.#description(input), - codeBlock: { - language: "html", - code: ` + static #methods = [ + "on", + "off", + "toggle", + "indeterminate", + "determinate", + "enable", + "disable", + "readonly", + ]; + + static build() { + const input = document.createElement("input"); + input.setAttribute("type", "checkbox"); + input.setAttribute("id", "events-silence-toggle"); + input.setAttribute("checked", ""); + input.setAttribute("data-toggle", "toggle"); + const console = new Console(); + + input.addEventListener("change", (e) => { + console.log({ + mode: "append", + data: `Input change fired. Checked state: ${input.checked}`, + }); + }); + + return super.build({ + title: "Silenced Actions", + description: SilencedActions.#description(input), + codeBlock: { + language: "html", + code: ` `, - }, - example: [input, console.htmlElement], - }); - } + }, + example: [input, console.htmlElement], + }); + } - static #description(input) { - const description = document.createElement("div"); + static #description(input) { + const description = document.createElement("div"); - const paragraph = document.createElement("p"); - paragraph.innerHTML = + const paragraph = document.createElement("p"); + paragraph.innerHTML = "Methods can be used in silence mode just specifying the second argument to true on the bootstrapToggle call. This is useful to prevent the control from propagating the change event in cases where you want to update the state, but do not want to fire the onChange event. The following methods are available in silence mode in the following way:"; - description.append(paragraph, SilencedActions.#table(input)); - - return description; - } - - static #table(input) { - const table = document.createElement("table"); - table.className = "table table-striped table-condensed"; - const caption = document.createElement("caption"); - caption.textContent = "Silenced methods demo"; - table.append( - caption, - SilencedActions.#thead(), - SilencedActions.#tbody(input), - ); - - return table; - } - - static #thead() { - const thead = document.createElement("thead"); - const tr = document.createElement("tr"); - tr.append( - ...["Method", "Example", "Normal", "Silenced"].map((label) => { - const th = document.createElement("th"); - th.textContent = label; - return th; - }), - ); - thead.append(tr); - return thead; - } - - static #tbody(input) { - const tbody = document.createElement("tbody"); - - const trs = SilencedActions.#methods.map((method) => { - const td1 = document.createElement("td"); - td1.innerHTML = `${method}`; - - const td2 = document.createElement("td"); - td2.innerHTML = `myToggle.bootstrapToggle("${method}", true)`; - - const td3 = document.createElement("td"); - const defaultBtn = document.createElement("button"); - defaultBtn.className = "btn btn-outline-dark btn-sm w-100"; - defaultBtn.textContent = method; - defaultBtn.onclick = () => { - input.bootstrapToggle(method, false); - }; - td3.append(defaultBtn); - - const td4 = document.createElement("td"); - const silenceBtn = document.createElement("button"); - silenceBtn.className = "btn btn-outline-dark btn-sm w-100"; - silenceBtn.textContent = method; - silenceBtn.onclick = () => { - input.bootstrapToggle(method, true); - }; - td4.append(silenceBtn); - - const tr = document.createElement("tr"); - tr.append(td1, td2, td3, td4); - return tr; - }); - - tbody.append(...trs); - return tbody; - } + description.append(paragraph, SilencedActions.#table(input)); + + return description; + } + + static #table(input) { + const table = document.createElement("table"); + table.className = "table table-striped table-condensed"; + const caption = document.createElement("caption"); + caption.textContent = "Silenced methods demo"; + table.append( + caption, + SilencedActions.#thead(), + SilencedActions.#tbody(input), + ); + + return table; + } + + static #thead() { + const thead = document.createElement("thead"); + const tr = document.createElement("tr"); + tr.append( + ...["Method", "Example", "Normal", "Silenced"].map((label) => { + const th = document.createElement("th"); + th.textContent = label; + return th; + }), + ); + thead.append(tr); + return thead; + } + + static #tbody(input) { + const tbody = document.createElement("tbody"); + + const trs = SilencedActions.#methods.map((method) => { + const td1 = document.createElement("td"); + td1.innerHTML = `${method}`; + + const td2 = document.createElement("td"); + td2.innerHTML = `myToggle.bootstrapToggle("${method}", true)`; + + const td3 = document.createElement("td"); + const defaultBtn = document.createElement("button"); + defaultBtn.className = "btn btn-outline-dark btn-sm w-100"; + defaultBtn.textContent = method; + defaultBtn.onclick = () => { + input.bootstrapToggle(method, false); + }; + td3.append(defaultBtn); + + const td4 = document.createElement("td"); + const silenceBtn = document.createElement("button"); + silenceBtn.className = "btn btn-outline-dark btn-sm w-100"; + silenceBtn.textContent = method; + silenceBtn.onclick = () => { + input.bootstrapToggle(method, true); + }; + td4.append(silenceBtn); + + const tr = document.createElement("tr"); + tr.append(td1, td2, td3, td4); + return tr; + }); + + tbody.append(...trs); + return tbody; + } } export default SilencedActions; diff --git a/docs/src/js/components/documentation/features/AnimationSpeed.js b/docs/src/js/components/documentation/features/AnimationSpeed.js index fe2c1e70..4d516768 100644 --- a/docs/src/js/components/documentation/features/AnimationSpeed.js +++ b/docs/src/js/components/documentation/features/AnimationSpeed.js @@ -1,11 +1,11 @@ import DocArticle from "../DocArticle"; class AnimationSpeed { - static build() { - const description = document.createElement("p"); - description.innerHTML = `Transition speed can be easily controlled with css transition property on .toggle-group. You can also turn animation off completely.`; + static build() { + const description = document.createElement("p"); + description.innerHTML = "Transition speed can be easily controlled with css transition property on .toggle-group. You can also turn animation off completely."; - const code = ` `; - return DocArticle.build({ - title: "Custom Style", - description, - example: CustomStyle.#example(), - codeBlock: { - language: "html", - code, - }, - }); - } - - static #example() { - const iosStyle = document.createElement("style"); - iosStyle.textContent = `.toggle.ios, .toggle.ios .toggle-on, .toggle.ios .toggle-off, .toggle.ios .toggle-handle { border-radius: 20rem; }`; - - const iosInput = document.createElement("input"); - iosInput.type = "checkbox"; - iosInput.checked = true; - iosInput.dataset.toggle = "toggle"; - iosInput.dataset.style = "ios"; - - const androidStyle = document.createElement("style"); - androidStyle.textContent = `.toggle.android , .toggle.android .toggle-on, .toggle.android .toggle-off, .toggle.android .toggle-handle{ border-radius: 0px; }`; - - const androidInput = document.createElement("input"); - androidInput.type = "checkbox"; - androidInput.checked = true; - androidInput.dataset.toggle = "toggle"; - androidInput.dataset.style = "android"; - - return [iosStyle, iosInput, androidStyle, androidInput]; - } + return DocArticle.build({ + title: "Custom Style", + description, + example: CustomStyle.#example(), + codeBlock: { + language: "html", + code, + }, + }); + } + + static #example() { + const iosStyle = document.createElement("style"); + iosStyle.textContent = ".toggle.ios, .toggle.ios .toggle-on, .toggle.ios .toggle-off, .toggle.ios .toggle-handle { border-radius: 20rem; }"; + + const iosInput = document.createElement("input"); + iosInput.type = "checkbox"; + iosInput.checked = true; + iosInput.dataset.toggle = "toggle"; + iosInput.dataset.style = "ios"; + + const androidStyle = document.createElement("style"); + androidStyle.textContent = ".toggle.android , .toggle.android .toggle-on, .toggle.android .toggle-off, .toggle.android .toggle-handle{ border-radius: 0px; }"; + + const androidInput = document.createElement("input"); + androidInput.type = "checkbox"; + androidInput.checked = true; + androidInput.dataset.toggle = "toggle"; + androidInput.dataset.style = "android"; + + return [iosStyle, iosInput, androidStyle, androidInput]; + } } export default CustomStyle; diff --git a/docs/src/js/components/documentation/features/CustomText.js b/docs/src/js/components/documentation/features/CustomText.js index 5ea4f21b..8769c2d8 100644 --- a/docs/src/js/components/documentation/features/CustomText.js +++ b/docs/src/js/components/documentation/features/CustomText.js @@ -1,51 +1,51 @@ import DocArticle from "../DocArticle"; class CustomText { - static build() { - const description = document.createElement("p"); - description.innerHTML = `Bootstrap toggle allows to set custom labels. Just add the data-onlabel or data-offlabel attribute to set a custom label. Plain text and HTML are supported.`; + static build() { + const description = document.createElement("p"); + description.innerHTML = "Bootstrap toggle allows to set custom labels. Just add the data-onlabel or data-offlabel attribute to set a custom label. Plain text and HTML are supported."; - const code = ` + const code = ` `; - return DocArticle.build({ - title: "Custom Text", - description, - example: CustomText.#example(), - codeBlock: { - language: "html", - code, - }, - alert: { - type: "warning", - title: "Deprecation notice", - versionPill: { - version: "5.0.0", - action: "DEPRECATED", - }, - content: `Using data-on and data-off data attributes is deprecated and will throw a console warning. Use data-onlabel and data-offlabel data attributes instead.`, - }, - }); - } + return DocArticle.build({ + title: "Custom Text", + description, + example: CustomText.#example(), + codeBlock: { + language: "html", + code, + }, + alert: { + type: "warning", + title: "Deprecation notice", + versionPill: { + version: "5.0.0", + action: "DEPRECATED", + }, + content: "Using data-on and data-off data attributes is deprecated and will throw a console warning. Use data-onlabel and data-offlabel data attributes instead.", + }, + }); + } - static #example() { - const input1 = document.createElement("input"); - input1.type = "checkbox"; - input1.checked = true; - input1.dataset.toggle = "toggle"; - input1.dataset.onlabel = "Ready"; - input1.dataset.offlabel = "Not Ready"; - input1.dataset.onstyle = "success"; - input1.dataset.offstyle = "danger"; + static #example() { + const input1 = document.createElement("input"); + input1.type = "checkbox"; + input1.checked = true; + input1.dataset.toggle = "toggle"; + input1.dataset.onlabel = "Ready"; + input1.dataset.offlabel = "Not Ready"; + input1.dataset.onstyle = "success"; + input1.dataset.offstyle = "danger"; - const input2 = document.createElement("input"); - input2.type = "checkbox"; - input2.checked = true; - input2.dataset.toggle = "toggle"; - input2.dataset.onlabel = " Play"; - input2.dataset.offlabel = " Pause"; + const input2 = document.createElement("input"); + input2.type = "checkbox"; + input2.checked = true; + input2.dataset.toggle = "toggle"; + input2.dataset.onlabel = " Play"; + input2.dataset.offlabel = " Pause"; - return [input1, input2]; - } + return [input1, input2]; + } } export default CustomText; diff --git a/docs/src/js/components/documentation/features/CustomTitle.js b/docs/src/js/components/documentation/features/CustomTitle.js index 9728616a..555cde72 100644 --- a/docs/src/js/components/documentation/features/CustomTitle.js +++ b/docs/src/js/components/documentation/features/CustomTitle.js @@ -1,44 +1,44 @@ import DocArticle from "../DocArticle"; class CustomTitle { - static build() { - const description = document.createElement("p"); - description.innerHTML = `Bootstrap toggle allows to set custom title. Just add the data-ontitle or data-offtitle attribute to set a custom label. To set the same title in + static build() { + const description = document.createElement("p"); + description.innerHTML = `Bootstrap toggle allows to set custom title. Just add the data-ontitle or data-offtitle attribute to set a custom label. To set the same title in both parts just use HTML standard title attribute.`; - const code = ` + const code = ` `; - return DocArticle.build({ - title: "Custom Title", - description, - versionPill: { - version: "5.0.0", - action: "SINCE", - }, - example: CustomTitle.#example(), - codeBlock: { - language: "html", - code, - }, - }); - } + return DocArticle.build({ + title: "Custom Title", + description, + versionPill: { + version: "5.0.0", + action: "SINCE", + }, + example: CustomTitle.#example(), + codeBlock: { + language: "html", + code, + }, + }); + } - static #example() { - const input1 = document.createElement("input"); - input1.type = "checkbox"; - input1.checked = true; - input1.dataset.toggle = "toggle"; - input1.dataset.ontitle = "ON"; - input1.dataset.offtitle = "OFF"; + static #example() { + const input1 = document.createElement("input"); + input1.type = "checkbox"; + input1.checked = true; + input1.dataset.toggle = "toggle"; + input1.dataset.ontitle = "ON"; + input1.dataset.offtitle = "OFF"; - const input2 = document.createElement("input"); - input2.type = "checkbox"; - input2.checked = true; - input2.dataset.toggle = "toggle"; - input2.title = "TOGGLE"; + const input2 = document.createElement("input"); + input2.type = "checkbox"; + input2.checked = true; + input2.dataset.toggle = "toggle"; + input2.title = "TOGGLE"; - return [input1, input2]; - } + return [input1, input2]; + } } export default CustomTitle; diff --git a/docs/src/js/components/documentation/features/FormLayout.js b/docs/src/js/components/documentation/features/FormLayout.js index 0633c843..17fbac8c 100644 --- a/docs/src/js/components/documentation/features/FormLayout.js +++ b/docs/src/js/components/documentation/features/FormLayout.js @@ -2,11 +2,11 @@ import Console from "../../Console"; import DocArticle from "../DocArticle"; class FormLayout { - static build() { - const description = document.createElement("p"); - description.innerHTML = `Bootstrap toggle can be used in all bootstrap form layout.`; + static build() { + const description = document.createElement("p"); + description.innerHTML = "Bootstrap toggle can be used in all bootstrap form layout."; - const code = ` + const code = `
@@ -47,21 +47,21 @@ class FormLayout { `; - return DocArticle.build({ - title: "Form Layout", - description, - example: FormLayout.#example(), - codeBlock: { - language: "html", - code, - }, - }); - } + return DocArticle.build({ + title: "Form Layout", + description, + example: FormLayout.#example(), + codeBlock: { + language: "html", + code, + }, + }); + } - static #example() { - const formStacked = document.createElement("form"); - formStacked.className = "border p-1 mb-1"; - formStacked.innerHTML = `
+ static #example() { + const formStacked = document.createElement("form"); + formStacked.className = "border p-1 mb-1"; + formStacked.innerHTML = `
@@ -70,9 +70,9 @@ class FormLayout {
`; - const fromInline = document.createElement("form"); - fromInline.className = "border p-1 mb-1"; - fromInline.innerHTML = `
+ const fromInline = document.createElement("form"); + fromInline.className = "border p-1 mb-1"; + fromInline.innerHTML = `
@@ -81,22 +81,22 @@ class FormLayout {
`; - const formsInputGroup = [ - { text: "Small", size: "sm" }, - { text: "Default", size: "md" }, - { text: "Large", size: "lg" }, - ].map(({ text, size }) => { - const form = document.createElement("form"); - form.className = `border p-1 ${size !== "lg" ? "mb-1" : ""}`; - form.classList.add("input-group", `input-group-${size}`); - form.innerHTML = ` + const formsInputGroup = [ + { text: "Small", size: "sm" }, + { text: "Default", size: "md" }, + { text: "Large", size: "lg" }, + ].map(({ text, size }) => { + const form = document.createElement("form"); + form.className = `border p-1 ${size !== "lg" ? "mb-1" : ""}`; + form.classList.add("input-group", `input-group-${size}`); + form.innerHTML = ` `; - return form; - }); + return form; + }); - return [formStacked, fromInline, ...formsInputGroup]; - } + return [formStacked, fromInline, ...formsInputGroup]; + } } export default FormLayout; diff --git a/docs/src/js/components/documentation/features/KeyboardInteraction.js b/docs/src/js/components/documentation/features/KeyboardInteraction.js index 28c48137..6d058678 100644 --- a/docs/src/js/components/documentation/features/KeyboardInteraction.js +++ b/docs/src/js/components/documentation/features/KeyboardInteraction.js @@ -1,36 +1,36 @@ import DocArticle from "../DocArticle"; class KeyboardInteraction { - static build() { - const description = document.createElement("p"); - description.innerHTML = `Bootstrap toggle supports keyboard interaction. Toggle can be switched pressing SPACEBAR or ENTER while it's focused and support TAB navigation. + static build() { + const description = document.createElement("p"); + description.innerHTML = `Bootstrap toggle supports keyboard interaction. Toggle can be switched pressing SPACEBAR or ENTER while it's focused and support TAB navigation. A custom tabindex can be set by adding the tabindex attribute, default value is 0.`; - const code = ` + const code = ` `; - return DocArticle.build({ - title: "Keyboard Interaction", - versionPill: { version: "5.0.0", action: "SINCE" }, - description, - example: KeyboardInteraction.#example(), - codeBlock: { - language: "html", - code, - }, - }); - } + return DocArticle.build({ + title: "Keyboard Interaction", + versionPill: { version: "5.0.0", action: "SINCE" }, + description, + example: KeyboardInteraction.#example(), + codeBlock: { + language: "html", + code, + }, + }); + } - static #example() { - return ["-1", "", "1"].map((tabindex) => { - const input = document.createElement("input"); - input.type = "checkbox"; - input.checked = true; - input.dataset.toggle = "toggle"; - if (tabindex !== "") input.setAttribute("tabindex", tabindex); - return input; - }); - } + static #example() { + return ["-1", "", "1"].map((tabindex) => { + const input = document.createElement("input"); + input.type = "checkbox"; + input.checked = true; + input.dataset.toggle = "toggle"; + if (tabindex !== "") input.setAttribute("tabindex", tabindex); + return input; + }); + } } export default KeyboardInteraction; diff --git a/docs/src/js/components/documentation/features/OutlineColors.js b/docs/src/js/components/documentation/features/OutlineColors.js index 56b2ae39..e7f2bcba 100644 --- a/docs/src/js/components/documentation/features/OutlineColors.js +++ b/docs/src/js/components/documentation/features/OutlineColors.js @@ -1,11 +1,11 @@ import DocArticle from "../DocArticle"; class OutlineColors { - static build() { - const description = document.createElement("p"); - description.innerHTML = `Bootstrap toggle implements also all Bootstrap outline colors. Just add the data-onstyle or data-offstyle attribute with one of the following values: outline-primary, outline-secondary, outline-success, outline-danger, outline-warning, outline-info, outline-light or outline-dark.`; + static build() { + const description = document.createElement("p"); + description.innerHTML = "Bootstrap toggle implements also all Bootstrap outline colors. Just add the data-onstyle or data-offstyle attribute with one of the following values: outline-primary, outline-secondary, outline-success, outline-danger, outline-warning, outline-info, outline-light or outline-dark."; - const code = ` + const code = ` @@ -14,36 +14,36 @@ class OutlineColors { `; - return DocArticle.build({ - title: "Outline Colors", - description, - example: OutlineColors.#example(), - codeBlock: { - language: "html", - code, - }, - }); - } + return DocArticle.build({ + title: "Outline Colors", + description, + example: OutlineColors.#example(), + codeBlock: { + language: "html", + code, + }, + }); + } - static #example() { - const example = [ - "outline-primary", - "outline-secondary", - "outline-success", - "outline-danger", - "outline-warning", - "outline-info", - "outline-light", - "outline-dark", - ].map((color) => { - const input = document.createElement("input"); - input.type = "checkbox"; - input.checked = true; - input.dataset.toggle = "toggle"; - input.dataset.onstyle = color; - return input; - }); - return example; - } + static #example() { + const example = [ + "outline-primary", + "outline-secondary", + "outline-success", + "outline-danger", + "outline-warning", + "outline-info", + "outline-light", + "outline-dark", + ].map((color) => { + const input = document.createElement("input"); + input.type = "checkbox"; + input.checked = true; + input.dataset.toggle = "toggle"; + input.dataset.onstyle = color; + return input; + }); + return example; + } } export default OutlineColors; diff --git a/docs/src/js/components/documentation/features/Size.js b/docs/src/js/components/documentation/features/Size.js index e72b452c..966c2215 100644 --- a/docs/src/js/components/documentation/features/Size.js +++ b/docs/src/js/components/documentation/features/Size.js @@ -1,36 +1,36 @@ import DocArticle from "../DocArticle"; class Size { - static build() { - const description = document.createElement("p"); - description.innerHTML = `Bootstrap toggle is available in different sizes. Just add the data-size attribute with one of the following values: lg, sm or xs.`; + static build() { + const description = document.createElement("p"); + description.innerHTML = "Bootstrap toggle is available in different sizes. Just add the data-size attribute with one of the following values: lg, sm or xs."; - const code = ` + const code = ` `; - return DocArticle.build({ - title: "Size", - description, - example: Size.#example(), - codeBlock: { - language: "html", - code, - }, - }); - } + return DocArticle.build({ + title: "Size", + description, + example: Size.#example(), + codeBlock: { + language: "html", + code, + }, + }); + } - static #example() { - const example = ["lg", "default", "sm", "xs"].map((size) => { - const input = document.createElement("input"); - input.type = "checkbox"; - input.checked = true; - input.dataset.toggle = "toggle"; - if (size != "default") input.dataset.size = size; - return input; - }); - return example; - } + static #example() { + const example = ["lg", "default", "sm", "xs"].map((size) => { + const input = document.createElement("input"); + input.type = "checkbox"; + input.checked = true; + input.dataset.toggle = "toggle"; + if (size != "default") input.dataset.size = size; + return input; + }); + return example; + } } export default Size; diff --git a/docs/src/js/components/documentation/features/StateStatus.js b/docs/src/js/components/documentation/features/StateStatus.js index ba1cc0d9..b26afb19 100644 --- a/docs/src/js/components/documentation/features/StateStatus.js +++ b/docs/src/js/components/documentation/features/StateStatus.js @@ -2,64 +2,64 @@ import Console from "../../Console"; import DocArticle from "../DocArticle"; class StateStatus { - static build() { - const code = `
+ static build() { + const code = `
`; - return DocArticle.build({ - title: "Toggle State and Status", - description: StateStatus.#description(), - example: StateStatus.#example(), - codeBlock: { - language: "html", - code, - }, - }); - } + return DocArticle.build({ + title: "Toggle State and Status", + description: StateStatus.#description(), + example: StateStatus.#example(), + codeBlock: { + language: "html", + code, + }, + }); + } - static #description() { - const description = document.createElement("div"); + static #description() { + const description = document.createElement("div"); - const paragraph = document.createElement("p"); - paragraph.innerHTML = `Toggle state and status is simply set adding vanilla html attributes to the checkbox:`; - description.append(paragraph); + const paragraph = document.createElement("p"); + paragraph.innerHTML = "Toggle state and status is simply set adding vanilla html attributes to the checkbox:"; + description.append(paragraph); - const ul = document.createElement("ul"); - const li1 = document.createElement("li"); - li1.innerHTML = `Simply add checked to convert checkboxes into on toggles.`; - const li2 = document.createElement("li"); - li2.innerHTML = `Simply add disabled to convert checkboxes into disabled toggles. Checkboxes will not be presents in the payload of a submitted form.`; - const li3 = document.createElement("li"); - li3.innerHTML = `Simply add readonly to convert checkboxes into disabled toggles but presents in the payload of a submitted form.`; - ul.append(li1, li2, li3); - description.append(ul); + const ul = document.createElement("ul"); + const li1 = document.createElement("li"); + li1.innerHTML = "Simply add checked to convert checkboxes into on toggles."; + const li2 = document.createElement("li"); + li2.innerHTML = "Simply add disabled to convert checkboxes into disabled toggles. Checkboxes will not be presents in the payload of a submitted form."; + const li3 = document.createElement("li"); + li3.innerHTML = "Simply add readonly to convert checkboxes into disabled toggles but presents in the payload of a submitted form."; + ul.append(li1, li2, li3); + description.append(ul); - return description; - } + return description; + } - static #example() { - const form = document.createElement("form"); - form.className = "d-flex justify-content-between align-items-center"; - form.innerHTML = ` + static #example() { + const form = document.createElement("form"); + form.className = "d-flex justify-content-between align-items-center"; + form.innerHTML = ` `; - const console = new Console(); + const console = new Console(); - form.onsubmit = (e) => { - e.preventDefault(); - console.json({ - mode: "replace", - data: Object.fromEntries(new FormData(e.target)), - }); - }; + form.onsubmit = (e) => { + e.preventDefault(); + console.json({ + mode: "replace", + data: Object.fromEntries(new FormData(e.target)), + }); + }; - return [form, console.htmlElement]; - } + return [form, console.htmlElement]; + } } export default StateStatus; diff --git a/docs/src/js/components/documentation/features/Tooltip.js b/docs/src/js/components/documentation/features/Tooltip.js index 3a14349d..38c68782 100644 --- a/docs/src/js/components/documentation/features/Tooltip.js +++ b/docs/src/js/components/documentation/features/Tooltip.js @@ -1,61 +1,61 @@ import DocArticle from "../DocArticle"; class Tooltip { - static build() { - const description = document.createDocumentFragment(); + static build() { + const description = document.createDocumentFragment(); - const p1 = document.createElement("p"); - p1.innerHTML = `Bootstrap toggle allows to set tooltip. Just add the data-tooltip-title-on or data-tooltip-title-off or data-tooltip-title-mixed attribute to set a custom label. Plain text and HTML are supported. The attribute data-tooltip-title is used as fallback when a title for the specific state is not provided.`; - description.appendChild(p1); + const p1 = document.createElement("p"); + p1.innerHTML = "Bootstrap toggle allows to set tooltip. Just add the data-tooltip-title-on or data-tooltip-title-off or data-tooltip-title-mixed attribute to set a custom label. Plain text and HTML are supported. The attribute data-tooltip-title is used as fallback when a title for the specific state is not provided."; + description.appendChild(p1); - const p2 = document.createElement("p"); - p2.innerHTML = `Tooltips placement can be set using the data-tooltip-placement attribute. Possible values are: top, bottom, left, and right. The default value is top.`; - description.appendChild(p2); + const p2 = document.createElement("p"); + p2.innerHTML = "Tooltips placement can be set using the data-tooltip-placement attribute. Possible values are: top, bottom, left, and right. The default value is top."; + description.appendChild(p2); - const code = ` + const code = ` Checked" data-tooltip-title-off=" Unchecked" />`; - return DocArticle.build({ - title: "Tooltip", - description, - example: Tooltip.#example(), - codeBlock: { - language: "html", - code, - }, - versionPill: { action: "SINCE", version: "5.3.0" }, - }); - } - - static #example() { - const input1 = document.createElement("input"); - input1.type = "checkbox"; - input1.checked = true; - input1.dataset.toggle = "toggle"; - input1.dataset.tooltipTitle = "Fallback"; - input1.dataset.tooltipTitleOn = "Checked"; - input1.dataset.tooltipTitleOff = "Unchecked"; - input1.setAttribute("tristate", ""); - - const input2 = document.createElement("input"); - input2.type = "checkbox"; - input2.checked = true; - input2.dataset.toggle = "toggle"; - input2.dataset.tooltipTitleOn = "Checked"; - input2.dataset.tooltipTitleOff = "Unchecked"; - input2.dataset.tooltipPlacement = "bottom"; - - const input3 = document.createElement("input"); - input3.type = "checkbox"; - input3.checked = true; - input3.dataset.toggle = "toggle"; - input3.dataset.tooltipTitleOn = + return DocArticle.build({ + title: "Tooltip", + description, + example: Tooltip.#example(), + codeBlock: { + language: "html", + code, + }, + versionPill: { action: "SINCE", version: "5.3.0" }, + }); + } + + static #example() { + const input1 = document.createElement("input"); + input1.type = "checkbox"; + input1.checked = true; + input1.dataset.toggle = "toggle"; + input1.dataset.tooltipTitle = "Fallback"; + input1.dataset.tooltipTitleOn = "Checked"; + input1.dataset.tooltipTitleOff = "Unchecked"; + input1.setAttribute("tristate", ""); + + const input2 = document.createElement("input"); + input2.type = "checkbox"; + input2.checked = true; + input2.dataset.toggle = "toggle"; + input2.dataset.tooltipTitleOn = "Checked"; + input2.dataset.tooltipTitleOff = "Unchecked"; + input2.dataset.tooltipPlacement = "bottom"; + + const input3 = document.createElement("input"); + input3.type = "checkbox"; + input3.checked = true; + input3.dataset.toggle = "toggle"; + input3.dataset.tooltipTitleOn = ' Checked'; - input3.dataset.tooltipTitleOff = + input3.dataset.tooltipTitleOff = ' Unchecked'; - return [input1, input2, input3]; - } + return [input1, input2, input3]; + } } export default Tooltip; diff --git a/docs/src/js/components/documentation/features/Tristate.js b/docs/src/js/components/documentation/features/Tristate.js index ccb5851a..b2555cc8 100644 --- a/docs/src/js/components/documentation/features/Tristate.js +++ b/docs/src/js/components/documentation/features/Tristate.js @@ -1,32 +1,32 @@ import DocArticle from "../DocArticle"; class Tristate { - static build() { - const description = document.createElement("p"); - description.innerHTML = `Bootstrap toggle is available in tristate mode. Just add the tristate attribute to make a tristate toggle.`; + static build() { + const description = document.createElement("p"); + description.innerHTML = "Bootstrap toggle is available in tristate mode. Just add the tristate attribute to make a tristate toggle."; - const code = ``; + const code = ""; - return DocArticle.build({ - title: "Tristate Toggle", - versionPill: { version: "4.3.0", action: "SINCE" }, - description, - example: Tristate.#example(), - codeBlock: { - language: "html", - code, - }, - }); - } + return DocArticle.build({ + title: "Tristate Toggle", + versionPill: { version: "4.3.0", action: "SINCE" }, + description, + example: Tristate.#example(), + codeBlock: { + language: "html", + code, + }, + }); + } - static #example() { - const input = document.createElement("input"); - input.type = "checkbox"; - input.checked = true; - input.dataset.toggle = "toggle"; - input.setAttribute("tristate", ""); + static #example() { + const input = document.createElement("input"); + input.type = "checkbox"; + input.checked = true; + input.dataset.toggle = "toggle"; + input.setAttribute("tristate", ""); - return [input]; - } + return [input]; + } } export default Tristate; diff --git a/docs/src/js/main.js b/docs/src/js/main.js index f5504b86..b8458be8 100644 --- a/docs/src/js/main.js +++ b/docs/src/js/main.js @@ -9,8 +9,8 @@ DOMBuilder.run(); LogoSwitcher.run({ toggleDelay: 3000 }); window.onload = () => { - BootstrapToggler.run(); - BootsrapDarkmodeToggler.run(); - CodeHighlighter.run(); - TOCBuilder.run(); + BootstrapToggler.run(); + BootsrapDarkmodeToggler.run(); + CodeHighlighter.run(); + TOCBuilder.run(); }; diff --git a/docs/src/js/tasks/BootstrapToggler.js b/docs/src/js/tasks/BootstrapToggler.js index fae8f102..8f18985e 100644 --- a/docs/src/js/tasks/BootstrapToggler.js +++ b/docs/src/js/tasks/BootstrapToggler.js @@ -1,9 +1,9 @@ class BootstrapToggler { - static run() { - document.querySelectorAll('[data-toggle="toggle"]').forEach((element) => { - element.bootstrapToggle(); - }); - } + static run() { + document.querySelectorAll('[data-toggle="toggle"]').forEach((element) => { + element.bootstrapToggle(); + }); + } } export default BootstrapToggler; diff --git a/docs/src/js/tasks/CodeHighlighter.js b/docs/src/js/tasks/CodeHighlighter.js index 659136f4..a060ba21 100644 --- a/docs/src/js/tasks/CodeHighlighter.js +++ b/docs/src/js/tasks/CodeHighlighter.js @@ -1,8 +1,8 @@ class CodeHighlighter { - static run() { - hljs.highlightAll(); - window.highlightJsBadge(); - } + static run() { + hljs.highlightAll(); + window.highlightJsBadge(); + } } export default CodeHighlighter; diff --git a/docs/src/js/tasks/DOMBuilder.js b/docs/src/js/tasks/DOMBuilder.js index a1d9472e..3463254e 100644 --- a/docs/src/js/tasks/DOMBuilder.js +++ b/docs/src/js/tasks/DOMBuilder.js @@ -4,14 +4,14 @@ import Main from "../components/Main"; import Notice from "../components/Notice"; class DOMBuilder { - static #sections = [ - Header.build(), - Notice.build(), - Main.build(), - Footer.build(), - ]; - static run() { - document.body.append(...this.#sections); - } + static #sections = [ + Header.build(), + Notice.build(), + Main.build(), + Footer.build(), + ]; + static run() { + document.body.append(...this.#sections); + } } export default DOMBuilder; diff --git a/docs/src/js/tasks/LogoSwitcher.js b/docs/src/js/tasks/LogoSwitcher.js index 3896a1b4..8f213616 100644 --- a/docs/src/js/tasks/LogoSwitcher.js +++ b/docs/src/js/tasks/LogoSwitcher.js @@ -1,16 +1,16 @@ class LogoSwitcher { - /** + /** * Run the logo switcher. * @param {Object} [options] - Options. * @param {number} [options.toggleDelay=2000] - Delay between toggle in milliseconds. */ - static run({ toggleDelay = 2000 } = {}) { - document.querySelectorAll(".img-toggle img").forEach((element) => { - setInterval(function () { - element.classList.toggle("invisible"); - }, toggleDelay); - }); - } + static run({ toggleDelay = 2000 } = {}) { + document.querySelectorAll(".img-toggle img").forEach((element) => { + setInterval(function () { + element.classList.toggle("invisible"); + }, toggleDelay); + }); + } } export default LogoSwitcher; diff --git a/docs/src/js/tasks/TOCBuilder.js b/docs/src/js/tasks/TOCBuilder.js index bd17ad0a..0f2b98d5 100644 --- a/docs/src/js/tasks/TOCBuilder.js +++ b/docs/src/js/tasks/TOCBuilder.js @@ -1,13 +1,13 @@ class TOCBuilder { - static run() { - $("#toc").html(""); - Toc.init({ - $nav: $("#toc"), - }); - const _scrollSpy = new bootstrap.ScrollSpy(document.body, { - target: "#toc", - }); - } + static run() { + $("#toc").html(""); + Toc.init({ + $nav: $("#toc"), + }); + const _scrollSpy = new bootstrap.ScrollSpy(document.body, { + target: "#toc", + }); + } } export default TOCBuilder; diff --git a/package.json b/package.json index 2c3146c3..f7625586 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bootstrap5-toggle", - "version": "5.4.1", + "version": "5.5.0", "type": "module", "author": { "name": "Pablo Alcaraz Martínez", @@ -54,7 +54,7 @@ "test": "npm run test:unit && npm run test:e2e", "test:unit": "jest --coverage", "test:e2e": "cypress run --headless", - "lint": "eslint ./src", + "lint": "eslint ./src && eslint ./docs/src", "prepare": "husky" }, "keywords": [ @@ -78,12 +78,14 @@ "url": "https://github.com/palcarazm/bootstrap5-toggle/issues" }, "engines": { - "node": ">=22 <25", - "npm": ">=10" + "node": "^22.0.0 || ^24.0.0 || ^26.0.0", + "npm": ">=10.9.9" }, "devDependencies": { "@commitlint/cli": "^21.0.2", "@commitlint/config-conventional": "^21.0.2", + "@rollup/plugin-commonjs": "^29.0.3", + "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-terser": "^1.0.0", "@types/bootstrap": "^5.2.10", "@types/jest": "^30.0.0", @@ -93,8 +95,8 @@ "@typescript-eslint/parser": "^8.58.0", "commitlint": "^21.0.2", "concurrently": "^10.0.3", - "cssnano": "^8.0.2", - "cypress": "^15.8.1", + "cssnano": "^9.0.4", + "cypress": "^16.0.0", "cypress-plugin-tab": "^2.0.0", "doctoc": "^2.2.0", "dompurify": "^3.0.0", @@ -105,7 +107,7 @@ "jest-environment-jsdom": "^30.2.0", "postcss": "^8.5.6", "postcss-banner": "^4.0.1", - "postcss-cli": "^11.0.1", + "postcss-cli": "^12.0.0", "rimraf": "^6.1.3", "rollup": "^4.54.0", "rollup-plugin-dts": "^6.4.1", @@ -122,5 +124,13 @@ }, "optionalDependencies": { "jquery": "^4.0.0" + }, + "dependencies": { + "component-lifecycle": "^2.1.0" + }, + "allowScripts": { + "cypress@16.2.0": true, + "unrs-resolver@1.12.2": true, + "@parcel/watcher@2.6.0": true } } diff --git a/rollup.config.js b/rollup.config.js index 27bb668f..c1c566b6 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,4 +1,6 @@ import terser from "@rollup/plugin-terser"; +import { nodeResolve } from "@rollup/plugin-node-resolve"; +import commonjs from "@rollup/plugin-commonjs"; import dts from "rollup-plugin-dts"; import { bannerContent } from "./scripts/package-banner.js"; @@ -23,6 +25,7 @@ const umdECMAS = { banner, }, ], + plugins: [nodeResolve(), commonjs()], }; const umdJquery = { @@ -49,10 +52,12 @@ const umdJquery = { banner, }, ], + plugins: [nodeResolve(), commonjs()], }; const mainConfig = { input: "src/main/js/index.js", + external: ["component-lifecycle"], output: [ { file: "dist/bootstrap5-toggle.cjs", diff --git a/src/main/js/index.jquery.js b/src/main/js/index.jquery.js index d5b58155..b68bb25e 100644 --- a/src/main/js/index.jquery.js +++ b/src/main/js/index.jquery.js @@ -1,4 +1,5 @@ import { Toggle } from "./BootstrapToggle"; +import { ToggleMonitor } from "./monitoring/ToggleMonitor"; import { default as Events } from "./types/ToggleEvents"; import { ToggleStateValue as StateValue, ToggleStateStatus as StateStatus } from "./core/StateReducer.types"; import { ToggleMethods as Methods } from "./types/ToggleMethods"; @@ -9,14 +10,25 @@ import { ToggleMethods as Methods } from "./types/ToggleMethods"; * Enables execution when used with ECMAScript */ globalThis.window.BootstrapToggle = globalThis.window.BootstrapToggle || {}; - Object.assign(globalThis.window.BootstrapToggle, {Events, Methods, StateValue, StateStatus}); + Object.assign(globalThis.window.BootstrapToggle, { + Events, + Methods, + StateValue, + StateStatus, + MONITOR: new ToggleMonitor(), + }); function Plugin(options, silent) { const optArg = Array.prototype.slice.call(arguments, 1)[0]; return ( this ).each(function () { - const $this = $(this); - let _bsToggle = this.bsToggle || new Toggle(this, (options && typeof options !== "string") ? options : {}); + let _bsToggle = this.bsToggle; + + if (!_bsToggle) { + _bsToggle = new Toggle(this, (options && typeof options !== "string") ? options : {}); + void _bsToggle.init().then(() => _bsToggle.attach()); + return; + } if (options && typeof options === "string") { switch (options.toLowerCase()) { @@ -24,10 +36,10 @@ import { ToggleMethods as Methods } from "./types/ToggleMethods"; _bsToggle.toggle(silent); break; case Methods.ON: - _bsToggle.on(silent); + _bsToggle.toggleOn(silent); break; case Methods.OFF: - _bsToggle.off(silent); + _bsToggle.toggleOff(silent); break; case Methods.INDETERMINATE: _bsToggle.indeterminate(silent); diff --git a/src/main/ts/BootstrapToggle.ts b/src/main/ts/BootstrapToggle.ts index 4aaf8fca..2b8d7067 100644 --- a/src/main/ts/BootstrapToggle.ts +++ b/src/main/ts/BootstrapToggle.ts @@ -1,45 +1,97 @@ +import { Component } from "component-lifecycle"; import { DOMBuilder } from "./core/DOMBuilder"; import { OptionResolver } from "./core/OptionResolver"; import { ToggleOptions, UserOptions } from "./core/OptionResolver.types"; import { StateReducer } from "./core/StateReducer"; import { ToggleActionType, ToggleState, ToggleStateValue } from "./core/StateReducer.types"; -import ToggleEvents, { ToggleEventDetail } from "./types/ToggleEvents"; +import ToggleEvents, { ToggleEventMap } from "./types/ToggleEvents"; -export class Toggle { - private readonly element: HTMLInputElement & { bsToggle?: Toggle }; +export class Toggle extends Component<"toggle", ToggleEventMap> { + declare readonly element: HTMLInputElement & { bsToggle?: Toggle }; + protected readonly PREFIX = "toggle" as const; private readonly userOptions: UserOptions; - private options: ToggleOptions; - private stateReducer: StateReducer; - private domBuilder: DOMBuilder; + private toggleOptions: ToggleOptions; + private stateReducer!: StateReducer; + private domBuilder!: DOMBuilder; private pointer: { x: number; y: number } | null = null; private readonly SCROLL_THRESHOLD = 10; - private eventsBound = false; private suppressExternalSync = false; private readonly originalDescriptors = new Map(); /** - * Initializes a new instance of the BootstrapToggle class. - * @param element The HTMLInputElement element which represents the toggle. - * @param options The options for the toggle. - * @returns The constructed BootstrapToggle instance. - */ + * Initializes a new instance of the Toggle component. + * + * @param element The checkbox element backing the toggle. + * @param options User-provided options. + */ constructor(element: HTMLInputElement, options: UserOptions) { - this.element = element; + super(element, {bubbleEvents: true}); + this.userOptions = options; - this.options = OptionResolver.resolve(element, options); - this.stateReducer = new StateReducer(element, this.options.tristate); + this.toggleOptions = OptionResolver.resolve(element, options); + + this.element.bsToggle = this; + } + + /** + * Initializes internal state. No DOM access, no side effects. + * @returns {Promise<{ cancelled: boolean }>} transition result + */ + protected doInit(): Promise<{ cancelled: boolean }> { + this.stateReducer = new StateReducer( + this.element, + this.toggleOptions.tristate + ); + return Promise.resolve({ cancelled: false }); + } + + /** + * Binds the component to the DOM: builds the toggle DOM, binds listeners, + * intercepts input properties, and registers the instance on the element. + * @returns {Promise<{ cancelled: boolean }>} transition result + */ + protected doAttach(): Promise<{ cancelled: boolean }> { this.domBuilder = new DOMBuilder( - element, - this.options, + this.element, + this.toggleOptions, this.stateReducer.get() ); this.bindEventListeners(); this.interceptInputProperties(); - this.element.bsToggle = this; + return Promise.resolve({ cancelled: false }); + } + + /** + * Tears down everything created by doAttach(), including the toggle DOM. + * The original checkbox is restored to the DOM. + * @returns {Promise<{ cancelled: boolean }>} transition result + */ + protected doDispose(): Promise<{ cancelled: boolean }> { + this.unbindEventListeners(); + this.restoreInputProperties(); + this.domBuilder.destroy(); + return Promise.resolve({ cancelled: false }); + } + + /** + * Final teardown. Defensively disposes if still attached (see decision 5b). + * @returns {Promise<{ cancelled: boolean; reason?: string }>} transition result + */ + protected async doDestroy(): Promise<{ cancelled: boolean, reason?: string }> { + + if (this.isAttached()) { + try { + await this.doDispose(); + } catch (e) { + return { cancelled: true, reason: (e as Error).message }; + } + } + delete this.element.bsToggle; + return { cancelled: false }; } /** @@ -103,43 +155,28 @@ export class Toggle { }; /** - * Binds event listeners to the toggle element. - * This method is called by the constructor and is responsible for - * binding the following event listeners: - * - Pointer events (click, touchstart, touchend) - * - Keyboard events (keydown, keyup) - * - Label events (click) - * If the event listeners are already bound (i.e. this.eventsBound is true), - * this method does nothing. - * @returns void - */ + * Binds all event listeners for the toggle component, including form reset, pointer, keyboard, and label events. + * This method is called during the attachment phase of the component lifecycle. + * It ensures that the toggle component responds to user interactions and external changes. + */ private bindEventListeners() { - if (this.eventsBound) return; this.bindFormResetListener(); this.bindPointerEventListener(); this.bindKeyboardEventListener(); this.bindLabelEventListener(); - this.eventsBound = true; } + /** - * Unbinds all event listeners from the toggle element. - * This method is called by the destructor and is responsible for - * unbinding the following event listeners: - * - Pointer events (click, touchstart, touchend) - * - Keyboard events (keydown, keyup) - * - Label events (click) - * If the event listeners are not bound (i.e. this.eventsBound is false), - * this method does nothing. - * @returns void - */ + * Unbinds all event listeners for the toggle component, including form reset, pointer, keyboard, and label events. + * This method is called during the disposal phase of the component lifecycle. + * It ensures that the toggle component no longer responds to user interactions and external changes. + */ private unbindEventListeners() { - if (!this.eventsBound) return; this.unbindFormResetListener(); this.unbindPointerEventListener(); this.unbindKeyboardEventListener(); this.unbindLabelEventListener(); - this.eventsBound = false; } private bindFormResetListener() { @@ -159,12 +196,12 @@ export class Toggle { }; /** - * Binds a pointerdown event listener to the root element of the toggle. - * The event listener is responsible for handling pointer events (e.g. mouse clicks, touch events) - * and triggering the toggle's state change when a pointer event occurs. - * The event listener is bound with the passive option, which means that it will not block - * other event listeners from being triggered. - */ + * Binds a pointerdown event listener to the root element of the toggle. + * The event listener is responsible for handling pointer events (e.g. mouse clicks, touch events) + * and triggering the toggle's state change when a pointer event occurs. + * The event listener is bound with the passive option, which means that it will not block + * other event listeners from being triggered. + */ private bindPointerEventListener() { this.domBuilder.root.addEventListener("pointerdown", this.onPointerDown, { passive: true, @@ -172,25 +209,25 @@ export class Toggle { } /** - * Unbinds the pointerdown event listener from the root element of the toggle. - * This method is responsible for unbinding the pointerdown event listener that was - * previously bound by the bindPointerEventListener method. - * If the event listener is not bound (i.e. this.eventsBound is false), this method does nothing. - * @returns void - */ + * Unbinds the pointerdown event listener from the root element of the toggle. + * This method is responsible for unbinding the pointerdown event listener that was + * previously bound by the bindPointerEventListener method. + * If the event listener is not bound (i.e. this.eventsBound is false), this method does nothing. + * @returns void + */ private unbindPointerEventListener() { this.domBuilder.root.removeEventListener("pointerdown", this.onPointerDown); } /** - * Handles pointer down events by initiating the toggle action and setting up - * listeners for pointer movement, release, and cancellation. - * - * The method early exits if: - * - the pointer event is not a primary mouse button click - * - the toggle cannot be interacted with (`disabled` or `readonly`) - * @param e The PointerEvent object representing the pointer down event. - */ + * Handles pointer down events by initiating the toggle action and setting up + * listeners for pointer movement, release, and cancellation. + * + * The method early exits if: + * - the pointer event is not a primary mouse button click + * - the toggle cannot be interacted with (`disabled` or `readonly`) + * @param e The PointerEvent object representing the pointer down event. + */ private readonly onPointerDown = (e: PointerEvent) => { if (e.pointerType === "mouse" && e.button !== 0) return; if (!this.stateReducer.canInteract()) return; @@ -210,12 +247,12 @@ export class Toggle { }; /** - * Handles pointer move events by checking the distance moved from the initial pointer down position. - * If the pointer has moved beyond a certain threshold, the pointer interaction is cancelled. - * - * Allows dragging within the width of the toggle but cancels if vertical movement exceeds the scroll threshold. - * @param e The PointerEvent object representing the pointer move event. - */ + * Handles pointer move events by checking the distance moved from the initial pointer down position. + * If the pointer has moved beyond a certain threshold, the pointer interaction is cancelled. + * + * Allows dragging within the width of the toggle but cancels if vertical movement exceeds the scroll threshold. + * @param e The PointerEvent object representing the pointer move event. + */ private readonly onPointerMove = (e: PointerEvent) => { const dx = Math.abs(e.clientX - this.pointer!.x); const dy = Math.abs(e.clientY - this.pointer!.y); @@ -226,16 +263,16 @@ export class Toggle { }; /** - * Handles pointer up events by determining if the pointer interaction - * should trigger a toggle action based on the distance moved. - * - * If the pointer has moved beyond a certain threshold, the pointer interaction is cancelled. - * Allows dragging within the width of the toggle but cancels if vertical movement exceeds the scroll threshold. - * Finally, it cleans up by calling the pointer cancel handler. - * - * If the pointer event is not a primary mouse button click, the interaction is cancelled. - * @param e The PointerEvent object representing the pointer up event. - */ + * Handles pointer up events by determining if the pointer interaction + * should trigger a toggle action based on the distance moved. + * + * If the pointer has moved beyond a certain threshold, the pointer interaction is cancelled. + * Allows dragging within the width of the toggle but cancels if vertical movement exceeds the scroll threshold. + * Finally, it cleans up by calling the pointer cancel handler. + * + * If the pointer event is not a primary mouse button click, the interaction is cancelled. + * @param e The PointerEvent object representing the pointer up event. + */ private readonly onPointerUp = (e: PointerEvent) => { if (e.pointerType === "mouse" && e.button !== 0) { this.onPointerCancel(); @@ -252,12 +289,12 @@ export class Toggle { }; /** - * Cleans up pointer event listeners after a pointer interaction is completed or cancelled. - * - * This method removes the `pointermove`, `pointerup`, and `pointercancel` event listeners - * from the root element of the toggle. - * However, `pointerdown` listener remains active for future interactions. - */ + * Cleans up pointer event listeners after a pointer interaction is completed or cancelled. + * + * This method removes the `pointermove`, `pointerup`, and `pointercancel` event listeners + * from the root element of the toggle. + * However, `pointerdown` listener remains active for future interactions. + */ private readonly onPointerCancel = () => { this.domBuilder.root.removeEventListener("pointermove", this.onPointerMove); this.domBuilder.root.removeEventListener("pointerup", this.onPointerUp); @@ -268,10 +305,10 @@ export class Toggle { }; /** - * Binds a keydown event listener to the root element of the toggle. - * The event listener is responsible for handling keydown events - * and triggering the toggle's state change when a keydown event occurs. - */ + * Binds a keydown event listener to the root element of the toggle. + * The event listener is responsible for handling keydown events + * and triggering the toggle's state change when a keydown event occurs. + */ private bindKeyboardEventListener() { this.domBuilder.root.addEventListener( "keydown", @@ -281,12 +318,12 @@ export class Toggle { } /** - * Unbinds the keydown event listener from the root element of the toggle. - * This method is responsible for unbinding the keydown event listener that was - * previously bound by the bindKeyboardEventListener method. - * If the event listener is not bound (i.e. this.eventsBound is false), this method does nothing. - * @returns void - */ + * Unbinds the keydown event listener from the root element of the toggle. + * This method is responsible for unbinding the keydown event listener that was + * previously bound by the bindKeyboardEventListener method. + * If the event listener is not bound (i.e. this.eventsBound is false), this method does nothing. + * @returns void + */ private unbindKeyboardEventListener() { this.domBuilder.root.removeEventListener( "keydown", @@ -301,13 +338,13 @@ export class Toggle { }; /** - * Binds a click event listener to all labels that are associated with the toggle's input element. - * The event listener is responsible for handling click events and triggering the toggle's state change when a click event occurs. - * The event listener is bound with the passive option set to false, which means that it will block other event listeners from being triggered until it has finished its execution. - * This method is called by the constructor and is responsible for binding the event listener to the toggle's labels. - * If the toggle's input element does not have an id (i.e. this.element.id is null or undefined), this method does nothing. - * @returns void - */ + * Binds a click event listener to all labels that are associated with the toggle's input element. + * The event listener is responsible for handling click events and triggering the toggle's state change when a click event occurs. + * The event listener is bound with the passive option set to false, which means that it will block other event listeners from being triggered until it has finished its execution. + * This method is called by the constructor and is responsible for binding the event listener to the toggle's labels. + * If the toggle's input element does not have an id (i.e. this.element.id is null or undefined), this method does nothing. + * @returns void + */ private bindLabelEventListener() { if (this.element.id) { document @@ -321,11 +358,11 @@ export class Toggle { } /** - * Unbinds the click event listener from all labels that are associated with the toggle's input element. - * This method is responsible for unbinding the event listener that was previously bound by the bindLabelEventListener method. - * If the toggle's input element does not have an id (i.e. this.element.id is null or undefined), this method does nothing. - * @returns void - */ + * Unbinds the click event listener from all labels that are associated with the toggle's input element. + * This method is responsible for unbinding the event listener that was previously bound by the bindLabelEventListener method. + * If the toggle's input element does not have an id (i.e. this.element.id is null or undefined), this method does nothing. + * @returns void + */ private unbindLabelEventListener() { if (this.element.id) { document @@ -343,12 +380,12 @@ export class Toggle { }; /** - * Applies a toggle action to the toggle state and renders the toggle element. - * If the action is successful, this method will render the toggle element with the new state. - * If the silent parameter is false, this method will also trigger the change event. - * @param action The toggle action to apply. - * @param silent A boolean indicating whether to trigger the change event after applying the action. - */ + * Applies a toggle action to the toggle state and renders the toggle element. + * If the action is successful, this method will render the toggle element with the new state. + * If the silent parameter is false, this method will also trigger the change event. + * @param action The toggle action to apply. + * @param silent A boolean indicating whether to trigger the change event after applying the action. + */ private apply(action: ToggleActionType, silent = false) { if (!this.stateReducer.do(action)) return; this.suppressExternalSync = true; @@ -362,81 +399,81 @@ export class Toggle { } /** - * Toggles the state of the toggle. - * If the toggle is currently in the on state, it will be set to the off state. - * If the toggle is currently in the off state, it will be set to the on state. - * If the toggle is currently in the indeterminate state, it will be set to the on state. - * If the silent parameter is false, this method will also trigger the change event. - * @param silent A boolean indicating whether to trigger the change event after applying the action. - */ + * Toggles the state of the toggle. + * If the toggle is currently in the on state, it will be set to the off state. + * If the toggle is currently in the off state, it will be set to the on state. + * If the toggle is currently in the indeterminate state, it will be set to the on state. + * If the silent parameter is false, this method will also trigger the change event. + * @param silent A boolean indicating whether to trigger the change event after applying the action. + */ toggle(silent = false) { this.apply(ToggleActionType.TOGGLE, silent); } /** - * Sets the toggle state to on. - * If the silent parameter is false, this method will also trigger the change event. - * @param silent A boolean indicating whether to trigger the change event after applying the action. - */ - on(silent = false) { + * Sets the toggle state to on. + * If the silent parameter is false, this method will also trigger the change event. + * @param silent A boolean indicating whether to trigger the change event after applying the action. + */ + toggleOn(silent = false) { this.apply(ToggleActionType.ON, silent); } /** - * Sets the toggle state to off. - * If the silent parameter is false, this method will also trigger the change event. - * @param silent A boolean indicating whether to trigger the change event after applying the action. - */ - off(silent = false) { + * Sets the toggle state to off. + * If the silent parameter is false, this method will also trigger the change event. + * @param silent A boolean indicating whether to trigger the change event after applying the action. + */ + toggleOff(silent = false) { this.apply(ToggleActionType.OFF, silent); } /** - * Sets the toggle state to indeterminate. - * If the silent parameter is false, this method will also trigger the change event. - * @param {boolean} silent A boolean indicating whether to trigger the change event after applying the action. - */ + * Sets the toggle state to indeterminate. + * If the silent parameter is false, this method will also trigger the change event. + * @param {boolean} silent A boolean indicating whether to trigger the change event after applying the action. + */ indeterminate(silent = false) { this.apply(ToggleActionType.INDETERMINATE, silent); } /** - * Sets the toggle state to determinate. - * If the silent parameter is false, this method will also trigger the change event. - * @param {boolean} silent A boolean indicating whether to trigger the change event after applying the action. - */ + * Sets the toggle state to determinate. + * If the silent parameter is false, this method will also trigger the change event. + * @param {boolean} silent A boolean indicating whether to trigger the change event after applying the action. + */ determinate(silent = false) { this.apply(ToggleActionType.DETERMINATE, silent); } /** - * Enables the toggle. - * If the toggle is currently disabled, this method will set the toggle state to enabled. - * If the silent parameter is false, this method will also trigger the change event. - * @param {boolean} silent A boolean indicating whether to trigger the change event after applying the action. - * @returns void - */ + * Enables the toggle. + * If the toggle is currently disabled, this method will set the toggle state to enabled. + * If the silent parameter is false, this method will also trigger the change event. + * @param {boolean} silent A boolean indicating whether to trigger the change event after applying the action. + * @returns void + */ enable(silent = false) { this.apply(ToggleActionType.ENABLE, silent); } /** - * Disables the toggle. - * If the toggle is currently enabled, this method will set the toggle state to disabled. - * If the silent parameter is false, this method will also trigger the change event. - * @param {boolean} silent A boolean indicating whether to trigger the change event after applying the action. - */ + * Disables the toggle. + * If the toggle is currently enabled, this method will set the toggle state to disabled. + * If the silent parameter is false, this method will also trigger the change event. + * @param {boolean} silent A boolean indicating whether to trigger the change event after applying the action. + */ disable(silent = false) { this.apply(ToggleActionType.DISABLE, silent); } /** - * Sets the toggle state to readonly. - * If the toggle is currently disabled or enabled, this method will set the toggle state to readonly. - * If the silent parameter is false, this method will also trigger the change event. - * @param {boolean} silent A boolean indicating whether to trigger the change event after applying the action. - * @returns void - */ + * Sets the toggle state to readonly. + * If the toggle is currently disabled or enabled, this method will set the toggle state to readonly. + * If the silent parameter is false, this method will also trigger the change event. + * @param {boolean} silent A boolean indicating whether to trigger the change event after applying the action. + * @returns void + */ readonly(silent = false) { this.apply(ToggleActionType.READONLY, silent); } @@ -464,15 +501,10 @@ export class Toggle { this.element.dispatchEvent(new Event("change", { bubbles: true })); const eventName = this.getEventForAction(action, state); - const detail: ToggleEventDetail = { state: state }; - this.element.dispatchEvent( - new CustomEvent(eventName, { - bubbles: true, - detail: detail - }) - ); + this.emit(eventName, { state }); } + /** * Returns the corresponding toggle event for the given toggle action and state. @@ -519,54 +551,24 @@ export class Toggle { return ToggleEvents.MIXED; } } - - /** - * Destroys the toggle element and unbinds all event listeners. - *This method is useful when you need to remove the toggle element from the DOM. - *After calling this method, the toggle element will be removed from the DOM and all event listeners will be unbound. - */ - destroy() { - this.restoreInputProperties(); - this.unbindEventListeners(); - this.domBuilder.destroy(); - delete this.element.bsToggle; - } /** - * Rebuilds the toggle component in-place without destroying the current instance. - * - * This method is useful when you need to reinitialize the toggle element with the same options, - * for example after HTML attribute changes or when the DOM structure has been modified externally. - * - * The method follows this sequence: - * 1. Restores original input properties (checked, disabled, readOnly, indeterminate) - * 2. Unbinds all event listeners (pointer, keyboard, label, form reset) - * 3. Destroys the DOM builder (removes toggle DOM, restores original checkbox) - * 4. Re-resolves options from the element and user options - * 5. Creates a fresh StateReducer with the updated tristate value - * 6. Builds new toggle DOM structure - * 7. Re-binds all event listeners - * 8. Re-intercepts input properties for external change detection - * - * Important: The instance identity is preserved (this.element.bsToggle remains unchanged), - * making it safe for external code that holds references to this instance. - * - * @returns void + * Rebuilds the toggle in-place, preserving instance identity. + * Emits `toggle:disposed` and `toggle:attached` lifecycle events. + * + * Sequence: + * 1. dispose() → doDispose() unbinds listeners, restores props, tears down DOM + * 2. re-resolve options from element (may have changed) + * 3. attach() → doAttach() rebuilds DOM and re-binds listeners + * + * @returns {Promise} resolves when re-attached. */ - rerender() { - this.restoreInputProperties(); - this.unbindEventListeners(); - this.domBuilder.destroy(); - - this.options = OptionResolver.resolve(this.element, this.userOptions); - this.stateReducer = new StateReducer(this.element, this.options.tristate); - this.domBuilder = new DOMBuilder( - this.element, - this.options, - this.stateReducer.get() - ); - - this.bindEventListeners(); - this.interceptInputProperties(); + async rerender(): Promise { + await this.dispose(); + + this.toggleOptions = OptionResolver.resolve(this.element, this.userOptions); + this.stateReducer = new StateReducer(this.element, this.toggleOptions.tristate); + + await this.attach(); } } diff --git a/src/main/ts/index.ecmas.ts b/src/main/ts/index.ecmas.ts index 8aebcebc..9bbc8836 100644 --- a/src/main/ts/index.ecmas.ts +++ b/src/main/ts/index.ecmas.ts @@ -1,4 +1,5 @@ import { Toggle as BootstrapToggle } from "./BootstrapToggle"; +import { ToggleMonitor } from "./monitoring/ToggleMonitor"; import { default as Events } from "./types/ToggleEvents"; import { UserOptions} from "./core/OptionResolver.types"; import { ToggleStateValue as StateValue, ToggleStateStatus as StateStatus } from "./core/StateReducer.types"; @@ -11,24 +12,36 @@ import { BootstrapToggleElement } from "./types/BootstrapToggleElement"; * Enables execution when used with ECMAScript */ globalThis.window.BootstrapToggle = globalThis.window.BootstrapToggle || {}; - Object.assign(globalThis.window.BootstrapToggle, {Events, Methods, StateValue, StateStatus}); + Object.assign(globalThis.window.BootstrapToggle, { + Events, + Methods, + StateValue, + StateStatus, + MONITOR: new ToggleMonitor(), + }); /** * Add `bootstrapToggle` prototype function to HTML Elements * Enables execution when used with HTML - ex: `document.getElementById('toggle').bootstrapToggle('on')` */ HTMLInputElement.prototype.bootstrapToggle = function (options?: UserOptions | Methods, silent?: boolean) { - let _bsToggle = (this as BootstrapToggleElement).bsToggle || new BootstrapToggle(this, (options && typeof options !== "string") ? options : {}); + const el = this as BootstrapToggleElement; + let _bsToggle = el.bsToggle; - // Execute method calls + if (!_bsToggle) { + _bsToggle = new BootstrapToggle(this, (options && typeof options !== "string") ? options : {}); + void _bsToggle.init().then(() => _bsToggle!.attach()); + return; + } + if (options && typeof options === "string") { switch (options.toLowerCase()) { case Methods.TOGGLE: return _bsToggle.toggle(silent); case Methods.ON: - return _bsToggle.on(silent); + return _bsToggle.toggleOn(silent); case Methods.OFF: - return _bsToggle.off(silent); + return _bsToggle.toggleOff(silent); case Methods.INDETERMINATE: return _bsToggle.indeterminate(silent); case Methods.DETERMINATE: diff --git a/src/main/ts/monitoring/ToggleMonitor.ts b/src/main/ts/monitoring/ToggleMonitor.ts new file mode 100644 index 00000000..605dd50e --- /dev/null +++ b/src/main/ts/monitoring/ToggleMonitor.ts @@ -0,0 +1,63 @@ +import { Monitor } from "component-lifecycle"; +import ToggleEvents, { ToggleEventDetail } from "../types/ToggleEvents"; + +/** + * Global singleton monitor for the Bootstrap5-Toggle component. + * + * Attached at module load to `globalThis.window.BootstrapToggle.MONITOR` + * and starts stopped. Start manually from devtools: + * + * window.BootstrapToggle.MONITOR.start("DEBUG"); + * + * The prefix argument MUST match `Toggle.PREFIX` ("toggle"). + * + * @remarks + * `MONITOR` visibility relies on `Toggle` being constructed with + * `{ bubbleEvents: true }`, which is the current default and is not + * configurable via the public API. + */ +export class ToggleMonitor extends Monitor<"toggle"> { + /** + * Constructs a new `ToggleMonitor` bound to the `"toggle"` event prefix. + */ + constructor() { + super("toggle"); + } + + /** + * Sets up DEBUG-level listeners. + * + * Calls `super.setupDebug()` first (which attaches lifecycle and + * transition listeners), then adds document-level listeners for all + * six domain events (`on`, `off`, `mixed`, `enabled`, `disabled`, + * `readonly`). + * + */ + protected setupDebug(): void { + super.setupDebug(); + + this.on("on", (ev) => this.logDebug(ToggleEvents.ON, ev)); + this.on("off", (ev) => this.logDebug(ToggleEvents.OFF, ev)); + this.on("mixed", (ev) => this.logDebug(ToggleEvents.MIXED, ev)); + this.on("enabled", (ev) => this.logDebug(ToggleEvents.ENABLED, ev)); + this.on("disabled", (ev) => this.logDebug(ToggleEvents.DISABLED, ev)); + this.on("readonly", (ev) => this.logDebug(ToggleEvents.READONLY, ev)); + } + + /** + * Emits a DEBUG log line for the given domain event. + * + * @param name The domain event name (e.g. `"on"`). + * @param ev The dispatched `CustomEvent`. + */ + private logDebug(name: ToggleEvents, ev: Event): void { + const target = ev.target as HTMLElement | null; + const id = target?.id || target?.getAttribute("name") || ""; + const detail = (ev as CustomEvent).detail; + + console.debug(`[toggle:DEBUG] ${name}`, { + state: detail?.state, + target: id, + }); + } +} diff --git a/src/main/ts/types/BootstrapToggle.d.ts b/src/main/ts/types/BootstrapToggle.d.ts index 41d4f340..f3b73da7 100644 --- a/src/main/ts/types/BootstrapToggle.d.ts +++ b/src/main/ts/types/BootstrapToggle.d.ts @@ -2,6 +2,7 @@ import Events from "./ToggleEvents"; import { ToggleMethods } from "./ToggleMethods"; import { ToggleStateValue, ToggleStateStatus } from "./core/StateReducer.types"; import { BootstrapToggleElementEventMap } from "./BootstrapToggleElement"; +import type { Monitor } from "component-lifecycle"; /* eslint-disable @typescript-eslint/no-empty-object-type */ declare global { @@ -16,10 +17,12 @@ declare global { interface Window { BootstrapToggle: { - Events: typeof Events, - Methods: typeof ToggleMethods, + Events: typeof Events; + Methods: typeof ToggleMethods; StateValue: typeof ToggleStateValue, - StateStatus: typeof ToggleStateStatus}; + StateStatus: typeof ToggleStateStatus, + MONITOR: Monitor<"toggle">; + }; } } diff --git a/src/main/ts/types/ToggleEvents.ts b/src/main/ts/types/ToggleEvents.ts index 849b2df2..b9209671 100644 --- a/src/main/ts/types/ToggleEvents.ts +++ b/src/main/ts/types/ToggleEvents.ts @@ -1,16 +1,37 @@ +import { ExtendableEventMap } from "component-lifecycle"; import { ToggleState } from "../core/StateReducer.types"; enum ToggleEvents{ - ON = "toggle:on", - OFF = "toggle:off", - MIXED = "toggle:mixed", - ENABLED = "toggle:enabled", - DISABLED = "toggle:disabled", - READONLY = "toggle:readonly" + ON = "on", + OFF = "off", + MIXED = "mixed", + ENABLED = "enabled", + DISABLED = "disabled", + READONLY = "readonly" } export interface ToggleEventDetail { state: ToggleState; } +/** + * Domain events emitted by the Toggle component. + * Keys are the un-prefixed event names; the prefix "toggle:" is added by the base class. + */ +export type ToggleDomainEvents = { + "on": ToggleEventDetail; + "off": ToggleEventDetail; + "mixed": ToggleEventDetail; + "enabled": ToggleEventDetail; + "disabled": ToggleEventDetail; + "readonly": ToggleEventDetail; +}; + +/** + * Full event map of the Toggle component. + * Includes all lifecycle events (initialized, attached, disposed, destroyed, transition-*) + * plus the domain events on/off/mixed/enabled/disabled/readonly. + */ +export type ToggleEventMap = ExtendableEventMap<"toggle", ToggleDomainEvents>; + export default ToggleEvents; \ No newline at end of file diff --git a/src/test/ts/BootstrapToggle.test.ts b/src/test/ts/BootstrapToggle.test.ts index c46d59f7..fce0794b 100644 --- a/src/test/ts/BootstrapToggle.test.ts +++ b/src/test/ts/BootstrapToggle.test.ts @@ -14,7 +14,7 @@ import ToggleEvents from "../../main/ts/types/ToggleEvents"; const renderMock = jest.fn(); const destroyMock = jest.fn(); const doMock = jest.fn(() => true); -const getMock = jest.fn(() => ({})); +const getMock = jest.fn(() => ({ value: "on" })); const syncMock = jest.fn(); const canInteractMock = jest.fn(() => true); @@ -61,6 +61,13 @@ jest.mock("../../main/ts/core/OptionResolver", () => ({ }, })); +async function attachToggleToInput(input: HTMLInputElement, options: Record = {}) { + const toggle = new Toggle(input, options); + await toggle.init(); + await toggle.attach(); + return toggle; +} + /* ========================= Tests ========================= */ @@ -87,49 +94,69 @@ describe("Toggle", () => { expect((input as any).bsToggle).toBe(toggle); }); - it("bind events listeners", () => { - input.id = "test-toggle"; - const label = document.createElement("label"); - label.setAttribute("for", "test-toggle"); - document.body.appendChild(label); + it("does NOT auto-start lifecycle", async () => { + const initSpy = jest.spyOn(Toggle.prototype, "init"); + const attachSpy = jest.spyOn(Toggle.prototype, "attach"); + const _ = new Toggle(input, {}); - const addEventListenerSpyDiv = jest.spyOn( - HTMLDivElement.prototype, - "addEventListener" - ); + await Promise.resolve(); - const addEventListenerSpyLabel = jest.spyOn( - HTMLLabelElement.prototype, - "addEventListener" - ); + expect(initSpy).not.toHaveBeenCalled(); + expect(attachSpy).not.toHaveBeenCalled(); + }); + }); - const _ = new Toggle(input, {}); + describe("lifecycle", () => { + it("init() → initialized state, doInit() creates stateReducer", async () => { + const toggle = new Toggle(input, {}); + await toggle.init(); + expect(toggle.isInitialized()).toBe(true); + expect((toggle as any).stateReducer).toBeDefined(); + }); - expect(addEventListenerSpyDiv).toHaveBeenCalledWith( - "pointerdown", - expect.any(Function), - expect.any(Object) - ); - expect(addEventListenerSpyDiv).toHaveBeenCalledWith( - "keydown", - expect.any(Function), - expect.any(Object) - ); + it("attach() → attached state, doAttach() builds DOM and binds listeners", async () => { + const toggle = new Toggle(input, {}); + const addDiv = jest.spyOn(HTMLDivElement.prototype, "addEventListener"); + await toggle.init(); + await toggle.attach(); + expect(toggle.isAttached()).toBe(true); + expect(addDiv).toHaveBeenCalledWith("pointerdown", expect.any(Function), expect.any(Object)); + expect(addDiv).toHaveBeenCalledWith("keydown", expect.any(Function), expect.any(Object)); + addDiv.mockRestore(); + }); - expect(addEventListenerSpyLabel).toHaveBeenCalledWith( - "click", - expect.any(Function), - expect.objectContaining({ passive: false }) - ); + it("dispose() → disposed state, doDispose() unbinds and destroys DOM", async () => { + const toggle = new Toggle(input, {}); + await toggle.init(); + await toggle.attach(); + await toggle.dispose(); + expect(toggle.isDisposed()).toBe(true); + expect(document.querySelector(".toggle")).toBeNull(); + }); - addEventListenerSpyDiv.mockRestore(); - addEventListenerSpyLabel.mockRestore(); + it("destroy() → destroyed state, doDestroy() deletes bsToggle", async () => { + const toggle = new Toggle(input, {}); + await toggle.init(); + await toggle.attach(); + await toggle.destroy(); + expect(toggle.isDestroyed()).toBe(true); + expect((input as any).bsToggle).toBeUndefined(); + }); + + it("destroy() from attached runs dispose() first then destroy()", async () => { + const toggle = new Toggle(input, {}); + await toggle.init(); + await toggle.attach(); + const disposeSpy = jest.spyOn(toggle as any, "doDispose"); + await toggle.destroy(); + expect(disposeSpy).toHaveBeenCalled(); + expect(toggle.isDestroyed()).toBe(true); }); }); describe("interceptInputProperties()", () => { - it("reacts to checked property change", () => { - const toggle = new Toggle(input, {}); + it("reacts to checked property change", async () => { + const toggle = await attachToggleToInput(input); const spy = jest.spyOn(toggle as any, "onExternalChange"); input.checked = true; @@ -137,8 +164,8 @@ describe("Toggle", () => { expect(spy).toHaveBeenCalled(); }); - it("reacts to disabled property change", () => { - const toggle = new Toggle(input, {}); + it("reacts to disabled property change", async () => { + const toggle = await attachToggleToInput(input); const spy = jest.spyOn(toggle as any, "onExternalChange"); input.disabled = true; @@ -146,8 +173,8 @@ describe("Toggle", () => { expect(spy).toHaveBeenCalled(); }); - it("does not throw when setting same value", () => { - const _ = new Toggle(input, {}); + it("does not throw when setting same value", async () => { + const _ = await attachToggleToInput(input); expect(() => { input.checked = false; @@ -159,8 +186,8 @@ describe("Toggle", () => { describe("Pointer interactions", () => { let root: HTMLElement; - beforeEach(() => { - const toggle = new Toggle(input, {}); + beforeEach(async () => { + const toggle = await attachToggleToInput(input); root = (DOMBuilder as any).mock.results[0].value.root; jest.spyOn(toggle as any, "apply"); }); @@ -301,8 +328,8 @@ describe("Toggle", () => { describe("Keyboard interactions", () => { let root: HTMLElement; - beforeEach(() => { - const _ = new Toggle(input, {}); + beforeEach(async () => { + const _ = await attachToggleToInput(input); root = (DOMBuilder as any).mock.results[0].value.root; }); @@ -332,14 +359,14 @@ describe("Toggle", () => { }); describe("Label interaction", () => { - it("clicking label toggles and focuses root", () => { + it("clicking label toggles and focuses root", async () => { input.id = "toggle-id"; const label = document.createElement("label"); label.setAttribute("for", "toggle-id"); document.body.appendChild(label); - const _ = new Toggle(input, {}); + const _ = await attachToggleToInput(input); const root = (DOMBuilder as any).mock.results[0].value.root; const focusSpy = jest.spyOn(root, "focus"); @@ -351,14 +378,14 @@ describe("Toggle", () => { }); describe("form reset handling", () => { - it("syncs after form reset", () => { + it("syncs after form reset", async () => { jest.useFakeTimers(); const form = document.createElement("form"); form.appendChild(input); document.body.appendChild(form); - const toggle = new Toggle(input, {}); + const toggle = await attachToggleToInput(input); const spy = jest.spyOn(toggle as any, "onExternalChange"); form.reset(); @@ -373,11 +400,9 @@ describe("Toggle", () => { }); }); - - describe("apply(action: ToggleActionType, silent = false)", () => { - it("calls StateReducer.do and DOMBuilder.render", () => { - const toggle = new Toggle(input, {}); + it("calls StateReducer.do and DOMBuilder.render", async () => { + const toggle = await attachToggleToInput(input); (toggle as any).apply(ToggleActionType.TOGGLE); @@ -385,8 +410,8 @@ describe("Toggle", () => { expect(renderMock).toHaveBeenCalled(); }); - it("dispatches change event when not silent", () => { - const toggle = new Toggle(input, {}); + it("dispatches change event when not silent", async () => { + const toggle = await attachToggleToInput(input); const spy = jest.fn(); input.addEventListener("change", spy); @@ -396,8 +421,8 @@ describe("Toggle", () => { expect(spy).toHaveBeenCalled(); }); - it("does not dispatch change event when silent", () => { - const toggle = new Toggle(input, {}); + it("does not dispatch change event when silent", async () => { + const toggle = await attachToggleToInput(input); const spy = jest.fn(); input.addEventListener("change", spy); @@ -407,8 +432,8 @@ describe("Toggle", () => { expect(spy).not.toHaveBeenCalled(); }); - it("guard against sync loops:does not react to internal apply-triggered input change", () => { - const toggle = new Toggle(input, {}); + it("guard against sync loops:does not react to internal apply-triggered input change", async () => { + const toggle = await attachToggleToInput(input); const spy = jest.spyOn(toggle as any, "onExternalChange"); (toggle as any).apply(ToggleActionType.TOGGLE); @@ -471,17 +496,24 @@ describe("Toggle", () => { action: ToggleActionType.READONLY, }]; - it.each(customEventsCases)("dispatches $eventName event for $action action", ({ eventName, action}) => { + it.each(customEventsCases)("dispatches $eventName event for $action action", async ({ eventName, action}) => { input.disabled = action === ToggleActionType.ENABLE; input.checked = action !== ToggleActionType.ON; - const toggle = new Toggle(input, {}); + const toggle = await attachToggleToInput(input); (toggle as any).apply(action, false); customEventsCases.forEach((evtSpy)=>{ if(evtSpy.eventName === eventName){ expect(evtSpy.eventSpy()).toHaveBeenCalled(); + const eventDetail = evtSpy.eventSpy().mock.calls[0][0].detail; + expect(eventDetail).toEqual({ + state: expect.objectContaining({ + value: expect.any(String), + }), + }); + expect(eventDetail.state).toEqual({ value: "on" }); } else { expect(evtSpy.eventSpy()).not.toHaveBeenCalled(); } @@ -514,14 +546,14 @@ describe("Toggle", () => { mockStateValue: ToggleStateValue.MIXED, }]; - it.each(valueBasedEventCases)("dispatches $eventName event for $action action based on state value $mockStateValue", ({ eventName, action, mockStateValue}) => { + it.each(valueBasedEventCases)("dispatches $eventName event for $action action based on state value $mockStateValue", async ({ eventName, action, mockStateValue}) => { const mockState = { value: mockStateValue }; getMock.mockReturnValue(mockState); input.disabled = action === ToggleActionType.ENABLE; input.checked = action !== ToggleActionType.ON; - const toggle = new Toggle(input, {}); + const toggle = await attachToggleToInput(input); (toggle as any).apply(action, false); @@ -547,8 +579,8 @@ describe("Toggle", () => { apply.mockRestore(); }); - it("toggle(silent=false)", () => { - const toggle = new Toggle(input, {}); + it("toggle(silent=false)", async () => { + const toggle = await attachToggleToInput(input); toggle.toggle(); expect(apply).toHaveBeenCalledWith(ToggleActionType.TOGGLE, false); @@ -557,28 +589,28 @@ describe("Toggle", () => { expect(apply).toHaveBeenCalledWith(ToggleActionType.TOGGLE, true); }); - it("on(silent=false)", () => { - const toggle = new Toggle(input, {}); + it("toggleOn(silent=false)", async () => { + const toggle = await attachToggleToInput(input); - toggle.on(); + toggle.toggleOn(); expect(apply).toHaveBeenCalledWith(ToggleActionType.ON, false); - toggle.on(true); + toggle.toggleOn(true); expect(apply).toHaveBeenCalledWith(ToggleActionType.ON, true); }); - it("off(silent=false)", () => { - const toggle = new Toggle(input, {}); + it("toggleOff(silent=false)", async () => { + const toggle = await attachToggleToInput(input); - toggle.off(); + toggle.toggleOff(); expect(apply).toHaveBeenCalledWith(ToggleActionType.OFF, false); - toggle.off(true); + toggle.toggleOff(true); expect(apply).toHaveBeenCalledWith(ToggleActionType.OFF, true); }); - it("determinate(silent=false)", () => { - const toggle = new Toggle(input, {}); + it("determinate(silent=false)", async () => { + const toggle = await attachToggleToInput(input); toggle.determinate(); expect(apply).toHaveBeenCalledWith(ToggleActionType.DETERMINATE, false); @@ -587,8 +619,8 @@ describe("Toggle", () => { expect(apply).toHaveBeenCalledWith(ToggleActionType.DETERMINATE, true); }); - it("indeterminate(silent=false)", () => { - const toggle = new Toggle(input, {}); + it("indeterminate(silent=false)", async () => { + const toggle = await attachToggleToInput(input); toggle.indeterminate(); expect(apply).toHaveBeenCalledWith(ToggleActionType.INDETERMINATE, false); @@ -597,8 +629,8 @@ describe("Toggle", () => { expect(apply).toHaveBeenCalledWith(ToggleActionType.INDETERMINATE, true); }); - it("enable(silent=false)", () => { - const toggle = new Toggle(input, {}); + it("enable(silent=false)", async () => { + const toggle = await attachToggleToInput(input); toggle.enable(); expect(apply).toHaveBeenCalledWith(ToggleActionType.ENABLE, false); @@ -607,8 +639,8 @@ describe("Toggle", () => { expect(apply).toHaveBeenCalledWith(ToggleActionType.ENABLE, true); }); - it("disable(silent=false)", () => { - const toggle = new Toggle(input, {}); + it("disable(silent=false)", async () => { + const toggle = await attachToggleToInput(input); toggle.disable(); expect(apply).toHaveBeenCalledWith(ToggleActionType.DISABLE, false); @@ -617,8 +649,8 @@ describe("Toggle", () => { expect(apply).toHaveBeenCalledWith(ToggleActionType.DISABLE, true); }); - it("readonly(silent=false)", () => { - const toggle = new Toggle(input, {}); + it("readonly(silent=false)", async () => { + const toggle = await attachToggleToInput(input); toggle.readonly(); expect(apply).toHaveBeenCalledWith(ToggleActionType.READONLY, false); @@ -629,10 +661,10 @@ describe("Toggle", () => { }); describe("update()", () => { - it("syncs state and renders on update()", () => { - const toggle = new Toggle(input, {}); + it("syncs state and renders on update()", async () => { + const toggle = await attachToggleToInput(input); - toggle.update(); + await toggle.update(); expect(syncMock).toHaveBeenCalledWith(input); expect(renderMock).toHaveBeenCalled(); @@ -640,23 +672,23 @@ describe("Toggle", () => { }); describe("destroy()", () => { - it("destroys DOM and unlink instance on destroy()", () => { - const toggle = new Toggle(input, {}); + it("destroys DOM and unlink instance on destroy()", async () => { + const toggle = await attachToggleToInput(input); - toggle.destroy(); + await toggle.destroy(); expect(destroyMock).toHaveBeenCalled(); expect((input as any).bsToggle).toBeUndefined(); }); - it("restores native input descriptors allowing reinitialization", () => { - const toggle1 = new Toggle(input, {}); + it("restores native input descriptors allowing reinitialization", async () => { + const toggle1 = await attachToggleToInput(input); toggle1.destroy(); - expect(() => { - const _ = new Toggle(input, {}); + expect(async () => { + const _ = await attachToggleToInput(input); }).not.toThrow(); }); - it("unbind events listeners", () => { + it("unbind events listeners", async () => { input.id = "test-toggle"; const label = document.createElement("label"); @@ -682,7 +714,7 @@ describe("Toggle", () => { "removeEventListener" ); - const toggle = new Toggle(input, {}); + const toggle = await attachToggleToInput(input); toggle.destroy(); expect(removeEventListenerSpyDiv).toHaveBeenCalledWith( @@ -710,31 +742,29 @@ describe("Toggle", () => { }); }); - describe("rerender()", () =>{ - it("rebuilds toggle in-place while preserving instance identity", () => { - const toggle = new Toggle(input, {}); - const instanceRef = toggle; - - const originalBsToggle = (input as any).bsToggle; - - toggle.rerender(); - - expect(toggle).toBe(instanceRef); - expect((input as any).bsToggle).toBe(originalBsToggle); - expect((input as any).bsToggle).toBe(toggle); - - expect(destroyMock).toHaveBeenCalled(); - expect(DOMBuilder).toHaveBeenCalledTimes(2); + describe("rerender()", () => { + it("routes through dispose() → attach() (emits lifecycle events)", async () => { + const toggle = await attachToggleToInput(input); + + const disposed = jest.fn(); + const attached = jest.fn(); + toggle.on("toggle:disposed", disposed).on("toggle:attached", attached); + + await toggle.rerender(); + + expect(disposed).toHaveBeenCalledTimes(1); + expect(attached).toHaveBeenCalledTimes(1); + expect(toggle.isAttached()).toBe(true); }); - it("re-resolves options from HTML attributes after change", () => { + it("re-resolves options from HTML attributes after change", async () => { - const toggle = new Toggle(input, { onlabel: "Old" }); + const toggle = await attachToggleToInput(input, { onlabel: "Old" }); const resolveSpy = jest.spyOn(OptionResolver, "resolve"); input.dataset.onlabel = "New"; - toggle.rerender(); + await toggle.rerender(); expect(resolveSpy).toHaveBeenCalledWith(input, toggle["userOptions"]); resolveSpy.mockRestore(); diff --git a/src/test/ts/monitoring/ToggleMonitor.test.ts b/src/test/ts/monitoring/ToggleMonitor.test.ts new file mode 100644 index 00000000..acba4c3f --- /dev/null +++ b/src/test/ts/monitoring/ToggleMonitor.test.ts @@ -0,0 +1,96 @@ +/// +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { Monitor, LogLevels } from "component-lifecycle"; +import { ToggleMonitor } from "../../../main/ts/monitoring/ToggleMonitor"; + +describe("ToggleMonitor", () => { + let monitor: ToggleMonitor; + + beforeEach(() => { + monitor = new ToggleMonitor(); + }); + + afterEach(() => { + try { + monitor.stop(); + } catch { + // Ignore: stop() on a non-started monitor throws. + } + }); + + describe("construction", () => { + it("extends Monitor", () => { + expect(monitor).toBeInstanceOf(Monitor); + }); + }); + + describe("setupDebug()", () => { + it("calls super.setupDebug() and registers listeners for all six domain events", () => { + const onSpy = jest.spyOn(monitor as any, "on"); + + monitor.start(LogLevels.DEBUG); + + ["on", "off", "mixed", "enabled", "disabled", "readonly"].forEach((eventName) => { + expect(onSpy).toHaveBeenCalledWith( + eventName, + expect.any(Function) + ); + }); + + onSpy.mockRestore(); + }); + + it("logs a line for a dispatched domain event", () => { + const debugSpy = jest.spyOn(console, "debug").mockImplementation(); + + monitor.start(LogLevels.DEBUG); + + const input = document.createElement("input"); + input.type = "checkbox"; + input.id = "my-toggle"; + document.body.appendChild(input); + + const ev = new CustomEvent("toggle:on", { + detail: { state: { value: "on", checked: true } }, + bubbles: true, + }); + input.dispatchEvent(ev); + + expect(debugSpy).toHaveBeenCalledWith( + expect.stringContaining("[toggle:DEBUG] on"), + expect.objectContaining({ + target: "my-toggle", + state: expect.objectContaining({ value: "on" }), + }) + ); + + debugSpy.mockRestore(); + document.body.innerHTML = ""; + }); + + it("falls back to '' when target has no id or name", () => { + const debugSpy = jest.spyOn(console, "debug").mockImplementation(); + + monitor.start(LogLevels.DEBUG); + + const input = document.createElement("input"); + input.type = "checkbox"; + document.body.appendChild(input); + + const ev = new CustomEvent("toggle:off", { + detail: { state: { value: "off", checked: false } }, + bubbles: true, + }); + input.dispatchEvent(ev); + + expect(debugSpy).toHaveBeenCalledWith( + expect.stringContaining("[toggle:DEBUG] off"), + expect.objectContaining({ target: "" }) + ); + + debugSpy.mockRestore(); + document.body.innerHTML = ""; + }); + }); +}); \ No newline at end of file