diff --git a/javascript/cable_ready.js b/javascript/cable_ready.js index adc1d9dd..7dec065c 100644 --- a/javascript/cable_ready.js +++ b/javascript/cable_ready.js @@ -27,7 +27,7 @@ const perform = ( } if (operation.element || options.emitMissingElementWarnings) { activeElement.set(document.activeElement) - DOMOperations[name](operation) + DOMOperations[name](operation, name) } } catch (e) { if (operation.element) { diff --git a/javascript/operations.js b/javascript/operations.js index e9d968f2..d169cb40 100644 --- a/javascript/operations.js +++ b/javascript/operations.js @@ -1,82 +1,92 @@ import morphdom from 'morphdom' import { shouldMorph, didMorph } from './morph_callbacks' -import { assignFocus, dispatch, getClassNames, processElements } from './utils' +import { + assignFocus, + dispatch, + getClassNames, + processElements, + before, + after, + operate +} from './utils' export default { // DOM Mutations - append: operation => { + append: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-append', operation) - const { html, focusSelector } = operation - if (!operation.cancel) { + before(element, callee, operation) + operate(operation, () => { + const { html, focusSelector } = operation element.insertAdjacentHTML('beforeend', html || '') assignFocus(focusSelector) - } - dispatch(element, 'cable-ready:after-append', operation) + }) + after(element, callee, operation) }) }, - graft: operation => { + graft: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-graft', operation) - const { parent, focusSelector } = operation - const parentElement = document.querySelector(parent) - if (!operation.cancel && parentElement) { - parentElement.appendChild(element) - assignFocus(focusSelector) - } - dispatch(element, 'cable-ready:after-graft', operation) + before(element, callee, operation) + operate(operation, () => { + const { parent, focusSelector } = operation + const parentElement = document.querySelector(parent) + if (parentElement) { + parentElement.appendChild(element) + assignFocus(focusSelector) + } + }) + after(element, callee, operation) }) }, - innerHtml: operation => { + innerHtml: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-inner-html', operation) - const { html, focusSelector } = operation - if (!operation.cancel) { + before(element, callee, operation) + operate(operation, () => { + const { html, focusSelector } = operation element.innerHTML = html || '' assignFocus(focusSelector) - } - dispatch(element, 'cable-ready:after-inner-html', operation) + }) + after(element, callee, operation) }) }, - insertAdjacentHtml: operation => { + insertAdjacentHtml: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-insert-adjacent-html', operation) - const { html, position, focusSelector } = operation - if (!operation.cancel) { + before(element, callee, operation) + operate(operation, () => { + const { html, position, focusSelector } = operation element.insertAdjacentHTML(position || 'beforeend', html || '') assignFocus(focusSelector) - } - dispatch(element, 'cable-ready:after-insert-adjacent-html', operation) + }) + after(element, callee, operation) }) }, - insertAdjacentText: operation => { + insertAdjacentText: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-insert-adjacent-text', operation) - const { text, position, focusSelector } = operation - if (!operation.cancel) { + before(element, callee, operation) + operate(operation, () => { + const { text, position, focusSelector } = operation element.insertAdjacentText(position || 'beforeend', text || '') assignFocus(focusSelector) - } - dispatch(element, 'cable-ready:after-insert-adjacent-text', operation) + }) + after(element, callee, operation) }) }, - morph: operation => { + morph: (operation, callee) => { processElements(operation, element => { const { html } = operation const template = document.createElement('template') template.innerHTML = String(html).trim() operation.content = template.content - dispatch(element, 'cable-ready:before-morph', operation) - const { childrenOnly, focusSelector } = operation const parent = element.parentElement const ordinal = Array.from(parent.children).indexOf(element) - if (!operation.cancel) { + before(element, callee, operation) + operate(operation, () => { + const { childrenOnly, focusSelector } = operation morphdom( element, childrenOnly ? template.content : template.innerHTML, @@ -87,177 +97,192 @@ export default { } ) assignFocus(focusSelector) - } - dispatch(parent.children[ordinal], 'cable-ready:after-morph', operation) + }) + after(parent.children[ordinal], callee, operation) }) }, - outerHtml: operation => { + outerHtml: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-outer-html', operation) - const { html, focusSelector } = operation const parent = element.parentElement const ordinal = Array.from(parent.children).indexOf(element) - if (!operation.cancel) { + before(element, callee, operation) + operate(operation, () => { + const { html, focusSelector } = operation element.outerHTML = html || '' assignFocus(focusSelector) - } - dispatch( - parent.children[ordinal], - 'cable-ready:after-outer-html', - operation - ) + }) + after(parent.children[ordinal], callee, operation) }) }, - prepend: operation => { + prepend: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-prepend', operation) - const { html, focusSelector } = operation - if (!operation.cancel) { + before(element, callee, operation) + operate(operation, () => { + const { html, focusSelector } = operation element.insertAdjacentHTML('afterbegin', html || '') assignFocus(focusSelector) - } - dispatch(element, 'cable-ready:after-prepend', operation) + }) + after(element, callee, operation) }) }, - remove: operation => { + remove: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-remove', operation) - const { focusSelector } = operation - if (!operation.cancel) { + before(element, callee, operation) + operate(operation, () => { + const { focusSelector } = operation element.remove() assignFocus(focusSelector) - } - dispatch(document, 'cable-ready:after-remove', operation) + }) + after(document, callee, operation) }) }, - replace: operation => { + replace: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-replace', operation) - const { html, focusSelector } = operation const parent = element.parentElement const ordinal = Array.from(parent.children).indexOf(element) - if (!operation.cancel) { + before(element, callee, operation) + operate(operation, () => { + const { html, focusSelector } = operation element.outerHTML = html || '' assignFocus(focusSelector) - } - dispatch(parent.children[ordinal], 'cable-ready:after-replace', operation) + }) + after(parent.children[ordinal], callee, operation) }) }, - textContent: operation => { + textContent: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-text-content', operation) - const { text, focusSelector } = operation - if (!operation.cancel) { + before(element, callee, operation) + operate(operation, () => { + const { text, focusSelector } = operation element.textContent = text || '' assignFocus(focusSelector) - } - dispatch(element, 'cable-ready:after-text-content', operation) + }) + after(element, callee, operation) }) }, // Element Property Mutations - addCssClass: operation => { + addCssClass: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-add-css-class', operation) - const { name } = operation - if (!operation.cancel) element.classList.add(...getClassNames(name || '')) - dispatch(element, 'cable-ready:after-add-css-class', operation) + before(element, callee, operation) + operate(operation, () => { + const { name } = operation + element.classList.add(...getClassNames(name || '')) + }) + after(element, callee, operation) }) }, - removeAttribute: operation => { + removeAttribute: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-remove-attribute', operation) - const { name } = operation - if (!operation.cancel) element.removeAttribute(name) - dispatch(element, 'cable-ready:after-remove-attribute', operation) + before(element, callee, operation) + operate(operation, () => { + const { name } = operation + element.removeAttribute(name) + }) + after(element, callee, operation) }) }, - removeCssClass: operation => { + removeCssClass: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-remove-css-class', operation) - const { name } = operation - if (!operation.cancel) element.classList.remove(...getClassNames(name)) - dispatch(element, 'cable-ready:after-remove-css-class', operation) + before(element, callee, operation) + operate(operation, () => { + const { name } = operation + element.classList.remove(...getClassNames(name)) + }) + after(element, callee, operation) }) }, - setAttribute: operation => { + setAttribute: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-set-attribute', operation) - const { name, value } = operation - if (!operation.cancel) element.setAttribute(name, value || '') - dispatch(element, 'cable-ready:after-set-attribute', operation) + before(element, callee, operation) + operate(operation, () => { + const { name, value } = operation + element.setAttribute(name, value || '') + }) + after(element, callee, operation) }) }, - setDatasetProperty: operation => { + setDatasetProperty: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-set-dataset-property', operation) - const { name, value } = operation - if (!operation.cancel) element.dataset[name] = value || '' - dispatch(element, 'cable-ready:after-set-dataset-property', operation) + before(element, callee, operation) + operate(operation, () => { + const { name, value } = operation + element.dataset[name] = value || '' + }) + after(element, callee, operation) }) }, - setProperty: operation => { + setProperty: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-set-property', operation) - const { name, value } = operation - if (!operation.cancel && name in element) element[name] = value || '' - dispatch(element, 'cable-ready:after-set-property', operation) + before(element, callee, operation) + operate(operation, () => { + const { name, value } = operation + if (name in element) element[name] = value || '' + }) + after(element, callee, operation) }) }, - setStyle: operation => { + setStyle: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-set-style', operation) - const { name, value } = operation - if (!operation.cancel) element.style[name] = value || '' - dispatch(element, 'cable-ready:after-set-style', operation) + before(element, callee, operation) + operate(operation, () => { + const { name, value } = operation + element.style[name] = value || '' + }) + after(element, callee, operation) }) }, - setStyles: operation => { + setStyles: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-set-styles', operation) - const { styles } = operation - for (let [name, value] of Object.entries(styles)) { - if (!operation.cancel) element.style[name] = value || '' - } - dispatch(element, 'cable-ready:after-set-styles', operation) + before(element, callee, operation) + operate(operation, () => { + const { styles } = operation + for (let [name, value] of Object.entries(styles)) + element.style[name] = value || '' + }) + after(element, callee, operation) }) }, - setValue: operation => { + setValue: (operation, callee) => { processElements(operation, element => { - dispatch(element, 'cable-ready:before-set-value', operation) - const { value } = operation - if (!operation.cancel) element.value = value || '' - dispatch(element, 'cable-ready:after-set-value', operation) + before(element, callee, operation) + operate(operation, () => { + const { value } = operation + element.value = value || '' + }) + after(element, callee, operation) }) }, // DOM Events - dispatchEvent: operation => { + dispatchEvent: (operation, callee) => { processElements(operation, element => { - const { name, detail } = operation - dispatch(element, name, detail) + operate(operation, () => { + const { name, detail } = operation + dispatch(element, name, detail) + }) }) }, - setMeta: operation => { - dispatch(document, 'cable-ready:before-set-meta', operation) - const { name, content } = operation - if (!operation.cancel) { + setMeta: (operation, callee) => { + before(document, callee, operation) + operate(operation, () => { + const { name, content } = operation let meta = document.head.querySelector(`meta[name='${name}']`) if (!meta) { meta = document.createElement('meta') @@ -265,116 +290,140 @@ export default { document.head.appendChild(meta) } meta.content = content - } - dispatch(document, 'cable-ready:after-set-meta', operation) + }) + after(document, callee, operation) }, // Browser Manipulations - clearStorage: operation => { - dispatch(document, 'cable-ready:before-clear-storage', operation) - const { type } = operation - const storage = type === 'session' ? sessionStorage : localStorage - if (!operation.cancel) storage.clear() - dispatch(document, 'cable-ready:after-clear-storage', operation) + clearStorage: (operation, callee) => { + before(document, callee, operation) + operate(operation, () => { + const { type } = operation + const storage = type === 'session' ? sessionStorage : localStorage + storage.clear() + }) + after(document, callee, operation) }, - go: operation => { - dispatch(window, 'cable-ready:before-go', operation) - const { delta } = operation - if (!operation.cancel) history.go(delta) - dispatch(window, 'cable-ready:after-go', operation) + go: (operation, callee) => { + before(window, callee, operation) + operate(operation, () => { + const { delta } = operation + history.go(delta) + }) + after(window, callee, operation) }, - pushState: operation => { - dispatch(window, 'cable-ready:before-push-state', operation) - const { state, title, url } = operation - if (!operation.cancel) history.pushState(state || {}, title || '', url) - dispatch(window, 'cable-ready:after-push-state', operation) + pushState: (operation, callee) => { + before(window, callee, operation) + operate(operation, () => { + const { state, title, url } = operation + history.pushState(state || {}, title || '', url) + }) + after(window, callee, operation) }, - removeStorageItem: operation => { - dispatch(document, 'cable-ready:before-remove-storage-item', operation) - const { key, type } = operation - const storage = type === 'session' ? sessionStorage : localStorage - if (!operation.cancel) storage.removeItem(key) - dispatch(document, 'cable-ready:after-remove-storage-item', operation) + removeStorageItem: (operation, callee) => { + before(document, callee, operation) + operate(operation, () => { + const { key, type } = operation + const storage = type === 'session' ? sessionStorage : localStorage + storage.removeItem(key) + }) + after(document, callee, operation) }, - replaceState: operation => { - dispatch(window, 'cable-ready:before-replace-state', operation) - const { state, title, url } = operation - if (!operation.cancel) history.replaceState(state || {}, title || '', url) - dispatch(window, 'cable-ready:after-replace-state', operation) + replaceState: (operation, callee) => { + before(window, callee, operation) + operate(operation, () => { + const { state, title, url } = operation + history.replaceState(state || {}, title || '', url) + }) + after(window, callee, operation) }, - scrollIntoView: operation => { + scrollIntoView: (operation, callee) => { const { element } = operation - dispatch(element, 'cable-ready:before-scroll-into-view', operation) - if (!operation.cancel) element.scrollIntoView(operation) - dispatch(element, 'cable-ready:after-scroll-into-view', operation) + before(element, callee, operation) + operate(operation, () => { + element.scrollIntoView(operation) + }) + after(element, callee, operation) }, - setCookie: operation => { - dispatch(document, 'cable-ready:before-set-cookie', operation) - const { cookie } = operation - if (!operation.cancel) document.cookie = cookie || '' - dispatch(document, 'cable-ready:after-set-cookie', operation) + setCookie: (operation, callee) => { + before(document, callee, operation) + operate(operation, () => { + const { cookie } = operation + document.cookie = cookie || '' + }) + after(document, callee, operation) }, - setFocus: operation => { + setFocus: (operation, callee) => { const { element } = operation - dispatch(element, 'cable-ready:before-set-focus', operation) - if (!operation.cancel) assignFocus(element) - dispatch(element, 'cable-ready:after-set-focus', operation) + before(element, callee, operation) + operate(operation, () => { + assignFocus(element) + }) + after(element, callee, operation) }, - setStorageItem: operation => { - dispatch(document, 'cable-ready:before-set-storage-item', operation) - const { key, value, type } = operation - const storage = type === 'session' ? sessionStorage : localStorage - if (!operation.cancel) storage.setItem(key, value || '') - dispatch(document, 'cable-ready:after-set-storage-item', operation) + setStorageItem: (operation, callee) => { + before(document, callee, operation) + operate(operation, () => { + const { key, value, type } = operation + const storage = type === 'session' ? sessionStorage : localStorage + storage.setItem(key, value || '') + }) + after(document, callee, operation) }, // Notifications - consoleLog: operation => { - const { message, level } = operation - level && ['warn', 'info', 'error'].includes(level) - ? console[level](message || '') - : console.log(message || '') + consoleLog: (operation, callee) => { + operate(operation, () => { + const { message, level } = operation + level && ['warn', 'info', 'error'].includes(level) + ? console[level](message || '') + : console.log(message || '') + }) }, - notification: operation => { - dispatch(document, 'cable-ready:before-notification', operation) - const { title, options } = operation - if (!operation.cancel) + notification: (operation, callee) => { + before(document, callee, operation) + operate(operation, () => { + const { title, options } = operation Notification.requestPermission().then(result => { operation.permission = result if (result === 'granted') new Notification(title || '', options) }) - dispatch(document, 'cable-ready:after-notification', operation) - }, - - playSound: operation => { - dispatch(document, 'cable-ready:before-play-sound', operation) - const { src } = operation - if (!operation.cancel) { - const canplaythrough = () => { - document.audio.removeEventListener('canplaythrough', canplaythrough) - document.audio.play() - } - const ended = () => { - document.audio.removeEventListener('ended', ended) - dispatch(document, 'cable-ready:after-play-sound', operation) - } - if (document.body.hasAttribute('data-unlock-audio')) { - document.audio.addEventListener('canplaythrough', canplaythrough) - document.audio.addEventListener('ended', ended) - if (src) document.audio.src = src - document.audio.play() - } else dispatch(document, 'cable-ready:after-play-sound', operation) - } else dispatch(document, 'cable-ready:after-play-sound', operation) + }) + after(document, callee, operation) + }, + + playSound: (operation, callee) => { + before(document, callee, operation) + if ( + !operate(operation, () => { + const { src } = operation + const canplaythrough = () => { + document.audio.removeEventListener('canplaythrough', canplaythrough) + document.audio.play() + } + const ended = () => { + document.audio.removeEventListener('ended', ended) + dispatch(document, 'cable-ready:after-play-sound', operation) + } + if (document.body.hasAttribute('data-unlock-audio')) { + document.audio.addEventListener('canplaythrough', canplaythrough) + document.audio.addEventListener('ended', ended) + if (src) document.audio.src = src + document.audio.play() + } else dispatch(document, 'cable-ready:after-play-sound', operation) + }) + ) + after(document, callee, operation) } } diff --git a/javascript/utils.js b/javascript/utils.js index 50867029..af36e430 100644 --- a/javascript/utils.js +++ b/javascript/utils.js @@ -61,3 +61,32 @@ export const processElements = (operation, callback) => { operation.selectAll ? operation.element : [operation.element] ).forEach(callback) } + +// camelCase to kebab-case +const kebabize = str => { + return str + .split('') + .map((letter, idx) => { + return letter.toUpperCase() === letter + ? `${idx !== 0 ? '-' : ''}${letter.toLowerCase()}` + : letter + }) + .join('') +} + +// Provide a standardized pipeline of checks and modifications to all operations based on provided options +// Currently skips execution if cancelled and implements an optional delay +export const operate = (operation, callback) => { + if (!operation.cancel) { + operation.delay ? setTimeout(callback, operation.delay) : callback() + return true + } + return false +} + +// Dispatch life-cycle events with standardized naming +export const before = (target, name, operation) => + dispatch(target, `cable-ready:before-${kebabize(name)}`, operation) + +export const after = (target, name, operation) => + dispatch(target, `cable-ready:after-${kebabize(name)}`, operation)