diff --git a/.eslintrc.js b/.eslintrc.js index f36c8d4d..a7e7f368 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -9,6 +9,9 @@ module.exports = { extends: [ 'eslint:recommended' ], + parserOptions: { + sourceType: 'module' + }, rules: { // Require `let` or `const` instead of `var` // https://eslint.org/docs/rules/no-var @@ -71,5 +74,11 @@ module.exports = { // Disallow the use of `console` // https://eslint.org/docs/rules/no-console 'no-console': 'error' + }, + globals: { + 'MzpDetails': 'readable', + 'MzpMenu': 'readable', + 'MzpSupports': 'readable', + 'MzpUtils': 'readable', } }; diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ecd6079..6be10a2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,19 @@ # HEAD +## Features + +* **js:** Protocol JS components are now written using modern JS and published as ES5/UMD format (#255). +* **js:** Removed pre-minified JS files from the published package. Consuming sites should handle their own minification. +* **css:** Removed pre-minified CSS files from the published package Consuming sites should handle their own minification. + ## Bug Fixes + * **js:** Fix syntax error in legacy IE when compiling JS (#861) # 16.1.0 ## Features + * **component:** Add centering classes for Logo and Wordmark. (#718) * **docs:** Migrate Protocol documentation site to Fractal. * **node:** Create a Webpack config for compiling docs using Fractal. @@ -15,6 +23,7 @@ * **docs:** Added 404 page ## Bug Fixes + * **js:** Ensure focus is moved to modal after animation completes (#829) * **node:** Make sure to build NPM package using production mode. * **html:** Added accessible attributes to menu bar (#815). @@ -26,6 +35,7 @@ # 16.0.1 ## Bug Fixes + * **css:** Fix repeating background on disabled search field (#767) * **js:** Fix keyboard focus capture on modal open animation (#749). diff --git a/README.md b/README.md index 7d1de3bf..af33d124 100755 --- a/README.md +++ b/README.md @@ -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 diff --git a/assets/.eslintrc.js b/assets/.eslintrc.js deleted file mode 100644 index 732b6cb8..00000000 --- a/assets/.eslintrc.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = { - env: { - 'browser': true, - 'node': false, - 'commonjs': false, - 'es2017': false - }, - 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', - }, - globals: { - 'Mzp': 'writable' - } -}; diff --git a/assets/js/protocol/base.js b/assets/js/protocol/base.js new file mode 100644 index 00000000..1b05ac7f --- /dev/null +++ b/assets/js/protocol/base.js @@ -0,0 +1,22 @@ +/* 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 module 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 `MazBase.init();` should be run in the
, before + * page CSS is parsed. + */ + +const MzpBase = {}; + +MzpBase.init = () => { + const doc = document.documentElement; + + // Add class to reflect javascript availability for CSS + doc.className = doc.className.replace(/\bno-js\b/, 'js'); +}; + +module.exports = MzpBase; diff --git a/assets/js/protocol/details.js b/assets/js/protocol/details.js new file mode 100644 index 00000000..41aad5e6 --- /dev/null +++ b/assets/js/protocol/details.js @@ -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; diff --git a/assets/js/protocol/footer.js b/assets/js/protocol/footer.js new file mode 100644 index 00000000..49288767 --- /dev/null +++ b/assets/js/protocol/footer.js @@ -0,0 +1,41 @@ +/* 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 MzpFooter = {}; + +MzpFooter.init = () => { + 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); + } + } + } + +}; + +module.exports = MzpFooter; diff --git a/assets/js/protocol/lang-switcher.js b/assets/js/protocol/lang-switcher.js new file mode 100644 index 00000000..90cda02d --- /dev/null +++ b/assets/js/protocol/lang-switcher.js @@ -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; diff --git a/assets/js/protocol/menu.js b/assets/js/protocol/menu.js new file mode 100644 index 00000000..52f3eea7 --- /dev/null +++ b/assets/js/protocol/menu.js @@ -0,0 +1,343 @@ +/* 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 MzpMenu = {}; +let _menuOpen = false; +let _hoverTimeout; +const _hoverTimeoutDelay = 150; +let _mqWideNav; +const _wideBreakpoint = '768px'; + +const _options = { + onMenuOpen: null, + onMenuClose: null, + onMenuButtonClose: null +}; + +/** + * Opens a menu panel. + * @param {Object} el - DOM element (`.mzp-c-menu-category.mzp-js-expandable`) + * @param {Boolean} animate - show animation when menu panel opens. + */ +MzpMenu.open = (el, animate) => { + if (animate) { + el.classList.add('mzp-is-animated'); + } + + el.classList.add('mzp-is-selected'); + + _menuOpen = true; // For checking menu state on keyup. + + el.querySelector('.mzp-c-menu-title').setAttribute('aria-expanded', true); + + if (typeof _options.onMenuOpen === 'function') { + _options.onMenuOpen(el); + } +}; + +/** + * Closes all currently open menu panels. + * Note: on small screens more than one menu can be open at the same time. + */ +MzpMenu.close = () => { + const current = document.querySelectorAll('.mzp-c-menu-category.mzp-is-selected'); + + for (let i = 0; i < current.length; i++) { + // The following classes must be removed in the correct order + // to work around a bug in bedrock's classList polyfill for IE9. + // https://github.com/mozilla/bedrock/issues/6221 :/ + current[i].classList.remove('mzp-is-selected'); + current[i].classList.remove('mzp-is-animated'); + + current[i].querySelector('.mzp-c-menu-title').setAttribute('aria-expanded', false); + } + + _menuOpen = false; // For checking menu state on keyup. + + if (typeof _options.onMenuClose === 'function' && current.length > 0) { + _options.onMenuClose(); + } + + return current.length > 0; +}; + +MzpMenu.onDocumentKeyUp = (e) => { + if (e.keyCode === 27 && _menuOpen) { + MzpMenu.close(); + } +}; + +/** + * Menu panel close button `click` event handler. + * @param {Object} e - Event object. + */ +MzpMenu.onCloseButtonClick = (e) => { + e.preventDefault(); + + if (typeof _options.onMenuButtonClose === 'function') { + _options.onMenuButtonClose(); + } + + MzpMenu.close(); +}; + +/** + * Toggles the open/closed state of a menu panel. + * @param {Object} el - DOM element (`.mzp-c-menu-category.mzp-js-expandable`) + */ +MzpMenu.toggle = (el) => { + const state = el.classList.contains('mzp-is-selected') ? true : false; + + if (!state) { + MzpMenu.open(el); + } else { + // The following classes must be removed in the correct order + // to work around a bug in bedrock's classList polyfill for IE9. + // https://github.com/mozilla/bedrock/issues/6221 :/ + el.classList.remove('mzp-is-selected'); + el.classList.remove('mzp-is-animated'); + el.querySelector('.mzp-c-menu-title').setAttribute('aria-expanded', false); + + if (typeof _options.onMenuClose === 'function') { + _options.onMenuClose(); + } + } +}; + +/** + * Menu `mouseenter` event handler. + * Opens the menu only when hover intent is shown. + * Animates only if a menu panel is not already open. + * @param {Object} e - Event object. + */ +MzpMenu.onMouseEnter = (e) => { + clearTimeout(_hoverTimeout); + + _hoverTimeout = setTimeout(() => { + const current = MzpMenu.close(); + const animate = current ? false: true; + + MzpMenu.open(e.target, animate); + }, _hoverTimeoutDelay); +}; + +/** + * Menu `mouseleave` event handler. + * Closes the menu only when hover intent is shown. + */ +MzpMenu.onMouseLeave = () => { + clearTimeout(_hoverTimeout); + + _hoverTimeout = setTimeout(() => { + MzpMenu.close(); + }, _hoverTimeoutDelay); +}; + +/** + * Menu `focusout` event handler. + * Closes the menu when focus moves to an element outside of the currently open panel. + */ +MzpMenu.onFocusOut = function() { + /** + * Note: 'this' needs to reference the active DOM node, so here `focusOut` is not + * an arrow (=>) function, since that would lexically bind `this` to `window`. + */ + const self = this; + + /** + * After an element loses focus, `document.activeElement` will always be `body` before + * moving to the next element. A `setTimeout` of `0` circumvents this issue as it + * re-queues the JavaScript to run at the end of the current execution. + */ + setTimeout(() => { + // If the menu is open and the newly focused element is not a child, then call close(). + if (!self.contains(document.activeElement) && self.classList.contains('mzp-is-selected')) { + MzpMenu.close(); + } + }, 0); +}; + +/** + * Menu link `click` event handler for wide viewports. + * Closes any currently open menu panels before opening the selected one. + * @param {Object} e - Event object. + */ +MzpMenu.onClickWide = (e) => { + e.preventDefault(); + MzpMenu.close(); + MzpMenu.open(e.target.parentNode); +}; + +/** + * Menu link `click` event handler for small viewports. + * Toggles the currently selected menu open open/close state. + * @param {Object} e - Event object. + */ +MzpMenu.onClickSmall = (e) => { + e.preventDefault(); + MzpMenu.toggle(e.target.parentNode); +}; + +/** + * Convenience function for checking `matchMedia` state. + * @return {Boolean} + */ +MzpMenu.isWideViewport = () => { + return _mqWideNav.matches; +}; + +/** + * Toggle desktop/mobile navigation using `matchMedia` event handler. + */ +MzpMenu.handleState = () => { + _mqWideNav = matchMedia('(min-width: ' + _wideBreakpoint + ')'); + + function menuBind(mq) { + MzpMenu.close(); + + if (mq.matches) { + MzpMenu.unbindEventsSmall(); + MzpMenu.bindEventsWide(); + } else { + MzpMenu.unbindEventsWide(); + MzpMenu.bindEventsSmall(); + } + } + + if (window.matchMedia('all').addEventListener) { + _mqWideNav.addEventListener('change', menuBind, false); + } else if (window.matchMedia('all').addListener) { + _mqWideNav.addListener(menuBind); + } + + if (MzpMenu.isWideViewport()) { + MzpMenu.bindEventsWide(); + } else { + MzpMenu.bindEventsSmall(); + } +}; + +/** + * Bind events for wide viewports. + */ +MzpMenu.bindEventsWide = () => { + const items = document.querySelectorAll('.mzp-c-menu-category.mzp-js-expandable'); + let link; + let close; + + for (let i = 0; i < items.length; i++) { + items[i].addEventListener('mouseenter', MzpMenu.onMouseEnter, false); + items[i].addEventListener('mouseleave', MzpMenu.onMouseLeave, false); + items[i].addEventListener('focusout', MzpMenu.onFocusOut, false); + link = items[i].querySelector('.mzp-c-menu-title'); + link.addEventListener('click', MzpMenu.onClickWide, false); + + close = items[i].querySelector('.mzp-c-menu-button-close'); + close.addEventListener('click', MzpMenu.onCloseButtonClick, false); + } + + // close with escape key + document.addEventListener('keyup', MzpMenu.onDocumentKeyUp, false); +}; + +/** + * Unbind events for wide viewports. + */ +MzpMenu.unbindEventsWide = () => { + const items = document.querySelectorAll('.mzp-c-menu-category.mzp-js-expandable'); + let link; + let close; + + for (let i = 0; i < items.length; i++) { + items[i].removeEventListener('mouseenter', MzpMenu.onMouseEnter, false); + items[i].removeEventListener('mouseleave', MzpMenu.onMouseLeave, false); + items[i].removeEventListener('focusout', MzpMenu.onFocusOut, false); + + link = items[i].querySelector('.mzp-c-menu-title'); + link.removeEventListener('click', MzpMenu.onClickWide, false); + + close = items[i].querySelector('.mzp-c-menu-button-close'); + close.removeEventListener('click', MzpMenu.onCloseButtonClick, false); + } + + document.removeEventListener('keyup', MzpMenu.onDocumentKeyUp, false); +}; + +/** + * Bind events for small viewports. + */ +MzpMenu.bindEventsSmall = () => { + const items = document.querySelectorAll('.mzp-c-menu-category.mzp-js-expandable .mzp-c-menu-title'); + + for (let i = 0; i < items.length; i++) { + items[i].addEventListener('click', MzpMenu.onClickSmall, false); + } +}; + +/** + * Unbind events for small viewports. + */ +MzpMenu.unbindEventsSmall = () => { + const items = document.querySelectorAll('.mzp-c-menu-category.mzp-js-expandable .mzp-c-menu-title'); + + for (let i = 0; i < items.length; i++) { + items[i].removeEventListener('click', MzpMenu.onClickSmall, false); + } +}; + +/** + * Set initial ARIA menu panel states. + */ +MzpMenu.setAria = () => { + const items = document.querySelectorAll('.mzp-c-menu-category.mzp-js-expandable .mzp-c-menu-title'); + + for (let i = 0; i < items.length; i++) { + items[i].setAttribute('aria-expanded', false); + } +}; + +/** + * Enhances the menu for 1st class JS support. + */ +MzpMenu.enhanceJS = () => { + const menu = document.querySelectorAll('.mzp-c-menu'); + + for (let i = 0; i < menu.length; i++) { + menu[i].classList.remove('mzp-is-basic'); + menu[i].classList.add('mzp-is-enhanced'); + } +}; + +/** + * Basic feature detect for 1st class menu JS support. + */ +MzpMenu.isSupported = () => { + if (typeof MzpSupports !== 'undefined') { + return MzpSupports.matchMedia && MzpSupports.classList; + } else { + return false; + } +}; + +/** + * Initialize menu. + * @param {Object} options - configurable options. + */ +MzpMenu.init = (options) => { + if (typeof options === 'object') { + for (const i in options) { + if (Object.prototype.hasOwnProperty.call(options, i)) { + _options[i] = options[i]; + } + } + } + + if (MzpMenu.isSupported()) { + MzpMenu.handleState(); + MzpMenu.setAria(); + MzpMenu.enhanceJS(); + } +}; + +module.exports = MzpMenu; diff --git a/assets/js/protocol/modal.js b/assets/js/protocol/modal.js new file mode 100644 index 00000000..db78ea6a --- /dev/null +++ b/assets/js/protocol/modal.js @@ -0,0 +1,154 @@ +/* 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 MzpModal = {}; +let open = false; +const body = document.body; +const html = document.documentElement; +let options = {}; +let pageContentParent; +let pageContent; +let modal; + +/* +origin: element that triggered the modal +content: content to display in the modal +options: object of optional params: + title: title to display at the top of the modal. + className: optional CSS class name to apply to the modal window. + onCreate: function to fire after modal has been created. + onDestroy: function to fire after modal has been closed. + allowScroll: boolean - allow/restrict page scrolling when modal is open. + closeText: string to use for close button a11y. +*/ +MzpModal.createModal = (origin, content, opts) => { + options = opts; + + const isSmallViewport = window.innerWidth < 760; + + // Make sure modal is closed (if one exists) + if (open) { + MzpModal.closeModal(); + } + + // Create new modal + const title = (options && options.title) ? options.title : ''; + const className = (options && options.className) ? options.className : ''; + const closeText = (options && options.closeText) ? options.closeText : ''; + + const modalFragment = ` +Paragraph 1
+Paragraph 2
+Paragraph 3
+Paragraph 1
+Paragraph 2
+Paragraph 3
+Paragraph 1
' + - 'Paragraph 2
' + - 'Paragraph 3
' + - 'Paragraph 1
' + - 'Paragraph 2
' + - 'Paragraph 3
' + - '