diff --git a/plugins/wporg-5ftf/assets/js/frontend.js b/plugins/wporg-5ftf/assets/js/frontend.js new file mode 100644 index 0000000..b8f4054 --- /dev/null +++ b/plugins/wporg-5ftf/assets/js/frontend.js @@ -0,0 +1,136 @@ +/* global FiveForTheFuture, jQuery */ +jQuery( document ).ready( function( $ ) { + const button = document.getElementById( 'toggle-management-link-form' ); + const template = wp.template( '5ftf-send-link-dialog' ); + + if ( ! template && ! button ) { + // No modal on this page. + return; + } + + $( document.body ).prepend( template() ); + const modal = document.getElementById( 'send-link-dialog' ); + const modalBg = document.getElementById( 'send-link-dialog-bg' ); + const children = document.querySelectorAll( 'body > *:not([role="dialog"])' ); + + /** + * Get the top/left position for the modal, based on the button location. + */ + function getModalPosition() { + const offsetTop = ( 'number' === typeof window.scrollY ) ? window.scrollY : window.pageYOffset; + const offsetLeft = ( 'number' === typeof window.scrollX ) ? window.scrollX : window.pageXOffset; + const bounds = button.getBoundingClientRect(); + const modalWidth = 300; // Modal width is hardcoded, because it's not visible yet. + + return { + top: bounds.y + offsetTop + bounds.height, + left: bounds.x + offsetLeft + bounds.width - modalWidth, + }; + } + + /** + * Open the modal. + */ + function openModal() { + const position = getModalPosition(); + // Hide other content on this page while modal is open. + for ( let i = 0; i < children.length; i++ ) { + if ( children[ i ].hasAttribute( 'data-no-inert' ) ) { + continue; + } + if ( children[ i ].getAttribute( 'inert' ) ) { + children[ i ].setAttribute( 'data-keep-inert', '' ); + } else { + children[ i ].setAttribute( 'inert', 'true' ); + } + } + + modal.removeAttribute( 'hidden' ); + modalBg.removeAttribute( 'hidden' ); + modal.style.top = position.top + 'px'; + modal.style.left = position.left + 'px'; + modal.focus(); + } + + /** + * Close the modal. + */ + function closeModal() { + // Reveal content again. + for ( let i = 0; i < children.length; i++ ) { + if ( ! children[ i ].hasAttribute( 'data-keep-inert' ) ) { + children[ i ].removeAttribute( 'inert' ); + } + + children[ i ].removeAttribute( 'data-keep-inert' ); + } + + modal.hidden = true; + modalBg.hidden = true; + + // Wait a tick before setting focus. See https://github.com/WICG/inert#performance-and-gotchas + setTimeout( function() { + if ( button ) { + button.focus(); + } else { + document.body.focus(); + } + }, 0 ); + } + + function sendRequest( event ) { + event.preventDefault(); + const email = $( event.target.querySelector( 'input[type="email"]' ) ).val(); + $( event.target.querySelector( '.message' ) ).html( '' ); + $.ajax( { + type: 'POST', + url: FiveForTheFuture.ajaxurl, + data: { + action: 'send-manage-email', + pledge_id: FiveForTheFuture.pledgeId, + email, + _ajax_nonce: FiveForTheFuture.ajaxNonce, + }, + success( response ) { + if ( response.message ) { + // Say the message for screen reader users. + wp.a11y.speak( response.message ); + + if ( response.success ) { + closeModal(); + $( button ).after( $( '

' ).html( '' + response.message + '' ) ); + } else { + const $message = $( '

' ) + .addClass( 'notice notice-alt' ) + .addClass( response.success ? 'notice-success' : 'notice-error' ) + .append( $( '

' ).html( response.message ) ); + + $( event.target.querySelector( '.message' ) ).html( $message ); + } + } + }, + dataType: 'json', + } ); + } + + // Initialize. + $( button ).on( 'click', function( event ) { + event.preventDefault(); + + if ( !! modal.hidden ) { + openModal( event ); + } else { + closeModal( event ); + } + } ); + + $( modalBg ).on( 'click', closeModal ); + $( modal ).on( 'click', '.pledge-dialog__close', closeModal ); + $( document ).on( 'keydown', function( event ) { + if ( 27 === event.which ) { // Esc + closeModal( event ); + } + } ); + + $( modal.querySelector( 'form' ) ).submit( sendRequest ); +} ); diff --git a/plugins/wporg-5ftf/assets/js/inert.js b/plugins/wporg-5ftf/assets/js/inert.js new file mode 100644 index 0000000..498e6d9 --- /dev/null +++ b/plugins/wporg-5ftf/assets/js/inert.js @@ -0,0 +1,833 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define('inert', factory) : + (factory()); +}(this, (function () { 'use strict'; + + var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + /** + * This work is licensed under the W3C Software and Document License + * (http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document). + */ + + // Convenience function for converting NodeLists. + /** @type {typeof Array.prototype.slice} */ + var slice = Array.prototype.slice; + + /** + * IE has a non-standard name for "matches". + * @type {typeof Element.prototype.matches} + */ + var matches = Element.prototype.matches || Element.prototype.msMatchesSelector; + + /** @type {string} */ + var _focusableElementsString = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', 'iframe', 'object', 'embed', '[contenteditable]'].join(','); + + /** + * `InertRoot` manages a single inert subtree, i.e. a DOM subtree whose root element has an `inert` + * attribute. + * + * Its main functions are: + * + * - to create and maintain a set of managed `InertNode`s, including when mutations occur in the + * subtree. The `makeSubtreeUnfocusable()` method handles collecting `InertNode`s via registering + * each focusable node in the subtree with the singleton `InertManager` which manages all known + * focusable nodes within inert subtrees. `InertManager` ensures that a single `InertNode` + * instance exists for each focusable node which has at least one inert root as an ancestor. + * + * - to notify all managed `InertNode`s when this subtree stops being inert (i.e. when the `inert` + * attribute is removed from the root node). This is handled in the destructor, which calls the + * `deregister` method on `InertManager` for each managed inert node. + */ + + var InertRoot = function () { + /** + * @param {!Element} rootElement The Element at the root of the inert subtree. + * @param {!InertManager} inertManager The global singleton InertManager object. + */ + function InertRoot(rootElement, inertManager) { + _classCallCheck(this, InertRoot); + + /** @type {!InertManager} */ + this._inertManager = inertManager; + + /** @type {!Element} */ + this._rootElement = rootElement; + + /** + * @type {!Set} + * All managed focusable nodes in this InertRoot's subtree. + */ + this._managedNodes = new Set(); + + // Make the subtree hidden from assistive technology + if (this._rootElement.hasAttribute('aria-hidden')) { + /** @type {?string} */ + this._savedAriaHidden = this._rootElement.getAttribute('aria-hidden'); + } else { + this._savedAriaHidden = null; + } + this._rootElement.setAttribute('aria-hidden', 'true'); + + // Make all focusable elements in the subtree unfocusable and add them to _managedNodes + this._makeSubtreeUnfocusable(this._rootElement); + + // Watch for: + // - any additions in the subtree: make them unfocusable too + // - any removals from the subtree: remove them from this inert root's managed nodes + // - attribute changes: if `tabindex` is added, or removed from an intrinsically focusable + // element, make that node a managed node. + this._observer = new MutationObserver(this._onMutation.bind(this)); + this._observer.observe(this._rootElement, { attributes: true, childList: true, subtree: true }); + } + + /** + * Call this whenever this object is about to become obsolete. This unwinds all of the state + * stored in this object and updates the state of all of the managed nodes. + */ + + + _createClass(InertRoot, [{ + key: 'destructor', + value: function destructor() { + this._observer.disconnect(); + + if (this._rootElement) { + if (this._savedAriaHidden !== null) { + this._rootElement.setAttribute('aria-hidden', this._savedAriaHidden); + } else { + this._rootElement.removeAttribute('aria-hidden'); + } + } + + this._managedNodes.forEach(function (inertNode) { + this._unmanageNode(inertNode.node); + }, this); + + // Note we cast the nulls to the ANY type here because: + // 1) We want the class properties to be declared as non-null, or else we + // need even more casts throughout this code. All bets are off if an + // instance has been destroyed and a method is called. + // 2) We don't want to cast "this", because we want type-aware optimizations + // to know which properties we're setting. + this._observer = /** @type {?} */null; + this._rootElement = /** @type {?} */null; + this._managedNodes = /** @type {?} */null; + this._inertManager = /** @type {?} */null; + } + + /** + * @return {!Set} A copy of this InertRoot's managed nodes set. + */ + + }, { + key: '_makeSubtreeUnfocusable', + + + /** + * @param {!Node} startNode + */ + value: function _makeSubtreeUnfocusable(startNode) { + var _this2 = this; + + composedTreeWalk(startNode, function (node) { + return _this2._visitNode(node); + }); + + var activeElement = document.activeElement; + + if (!document.body.contains(startNode)) { + // startNode may be in shadow DOM, so find its nearest shadowRoot to get the activeElement. + var node = startNode; + /** @type {!ShadowRoot|undefined} */ + var root = undefined; + while (node) { + if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + root = /** @type {!ShadowRoot} */node; + break; + } + node = node.parentNode; + } + if (root) { + activeElement = root.activeElement; + } + } + if (startNode.contains(activeElement)) { + activeElement.blur(); + // In IE11, if an element is already focused, and then set to tabindex=-1 + // calling blur() will not actually move the focus. + // To work around this we call focus() on the body instead. + if (activeElement === document.activeElement) { + document.body.focus(); + } + } + } + + /** + * @param {!Node} node + */ + + }, { + key: '_visitNode', + value: function _visitNode(node) { + if (node.nodeType !== Node.ELEMENT_NODE) { + return; + } + var element = /** @type {!Element} */node; + + // If a descendant inert root becomes un-inert, its descendants will still be inert because of + // this inert root, so all of its managed nodes need to be adopted by this InertRoot. + if (element !== this._rootElement && element.hasAttribute('inert')) { + this._adoptInertRoot(element); + } + + if (matches.call(element, _focusableElementsString) || element.hasAttribute('tabindex')) { + this._manageNode(element); + } + } + + /** + * Register the given node with this InertRoot and with InertManager. + * @param {!Node} node + */ + + }, { + key: '_manageNode', + value: function _manageNode(node) { + var inertNode = this._inertManager.register(node, this); + this._managedNodes.add(inertNode); + } + + /** + * Unregister the given node with this InertRoot and with InertManager. + * @param {!Node} node + */ + + }, { + key: '_unmanageNode', + value: function _unmanageNode(node) { + var inertNode = this._inertManager.deregister(node, this); + if (inertNode) { + this._managedNodes['delete'](inertNode); + } + } + + /** + * Unregister the entire subtree starting at `startNode`. + * @param {!Node} startNode + */ + + }, { + key: '_unmanageSubtree', + value: function _unmanageSubtree(startNode) { + var _this3 = this; + + composedTreeWalk(startNode, function (node) { + return _this3._unmanageNode(node); + }); + } + + /** + * If a descendant node is found with an `inert` attribute, adopt its managed nodes. + * @param {!Element} node + */ + + }, { + key: '_adoptInertRoot', + value: function _adoptInertRoot(node) { + var inertSubroot = this._inertManager.getInertRoot(node); + + // During initialisation this inert root may not have been registered yet, + // so register it now if need be. + if (!inertSubroot) { + this._inertManager.setInert(node, true); + inertSubroot = this._inertManager.getInertRoot(node); + } + + inertSubroot.managedNodes.forEach(function (savedInertNode) { + this._manageNode(savedInertNode.node); + }, this); + } + + /** + * Callback used when mutation observer detects subtree additions, removals, or attribute changes. + * @param {!Array} records + * @param {!MutationObserver} self + */ + + }, { + key: '_onMutation', + value: function _onMutation(records, self) { + records.forEach(function (record) { + var target = /** @type {!Element} */record.target; + if (record.type === 'childList') { + // Manage added nodes + slice.call(record.addedNodes).forEach(function (node) { + this._makeSubtreeUnfocusable(node); + }, this); + + // Un-manage removed nodes + slice.call(record.removedNodes).forEach(function (node) { + this._unmanageSubtree(node); + }, this); + } else if (record.type === 'attributes') { + if (record.attributeName === 'tabindex') { + // Re-initialise inert node if tabindex changes + this._manageNode(target); + } else if (target !== this._rootElement && record.attributeName === 'inert' && target.hasAttribute('inert')) { + // If a new inert root is added, adopt its managed nodes and make sure it knows about the + // already managed nodes from this inert subroot. + this._adoptInertRoot(target); + var inertSubroot = this._inertManager.getInertRoot(target); + this._managedNodes.forEach(function (managedNode) { + if (target.contains(managedNode.node)) { + inertSubroot._manageNode(managedNode.node); + } + }); + } + } + }, this); + } + }, { + key: 'managedNodes', + get: function get() { + return new Set(this._managedNodes); + } + + /** @return {boolean} */ + + }, { + key: 'hasSavedAriaHidden', + get: function get() { + return this._savedAriaHidden !== null; + } + + /** @param {?string} ariaHidden */ + + }, { + key: 'savedAriaHidden', + set: function set(ariaHidden) { + this._savedAriaHidden = ariaHidden; + } + + /** @return {?string} */ + , + get: function get() { + return this._savedAriaHidden; + } + }]); + + return InertRoot; + }(); + + /** + * `InertNode` initialises and manages a single inert node. + * A node is inert if it is a descendant of one or more inert root elements. + * + * On construction, `InertNode` saves the existing `tabindex` value for the node, if any, and + * either removes the `tabindex` attribute or sets it to `-1`, depending on whether the element + * is intrinsically focusable or not. + * + * `InertNode` maintains a set of `InertRoot`s which are descendants of this `InertNode`. When an + * `InertRoot` is destroyed, and calls `InertManager.deregister()`, the `InertManager` notifies the + * `InertNode` via `removeInertRoot()`, which in turn destroys the `InertNode` if no `InertRoot`s + * remain in the set. On destruction, `InertNode` reinstates the stored `tabindex` if one exists, + * or removes the `tabindex` attribute if the element is intrinsically focusable. + */ + + + var InertNode = function () { + /** + * @param {!Node} node A focusable element to be made inert. + * @param {!InertRoot} inertRoot The inert root element associated with this inert node. + */ + function InertNode(node, inertRoot) { + _classCallCheck(this, InertNode); + + /** @type {!Node} */ + this._node = node; + + /** @type {boolean} */ + this._overrodeFocusMethod = false; + + /** + * @type {!Set} The set of descendant inert roots. + * If and only if this set becomes empty, this node is no longer inert. + */ + this._inertRoots = new Set([inertRoot]); + + /** @type {?number} */ + this._savedTabIndex = null; + + /** @type {boolean} */ + this._destroyed = false; + + // Save any prior tabindex info and make this node untabbable + this.ensureUntabbable(); + } + + /** + * Call this whenever this object is about to become obsolete. + * This makes the managed node focusable again and deletes all of the previously stored state. + */ + + + _createClass(InertNode, [{ + key: 'destructor', + value: function destructor() { + this._throwIfDestroyed(); + + if (this._node && this._node.nodeType === Node.ELEMENT_NODE) { + var element = /** @type {!Element} */this._node; + if (this._savedTabIndex !== null) { + element.setAttribute('tabindex', this._savedTabIndex); + } else { + element.removeAttribute('tabindex'); + } + + // Use `delete` to restore native focus method. + if (this._overrodeFocusMethod) { + delete element.focus; + } + } + + // See note in InertRoot.destructor for why we cast these nulls to ANY. + this._node = /** @type {?} */null; + this._inertRoots = /** @type {?} */null; + this._destroyed = true; + } + + /** + * @type {boolean} Whether this object is obsolete because the managed node is no longer inert. + * If the object has been destroyed, any attempt to access it will cause an exception. + */ + + }, { + key: '_throwIfDestroyed', + + + /** + * Throw if user tries to access destroyed InertNode. + */ + value: function _throwIfDestroyed() { + if (this.destroyed) { + throw new Error('Trying to access destroyed InertNode'); + } + } + + /** @return {boolean} */ + + }, { + key: 'ensureUntabbable', + + + /** Save the existing tabindex value and make the node untabbable and unfocusable */ + value: function ensureUntabbable() { + if (this.node.nodeType !== Node.ELEMENT_NODE) { + return; + } + var element = /** @type {!Element} */this.node; + if (matches.call(element, _focusableElementsString)) { + if ( /** @type {!HTMLElement} */element.tabIndex === -1 && this.hasSavedTabIndex) { + return; + } + + if (element.hasAttribute('tabindex')) { + this._savedTabIndex = /** @type {!HTMLElement} */element.tabIndex; + } + element.setAttribute('tabindex', '-1'); + if (element.nodeType === Node.ELEMENT_NODE) { + element.focus = function () {}; + this._overrodeFocusMethod = true; + } + } else if (element.hasAttribute('tabindex')) { + this._savedTabIndex = /** @type {!HTMLElement} */element.tabIndex; + element.removeAttribute('tabindex'); + } + } + + /** + * Add another inert root to this inert node's set of managing inert roots. + * @param {!InertRoot} inertRoot + */ + + }, { + key: 'addInertRoot', + value: function addInertRoot(inertRoot) { + this._throwIfDestroyed(); + this._inertRoots.add(inertRoot); + } + + /** + * Remove the given inert root from this inert node's set of managing inert roots. + * If the set of managing inert roots becomes empty, this node is no longer inert, + * so the object should be destroyed. + * @param {!InertRoot} inertRoot + */ + + }, { + key: 'removeInertRoot', + value: function removeInertRoot(inertRoot) { + this._throwIfDestroyed(); + this._inertRoots['delete'](inertRoot); + if (this._inertRoots.size === 0) { + this.destructor(); + } + } + }, { + key: 'destroyed', + get: function get() { + return (/** @type {!InertNode} */this._destroyed + ); + } + }, { + key: 'hasSavedTabIndex', + get: function get() { + return this._savedTabIndex !== null; + } + + /** @return {!Node} */ + + }, { + key: 'node', + get: function get() { + this._throwIfDestroyed(); + return this._node; + } + + /** @param {?number} tabIndex */ + + }, { + key: 'savedTabIndex', + set: function set(tabIndex) { + this._throwIfDestroyed(); + this._savedTabIndex = tabIndex; + } + + /** @return {?number} */ + , + get: function get() { + this._throwIfDestroyed(); + return this._savedTabIndex; + } + }]); + + return InertNode; + }(); + + /** + * InertManager is a per-document singleton object which manages all inert roots and nodes. + * + * When an element becomes an inert root by having an `inert` attribute set and/or its `inert` + * property set to `true`, the `setInert` method creates an `InertRoot` object for the element. + * The `InertRoot` in turn registers itself as managing all of the element's focusable descendant + * nodes via the `register()` method. The `InertManager` ensures that a single `InertNode` instance + * is created for each such node, via the `_managedNodes` map. + */ + + + var InertManager = function () { + /** + * @param {!Document} document + */ + function InertManager(document) { + _classCallCheck(this, InertManager); + + if (!document) { + throw new Error('Missing required argument; InertManager needs to wrap a document.'); + } + + /** @type {!Document} */ + this._document = document; + + /** + * All managed nodes known to this InertManager. In a map to allow looking up by Node. + * @type {!Map} + */ + this._managedNodes = new Map(); + + /** + * All inert roots known to this InertManager. In a map to allow looking up by Node. + * @type {!Map} + */ + this._inertRoots = new Map(); + + /** + * Observer for mutations on `document.body`. + * @type {!MutationObserver} + */ + this._observer = new MutationObserver(this._watchForInert.bind(this)); + + // Add inert style. + addInertStyle(document.head || document.body || document.documentElement); + + // Wait for document to be loaded. + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', this._onDocumentLoaded.bind(this)); + } else { + this._onDocumentLoaded(); + } + } + + /** + * Set whether the given element should be an inert root or not. + * @param {!Element} root + * @param {boolean} inert + */ + + + _createClass(InertManager, [{ + key: 'setInert', + value: function setInert(root, inert) { + if (inert) { + if (this._inertRoots.has(root)) { + // element is already inert + return; + } + + var inertRoot = new InertRoot(root, this); + root.setAttribute('inert', ''); + this._inertRoots.set(root, inertRoot); + // If not contained in the document, it must be in a shadowRoot. + // Ensure inert styles are added there. + if (!this._document.body.contains(root)) { + var parent = root.parentNode; + while (parent) { + if (parent.nodeType === 11) { + addInertStyle(parent); + } + parent = parent.parentNode; + } + } + } else { + if (!this._inertRoots.has(root)) { + // element is already non-inert + return; + } + + var _inertRoot = this._inertRoots.get(root); + _inertRoot.destructor(); + this._inertRoots['delete'](root); + root.removeAttribute('inert'); + } + } + + /** + * Get the InertRoot object corresponding to the given inert root element, if any. + * @param {!Node} element + * @return {!InertRoot|undefined} + */ + + }, { + key: 'getInertRoot', + value: function getInertRoot(element) { + return this._inertRoots.get(element); + } + + /** + * Register the given InertRoot as managing the given node. + * In the case where the node has a previously existing inert root, this inert root will + * be added to its set of inert roots. + * @param {!Node} node + * @param {!InertRoot} inertRoot + * @return {!InertNode} inertNode + */ + + }, { + key: 'register', + value: function register(node, inertRoot) { + var inertNode = this._managedNodes.get(node); + if (inertNode !== undefined) { + // node was already in an inert subtree + inertNode.addInertRoot(inertRoot); + } else { + inertNode = new InertNode(node, inertRoot); + } + + this._managedNodes.set(node, inertNode); + + return inertNode; + } + + /** + * De-register the given InertRoot as managing the given inert node. + * Removes the inert root from the InertNode's set of managing inert roots, and remove the inert + * node from the InertManager's set of managed nodes if it is destroyed. + * If the node is not currently managed, this is essentially a no-op. + * @param {!Node} node + * @param {!InertRoot} inertRoot + * @return {?InertNode} The potentially destroyed InertNode associated with this node, if any. + */ + + }, { + key: 'deregister', + value: function deregister(node, inertRoot) { + var inertNode = this._managedNodes.get(node); + if (!inertNode) { + return null; + } + + inertNode.removeInertRoot(inertRoot); + if (inertNode.destroyed) { + this._managedNodes['delete'](node); + } + + return inertNode; + } + + /** + * Callback used when document has finished loading. + */ + + }, { + key: '_onDocumentLoaded', + value: function _onDocumentLoaded() { + // Find all inert roots in document and make them actually inert. + var inertElements = slice.call(this._document.querySelectorAll('[inert]')); + inertElements.forEach(function (inertElement) { + this.setInert(inertElement, true); + }, this); + + // Comment this out to use programmatic API only. + this._observer.observe(this._document.body, { attributes: true, subtree: true, childList: true }); + } + + /** + * Callback used when mutation observer detects attribute changes. + * @param {!Array} records + * @param {!MutationObserver} self + */ + + }, { + key: '_watchForInert', + value: function _watchForInert(records, self) { + var _this = this; + records.forEach(function (record) { + switch (record.type) { + case 'childList': + slice.call(record.addedNodes).forEach(function (node) { + if (node.nodeType !== Node.ELEMENT_NODE) { + return; + } + var inertElements = slice.call(node.querySelectorAll('[inert]')); + if (matches.call(node, '[inert]')) { + inertElements.unshift(node); + } + inertElements.forEach(function (inertElement) { + this.setInert(inertElement, true); + }, _this); + }, _this); + break; + case 'attributes': + if (record.attributeName !== 'inert') { + return; + } + var target = /** @type {!Element} */record.target; + var inert = target.hasAttribute('inert'); + _this.setInert(target, inert); + break; + } + }, this); + } + }]); + + return InertManager; + }(); + + /** + * Recursively walk the composed tree from |node|. + * @param {!Node} node + * @param {(function (!Element))=} callback Callback to be called for each element traversed, + * before descending into child nodes. + * @param {?ShadowRoot=} shadowRootAncestor The nearest ShadowRoot ancestor, if any. + */ + + + function composedTreeWalk(node, callback, shadowRootAncestor) { + if (node.nodeType == Node.ELEMENT_NODE) { + var element = /** @type {!Element} */node; + if (callback) { + callback(element); + } + + // Descend into node: + // If it has a ShadowRoot, ignore all child elements - these will be picked + // up by the or elements. Descend straight into the + // ShadowRoot. + var shadowRoot = /** @type {!HTMLElement} */element.shadowRoot; + if (shadowRoot) { + composedTreeWalk(shadowRoot, callback, shadowRoot); + return; + } + + // If it is a element, descend into distributed elements - these + // are elements from outside the shadow root which are rendered inside the + // shadow DOM. + if (element.localName == 'content') { + var content = /** @type {!HTMLContentElement} */element; + // Verifies if ShadowDom v0 is supported. + var distributedNodes = content.getDistributedNodes ? content.getDistributedNodes() : []; + for (var i = 0; i < distributedNodes.length; i++) { + composedTreeWalk(distributedNodes[i], callback, shadowRootAncestor); + } + return; + } + + // If it is a element, descend into assigned nodes - these + // are elements from outside the shadow root which are rendered inside the + // shadow DOM. + if (element.localName == 'slot') { + var slot = /** @type {!HTMLSlotElement} */element; + // Verify if ShadowDom v1 is supported. + var _distributedNodes = slot.assignedNodes ? slot.assignedNodes({ flatten: true }) : []; + for (var _i = 0; _i < _distributedNodes.length; _i++) { + composedTreeWalk(_distributedNodes[_i], callback, shadowRootAncestor); + } + return; + } + } + + // If it is neither the parent of a ShadowRoot, a element, a + // element, nor a element recurse normally. + var child = node.firstChild; + while (child != null) { + composedTreeWalk(child, callback, shadowRootAncestor); + child = child.nextSibling; + } + } + + /** + * Adds a style element to the node containing the inert specific styles + * @param {!Node} node + */ + function addInertStyle(node) { + if (node.querySelector('style#inert-style')) { + return; + } + var style = document.createElement('style'); + style.setAttribute('id', 'inert-style'); + style.textContent = '\n' + '[inert] {\n' + ' pointer-events: none;\n' + ' cursor: default;\n' + '}\n' + '\n' + '[inert], [inert] * {\n' + ' user-select: none;\n' + ' -webkit-user-select: none;\n' + ' -moz-user-select: none;\n' + ' -ms-user-select: none;\n' + '}\n'; + node.appendChild(style); + } + + /** @type {!InertManager} */ + var inertManager = new InertManager(document); + + if (!Element.prototype.hasOwnProperty('inert')) { + Object.defineProperty(Element.prototype, 'inert', { + enumerable: true, + /** @this {!Element} */ + get: function get() { + return this.hasAttribute('inert'); + }, + /** @this {!Element} */ + set: function set(inert) { + inertManager.setInert(this, inert); + } + }); + } + +}))); diff --git a/plugins/wporg-5ftf/assets/js/inert.min.js b/plugins/wporg-5ftf/assets/js/inert.min.js new file mode 100644 index 0000000..9a70caf --- /dev/null +++ b/plugins/wporg-5ftf/assets/js/inert.min.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t():"function"==typeof define&&define.amd?define("inert",t):t()}(0,function(){"use strict";var e=function(){function i(e,t){for(var n=0;nemail us for help.', 'wporg-5ftf' ), + __( 'That\'s not the address that we have for this pledge. If you don\'t know the email associated with this pledge, please contact us for help.', 'wporg-5ftf' ), get_permalink( get_page_by_path( 'report' ) ) ); diff --git a/plugins/wporg-5ftf/includes/pledge-form.php b/plugins/wporg-5ftf/includes/pledge-form.php index 18d2a03..69d7596 100755 --- a/plugins/wporg-5ftf/includes/pledge-form.php +++ b/plugins/wporg-5ftf/includes/pledge-form.php @@ -14,7 +14,6 @@ defined( 'WPINC' ) || die(); // Todo make this into simple optionless blocks instead? add_shortcode( '5ftf_pledge_form_new', __NAMESPACE__ . '\render_form_new' ); add_shortcode( '5ftf_pledge_form_manage', __NAMESPACE__ . '\render_form_manage' ); -add_shortcode( '5ftf_pledge_form_manage_link', __NAMESPACE__ . '\render_manage_link_request' ); /** * Render the form(s) for creating new pledges. @@ -175,19 +174,6 @@ function render_form_manage() { return ob_get_clean(); } - -/** - * Render the `render_manage_link_request` shortcode. - */ -function render_manage_link_request() { - // @todo enable when https://github.com/WordPress/five-for-the-future/issues/6 is done - if ( ! defined( 'WPORG_SANDBOXED' ) || ! WPORG_SANDBOXED ) { - return; - } - - require_once FiveForTheFuture\get_views_path() . 'form-pledge-request-manage-link.php'; -} - /** * Process a submission from the Manage Existing Pledge form. * diff --git a/plugins/wporg-5ftf/includes/pledge.php b/plugins/wporg-5ftf/includes/pledge.php index 1acf7ef..a54af34 100755 --- a/plugins/wporg-5ftf/includes/pledge.php +++ b/plugins/wporg-5ftf/includes/pledge.php @@ -18,12 +18,19 @@ const SLUG = 'pledge'; const SLUG_PL = 'pledges'; const CPT_ID = FiveForTheFuture\PREFIX . '_' . SLUG; -add_action( 'init', __NAMESPACE__ . '\register', 0 ); -add_action( 'admin_menu', __NAMESPACE__ . '\admin_menu' ); +// Admin hooks. +add_action( 'init', __NAMESPACE__ . '\register', 0 ); +add_action( 'admin_menu', __NAMESPACE__ . '\admin_menu' ); add_action( 'pre_get_posts', __NAMESPACE__ . '\filter_query' ); +// List table columns. add_filter( 'manage_edit-' . CPT_ID . '_columns', __NAMESPACE__ . '\add_list_table_columns' ); add_action( 'manage_' . CPT_ID . '_posts_custom_column', __NAMESPACE__ . '\populate_list_table_columns', 10, 2 ); +// Front end hooks. +add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\enqueue_assets' ); +add_action( 'pledge_footer', __NAMESPACE__ . '\render_manage_link_request' ); +add_action( 'wp_footer', __NAMESPACE__ . '\render_js_templates' ); + /** * Register all the things. * @@ -252,3 +259,56 @@ function filter_query( $query ) { // see https://github.com/WordPress/five-for-the-future/issues/70#issuecomment-549066883. $query->set( 'posts_per_page', 100 ); } + +/** + * Enqueue assets for front-end management. + * + * @return void + */ +function enqueue_assets() { + wp_register_script( 'wicg-inert', plugins_url( 'assets/js/inert.min.js', __DIR__ ), [], '3.0.0', true ); + + if ( CPT_ID === get_post_type() ) { + $ver = filemtime( FiveForTheFuture\PATH . '/assets/js/frontend.js' ); + wp_enqueue_script( '5ftf-frontend', plugins_url( 'assets/js/frontend.js', __DIR__ ), [ 'jquery', 'wp-a11y', 'wp-util', 'wicg-inert' ], $ver, true ); + + $script_data = [ + 'ajaxurl' => admin_url( 'admin-ajax.php', 'relative' ), // The global ajaxurl is not set on the frontend. + 'pledgeId' => get_the_ID(), + 'ajaxNonce' => wp_create_nonce( 'send-manage-email' ), + ]; + wp_add_inline_script( + '5ftf-frontend', + sprintf( + 'var FiveForTheFuture = JSON.parse( decodeURIComponent( \'%s\' ) );', + rawurlencode( wp_json_encode( $script_data ) ) + ), + 'before' + ); + } +} + +/** + * Render the button to toggle the "Request Manage Email" dialog. + * + * @return void + */ +function render_manage_link_request() { + // @todo enable when https://github.com/WordPress/five-for-the-future/issues/6 is done + if ( ! defined( 'WPORG_SANDBOXED' ) || ! WPORG_SANDBOXED ) { + return; + } + + require_once FiveForTheFuture\get_views_path() . 'button-request-manage-link.php'; +} + +/** + * Render JS templates at the end of the page. + * + * @return void + */ +function render_js_templates() { + if ( CPT_ID === get_post_type() ) { + require_once FiveForTheFuture\get_views_path() . 'modal-request-manage-link.php'; + } +} diff --git a/plugins/wporg-5ftf/views/button-request-manage-link.php b/plugins/wporg-5ftf/views/button-request-manage-link.php new file mode 100644 index 0000000..d546764 --- /dev/null +++ b/plugins/wporg-5ftf/views/button-request-manage-link.php @@ -0,0 +1,13 @@ + +

+ +
diff --git a/plugins/wporg-5ftf/views/form-pledge-request-manage-link.php b/plugins/wporg-5ftf/views/form-pledge-request-manage-link.php deleted file mode 100644 index d0a94ad..0000000 --- a/plugins/wporg-5ftf/views/form-pledge-request-manage-link.php +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - diff --git a/plugins/wporg-5ftf/views/modal-request-manage-link.php b/plugins/wporg-5ftf/views/modal-request-manage-link.php new file mode 100644 index 0000000..ace5eab --- /dev/null +++ b/plugins/wporg-5ftf/views/modal-request-manage-link.php @@ -0,0 +1,44 @@ + + diff --git a/themes/wporg-5ftf/css/components/_components.scss b/themes/wporg-5ftf/css/components/_components.scss index 0958305..b879cfb 100644 --- a/themes/wporg-5ftf/css/components/_components.scss +++ b/themes/wporg-5ftf/css/components/_components.scss @@ -27,6 +27,7 @@ @import "page"; @import "pagination"; @import "pledge-contributors"; +@import "pledge-dialog"; @import "pledge-list"; @import "pledge-single"; @import "search-form"; diff --git a/themes/wporg-5ftf/css/components/_pledge-dialog.scss b/themes/wporg-5ftf/css/components/_pledge-dialog.scss new file mode 100644 index 0000000..83d31a4 --- /dev/null +++ b/themes/wporg-5ftf/css/components/_pledge-dialog.scss @@ -0,0 +1,77 @@ +.pledge-dialog { + position: absolute; + z-index: 100; + width: 300px; + margin-top: 20px; + padding: 20px; + background: #fff; + border: 1px solid $color-gray-light-400; + box-shadow: rgba($color-gray-900, 0.1) 0 3px 30px; + font-size: ms(-3); + + &[hidden] { + display: none; + } + + &:before, + &:after { + content: ''; + position: absolute; + top: -11px; + right: 40px; + border-style: solid; + border-color: transparent transparent #fff; + border-width: 0 12px 12px; + } + + &:before { + top: -12px; + border-bottom-color: $color-gray-light-400; + } + + label { + display: inline-block; + margin-bottom: 4px; + font-size: ms(-4); + font-weight: bold; + color: $color-gray-500; + } + + input[type="email"] { + display: block; + width: 100%; + margin: 0 0 20px 0; + padding: ms(-7) ms(-2); + border: 1px solid $color-gray-100; + box-shadow: none; + } + + input[type="submit"] { + font-size: ms(-3); + } + + .notice p { + font-size: 1em; + } + + .pledge-dialog__close { + position: absolute; + top: 0; + right: 0; + padding: 10px; + text-decoration: none; + } +} + +.pledge-dialog__background { + position: fixed; + z-index: 99; + top: 0; + left: 0; + bottom: 0; + right: 0; + + &[hidden] { + display: none; + } +} diff --git a/themes/wporg-5ftf/css/components/_pledge-single.scss b/themes/wporg-5ftf/css/components/_pledge-single.scss index 53ec1b8..223a6ca 100644 --- a/themes/wporg-5ftf/css/components/_pledge-single.scss +++ b/themes/wporg-5ftf/css/components/_pledge-single.scss @@ -51,54 +51,43 @@ body.single.single-5ftf_pledge { .entry-footer { margin-top: ms(2); + margin-bottom: ms(20); border-top: 1px solid $color-gray-light-500; padding-top: ms(2); .report-wrapper { margin-bottom: 1em; + + a { + color: $color-alert-red; + } + } + + .edit-pledge-wrapper { + text-align: right; + + p { + font-size: ms(-2); + } + + .button { + padding-right: 4px; + text-decoration: none; + color: $color__wp-blue; + font-size: 1rem; + + &:hover { + border-bottom: 1px solid currentColor; + } + } } @include breakpoint( $breakpoint-tablet ) { - display: grid; - grid-template-columns: 50% 50%; + display: flex; - #toggle-management-link-form, - #request-management-link { - float: right; - clear: both; + .report-wrapper { + flex: 1 0 auto; } } } - - #request-management-link { - width: 300px; - border: 1px solid #ebeced; // setup var - box-shadow: rgba(25, 30, 35, 0.1) 0 3px 30px; - margin-top: 20px; - padding: 15px; - - // todo add the upper ^ carot thing - // border-radius or something to draw triangle - // element inside the box, but then abs position outside it? - // z-index above parent so it's above box-shadow - probably happens by default? - - p:first-child { - margin-top: 0; - } - - input[type="email"] { - width: 100%; - margin: 0 0 20px 0; - } - - input[type="submit"] { - float: right; - clear: both; - margin-bottom: 20px; - } - - #form-messages { - clear: both; - } - } } diff --git a/themes/wporg-5ftf/css/settings/_colors.scss b/themes/wporg-5ftf/css/settings/_colors.scss index c6db87a..8b1cd96 100644 --- a/themes/wporg-5ftf/css/settings/_colors.scss +++ b/themes/wporg-5ftf/css/settings/_colors.scss @@ -30,6 +30,7 @@ $color-gray-light-200: lighten($color-base-gray,74%); $color-gray-light-100: lighten($color-base-gray,77%); $color-error-red: #c92c2c; +$color-alert-red: #d94f4f; // Theme colors $color__background-input: $color-gray-light-200; diff --git a/themes/wporg-5ftf/single-5ftf_pledge.php b/themes/wporg-5ftf/single-5ftf_pledge.php index eb0ea51..0213bb7 100644 --- a/themes/wporg-5ftf/single-5ftf_pledge.php +++ b/themes/wporg-5ftf/single-5ftf_pledge.php @@ -55,7 +55,10 @@ get_header();

- { META_PREFIX . 'org-description' } ) ); ?> + { META_PREFIX . 'org-description' } ) ); + ?>
@@ -130,9 +133,7 @@ get_header();
-
- -
+