mirror of
https://github.com/locomotivemtl/locomotive-boilerplate.git
synced 2026-01-15 00:55:08 +08:00
Compare commits
3 Commits
3a9038c832
...
feature/mo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
344f4bc98c | ||
|
|
d593fe5409 | ||
|
|
f8a46043a6 |
12
.gitignore
vendored
12
.gitignore
vendored
@@ -5,13 +5,9 @@ loconfig.*.json
|
||||
!loconfig.example.json
|
||||
.prettierrc
|
||||
|
||||
www/assets/scripts/app.js
|
||||
www/assets/scripts/app.js.map
|
||||
www/assets/scripts/vendors.js
|
||||
|
||||
www/assets/styles/main.css
|
||||
www/assets/styles/main.css.map
|
||||
www/assets/styles/critical.css
|
||||
www/assets/styles/critical.css.map
|
||||
www/assets/scripts/*
|
||||
!www/assets/scripts/.gitkeep
|
||||
www/assets/styles/*
|
||||
!www/assets/styles/.gitkeep
|
||||
|
||||
assets.json
|
||||
|
||||
@@ -23,8 +23,8 @@ Learn more about [languages and technologies](docs/technologies.md).
|
||||
|
||||
Make sure you have the following installed:
|
||||
|
||||
* [Node] — at least 17.9, the latest LTS is recommended.
|
||||
* [NPM] — at least 8.0, the latest LTS is recommended.
|
||||
* [Node] — at least 20, the latest LTS is recommended.
|
||||
* [NPM] — at least 10, the latest LTS is recommended.
|
||||
|
||||
> 💡 You can use [NVM] to install and use different versions of Node via the command-line.
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ const CSS_CLASS = Object.freeze({
|
||||
// Custom js events
|
||||
const CUSTOM_EVENT = Object.freeze({
|
||||
RESIZE_END: 'loco.resizeEnd',
|
||||
VISIT_START: 'visit.start',
|
||||
MODAL_OPEN: 'modal.open',
|
||||
// ...
|
||||
})
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export {default as Example} from './modules/Example';
|
||||
export {default as Load} from './modules/Load';
|
||||
export {default as Modal} from './modules/Modal';
|
||||
export {default as Scroll} from './modules/Scroll';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { module } from 'modujs';
|
||||
import modularLoad from 'modularload';
|
||||
import { CUSTOM_EVENT } from '../config';
|
||||
|
||||
export default class extends module {
|
||||
constructor(m) {
|
||||
@@ -14,6 +15,12 @@ export default class extends module {
|
||||
}
|
||||
});
|
||||
|
||||
load.on('loading', (transition, oldContainer) => {
|
||||
const args = { transition, oldContainer };
|
||||
// Dispatch custom event
|
||||
window.dispatchEvent(new CustomEvent(CUSTOM_EVENT.VISIT_START, { detail: args }))
|
||||
});
|
||||
|
||||
load.on('loaded', (transition, oldContainer, newContainer) => {
|
||||
this.call('destroy', oldContainer, 'app');
|
||||
this.call('update', newContainer, 'app');
|
||||
|
||||
195
assets/scripts/modules/Modal.js
Normal file
195
assets/scripts/modules/Modal.js
Normal file
@@ -0,0 +1,195 @@
|
||||
import { createFocusTrap } from 'focus-trap'
|
||||
import { module as Module } from 'modujs'
|
||||
import { $html } from '../utils/dom'
|
||||
import { CUSTOM_EVENT } from '../config'
|
||||
|
||||
/**
|
||||
* Generic component to display a modal.
|
||||
*
|
||||
*/
|
||||
export default class Modal extends Module {
|
||||
/**
|
||||
* Creates a new Modal.
|
||||
*
|
||||
* @param {object} options - The module options.
|
||||
* @param {string} options.dataName - The module data attribute name.
|
||||
* @throws {TypeError} If the class does not have an active CSS class defined.
|
||||
*/
|
||||
|
||||
static CLASS = {
|
||||
EL: 'is-open',
|
||||
HTML: 'has-modal-open',
|
||||
}
|
||||
|
||||
constructor(options) {
|
||||
super(options)
|
||||
|
||||
// Data
|
||||
this.moduleName = options.name
|
||||
this.dataName = this.getData('name') || options.dataName
|
||||
|
||||
// Bindings
|
||||
this.toggle = this.toggle.bind(this)
|
||||
this.onModalOpen = this.onModalOpen.bind(this)
|
||||
this.onVisitStart = this.onVisitStart.bind(this)
|
||||
|
||||
// UI
|
||||
this.$togglers = document.querySelectorAll(`[data-${this.dataName}-toggler]`)
|
||||
this.$focusTrapTargets = Array.from(this.el.querySelectorAll(`[data-${this.dataName}-target]`))
|
||||
|
||||
// Focus trap options
|
||||
this.focusTrapOptions = {
|
||||
/**
|
||||
* There is a delay between when the class is applied
|
||||
* and when the element is focusable
|
||||
*/
|
||||
checkCanFocusTrap: (trapContainers) => {
|
||||
const results = trapContainers.map((trapContainer) => {
|
||||
return new Promise((resolve) => {
|
||||
const interval = setInterval(() => {
|
||||
if (
|
||||
getComputedStyle(trapContainer).visibility !==
|
||||
'hidden'
|
||||
) {
|
||||
resolve()
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 5)
|
||||
})
|
||||
})
|
||||
|
||||
// Return a promise that resolves when all the trap containers are able to receive focus
|
||||
return Promise.all(results)
|
||||
},
|
||||
|
||||
onActivate: () => {
|
||||
this.el.classList.add(Modal.CLASS.EL)
|
||||
$html.classList.add(Modal.CLASS.HTML)
|
||||
$html.classList.add('has-'+this.dataName+'-open')
|
||||
this.el.setAttribute('aria-hidden', false)
|
||||
this.isOpen = true
|
||||
|
||||
this.onActivate?.();
|
||||
},
|
||||
|
||||
onPostActivate: () => {
|
||||
this.$togglers.forEach(($toggler) => {
|
||||
$toggler.setAttribute('aria-expanded', true)
|
||||
})
|
||||
},
|
||||
|
||||
onDeactivate: () => {
|
||||
this.el.classList.remove(Modal.CLASS.EL)
|
||||
$html.classList.remove(Modal.CLASS.HTML)
|
||||
$html.classList.remove('has-'+this.dataName+'-open')
|
||||
this.el.setAttribute('aria-hidden', true)
|
||||
this.isOpen = false
|
||||
|
||||
this.onDeactivate?.();
|
||||
},
|
||||
|
||||
onPostDeactivate: () => {
|
||||
this.$togglers.forEach(($toggler) => {
|
||||
$toggler.setAttribute('aria-expanded', false)
|
||||
})
|
||||
},
|
||||
|
||||
clickOutsideDeactivates: true,
|
||||
}
|
||||
|
||||
this.isOpen = false
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Lifecycle
|
||||
/////////////////
|
||||
init() {
|
||||
this.onBeforeInit?.()
|
||||
|
||||
this.focusTrap = createFocusTrap(
|
||||
this.$focusTrapTargets.length > 0 ? this.$focusTrapTargets : [this.el],
|
||||
this.focusTrapOptions
|
||||
)
|
||||
|
||||
this.bindEvents()
|
||||
|
||||
this.onInit?.()
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.focusTrap?.deactivate?.({
|
||||
returnFocus: false,
|
||||
})
|
||||
|
||||
this.unbindEvents()
|
||||
|
||||
this.onDestroy?.()
|
||||
|
||||
super.destroy()
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Events
|
||||
/////////////////
|
||||
bindEvents() {
|
||||
window.addEventListener(CUSTOM_EVENT.VISIT_START, this.onVisitStart)
|
||||
window.addEventListener(CUSTOM_EVENT.MODAL_OPEN, this.onModalOpen)
|
||||
|
||||
this.$togglers.forEach(($toggler) => {
|
||||
$toggler.addEventListener('click', this.toggle)
|
||||
})
|
||||
}
|
||||
|
||||
unbindEvents() {
|
||||
window.removeEventListener(CUSTOM_EVENT.VISIT_START, this.onVisitStart)
|
||||
window.removeEventListener(CUSTOM_EVENT.MODAL_OPEN, this.onModalOpen)
|
||||
|
||||
this.$togglers.forEach(($toggler) => {
|
||||
$toggler.removeEventListener('click', this.toggle)
|
||||
})
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Callbacks
|
||||
/////////////////
|
||||
onVisitStart() {
|
||||
// Close the modal on page change
|
||||
this.close()
|
||||
}
|
||||
|
||||
onModalOpen(event) {
|
||||
// Close the modal if another one is opened
|
||||
if (event.detail !== this.el) {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Methods
|
||||
/////////////////
|
||||
toggle(event) {
|
||||
if (this.el.classList.contains(Modal.CLASS.EL)) {
|
||||
this.close(event)
|
||||
} else {
|
||||
this.open(event)
|
||||
}
|
||||
}
|
||||
|
||||
open(args) {
|
||||
if (this.isOpen) return
|
||||
|
||||
this.focusTrap?.activate?.()
|
||||
|
||||
this.onOpen?.(args)
|
||||
|
||||
window.dispatchEvent(new CustomEvent(CUSTOM_EVENT.MODAL_OPEN, { detail: this.el }))
|
||||
}
|
||||
|
||||
close(args) {
|
||||
if (!this.isOpen) return
|
||||
|
||||
this.focusTrap?.deactivate?.()
|
||||
|
||||
this.onClose?.(args)
|
||||
}
|
||||
}
|
||||
52
assets/styles/components/_modal.scss
Normal file
52
assets/styles/components/_modal.scss
Normal file
@@ -0,0 +1,52 @@
|
||||
// ==========================================================================
|
||||
// Components / Modal
|
||||
// ==========================================================================
|
||||
|
||||
.c-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
max-width: inherit;
|
||||
max-height: 100lvh;
|
||||
margin: 0;
|
||||
padding: 0 var(--grid-margin);
|
||||
background: transparent;
|
||||
border: none;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
|
||||
// Backdrop
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: color(darkest, 0.5);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html.is-first-loaded & {
|
||||
transition: visibility speed(normal);
|
||||
}
|
||||
|
||||
&.is-open {
|
||||
pointer-events: auto;
|
||||
visibility: visible;
|
||||
transition-duration: 0s;
|
||||
}
|
||||
}
|
||||
|
||||
.c-modal_inner {
|
||||
width: 100%;
|
||||
max-width: rem(500px);
|
||||
padding: $unit-small;
|
||||
background-color: color(lightest);
|
||||
}
|
||||
@@ -57,6 +57,7 @@
|
||||
@import "components/text";
|
||||
@import "components/button";
|
||||
@import "components/form";
|
||||
@import "components/modal";
|
||||
|
||||
// Utilities
|
||||
// ==========================================================================
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
* @file Provides simple user configuration options.
|
||||
*/
|
||||
|
||||
import loconfig from '../../loconfig.json' assert { type: 'json' };
|
||||
import loconfig from '../../loconfig.json' with { type: 'json' };
|
||||
import { merge } from '../utils/index.js';
|
||||
|
||||
let usrconfig;
|
||||
|
||||
try {
|
||||
usrconfig = await import('../../loconfig.local.json', {
|
||||
assert: { type: 'json' },
|
||||
with: { type: 'json' },
|
||||
});
|
||||
usrconfig = usrconfig.default;
|
||||
|
||||
|
||||
4188
package-lock.json
generated
4188
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@@ -6,39 +6,44 @@
|
||||
"author": "Locomotive <info@locomotive.ca>",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "20.x",
|
||||
"npm": ">=8.0"
|
||||
"node": ">=20",
|
||||
"npm": ">=10"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node --experimental-json-modules --no-warnings build/watch.js",
|
||||
"build": "node --experimental-json-modules --no-warnings build/build.js"
|
||||
"start": "node --no-warnings build/watch.js",
|
||||
"build": "node --no-warnings build/build.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"locomotive-scroll": "^5.0.0-beta.11",
|
||||
"focus-trap": "^7.5.4",
|
||||
"locomotive-scroll": "^5.0.0-beta.13",
|
||||
"modujs": "^1.4.2",
|
||||
"modularload": "^1.2.6",
|
||||
"svg4everybody": "^2.1.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.17",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"browser-sync": "^3.0.2",
|
||||
"common-path": "^1.0.1",
|
||||
"concat": "^1.0.3",
|
||||
"esbuild": "^0.20.0",
|
||||
"esbuild": "^0.21.5",
|
||||
"kleur": "^4.1.5",
|
||||
"node-notifier": "^10.0.1",
|
||||
"postcss": "^8.4.21",
|
||||
"purgecss": "^5.0.0",
|
||||
"sass": "^1.70.0",
|
||||
"svg-mixer": "~2.3.14",
|
||||
"postcss": "^8.4.38",
|
||||
"purgecss": "^6.0.0",
|
||||
"sass": "^1.77.6",
|
||||
"svg-mixer": "^2.3.14",
|
||||
"tiny-glob": "^0.2.9"
|
||||
},
|
||||
"overrides": {
|
||||
"browser-sync": {
|
||||
"ua-parser-js": "~1.0.33"
|
||||
"ua-parser-js": "^1.0.33"
|
||||
},
|
||||
"svg-mixer": {
|
||||
"postcss": "^8.4.20"
|
||||
"micromatch": "^4.0.4",
|
||||
"postcss": "^8.4.38"
|
||||
},
|
||||
"svg-mixer-utils": {
|
||||
"anymatch": "^3.1.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,8 +96,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nomodule src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.6.0/polyfill.min.js" crossorigin="anonymous"></script>
|
||||
|
||||
<script src="assets/scripts/vendors.js" defer></script>
|
||||
<script src="assets/scripts/app.js" defer></script>
|
||||
</body>
|
||||
|
||||
@@ -83,9 +83,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nomodule src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.6.0/polyfill.min.js"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<script src="assets/scripts/vendors.js" defer></script>
|
||||
<script src="assets/scripts/app.js" defer></script>
|
||||
</body>
|
||||
|
||||
@@ -122,8 +122,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nomodule src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.6.0/polyfill.min.js" crossorigin="anonymous"></script>
|
||||
|
||||
<script src="assets/scripts/vendors.js" defer></script>
|
||||
<script src="assets/scripts/app.js" defer></script>
|
||||
</body>
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
<main data-module-example>
|
||||
<div class="o-container">
|
||||
<h1 class="c-heading -h1">Hello</h1>
|
||||
<button data-modal-toggler>Open modal</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -66,12 +67,21 @@
|
||||
<p>Made with <a href="https://github.com/locomotivemtl/locomotive-boilerplate"
|
||||
title="Locomotive Boilerplate" target="_blank" rel="noopener">🚂</a></p>
|
||||
</footer>
|
||||
|
||||
<div class="c-modal" data-module-modal>
|
||||
<div class="c-modal_inner" data-modal-target>
|
||||
<button data-modal-toggler>Close</button>
|
||||
<h2>Modal</h2>
|
||||
<p>Content</p>
|
||||
<form action="">
|
||||
<input type="text">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nomodule src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.6.0/polyfill.min.js"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<script src="assets/scripts/vendors.js" defer></script>
|
||||
<script src="assets/scripts/app.js" defer></script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user