From 392dfd024c505f5ae1bbb2f0d3e0793c251a1f35 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Wed, 13 Jun 2018 00:02:55 +1000 Subject: Utils broken down into seperate files and exports --- src/js/utils/elements.js | 307 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 src/js/utils/elements.js (limited to 'src/js/utils/elements.js') diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js new file mode 100644 index 00000000..4d4f97cd --- /dev/null +++ b/src/js/utils/elements.js @@ -0,0 +1,307 @@ +// ========================================================================== +// Element utils +// ========================================================================== + +import { off, on } from './events'; +import is from './is'; + +// Wrap an element +export function wrap(elements, wrapper) { + // Convert `elements` to an array, if necessary. + const targets = elements.length ? elements : [elements]; + + // Loops backwards to prevent having to clone the wrapper on the + // first element (see `child` below). + Array.from(targets) + .reverse() + .forEach((element, index) => { + const child = index > 0 ? wrapper.cloneNode(true) : wrapper; + + // Cache the current parent and sibling. + const parent = element.parentNode; + const sibling = element.nextSibling; + + // Wrap the element (is automatically removed from its current + // parent). + child.appendChild(element); + + // If the element had a sibling, insert the wrapper before + // the sibling to maintain the HTML structure; otherwise, just + // append it to the parent. + if (sibling) { + parent.insertBefore(child, sibling); + } else { + parent.appendChild(child); + } + }); +} + +// Set attributes +export function setAttributes(element, attributes) { + if (!is.element(element) || is.empty(attributes)) { + return; + } + + Object.entries(attributes).forEach(([ + key, + value, + ]) => { + element.setAttribute(key, value); + }); +} + +// Create a DocumentFragment +export function createElement(type, attributes, text) { + // Create a new + const element = document.createElement(type); + + // Set all passed attributes + if (is.object(attributes)) { + setAttributes(element, attributes); + } + + // Add text node + if (is.string(text)) { + element.innerText = text; + } + + // Return built element + return element; +} + +// Inaert an element after another +export function insertAfter(element, target) { + target.parentNode.insertBefore(element, target.nextSibling); +} + +// Insert a DocumentFragment +export function insertElement(type, parent, attributes, text) { + // Inject the new + parent.appendChild(createElement(type, attributes, text)); +} + +// Remove element(s) +export function removeElement(element) { + if (is.nodeList(element) || is.array(element)) { + Array.from(element).forEach(removeElement); + return; + } + + if (!is.element(element) || !is.element(element.parentNode)) { + return; + } + + element.parentNode.removeChild(element); +} + +// Remove all child elements +export function emptyElement(element) { + let { length } = element.childNodes; + + while (length > 0) { + element.removeChild(element.lastChild); + length -= 1; + } +} + +// Replace element +export function replaceElement(newChild, oldChild) { + if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) { + return null; + } + + oldChild.parentNode.replaceChild(newChild, oldChild); + + return newChild; +} + +// Get an attribute object from a string selector +export function getAttributesFromSelector(sel, existingAttributes) { + // For example: + // '.test' to { class: 'test' } + // '#test' to { id: 'test' } + // '[data-test="test"]' to { 'data-test': 'test' } + + if (!is.string(sel) || is.empty(sel)) { + return {}; + } + + const attributes = {}; + const existing = existingAttributes; + + sel.split(',').forEach(s => { + // Remove whitespace + const selector = s.trim(); + const className = selector.replace('.', ''); + const stripped = selector.replace(/[[\]]/g, ''); + + // Get the parts and value + const parts = stripped.split('='); + const key = parts[0]; + const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : ''; + + // Get the first character + const start = selector.charAt(0); + + switch (start) { + case '.': + // Add to existing classname + if (is.object(existing) && is.string(existing.class)) { + existing.class += ` ${className}`; + } + + attributes.class = className; + break; + + case '#': + // ID selector + attributes.id = selector.replace('#', ''); + break; + + case '[': + // Attribute selector + attributes[key] = value; + + break; + + default: + break; + } + }); + + return attributes; +} + +// Toggle hidden +export function toggleHidden(element, hidden) { + if (!is.element(element)) { + return; + } + + let hide = hidden; + + if (!is.boolean(hide)) { + hide = !element.hasAttribute('hidden'); + } + + if (hide) { + element.setAttribute('hidden', ''); + } else { + element.removeAttribute('hidden'); + } +} + +// Mirror Element.classList.toggle, with IE compatibility for "force" argument +export function toggleClass(element, className, force) { + if (is.element(element)) { + let method = 'toggle'; + if (typeof force !== 'undefined') { + method = force ? 'add' : 'remove'; + } + + element.classList[method](className); + return element.classList.contains(className); + } + + return null; +} + +// Has class name +export function hasClass(element, className) { + return is.element(element) && element.classList.contains(className); +} + +// Element matches selector +export function matches(element, selector) { + const prototype = { Element }; + + function match() { + return Array.from(document.querySelectorAll(selector)).includes(this); + } + + const matches = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match; + + return matches.call(element, selector); +} + +// Find all elements +export function getElements(selector) { + return this.elements.container.querySelectorAll(selector); +} + +// Find a single element +export function getElement(selector) { + return this.elements.container.querySelector(selector); +} + +// Get the focused element +export function getFocusElement() { + let focused = document.activeElement; + + if (!focused || focused === document.body) { + focused = null; + } else { + focused = document.querySelector(':focus'); + } + + return focused; +} + +// Trap focus inside container +export function trapFocus(element = null, toggle = false) { + if (!is.element(element)) { + return; + } + + const focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]'); + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + + const trap = event => { + // Bail if not tab key or not fullscreen + if (event.key !== 'Tab' || event.keyCode !== 9) { + return; + } + + // Get the current focused element + const focused = getFocusElement(); + + if (focused === last && !event.shiftKey) { + // Move focus to first element that can be tabbed if Shift isn't used + first.focus(); + event.preventDefault(); + } else if (focused === first && event.shiftKey) { + // Move focus to last element that can be tabbed if Shift is used + last.focus(); + event.preventDefault(); + } + }; + + if (toggle) { + on(this.elements.container, 'keydown', trap, false); + } else { + off(this.elements.container, 'keydown', trap, false); + } +} + +// Toggle aria-pressed state on a toggle button +// http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles +export function toggleState(element, input) { + // If multiple elements passed + if (is.array(element) || is.nodeList(element)) { + Array.from(element).forEach(target => toggleState(target, input)); + return; + } + + // Bail if no target + if (!is.element(element)) { + return; + } + + // Get state + const pressed = element.getAttribute('aria-pressed') === 'true'; + const state = is.boolean(input) ? input : !pressed; + + // Set the attribute on target + element.setAttribute('aria-pressed', state); +} -- cgit v1.2.3 From 53933dff7eaa8074e57cccc7cab18d5be6c83fc4 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Tue, 12 Jun 2018 19:39:26 +0200 Subject: Use toggleListener in trapFocus --- src/js/utils/elements.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'src/js/utils/elements.js') diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 4d4f97cd..55866367 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -2,7 +2,7 @@ // Element utils // ========================================================================== -import { off, on } from './events'; +import { toggleListener } from './events'; import is from './is'; // Wrap an element @@ -277,11 +277,7 @@ export function trapFocus(element = null, toggle = false) { } }; - if (toggle) { - on(this.elements.container, 'keydown', trap, false); - } else { - off(this.elements.container, 'keydown', trap, false); - } + toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false); } // Toggle aria-pressed state on a toggle button -- cgit v1.2.3 From 6bff6b317d6adcd9f94c8d4d8ee225d39f784e0f Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Wed, 13 Jun 2018 22:18:57 +0200 Subject: Remove line breaks in arrays --- src/js/utils/elements.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'src/js/utils/elements.js') diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 55866367..39b944d2 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -42,10 +42,7 @@ export function setAttributes(element, attributes) { return; } - Object.entries(attributes).forEach(([ - key, - value, - ]) => { + Object.entries(attributes).forEach(([key, value]) => { element.setAttribute(key, value); }); } -- cgit v1.2.3 From f1c4752036f58e01df95c30cc9cba4156a0737cd Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Fri, 15 Jun 2018 22:52:19 +0200 Subject: Filter out null / undefined in elements.setAttributes --- src/js/utils/elements.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'src/js/utils/elements.js') diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 39b944d2..2d314ed8 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -42,9 +42,11 @@ export function setAttributes(element, attributes) { return; } - Object.entries(attributes).forEach(([key, value]) => { - element.setAttribute(key, value); - }); + // Assume null and undefined attributes should be left out, + // Setting them would otherwise convert them to "null" and "undefined" + Object.entries(attributes) + .filter(([, value]) => !is.nullOrUndefined(value)) + .forEach(([key, value]) => element.setAttribute(key, value)); } // Create a DocumentFragment -- cgit v1.2.3 From d4abb4b1438cb316aacae480e7b7e9b055a60b24 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 17 Jun 2018 01:04:55 +1000 Subject: 120 line width, package upgrade --- src/js/utils/elements.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'src/js/utils/elements.js') diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index 2d314ed8..19e98f6f 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -218,7 +218,12 @@ export function matches(element, selector) { return Array.from(document.querySelectorAll(selector)).includes(this); } - const matches = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match; + const matches = + prototype.matches || + prototype.webkitMatchesSelector || + prototype.mozMatchesSelector || + prototype.msMatchesSelector || + match; return matches.call(element, selector); } -- cgit v1.2.3