diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fa374b05..0ad8d9fb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -19,6 +19,20 @@ updates: prefix: "chore(deps): " prefix-development: "chore(deps-dev): " + # ========== DOCS DEPENDENCIES ========== + - package-ecosystem: "npm" + directory: "/docs" + schedule: + interval: "weekly" + day: "friday" + groups: + version-updates: + applies-to: "version-updates" + update-types: ["major", "minor", "patch"] + commit-message: + prefix: "chore(docs): " + prefix-development: "chore(docs): " + # ========== GITHUB ACTIONS ========== - package-ecosystem: "github-actions" directory: "/" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64d73206..de1caa2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: run: npm run test:unit - name: Upload coverage report to Github if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: coverage-report-${{ matrix.node-version }} path: coverage @@ -77,7 +77,7 @@ jobs: run: npm run build - name: Cypress run #v6.10.8 - uses: cypress-io/github-action@84d178e4bbce871e23f2ffa3085898cde0e4f0ec + uses: cypress-io/github-action@783cb3f07983868532cabaedaa1e6c00ff4786a8 with: browser: chrome @@ -112,13 +112,13 @@ jobs: fetch-depth: 0 - name: Download coverage artifact - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: coverage-report-22.x path: coverage - name: SonarQube Scan # v7.0.0 - uses: SonarSource/sonarqube-scan-action@a31c9398be7ace6bbfaf30c0bd5d415f843d45e9 + uses: SonarSource/sonarqube-scan-action@299e4b793aaa83bf2aba7c9c14bedbb485688ec4 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 7ab8d2de..528d4266 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -2,9 +2,9 @@ name: Deploy documentation on: push: - branches: [ "gh-pages" ] + branches: [ "v5" ] pull_request: - branches: [ "gh-pages" ] + branches: [ "v5" ] workflow_dispatch: permissions: @@ -19,6 +19,9 @@ concurrency: jobs: build: runs-on: ubuntu-latest + defaults: + run: + working-directory: docs steps: - name: Checkout uses: actions/checkout@v6 @@ -31,11 +34,11 @@ jobs: - name: Build run: npm run build - name: Setup Pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@v6 - name: Upload artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@v5 with: - path: './dist' + path: ./docs/dist - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/.gitignore b/.gitignore index f78b2911..6735a366 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,32 @@ -.DS_Store +# Node modules node_modules package-lock.json + +# Packaging +bootstrap5-toggle-*.tgz + +# Debug +debug + +# Dist +js +!src/main/js +!docs/src/js +css +!docs/src/css +dist +@types + +# Logs *.log -bootstrap*.tgz -cypress/videos -cypress/screenshots -cypress/downloads + +# Cache and config dirs and files .vscode .dccache -js -css -debug +.DS_Store + +# Test coverage -dist -@types \ No newline at end of file +cypress/videos +cypress/screenshots +cypress/downloads diff --git a/.npmignore b/.npmignore index 94067775..95284e5f 100644 --- a/.npmignore +++ b/.npmignore @@ -1,14 +1,30 @@ -.DS_Store +# Node modules node_modules -*.log + +# Packaging +bootstrap5-toggle-*.tgz + +# Debug debug -img + +# Docs +docs + +# Logs +*.log + +# Cache and config dirs and files .github .vscode -.prettierrc .dccache -test -bootstrap*.tgz -cypress +.DS_Store +.prettierrc + +# Cypress and testing +test/* +cypress/* cypress.config.js -README.template.md +coverage + +# Templates +*.template* \ No newline at end of file diff --git a/README.md b/README.md index b2543d70..999e3f80 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ See EOL for each version in [Security Policy Page](https://github.com/palcarazm/bootstrap5-toggle/security/policy). -# Demos +# Demo and documentation -**Demos and API Docs:** https://palcarazm.github.io/bootstrap5-toggle/ +**Please read our documentation page for a completed usage explanation:** https://palcarazm.github.io/bootstrap5-toggle/ --- @@ -50,14 +50,7 @@ See EOL for each version in [Security Policy Page](https://github.com/palcarazm/ - [Usage](#usage) - [Initialize With HTML](#initialize-with-html) - [Initialize With Code](#initialize-with-code) -- [API](#api) - - [Options](#options) - - [Methods](#methods) -- [Events](#events) - - [Event Propagation](#event-propagation) - - [Stopping Event Propagation](#stopping-event-propagation) - - [API vs Input](#api-vs-input) -- [Collaborators welcom!](#collaborators-welcom) +- [Collaborators welcome!](#collaborators-welcome) @@ -128,151 +121,7 @@ EX: Initialize id `chkToggle` with a single line of JavaScript. ``` -# API - -## Options - -- Options can be passed via data attributes or JavaScript -- For data attributes, append the option name to `data-` (ex: `data-on="Enabled"`) - -```html - - - -``` - -| Name | Type | Default | Description | -| ---------- | ----------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `onlabel` | string/html | "On" | Text of the on toggle | -| `offlabel` | string/html | "Off" | Text of the off toggle | -| `size` | string | "normal" | Size of the toggle. Possible values are: `large`, `normal`, `small`, `mini`. | -| `onstyle` | string | "primary" | Style of the on toggle. Possible values are: `primary`, `secondary`, `success`, `danger`, `warning`, `info`, `light`, `dark` and with `outline-` prefix | -| `offstyle` | string | "secondary" | Style of the off toggle. Possible values are: `primary`, `secondary`, `success`, `danger`, `warning`, `info`, `light`, `dark` and with `outline-` prefix | -| `onvalue` | string | _null_ | Sets on state value | -| `offvalue` | string | _null_ | Sets off state value | -| `ontitle` | string | _null_ | Title of the on toggle | -| `offtitle` | string | _null_ | Title of the off toggle | -| `style` | string | | Appends the value to the class attribute of the toggle. This can be used to apply custom styles. Refer to Custom Styles for reference. | -| `width` | integer | _null_ | Sets the width of the toggle. if set to _null_, width will be auto-calculated. | -| `height` | integer | _null_ | Sets the height of the toggle. if set to _null_, height will be auto-calculated. | -| `tabindex` | integer | 0 | Sets the tabindex of the toggle. | -| `tristate` | boolean | false | Sets tristate support | - -## Methods - -Methods can be used to control toggles directly. - -```html - - -``` - -| Method | Example | Description | -| ------------- | --------------------------------------------- | ------------------------------------------------------------------------------------- | -| initialize | `toggleDemo.bootstrapToggle()` | Initializes the toggle plugin with options | -| destroy | `toggleDemo.bootstrapToggle('destroy')` | Destroys the toggle | -| rerender | `toggleDemo.bootstrapToggle('rerender')` | Rerender toggle with the appropriated size. Useful when parent is collapsed at first. | -| on | `toggleDemo.bootstrapToggle('on')` | Sets the toggle to 'On' state | -| off | `toggleDemo.bootstrapToggle('off')` | Sets the toggle to 'Off' state | -| toggle | `toggleDemo.bootstrapToggle('toggle')` | Toggles the state of the toggle on/off | -| enable | `toggleDemo.bootstrapToggle('enable')` | Enables the toggle | -| disable | `toggleDemo.bootstrapToggle('disable')` | Disables the toggle | -| readonly | `toggleDemo.bootstrapToggle('readonly')` | Disables the toggle but preserve checkbox enabled | -| indeterminate | `toggleDemo.bootstrapToggle('indeterminate')` | Sets the toggle to 'indeterminate' state | -| determinate | `toggleDemo.bootstrapToggle('determinate')` | Sets the toggle to 'determinate' state | - -# Events - -## Event Propagation - -Note All events are propagated to and from input element to the toggle. - -You should listen to events from the `` directly rather than look for custom events. - -```html - -
- -``` - -## Stopping Event Propagation - -Passing `true` to the on, off, toggle, determinate and indeterminate methods will enable the silent option to prevent the control from propagating the change event in cases where you want to update the controls on/off state, but do not want to fire the onChange event. - -```html - - - - - - -``` - -## API vs Input - -This also means that using the API or Input to trigger events will work both ways. - -```html - - - - - - -``` - -# Collaborators welcom! +# Collaborators welcome! - :sos: Do you need some help? Open a thread in [GitHub Discussions Q&A](https://github.com/palcarazm/bootstrap5-toggle/discussions/new?category=q-a) - :bug: Do you find a bug? Open an issue in [GitHub bug report](https://github.com/palcarazm/bootstrap5-toggle/issues/new?template=01-BUG_REPORT.yml) diff --git a/README.template.md b/README.template.md index 6d4de384..e4b95ade 100644 --- a/README.template.md +++ b/README.template.md @@ -29,9 +29,9 @@ See EOL for each version in [Security Policy Page](https://github.com/palcarazm/bootstrap5-toggle/security/policy). -# Demos +# Demo and documentation -**Demos and API Docs:** https://palcarazm.github.io/bootstrap5-toggle/ +**Please read our documentation page for a completed usage explanation:** https://palcarazm.github.io/bootstrap5-toggle/ --- @@ -39,27 +39,6 @@ See EOL for each version in [Security Policy Page](https://github.com/palcarazm/ -- [Bootstrap 5 Toggle](#bootstrap-5-toggle) - [Library Distributions](#library-distributions) -- [Demos](#demos) -- [Related Bootstrap Plugins](#related-bootstrap-plugins) -- [Installation](#installation) - - [CDN](#cdn) - - [ECMAS Interface](#ecmas-interface) - - [jQuery Interface](#jquery-interface) - - [Download](#download) - - [NPM](#npm) - - [Yarn](#yarn) -- [Usage](#usage) - - [Initialize With HTML](#initialize-with-html) - - [Initialize With Code](#initialize-with-code) -- [API](#api) - - [Options](#options) - - [Methods](#methods) -- [Events](#events) - - [Event Propagation](#event-propagation) - - [Stopping Event Propagation](#stopping-event-propagation) - - [API vs Input](#api-vs-input) -- [Collaborators welcom!](#collaborators-welcom) @@ -130,151 +109,7 @@ EX: Initialize id `chkToggle` with a single line of JavaScript. ``` -# API - -## Options - -- Options can be passed via data attributes or JavaScript -- For data attributes, append the option name to `data-` (ex: `data-on="Enabled"`) - -```html - - - -``` - -| Name | Type | Default | Description | -| ---------- | ----------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `onlabel` | string/html | "On" | Text of the on toggle | -| `offlabel` | string/html | "Off" | Text of the off toggle | -| `size` | string | "normal" | Size of the toggle. Possible values are: `large`, `normal`, `small`, `mini`. | -| `onstyle` | string | "primary" | Style of the on toggle. Possible values are: `primary`, `secondary`, `success`, `danger`, `warning`, `info`, `light`, `dark` and with `outline-` prefix | -| `offstyle` | string | "secondary" | Style of the off toggle. Possible values are: `primary`, `secondary`, `success`, `danger`, `warning`, `info`, `light`, `dark` and with `outline-` prefix | -| `onvalue` | string | _null_ | Sets on state value | -| `offvalue` | string | _null_ | Sets off state value | -| `ontitle` | string | _null_ | Title of the on toggle | -| `offtitle` | string | _null_ | Title of the off toggle | -| `style` | string | | Appends the value to the class attribute of the toggle. This can be used to apply custom styles. Refer to Custom Styles for reference. | -| `width` | integer | _null_ | Sets the width of the toggle. if set to _null_, width will be auto-calculated. | -| `height` | integer | _null_ | Sets the height of the toggle. if set to _null_, height will be auto-calculated. | -| `tabindex` | integer | 0 | Sets the tabindex of the toggle. | -| `tristate` | boolean | false | Sets tristate support | - -## Methods - -Methods can be used to control toggles directly. - -```html - - -``` - -| Method | Example | Description | -| ------------- | --------------------------------------------- | ------------------------------------------------------------------------------------- | -| initialize | `toggleDemo.bootstrapToggle()` | Initializes the toggle plugin with options | -| destroy | `toggleDemo.bootstrapToggle('destroy')` | Destroys the toggle | -| rerender | `toggleDemo.bootstrapToggle('rerender')` | Rerender toggle with the appropriated size. Useful when parent is collapsed at first. | -| on | `toggleDemo.bootstrapToggle('on')` | Sets the toggle to 'On' state | -| off | `toggleDemo.bootstrapToggle('off')` | Sets the toggle to 'Off' state | -| toggle | `toggleDemo.bootstrapToggle('toggle')` | Toggles the state of the toggle on/off | -| enable | `toggleDemo.bootstrapToggle('enable')` | Enables the toggle | -| disable | `toggleDemo.bootstrapToggle('disable')` | Disables the toggle | -| readonly | `toggleDemo.bootstrapToggle('readonly')` | Disables the toggle but preserve checkbox enabled | -| indeterminate | `toggleDemo.bootstrapToggle('indeterminate')` | Sets the toggle to 'indeterminate' state | -| determinate | `toggleDemo.bootstrapToggle('determinate')` | Sets the toggle to 'determinate' state | - -# Events - -## Event Propagation - -Note All events are propagated to and from input element to the toggle. - -You should listen to events from the `` directly rather than look for custom events. - -```html - -
- -``` - -## Stopping Event Propagation - -Passing `true` to the on, off, toggle, determinate and indeterminate methods will enable the silent option to prevent the control from propagating the change event in cases where you want to update the controls on/off state, but do not want to fire the onChange event. - -```html - - - - - - -``` - -## API vs Input - -This also means that using the API or Input to trigger events will work both ways. - -```html - - - - - - -``` - -# Collaborators welcom! +# Collaborators welcome! - :sos: Do you need some help? Open a thread in [GitHub Discussions Q&A](https://github.com/palcarazm/bootstrap5-toggle/discussions/new?category=q-a) - :bug: Do you find a bug? Open an issue in [GitHub bug report](https://github.com/palcarazm/bootstrap5-toggle/issues/new?template=01-BUG_REPORT.yml) diff --git a/cypress.config.js b/cypress.config.js index 4036afe7..460e0b48 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -1,11 +1,12 @@ const { defineConfig } = require("cypress"); module.exports = defineConfig({ - e2e: { - setupNodeEvents(on, config) { - // implement node event listeners here + allowCypressEnv: false, + e2e: { + setupNodeEvents(on, config) { + // implement node event listeners here + }, }, - }, - video: false, - videoCompression: false + video: false, + videoCompression: false, }); diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..956b179a --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +.dccache +node_modules +package-lock.json +*.log +dist \ No newline at end of file diff --git a/docs/Gruntfile.js b/docs/Gruntfile.js new file mode 100644 index 00000000..9491feab --- /dev/null +++ b/docs/Gruntfile.js @@ -0,0 +1,104 @@ +module.exports = function (grunt) { + const pkg = grunt.file.readJSON("package.json"); + + grunt.initConfig({ + pkg, + clean: { + dist: ["dist"], + }, + copy: { + assets: { + expand: true, + cwd: "src/assets/", + src: "**/*", + dest: "dist/assets/", + }, + api: { + expand: true, + cwd: "src/api/", + src: "**/*", + dest: "dist/api/", + }, + cname: { + src: "CNAME", + dest: "dist/CNAME", + }, + html: { + src: "src/index.html", + dest: "dist/index.html", + options: { + process: function (content, _srcpath) { + return content + .replace(/\{\{bootstrapVersion\}\}/g, pkg.dependencies.bootstrap) + .replace(/\{\{jqueryVersion\}\}/g, pkg.dependencies.jquery) + .replace( + /\{\{fontawesomeVersion\}\}/g, + pkg.dependencies["@fortawesome/fontawesome-free"], + ) + .replace( + /\{\{highlightjsVersion\}\}/g, + pkg.dependencies["@highlightjs/cdn-assets"], + ) + .replace( + /\{\{highlightjsBadgeVersion\}\}/g, + pkg.dependencies["highlightjs-badge"], + ) + .replace( + /\{\{bootstrapTocVersion\}\}/g, + pkg.config.externalDependencies["afeld/bootstrap-toc"], + ) + .replace( + /\{\{bsDarkmodeToggleVersion\}\}/g, + pkg.dependencies["bs-darkmode-toggle"], + ) + .replace( + /\{\{bootstrap5ToggleVersion\}\}/g, + pkg.dependencies["bootstrap5-toggle"], + ); + }, + }, + }, + }, + exec: { + rollup: "npx rollup -c", + postcss: + "npx postcss src/css/styles.css -o dist/css/styles.min.css --map --env production", + }, + watch: { + assets: { + files: ["src/assets/**/*"], + tasks: ["copy:assets"], + options: { spawn: false }, + }, + api: { + files: ["src/api/**/*"], + tasks: ["copy:api"], + options: { spawn: false }, + }, + css: { + files: ["src/css/styles.css"], + tasks: ["exec:postcss"], + options: { spawn: false }, + }, + js: { + files: ["src/js/**/*.js"], + tasks: ["exec:rollup"], + options: { spawn: false }, + }, + html: { + files: ["src/index.html"], + tasks: ["copy:html"], + options: { spawn: false }, + }, + }, + }); + + grunt.loadNpmTasks("grunt-contrib-clean"); + grunt.loadNpmTasks("grunt-contrib-copy"); + grunt.loadNpmTasks("grunt-exec"); + grunt.loadNpmTasks("grunt-contrib-watch"); + + grunt.registerTask("build", ["clean", "copy", "exec:rollup", "exec:postcss"]); + + grunt.registerTask("dev", ["build", "watch"]); +}; diff --git a/docs/LICENSE b/docs/LICENSE new file mode 100644 index 00000000..8687b234 --- /dev/null +++ b/docs/LICENSE @@ -0,0 +1,23 @@ +The MIT License (MIT) + +- Copyright (c) 2011-2014 Min Hur, The New York Times Company +- Copyright (c) 2018-2019 Brent Ely +- Copyright (c) 2022 Pablo Alcaraz Martínez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..d9abee74 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,51 @@ +{ + "name": "bootstrap5-toggle-doc", + "version": "1.0.0", + "description": "Documentation of bootstrap5-toggle", + "main": "src/js/main.js", + "repository": { + "type": "git", + "url": "git+https://github.com/palcarazm/bootstrap5-toggle.git" + }, + "keywords": [ + "documentation" + ], + "author": { + "name": "Pablo Alcaraz Martínez", + "url": "https://github.com/palcarazm/" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/palcarazm/bootstrap5-toggle/issues" + }, + "homepage": "https://palcarazm.github.io/bootstrap5-toggle/", + "scripts": { + "build": "grunt build", + "dev": "grunt dev" + }, + "config": { + "externalDependencies": { + "afeld/bootstrap-toc": "1.0.1" + } + }, + "dependencies": { + "@fortawesome/fontawesome-free": "7.2.0", + "@highlightjs/cdn-assets": "11.11.1", + "bootstrap": "5.3.8", + "bootstrap5-toggle": "5.3.3", + "bs-darkmode-toggle": "1.0.0", + "highlightjs-badge": "0.1.9", + "jquery": "4.0.0" + }, + "devDependencies": { + "@rollup/plugin-terser": "^1.0.0", + "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", + "rollup": "^4.59.0" + } +} diff --git a/docs/rollup.config.js b/docs/rollup.config.js new file mode 100644 index 00000000..dc451572 --- /dev/null +++ b/docs/rollup.config.js @@ -0,0 +1,15 @@ +const terser = require("@rollup/plugin-terser"); + +module.exports = [ + { + input: "src/js/main.js", + output: [ + { + file: "dist/js/bundle.min.js", + format: "umd", + sourcemap: true, + plugins: [terser()] + } + ] + } +]; \ No newline at end of file diff --git a/docs/src/CNAME b/docs/src/CNAME new file mode 100644 index 00000000..e52f285c --- /dev/null +++ b/docs/src/CNAME @@ -0,0 +1 @@ +https://palcarazm.github.io/bootstrap5-toggle/ diff --git a/docs/src/api/eol/v3 b/docs/src/api/eol/v3 new file mode 100644 index 00000000..efe16d8d --- /dev/null +++ b/docs/src/api/eol/v3 @@ -0,0 +1,6 @@ +{ + "schemaVersion": 1, + "label": "EOL", + "message": "ended", + "color": "critical" +} diff --git a/docs/src/api/eol/v4 b/docs/src/api/eol/v4 new file mode 100644 index 00000000..a92a1084 --- /dev/null +++ b/docs/src/api/eol/v4 @@ -0,0 +1,6 @@ +{ + "schemaVersion": 1, + "label": "EOL", + "message": "critical support since 30 Jun 2026", + "color": "important" +} diff --git a/docs/src/api/eol/v5 b/docs/src/api/eol/v5 new file mode 100644 index 00000000..51ee0a96 --- /dev/null +++ b/docs/src/api/eol/v5 @@ -0,0 +1,6 @@ +{ + "schemaVersion": 1, + "label": "EOL", + "message": "active support", + "color": "success" +} diff --git a/docs/src/assets/img/card.png b/docs/src/assets/img/card.png new file mode 100644 index 00000000..da0084cc Binary files /dev/null and b/docs/src/assets/img/card.png differ diff --git a/docs/src/assets/img/favicon.ico b/docs/src/assets/img/favicon.ico new file mode 100644 index 00000000..c6fb1f87 Binary files /dev/null and b/docs/src/assets/img/favicon.ico differ diff --git a/docs/src/assets/img/fork-me.webp b/docs/src/assets/img/fork-me.webp new file mode 100644 index 00000000..21c460de Binary files /dev/null and b/docs/src/assets/img/fork-me.webp differ diff --git a/docs/src/assets/img/logo_off.png b/docs/src/assets/img/logo_off.png new file mode 100644 index 00000000..db7bf487 Binary files /dev/null and b/docs/src/assets/img/logo_off.png differ diff --git a/docs/src/assets/img/logo_on.png b/docs/src/assets/img/logo_on.png new file mode 100644 index 00000000..b5fcc7c3 Binary files /dev/null and b/docs/src/assets/img/logo_on.png differ diff --git a/docs/src/assets/img/logo_settings.json b/docs/src/assets/img/logo_settings.json new file mode 100644 index 00000000..7ee7aa14 --- /dev/null +++ b/docs/src/assets/img/logo_settings.json @@ -0,0 +1,37 @@ +{ + "version": "v1.0.0", + "components": { + "icon": { + "name": "icon", + "text": "", + "color": "#7337cd", + "fontFamily": "\"Font Awesome 5 Free\"", + "fontWeight": "900", + "fontSize": 30, + "letterSpacing": 0 + }, + "main": { + "name": "main", + "text": "Bootstrap 5", + "color": "#7337cd", + "fontFamily": "Quicksand", + "fontWeight": "normal", + "fontSize": 20, + "letterSpacing": 0 + }, + "accent": { + "name": "accent", + "text": "Toggle", + "color": "#ffffff", + "fontFamily": "Quicksand", + "fontWeight": "bold", + "fontSize": 20, + "letterSpacing": 0 + } + }, + "global": { + "offset": { "size": 0, "color": "#e2e2e2" }, + "layout": "HORIZONTAL", + "shapes": true + } +} diff --git a/docs/src/css/styles.css b/docs/src/css/styles.css new file mode 100644 index 00000000..f47a6d2f --- /dev/null +++ b/docs/src/css/styles.css @@ -0,0 +1,75 @@ +a { + text-decoration: none; +} + +code.hljs { + border: 1px solid #ccc; + padding: 1em; + white-space: pre; + margin-bottom: 10px; +} + +code.hljs .hljs-tag { + display: inline-flex; +} + +code.hljs .hljs-tag .hljs-attr { + margin-left: 0.3rem; +} + +.example { + position: relative; + border: 1px solid #ccc; + padding: 1em 1em 0.5em 1em; + border-radius: 4px 4px 0 0; +} + +.example:after { + content: "Example"; + position: absolute; + top: 0px; + right: 0px; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + background-color: #f5f5f5; + border: 1px solid #ccc; + color: #9da0a4; + border-radius: 0px 4px 0px 4px; + border-width: 0px 0px 1px 1px; +} + +.example + code.hljs { + border-top: 0; + border-radius: 0px 0px 4px 4px; +} + +.example > * { + margin-bottom: 10px; +} + +.example > div.toggle { + margin-right: 10px; +} +nav#toc .navbar-nav { + flex-direction: column; +} + +nav[data-toggle="toc"] .nav > li > a { + color: var(--gray); +} +nav[data-toggle="toc"] .nav-link.active, +nav[data-toggle="toc"] .nav-link.active:focus, +nav[data-toggle="toc"] .nav-link.active:hover { + color: var(--purple); + border-color: var(--purple); +} +.bs-darkmode-dark nav[data-toggle="toc"] .nav-link.active, +.bs-darkmode-dark nav[data-toggle="toc"] .nav-link.active:focus, +.bs-darkmode-dark nav[data-toggle="toc"] .nav-link.active:hover { + filter: brightness(1.4); +} + +.bs-darkmode-dark .img-toggle img { + filter: brightness(1.7); +} diff --git a/docs/src/index.html b/docs/src/index.html new file mode 100644 index 00000000..51add852 --- /dev/null +++ b/docs/src/index.html @@ -0,0 +1,113 @@ + + + + + + + + + + + + + Bootstrap 5 Toggle | v5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/src/js/components/Aside.js b/docs/src/js/components/Aside.js new file mode 100644 index 00000000..373e3957 --- /dev/null +++ b/docs/src/js/components/Aside.js @@ -0,0 +1,122 @@ +import Badge from "./Badge"; + +class Aside { + 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: + "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: + "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 = + "d-none d-md-block position-relative pb-5 mb-3 img-toggle"; + + 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); + + 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); + + return container; + } + + 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; + } +} +export default Aside; diff --git a/docs/src/js/components/Badge.js b/docs/src/js/components/Badge.js new file mode 100644 index 00000000..a925bf43 --- /dev/null +++ b/docs/src/js/components/Badge.js @@ -0,0 +1,23 @@ +class Badge { + 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 badgeImg = document.createElement("img"); + badgeImg.src = badge.imgSrc; + badgeImg.alt = badge.name; + + 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 new file mode 100644 index 00000000..f097664e --- /dev/null +++ b/docs/src/js/components/CodeBlock.js @@ -0,0 +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; + + 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 new file mode 100644 index 00000000..cafeadff --- /dev/null +++ b/docs/src/js/components/CodePanel.js @@ -0,0 +1,73 @@ +import CodeBlock from "./CodeBlock"; +import Example from "./Example"; + +class CodePanel { + 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 === 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; + } + + 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"; + + 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); + }); + + return navTabs; + } + + 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`); + + 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 new file mode 100644 index 00000000..796b377a --- /dev/null +++ b/docs/src/js/components/Console.js @@ -0,0 +1,80 @@ +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"); + } + } + + clear() { + this.#pre.innerHTML = ""; + } + + json({ mode, data }) { + this.#handleMode(mode); + + const code = document.createElement("code"); + code.className = "highlight language-json"; + code.textContent = JSON.stringify(data, null, 2); + + 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 new file mode 100644 index 00000000..2aa0dde3 --- /dev/null +++ b/docs/src/js/components/Documentation.js @@ -0,0 +1,30 @@ +import Distribution from "./documentation/Distribution"; +import Installation from "./documentation/Installation"; +import Usage from "./documentation/Usage"; +import Features from "./documentation/Features"; +import Api from "./documentation/Api"; +import Events from "./documentation/Events"; +import Accessibility from "./documentation/Accessibility"; + +class Documentation { + static #sections = [ + Distribution.build(), + Installation.build(), + Usage.build(), + Features.build(), + Api.build(), + Events.build(), + Accessibility.build(), + ]; + + static build() { + const documentation = document.createElement("div"); + documentation.className = "col-md-9 col-lg-10"; + + documentation.append(...Documentation.#sections); + + return documentation; + } +} + +export default Documentation; diff --git a/docs/src/js/components/Example.js b/docs/src/js/components/Example.js new file mode 100644 index 00000000..0ef9eab5 --- /dev/null +++ b/docs/src/js/components/Example.js @@ -0,0 +1,10 @@ +class 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 new file mode 100644 index 00000000..e7ba84e7 --- /dev/null +++ b/docs/src/js/components/Footer.js @@ -0,0 +1,31 @@ +class Footer { + 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"; + + const container = document.createElement("div"); + container.className = "container"; + + 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); + }); + + 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 new file mode 100644 index 00000000..bb0f31b1 --- /dev/null +++ b/docs/src/js/components/Header.js @@ -0,0 +1,67 @@ +class 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"; + + 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; + } + + 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 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); + + 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); + + container.appendChild(logoWrapper); + + const heading = document.createElement("div"); + heading.className = "col-lg-6 mx-auto"; + + 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); + + return container; + } +} + +export default Header; diff --git a/docs/src/js/components/Main.js b/docs/src/js/components/Main.js new file mode 100644 index 00000000..4d54c81e --- /dev/null +++ b/docs/src/js/components/Main.js @@ -0,0 +1,21 @@ +import Aside from "./Aside"; +import Documentation from "./Documentation"; + +class Main { + static #sections = [Aside.build(), Documentation.build()]; + + static build() { + const main = document.createElement("main"); + main.className = "container mb-3"; + + const row = document.createElement("div"); + row.className = "row g5"; + + row.append(...Main.#sections); + main.appendChild(row); + + return main; + } +} + +export default Main; diff --git a/docs/src/js/components/Notice.js b/docs/src/js/components/Notice.js new file mode 100644 index 00000000..4fa980a2 --- /dev/null +++ b/docs/src/js/components/Notice.js @@ -0,0 +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 = + "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); + + 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); + + return iconCol; + } +} + +export default Notice; diff --git a/docs/src/js/components/VersionPill.js b/docs/src/js/components/VersionPill.js new file mode 100644 index 00000000..0a90de68 --- /dev/null +++ b/docs/src/js/components/VersionPill.js @@ -0,0 +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"); + } + 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 new file mode 100644 index 00000000..83d43183 --- /dev/null +++ b/docs/src/js/components/documentation/Accessibility.js @@ -0,0 +1,266 @@ +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 #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.`, + }, + ]; + + 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 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 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); + + col.appendChild(card); + container.appendChild(col); + }); + + 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: + "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: + "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: + "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: + "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: + "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: + "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: + "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: + "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: + "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: + "There is not aria-live, aria-atomic, or aria-relevant for dynamic state announcements", + 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(); + + 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 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 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 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); + } + + list.appendChild(listItem); + }); + + 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 new file mode 100644 index 00000000..f39757f5 --- /dev/null +++ b/docs/src/js/components/documentation/Api.js @@ -0,0 +1,20 @@ +import InputInteraction from "./api/InputInteraction"; +import Methods from "./api/Methods"; +import Options from "./api/Options"; +import Rerender from "./api/Rerender"; +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(), + ]); + } +} + +export default Api; diff --git a/docs/src/js/components/documentation/Distribution.js b/docs/src/js/components/documentation/Distribution.js new file mode 100644 index 00000000..70be81db --- /dev/null +++ b/docs/src/js/components/documentation/Distribution.js @@ -0,0 +1,105 @@ +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 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"; + + const table = document.createElement("table"); + table.className = "table table-striped table-condensed mb-0"; + + const caption = document.createElement("caption"); + caption.textContent = "Library Distributions"; + + table.append( + caption, + Distribution.#tableHeader(), + Distribution.#tableBody() + ); + container.appendChild(table); + return container; + } + + 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); + }); + + 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); + + 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); + + 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 new file mode 100644 index 00000000..c91d2ecf --- /dev/null +++ b/docs/src/js/components/documentation/DocAlert.js @@ -0,0 +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}`; + + const alertBody = document.createElement("div"); + alertBody.innerHTML = content; + + alert.append(DocAlert.#alertHeader(type, title, versionPill), alertBody); + return alert; + } + + 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; + } + + 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 new file mode 100644 index 00000000..be1a4053 --- /dev/null +++ b/docs/src/js/components/documentation/DocArticle.js @@ -0,0 +1,57 @@ +import Badge from "../Badge"; +import CodeBlock from "../CodeBlock"; +import CodePanel from "../CodePanel"; +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; + } +} + +export default DocArticle; diff --git a/docs/src/js/components/documentation/DocSection.js b/docs/src/js/components/documentation/DocSection.js new file mode 100644 index 00000000..3bd08ee8 --- /dev/null +++ b/docs/src/js/components/documentation/DocSection.js @@ -0,0 +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; + } +} + +export default DocSection; diff --git a/docs/src/js/components/documentation/Events.js b/docs/src/js/components/documentation/Events.js new file mode 100644 index 00000000..36e16467 --- /dev/null +++ b/docs/src/js/components/documentation/Events.js @@ -0,0 +1,16 @@ +import DocSection from "./DocSection"; +import CustomEvents from "./events/CustomEvents"; +import EventPropagation from "./events/EventPropagation"; +import SilencedActions from "./events/SilencedActions"; + +class Events extends DocSection { + static build() { + return super.build("events", "Events", [ + EventPropagation.build(), + CustomEvents.build(), + SilencedActions.build(), + ]); + } +} + +export default Events; diff --git a/docs/src/js/components/documentation/Features.js b/docs/src/js/components/documentation/Features.js new file mode 100644 index 00000000..a9be4660 --- /dev/null +++ b/docs/src/js/components/documentation/Features.js @@ -0,0 +1,38 @@ +import DocSection from "./DocSection"; +import AnimationSpeed from "./features/AnimationSpeed"; +import Colors from "./features/Colors"; +import CustomFormValue from "./features/CustomFormValue"; +import CustomSize from "./features/CustomSize"; +import CustomStyle from "./features/CustomStyle"; +import CustomText from "./features/CustomText"; +import CustomTitle from "./features/CustomTitle"; +import FormLayout from "./features/FormLayout"; +import KeyboardInteraction from "./features/KeyboardInteraction"; +import OutlineColors from "./features/OutlineColors"; +import Size from "./features/Size"; +import StateStatus from "./features/StateStatus"; +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(), + ]); + } +} + +export default Features; diff --git a/docs/src/js/components/documentation/Installation.js b/docs/src/js/components/documentation/Installation.js new file mode 100644 index 00000000..70e07e6b --- /dev/null +++ b/docs/src/js/components/documentation/Installation.js @@ -0,0 +1,73 @@ +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 #cdn() { + const ecmas = ` +`; + const jquery = ` +`; + 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], + }, + }); + } + + 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 #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 new file mode 100644 index 00000000..a507740d --- /dev/null +++ b/docs/src/js/components/documentation/Usage.js @@ -0,0 +1,42 @@ +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], + }, + }); + } +} + +export default Usage; diff --git a/docs/src/js/components/documentation/api/InputInteraction.js b/docs/src/js/components/documentation/api/InputInteraction.js new file mode 100644 index 00000000..f33d0727 --- /dev/null +++ b/docs/src/js/components/documentation/api/InputInteraction.js @@ -0,0 +1,167 @@ +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: + "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: + "Disables the toggle but the checkbox stay readable in form data.", + 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: ` +`, + }, + example: [input], + }); + } + + static #description(input) { + const description = document.createElement("div"); + + 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 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 new file mode 100644 index 00000000..cf7f0089 --- /dev/null +++ b/docs/src/js/components/documentation/api/Methods.js @@ -0,0 +1,151 @@ +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: + "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: + "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: ` +`, + }, + example: [input], + }); + } + + static #description(input) { + const description = document.createElement("div"); + + 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; + } +} + +export default Methods; diff --git a/docs/src/js/components/documentation/api/Options.js b/docs/src/js/components/documentation/api/Options.js new file mode 100644 index 00000000..1e56a466 --- /dev/null +++ b/docs/src/js/components/documentation/api/Options.js @@ -0,0 +1,286 @@ +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 #codePanel() { + const ecmas = `
+ + +
+`; + const 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"; + + 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"; + + 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" }, + }, + }); + }; + + 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.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
API constructor options
NameTypeDefaultDescription
onlabelstring | html"On"Text of the on toggle label.
offlabelstring | html"Off"Text of the off toggle label.
onstylestring"primary" + Style of the on toggle.
Possible values are: + primary, secondary, success, danger, + warning, info, light, dark
+
offstylestring"secondary" + Style of the off toggle.
Possible values are: + primary, secondary, success, danger, + warning, info, light, dark
+
onvaluestringnullSets on state value.
offvaluestringnullSets off state value.
sizestringnull + Size of the toggle. If set to null, button is default/normal size.
+ Possible values are: + lg, sm, xs
+
stylestringnull + Appends the provided value to the toggle's class attribute. + Use this to apply custom styles to the toggle. +
widthintegernull + Sets the width of the toggle.
+ If set to null, width will be calculated. +
heightintegernull + Sets the height of the toggle.
+ If set to null, height will be calculated. +
tabindexinteger0Sets the tabindex.
tristatebooleanfalseSets tristate support.
tooltipobjectundefined + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Tooltip options
NameTypeDefaultDescription
placementstring"top"Placement of the tooltip. Possible values are: top, bottom, left, and right.
title.onstring | htmlundefinedTitle of the tooltip when the toggle is on.
title.offstring | htmlundefinedTitle of the tooltip when the toggle is off.
title.mixedstring | htmlundefinedTitle of the tooltip when the toggle is in mixed state (if tristate is enabled).
+
+
`; + 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 new file mode 100644 index 00000000..6f6960bb --- /dev/null +++ b/docs/src/js/components/documentation/api/Rerender.js @@ -0,0 +1,61 @@ +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 = `
+ + +
+`; + + 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 new file mode 100644 index 00000000..31ad0c5c --- /dev/null +++ b/docs/src/js/components/documentation/api/State.js @@ -0,0 +1,47 @@ +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).`; + + 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 = ` + `; + + const console = new Console(); + + form.onsubmit = (e) => { + e.preventDefault(); + console.log({ + mode: "append", + data: `Checked: ${e.target.elements["api-state-toggle"].checked}`, + }); + }; + + return [form, console.htmlElement]; + } +} + +export default State; diff --git a/docs/src/js/components/documentation/events/CustomEvents.js b/docs/src/js/components/documentation/events/CustomEvents.js new file mode 100644 index 00000000..373361b0 --- /dev/null +++ b/docs/src/js/components/documentation/events/CustomEvents.js @@ -0,0 +1,136 @@ +import { version } from "grunt"; +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)}`, + }); + }); + }); + + 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; + }); + + 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 new file mode 100644 index 00000000..8f1ead11 --- /dev/null +++ b/docs/src/js/components/documentation/events/EventPropagation.js @@ -0,0 +1,49 @@ +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 + input.`; + + 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]; + } +} + +export default EventPropagation; diff --git a/docs/src/js/components/documentation/events/SilencedActions.js b/docs/src/js/components/documentation/events/SilencedActions.js new file mode 100644 index 00000000..c1f382f1 --- /dev/null +++ b/docs/src/js/components/documentation/events/SilencedActions.js @@ -0,0 +1,125 @@ +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: ` +`, + }, + example: [input, console.htmlElement], + }); + } + + static #description(input) { + const description = document.createElement("div"); + + 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; + } +} + +export default SilencedActions; diff --git a/docs/src/js/components/documentation/features/AnimationSpeed.js b/docs/src/js/components/documentation/features/AnimationSpeed.js new file mode 100644 index 00000000..fe2c1e70 --- /dev/null +++ b/docs/src/js/components/documentation/features/AnimationSpeed.js @@ -0,0 +1,58 @@ +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.`; + + const code = ` + + +`; + + return DocArticle.build({ + title: "Animation Speed", + description, + example: AnimationSpeed.#example(), + codeBlock: { + language: "html", + code, + }, + }); + } + + static #example() { + const style = document.createElement("style"); + style.textContent = ` + .slow .toggle-group { transition: left 0.7s; -webkit-transition: left 0.7s; } + .fast .toggle-group { transition: left 0.1s; -webkit-transition: left 0.1s; } + .none .toggle-group { transition: none; -webkit-transition: none; }`; + + const inputs = ["slow", "fast", "none"].map((style) => { + const input = document.createElement("input"); + input.type = "checkbox"; + input.checked = true; + input.dataset.toggle = "toggle"; + input.dataset.style = style; + return input; + }); + + return [style, ...inputs]; + } +} +export default AnimationSpeed; diff --git a/docs/src/js/components/documentation/features/Colors.js b/docs/src/js/components/documentation/features/Colors.js new file mode 100644 index 00000000..3fd06453 --- /dev/null +++ b/docs/src/js/components/documentation/features/Colors.js @@ -0,0 +1,49 @@ +import DocArticle from "../DocArticle"; + +class Colors { + static build() { + const description = document.createElement("p"); + description.innerHTML = `Bootstrap toggle implements all Bootstrap colors. Just add the data-onstyle or data-offstyle attribute with one of the following values: primary, secondary, success, danger, warning, info, light or dark.`; + + const code = ` + + + + + + +`; + + return DocArticle.build({ + title: "Colors", + description, + example: Colors.#example(), + codeBlock: { + language: "html", + code, + }, + }); + } + + static #example() { + const example = [ + "primary", + "secondary", + "success", + "danger", + "warning", + "info", + "light", + "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 Colors; diff --git a/docs/src/js/components/documentation/features/CustomFormValue.js b/docs/src/js/components/documentation/features/CustomFormValue.js new file mode 100644 index 00000000..1bb5adab --- /dev/null +++ b/docs/src/js/components/documentation/features/CustomFormValue.js @@ -0,0 +1,50 @@ +import Console from "../../Console"; +import DocArticle from "../DocArticle"; + +class CustomFormValue { + static build() { + const description = document.createElement("p"); + description.innerHTML = `Form submit values can be customized with the data-onvalue and data-offvalue attributes.If original checkbox have a value set, data-onvalue will be ignored preserving the original value.`; + + const code = `
+ + + + +
`; + + return DocArticle.build({ + title: "Custom form submit values", + description, + example: CustomFormValue.#example(), + codeBlock: { + language: "html", + code, + }, + versionPill: { version: "4.3.0", action: "SINCE" }, + }); + } + + static #example() { + const form = document.createElement("form"); + form.id = "form_state_status"; + form.className = "d-flex justify-content-between align-items-center"; + form.innerHTML = ` + + + `; + + const console = new Console(); + + form.onsubmit = (e) => { + e.preventDefault(); + console.json({ + mode: "replace", + data: Object.fromEntries(new FormData(e.target)), + }); + }; + + return [form, console.htmlElement]; + } +} +export default CustomFormValue; diff --git a/docs/src/js/components/documentation/features/CustomSize.js b/docs/src/js/components/documentation/features/CustomSize.js new file mode 100644 index 00000000..9688a52f --- /dev/null +++ b/docs/src/js/components/documentation/features/CustomSize.js @@ -0,0 +1,40 @@ +import DocArticle from "../DocArticle"; + +class CustomSize { + static build() { + const description = document.createElement("p"); + description.innerHTML = ` Bootstrap toggle can handle custom sizes by data-width and data-height options.`; + + const code = ` + +`; + + return DocArticle.build({ + title: "Custom Sizes", + description, + example: CustomSize.#example(), + codeBlock: { + language: "html", + code, + }, + }); + } + + static #example() { + const example = [ + { width: "100", height: "75" }, + { height: "5rem" }, + { width: "6em" }, + ].map(({ width, height }) => { + const input = document.createElement("input"); + input.type = "checkbox"; + input.checked = true; + input.dataset.toggle = "toggle"; + if (width) input.dataset.width = width; + if (height) input.dataset.height = height; + return input; + }); + return example; + } +} +export default CustomSize; diff --git a/docs/src/js/components/documentation/features/CustomStyle.js b/docs/src/js/components/documentation/features/CustomStyle.js new file mode 100644 index 00000000..f34d3f5d --- /dev/null +++ b/docs/src/js/components/documentation/features/CustomStyle.js @@ -0,0 +1,63 @@ +import DocArticle from "../DocArticle"; + +class CustomStyle { + static build() { + const description = document.createElement("p"); + description.innerHTML = `Bootstrap toggle allows to style the buttons to fit an existing UX. Just add the data-style attribute to set a custom css class.`; + + 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]; + } +} +export default CustomStyle; diff --git a/docs/src/js/components/documentation/features/CustomText.js b/docs/src/js/components/documentation/features/CustomText.js new file mode 100644 index 00000000..5ea4f21b --- /dev/null +++ b/docs/src/js/components/documentation/features/CustomText.js @@ -0,0 +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.`; + + 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.`, + }, + }); + } + + 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"; + + 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 new file mode 100644 index 00000000..9728616a --- /dev/null +++ b/docs/src/js/components/documentation/features/CustomTitle.js @@ -0,0 +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 + both parts just use HTML standard title attribute.`; + + const 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"; + + const input2 = document.createElement("input"); + input2.type = "checkbox"; + input2.checked = true; + input2.dataset.toggle = "toggle"; + input2.title = "TOGGLE"; + + 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 new file mode 100644 index 00000000..0633c843 --- /dev/null +++ b/docs/src/js/components/documentation/features/FormLayout.js @@ -0,0 +1,102 @@ +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.`; + + const 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 = `
+ + +
+
+ + +
`; + + const fromInline = document.createElement("form"); + fromInline.className = "border p-1 mb-1"; + fromInline.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 [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 new file mode 100644 index 00000000..28c48137 --- /dev/null +++ b/docs/src/js/components/documentation/features/KeyboardInteraction.js @@ -0,0 +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. +A custom tabindex can be set by adding the tabindex attribute, default value is 0.`; + + const 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; + }); + } +} +export default KeyboardInteraction; diff --git a/docs/src/js/components/documentation/features/OutlineColors.js b/docs/src/js/components/documentation/features/OutlineColors.js new file mode 100644 index 00000000..56b2ae39 --- /dev/null +++ b/docs/src/js/components/documentation/features/OutlineColors.js @@ -0,0 +1,49 @@ +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.`; + + const 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; + } +} +export default OutlineColors; diff --git a/docs/src/js/components/documentation/features/Size.js b/docs/src/js/components/documentation/features/Size.js new file mode 100644 index 00000000..e72b452c --- /dev/null +++ b/docs/src/js/components/documentation/features/Size.js @@ -0,0 +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.`; + + const 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; + } +} +export default Size; diff --git a/docs/src/js/components/documentation/features/StateStatus.js b/docs/src/js/components/documentation/features/StateStatus.js new file mode 100644 index 00000000..ba1cc0d9 --- /dev/null +++ b/docs/src/js/components/documentation/features/StateStatus.js @@ -0,0 +1,65 @@ +import Console from "../../Console"; +import DocArticle from "../DocArticle"; + +class StateStatus { + static build() { + const 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"); + + 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); + + return description; + } + + static #example() { + const form = document.createElement("form"); + form.className = "d-flex justify-content-between align-items-center"; + form.innerHTML = ` + + + `; + + const console = new Console(); + + form.onsubmit = (e) => { + e.preventDefault(); + console.json({ + mode: "replace", + data: Object.fromEntries(new FormData(e.target)), + }); + }; + + 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 new file mode 100644 index 00000000..3a14349d --- /dev/null +++ b/docs/src/js/components/documentation/features/Tooltip.js @@ -0,0 +1,61 @@ +import DocArticle from "../DocArticle"; + +class Tooltip { + 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 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 = ` + + 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 = + ' Checked'; + input3.dataset.tooltipTitleOff = + ' Unchecked'; + + 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 new file mode 100644 index 00000000..ccb5851a --- /dev/null +++ b/docs/src/js/components/documentation/features/Tristate.js @@ -0,0 +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.`; + + const 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", ""); + + return [input]; + } +} +export default Tristate; diff --git a/docs/src/js/main.js b/docs/src/js/main.js new file mode 100644 index 00000000..f5504b86 --- /dev/null +++ b/docs/src/js/main.js @@ -0,0 +1,16 @@ +import BootstrapToggler from "./tasks/BootstrapToggler"; +import BootsrapDarkmodeToggler from "./tasks/BootsrapDarkmodeToggler"; +import CodeHighlighter from "./tasks/CodeHighlighter"; +import DOMBuilder from "./tasks/DOMBuilder"; +import LogoSwitcher from "./tasks/LogoSwitcher"; +import TOCBuilder from "./tasks/TOCBuilder"; + +DOMBuilder.run(); +LogoSwitcher.run({ toggleDelay: 3000 }); + +window.onload = () => { + BootstrapToggler.run(); + BootsrapDarkmodeToggler.run(); + CodeHighlighter.run(); + TOCBuilder.run(); +}; diff --git a/docs/src/js/tasks/BootsrapDarkmodeToggler.js b/docs/src/js/tasks/BootsrapDarkmodeToggler.js new file mode 100644 index 00000000..8634b73d --- /dev/null +++ b/docs/src/js/tasks/BootsrapDarkmodeToggler.js @@ -0,0 +1,9 @@ +class BootsrapDarkmodeToggler { + static run() { + document.querySelectorAll('[data-plugin="bs-darkmode-toggle"]').forEach((element) => { + element.bsDarkmodeToggle(); + }); + } +} + +export default BootsrapDarkmodeToggler; diff --git a/docs/src/js/tasks/BootstrapToggler.js b/docs/src/js/tasks/BootstrapToggler.js new file mode 100644 index 00000000..fae8f102 --- /dev/null +++ b/docs/src/js/tasks/BootstrapToggler.js @@ -0,0 +1,9 @@ +class BootstrapToggler { + 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 new file mode 100644 index 00000000..659136f4 --- /dev/null +++ b/docs/src/js/tasks/CodeHighlighter.js @@ -0,0 +1,8 @@ +class CodeHighlighter { + 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 new file mode 100644 index 00000000..a1d9472e --- /dev/null +++ b/docs/src/js/tasks/DOMBuilder.js @@ -0,0 +1,17 @@ +import Footer from "../components/Footer"; +import Header from "../components/Header"; +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); + } +} +export default DOMBuilder; diff --git a/docs/src/js/tasks/LogoSwitcher.js b/docs/src/js/tasks/LogoSwitcher.js new file mode 100644 index 00000000..3896a1b4 --- /dev/null +++ b/docs/src/js/tasks/LogoSwitcher.js @@ -0,0 +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); + }); + } +} + +export default LogoSwitcher; diff --git a/docs/src/js/tasks/TOCBuilder.js b/docs/src/js/tasks/TOCBuilder.js new file mode 100644 index 00000000..bd17ad0a --- /dev/null +++ b/docs/src/js/tasks/TOCBuilder.js @@ -0,0 +1,13 @@ +class TOCBuilder { + 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 a1d09047..d37298ea 100644 --- a/package.json +++ b/package.json @@ -70,15 +70,15 @@ "devDependencies": { "@commitlint/cli": "^20.3.1", "@commitlint/config-conventional": "^20.3.1", - "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-terser": "^1.0.0", "@types/bootstrap": "^5.2.10", "@types/jest": "^30.0.0", - "@types/jquery": "^3.5.33", - "@typescript-eslint/eslint-plugin": "^8.20.0", - "@typescript-eslint/parser": "^8.20.0", + "@types/jquery": "^4.0.0", + "@typescript-eslint/eslint-plugin": "^8.58.0", + "@typescript-eslint/parser": "^8.58.0", "commitlint": "^20.3.1", "cypress": "^15.8.1", - "cypress-plugin-tab": "^1.0.5", + "cypress-plugin-tab": "^2.0.0", "doctoc": "^2.2.0", "dompurify": "^3.0.0", "eslint": "^10.0.0", @@ -96,7 +96,7 @@ "rollup": "^4.54.0", "rollup-plugin-dts": "^6.4.1", "ts-jest": "^29.4.6", - "typescript": "^5.9.3" + "typescript": "^6.0.2" }, "peerDependencies": { "bootstrap": ">=5 <6" diff --git a/sonar-project.properties b/sonar-project.properties index 514dc7bb..429d438d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,5 +3,5 @@ sonar.organization=palcarazm sonar.javascript.lcov.reportPaths=coverage/lcov.info sonar.typescript.tsconfigPath=tsconfig.json -sonar.coverage.exclusions=test/**,cypress/**,src/test/**,src/main/js/**,src/main/ts/**/index*.ts,src/main/ts/types/** +sonar.coverage.exclusions=docs/**,test/**,cypress/**,src/test/**,src/main/js/**,src/main/ts/**/index*.ts,src/main/ts/types/** sonar.cpd.exclusions=test/**,cypress/**,src/test/** \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 7350735b..b2430f20 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES5", + "target": "ES2016", "module": "ESNext", "strict": true, "declaration": true, @@ -10,7 +10,8 @@ "skipLibCheck": true, "rootDir": "src/main/ts", "outDir": "src/main/js", - "typeRoots": [ + "types": ["node"], + "typeRoots": [ "src/main/ts/types", "node_modules/@types" ] diff --git a/tsconfig.test.json b/tsconfig.test.json index d0a7d503..27a2156a 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -1,14 +1,16 @@ { "compilerOptions": { - "target": "ES5", - "module": "ESNext", + "target": "ES2016", + "module": "commonjs", // CommonJS used cause of jest need "strict": true, "declaration": false, "esModuleInterop": true, "skipLibCheck": true, "rootDir": "src/main/ts", "outDir": "src/main/js", - "typeRoots": [ + "noEmit": true, + "types": ["jest", "node"], + "typeRoots": [ "src/main/ts/types", "node_modules/@types" ]