Skip to content

Commit

Permalink
Convert Protocol JS components to modern JS formats (Fixes mozilla#255)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexgibson committed Apr 4, 2023
1 parent e0566d7 commit 4062ea1
Show file tree
Hide file tree
Showing 68 changed files with 13,212 additions and 10,031 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ $ npm install
Running `npm install` will install dependencies. Then:

```
$ npm run webpack-docs
$ npm run webpack
```

This will compile the Sass and copy assets into a local folder in preparation to
Expand Down
19 changes: 8 additions & 11 deletions assets/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,19 @@ module.exports = {
env: {
'browser': true,
'node': false,
'commonjs': false,
'es2017': false
'commonjs': true,
'es2017': true
},
extends: [
'eslint:recommended'
],
rules: {
// Require strict mode directive in top level functions
// https://eslint.org/docs/rules/strict
'strict': ['error', 'function'],

// Require `let` or `const` instead of `var`
// https://eslint.org/docs/rules/no-var
'no-var': 'off',
parserOptions: {
sourceType: 'module'
},
globals: {
'Mzp': 'writable'
'MzpDetails': 'readable',
'MzpMenu': 'readable',
'MzpSupports': 'readable',
'MzpUtils': 'readable',
}
};
16 changes: 16 additions & 0 deletions assets/js/protocol/base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
* This script is used to determine that JS is enabled in the browser,
* and provides `.js` and `.no-js` styling hooks used in component CSS.
* In order to avoid content flashing and repaints on page load, it is
* recommended that this script should be run in the <head>, before
* page CSS is parsed.
*/

const doc = document.documentElement;

// Add class to reflect javascript availability for CSS
doc.className = doc.className.replace(/\bno-js\b/, 'js');
232 changes: 232 additions & 0 deletions assets/js/protocol/details.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const MzpDetails = {};
let _count = 0;

MzpDetails.isSupported = () => {
if (typeof MzpSupports !== 'undefined' && typeof MzpUtils !== 'undefined') {
return MzpSupports.classList;
} else {
return false;
}
};

/**
* open
* @param {String} id - id of the container to open
* @param {Object} options - configurable options
*/
MzpDetails.open = (id, options) => {
const control = document.querySelector(`[aria-controls=${id}]`);
const details = document.getElementById(id);
control.setAttribute('aria-expanded', true);
details.setAttribute('aria-hidden', false);
details.classList.remove('is-closed');
if (typeof options.onDetailsOpen === 'function') {
options.onDetailsOpen(details);
}
};

/**
* close
* @param {String} id - id of the container to close
* @param {Object} options - configurable options
*/
MzpDetails.close = (id, options) => {
const control = document.querySelector(`[aria-controls=${id}]`);
const details = document.getElementById(id);
control.setAttribute('aria-expanded', false);
details.setAttribute('aria-hidden', true);
details.classList.add('is-closed');
if (typeof options.onDetailsClose === 'function') {
options.onDetailsClose(details);
}
};

/**
* toggle
* @param {String} id - id of the container to toggle
* @param {Object} options - configurable options
*/
MzpDetails.toggle = (id, options) => {
const details = document.getElementById(id);
const isClosed = details.getAttribute('aria-hidden');

if (isClosed === 'true') {
MzpDetails.open(id, options);
} else {
MzpDetails.close(id, options);
}
};

/**
* handleControlActivation
* @param {Event} e - event to handle
* @param {Object} options - configurable options
*/
MzpDetails.handleControlActivation = (e, options) => {
const control = e.target;
const id = control.getAttribute('aria-controls');
MzpDetails.toggle(id, options);
};

/**
* initItem
* @param {Object} el - Element to place the control inside of
* @param {String} selector - Selector for all control wrappers
* - assumes every sibling until the next control is associated with the control
* @param {Object} options - configurable options
*/
MzpDetails.initItem = (el, selector, options) => {
const summary = el;
const control = document.createElement('button');
let details;
const parent = summary.parentNode;

// if it's already been initialized, don't do it again
if (summary.querySelectorAll('button').length !== 0) {
return;
}

// Expand
// siblings of the summary, until next summary
const summarySiblings = MzpUtils.nextUntil(summary, selector);

// look to see if all children are already in a wrapper we can use
if (summarySiblings.length === 1) {
details = summarySiblings[0];
} else if (summarySiblings.length > 1){
details = document.createElement('div');
summarySiblings.forEach(function(sibling) {
details.appendChild(sibling);
});
summary.parentNode.insertBefore(details, summary.nextSibling);
} else {
// no children were found, something is probably wrong, let's stop here
return;
}

// add class to parent to indicate js initialized
parent.classList.add('is-details');

// add class to content wrapper
details.classList.add('mzp-js-details-wrapper');

// look for existing ID to use
if(!details.id) {
// if details already has ID, use that, if not assign one using the selector minus all not-letters
const unique = selector.replace(/[^a-zA-Z]+/g, '');
details.id = 'expand-' + unique + '-'+ _count;
_count += 1;
}

// close by default
// TODO: add support for open attribute
details.setAttribute('aria-hidden', true);
details.classList.add('is-closed');

// Control
control.setAttribute('type', 'button');
// add aria-controls
control.setAttribute('aria-controls', details.id);
// add aria-expanded
control.setAttribute('aria-expanded', false);
// add listener
control.addEventListener('click', function(e) {
MzpDetails.handleControlActivation(e, options);
}, false);
// copy the summary's contents into the control
const summaryChildren = Array.prototype.slice.call(summary.childNodes);
summaryChildren.forEach(function(child) {
control.appendChild(child);
});
// append control element
summary.appendChild(control);
summary.classList.add('is-summary');
};

/**
* destroyItem
* @param {Object} el - Element the control was placed inside of
* - does not attempt to remove the details wrapper
*/
MzpDetails.destroyItem = (el) => {
const summary = el;
const parent = summary.parentNode;
const details = summary.nextElementSibling;
const control = summary.querySelector('button');

// if it's already been destroyed, don't do it again
if (summary.querySelectorAll('button').length === 0) {
return;
}

parent.classList.remove('is-details');
details.removeAttribute('aria-hidden');
details.classList.remove('is-closed');
// move control's contents back to summary
const controlChildren = Array.prototype.slice.call(control.childNodes);
controlChildren.forEach(function(child) {
summary.appendChild(child);
});
summary.removeChild(control);
summary.classList.remove('is-summary');
};

/**
* Init
* @param {Object} selector - CSS selector matching "summary" elements
* @param {Object} options - configurable options
- passed in to the init function and passed around from there
example:
var testOptions = {
onDetailsOpen : myDetailsOpenCallback(),
onDetailsClose : function(){ //anonymous callback }
};
*/
MzpDetails.init = (selector, options) => {
if (!MzpDetails.isSupported()) {
return;
}
if (typeof options === 'undefined') {
options = {};
}

const summaries = document.querySelectorAll(selector);
// loop through controls on the page and init them one at a time
for (let i = 0; i < summaries.length; i++) {
MzpDetails.initItem(summaries[i], selector, options);
}
};

/**
* Destroy
* @param {Object} selector - CSS selector matching "summary" elements
* @param {Object} options - configurable options
*/
MzpDetails.destroy = (selector, options) => {
const summaries = document.querySelectorAll(selector, options);
// loop through controls on the page and destroy them one at a time
for (let i = 0; i < summaries.length; i++) {
MzpDetails.destroyItem(summaries[i], selector, options);
}
};

// check if details is supported, if not, init this as a polyfill
if (typeof MzpSupports !== 'undefined') {
// not supported, add support
if(!MzpSupports.details) {
MzpDetails.init('summary');
}
}

// init generic class indicating headings should be made into open/close component
MzpDetails.init('.mzp-c-details > h2');
MzpDetails.init('.mzp-c-details > h3');
MzpDetails.init('.mzp-c-details > h4');
MzpDetails.init('.mzp-c-details > h5');
MzpDetails.init('.mzp-c-details > h6');

module.exports = MzpDetails;
34 changes: 34 additions & 0 deletions assets/js/protocol/footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const footerHeadings = '.mzp-c-footer-sections .mzp-c-footer-heading';

// removes Details component if screen size is big
function screenChange(mq) {
if (mq.matches) {
MzpDetails.init(footerHeadings);
} else {
MzpDetails.destroy(footerHeadings);
}
}

// check we have global Supports and Details library
if (typeof MzpSupports !== 'undefined' && typeof MzpDetails !== 'undefined') {

// check browser supports matchMedia
if (MzpSupports.matchMedia) {
const _mqWide = matchMedia('(max-width: 479px)');

// initialize details if screen is small
if (_mqWide.matches) {
MzpDetails.init(footerHeadings);
}

if (window.matchMedia('all').addEventListener) {
_mqWide.addEventListener('change', screenChange, false);
} else if (window.matchMedia('all').addListener) {
_mqWide.addListener(screenChange);
}
}
}
61 changes: 61 additions & 0 deletions assets/js/protocol/lang-switcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const MzpLangSwitcher = {};

/**
* Returns URL pathname with preceded by a new page locale.
* Assumes first path immediately after hostname is the page locale.
* @param {Object} Location interface
* @param {String} Newly selected language code e.g. `de`
* @return {String} pathname e.g. `/de/firefox/`
*/
MzpLangSwitcher.switchPath = (location, newLang) => {
const parts = location.pathname.slice(1).split('/');
const currentLang = '/' + parts[0] + '/';

// check that first path is a valid lang code.
if (!/^(\/\w{2}-\w{2}\/|\/\w{2,3}\/)/.test(currentLang)) {
return false;
}

const urlpath = parts.slice(1).join('/');
return '/' + newLang + '/' + urlpath + location.search;
};

/**
* Redirect page to destination URL if valid
* @param {String} destination
*/
MzpLangSwitcher.doRedirect = (destination) => {
if (destination) {
window.location.href = destination;
}
};

/**
* Initialize footer lang switcher.
* @param {function} Custom callback for analytics.
*/
MzpLangSwitcher.init = (callback) => {
const language = document.querySelectorAll('.mzp-js-language-switcher-select');

for (let i = 0; i < language.length; i++) {
language[i].setAttribute('data-previous-language', language[i].value);

language[i].addEventListener('change', function(e) {
const newLanguage = e.target.value;
const previousLanguage = e.target.getAttribute('data-previous-language');

// support custom callback for page analytics.
if (typeof callback === 'function') {
callback(previousLanguage, newLanguage);
}

MzpLangSwitcher.doRedirect(MzpLangSwitcher.switchPath(window.location, newLanguage));
}, false);
}
};

module.exports = MzpLangSwitcher;
Loading

0 comments on commit 4062ea1

Please sign in to comment.