From 8eba821491e06ea1f9ed91914943d35226166b00 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Tue, 11 May 2021 18:52:06 +0200 Subject: [PATCH 1/4] Introduce `OperationStore` to add operations on-the-fly This also removes the `playSound` operation. The operation has been extracted to: https://github.com/cableready/audio_operations --- .github/workflows/prettier-standard.yml | 2 +- bin/standardize | 2 +- javascript/cable_ready.js | 44 +++++++------------------ javascript/index.js | 27 +++++++++++++++ javascript/morph_callbacks.js | 13 ++++++-- javascript/operation_store.js | 26 +++++++++++++++ javascript/operations.js | 24 -------------- javascript/utils.js | 32 +++++++++++++----- package.json | 7 ++-- 9 files changed, 105 insertions(+), 72 deletions(-) create mode 100644 javascript/index.js create mode 100644 javascript/operation_store.js diff --git a/.github/workflows/prettier-standard.yml b/.github/workflows/prettier-standard.yml index 6150e571..8625fc74 100644 --- a/.github/workflows/prettier-standard.yml +++ b/.github/workflows/prettier-standard.yml @@ -20,5 +20,5 @@ jobs: version: '12.x' - run: yarn working-directory: javascript/ - - run: yarn run prettier-standard-check + - run: yarn lint working-directory: javascript/ diff --git a/bin/standardize b/bin/standardize index b9ce0951..b0e942f4 100755 --- a/bin/standardize +++ b/bin/standardize @@ -2,4 +2,4 @@ bundle exec magic_frozen_string_literal bundle exec standardrb --fix -yarn run prettier-standard ./javascript/*.js +yarn format diff --git a/javascript/cable_ready.js b/javascript/cable_ready.js index 21745f09..85a5e6bc 100644 --- a/javascript/cable_ready.js +++ b/javascript/cable_ready.js @@ -1,12 +1,7 @@ -import { verifyNotMutable, verifyNotPermanent } from './morph_callbacks' import { xpathToElement } from './utils' import activeElement from './active_element' -import DOMOperations from './operations' +import OperationStore from './operation_store' import actionCable from './action_cable' -import './stream_from_element' - -export const shouldMorphCallbacks = [verifyNotMutable, verifyNotPermanent] -export const didMorphCallbacks = [] const perform = ( operations, @@ -29,7 +24,15 @@ const perform = ( } if (operation.element || options.emitMissingElementWarnings) { activeElement.set(document.activeElement) - DOMOperations[name](operation, name) + const cableReadyOperation = OperationStore.all[name] + + if (cableReadyOperation) { + cableReadyOperation(operation, name) + } else { + console.error( + `CableReady couldn't find the "${name}" operation. Make sure you haven't misspelled the operation name and that you've added all required operations.` + ) + } } } catch (e) { if (operation.element) { @@ -66,29 +69,4 @@ const initialize = (initializeOptions = {}) => { actionCable.setConsumer(consumer) } -document.addEventListener('DOMContentLoaded', function () { - if (!document.audio && document.body.hasAttribute('data-unlock-audio')) { - document.audio = new Audio( - 'data:audio/mpeg;base64,//OExAAAAAAAAAAAAEluZm8AAAAHAAAABAAAASAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/P39/f39/f39/f39/f39/f39/f39/f39/f3+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/AAAAAAAAAAAAAAAAAAAAAAAAAAAAJAa/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//MUxAAAAANIAAAAAExBTUUzLjk2LjFV//MUxAsAAANIAAAAAFVVVVVVVVVVVVVV//MUxBYAAANIAAAAAFVVVVVVVVVVVVVV//MUxCEAAANIAAAAAFVVVVVVVVVVVVVV' - ) - const unlockAudio = () => { - document.body.removeEventListener('click', unlockAudio) - document.body.removeEventListener('touchstart', unlockAudio) - document.audio - .play() - .then(() => {}) - .catch(() => {}) - } - document.body.addEventListener('click', unlockAudio) - document.body.addEventListener('touchstart', unlockAudio) - } -}) - -export default { - perform, - performAsync, - DOMOperations, - shouldMorphCallbacks, - didMorphCallbacks, - initialize -} +export { perform, performAsync, initialize } diff --git a/javascript/index.js b/javascript/index.js new file mode 100644 index 00000000..40b8e08e --- /dev/null +++ b/javascript/index.js @@ -0,0 +1,27 @@ +import * as MorphCallbacks from './morph_callbacks' +import { shouldMorphCallbacks, didMorphCallbacks } from './morph_callbacks' +import * as Utils from './utils' +import OperationStore, { addOperation, addOperations } from './operation_store' +import { perform, performAsync, initialize } from './cable_ready' +import './stream_from_element' + +export { Utils, MorphCallbacks } + +export default { + perform, + performAsync, + shouldMorphCallbacks, + didMorphCallbacks, + initialize, + addOperation, + addOperations, + get DOMOperations () { + console.warn( + 'DEPRECATED: Please use `CableReady.operations.jazzHands = ...` instead of `CableReady.DOMOperations.jazzHands = ...`' + ) + return OperationStore.all + }, + get operations () { + return OperationStore.all + } +} diff --git a/javascript/morph_callbacks.js b/javascript/morph_callbacks.js index 2d5f78cc..1f72745d 100644 --- a/javascript/morph_callbacks.js +++ b/javascript/morph_callbacks.js @@ -1,6 +1,5 @@ import { mutableTags } from './enums' import { isTextInput } from './utils' -import { shouldMorphCallbacks, didMorphCallbacks } from './cable_ready' import activeElement from './active_element' // Indicates whether or not we should morph an element via onBeforeElUpdated callback @@ -50,4 +49,14 @@ const verifyNotPermanent = (detail, fromEl, toEl) => { return !permanent } -export { shouldMorph, didMorph, verifyNotMutable, verifyNotPermanent } +const shouldMorphCallbacks = [verifyNotMutable, verifyNotPermanent] +const didMorphCallbacks = [] + +export { + shouldMorphCallbacks, + didMorphCallbacks, + shouldMorph, + didMorph, + verifyNotMutable, + verifyNotPermanent +} diff --git a/javascript/operation_store.js b/javascript/operation_store.js new file mode 100644 index 00000000..26fa9ee9 --- /dev/null +++ b/javascript/operation_store.js @@ -0,0 +1,26 @@ +import Operations from './operations' + +let operations = Operations + +const add = newOperations => { + operations = { ...operations, ...newOperations } +} + +const addOperations = operations => { + add(operations) +} + +const addOperation = (name, operation) => { + const operations = {} + operations[name] = operation + + add(operations) +} + +export { addOperation, addOperations } + +export default { + get all () { + return operations + } +} diff --git a/javascript/operations.js b/javascript/operations.js index d169cb40..4d6f6a60 100644 --- a/javascript/operations.js +++ b/javascript/operations.js @@ -401,29 +401,5 @@ export default { }) }) 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 af36e430..3795b7fa 100644 --- a/javascript/utils.js +++ b/javascript/utils.js @@ -3,7 +3,7 @@ import activeElement from './active_element' // Indicates if the passed element is considered a text input. // -export const isTextInput = element => { +const isTextInput = element => { return inputTags[element.tagName] && textInputTypes[element.type] } @@ -11,7 +11,7 @@ export const isTextInput = element => { // // * selector - a CSS selector for the element that should have focus // -export const assignFocus = selector => { +const assignFocus = selector => { const element = selector && selector.nodeType === Node.ELEMENT_NODE ? selector @@ -26,7 +26,7 @@ export const assignFocus = selector => { // * name - the name of the event // * detail - the event detail // -export const dispatch = (element, name, detail = {}) => { +const dispatch = (element, name, detail = {}) => { const init = { bubbles: true, cancelable: true, detail: detail } const evt = new CustomEvent(name, init) element.dispatchEvent(evt) @@ -35,7 +35,7 @@ export const dispatch = (element, name, detail = {}) => { // Accepts an xPath query and returns the element found at that position in the DOM // -export const xpathToElement = xpath => { +const xpathToElement = xpath => { return document.evaluate( xpath, document, @@ -49,20 +49,21 @@ export const xpathToElement = xpath => { // // * names - could be a string or an array of strings for multiple classes. // -export const getClassNames = names => Array(names).flat() +const getClassNames = names => Array(names).flat() // Perform operation for either the first or all of the elements returned by CSS selector // // * operation - the instruction payload from perform // * callback - the operation function to run for each element // -export const processElements = (operation, callback) => { +const processElements = (operation, callback) => { Array.from( operation.selectAll ? operation.element : [operation.element] ).forEach(callback) } // camelCase to kebab-case +// const kebabize = str => { return str .split('') @@ -76,7 +77,8 @@ const kebabize = str => { // 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) => { +// +const operate = (operation, callback) => { if (!operation.cancel) { operation.delay ? setTimeout(callback, operation.delay) : callback() return true @@ -85,8 +87,20 @@ export const operate = (operation, callback) => { } // Dispatch life-cycle events with standardized naming -export const before = (target, name, operation) => +const before = (target, name, operation) => dispatch(target, `cable-ready:before-${kebabize(name)}`, operation) -export const after = (target, name, operation) => +const after = (target, name, operation) => dispatch(target, `cable-ready:after-${kebabize(name)}`, operation) + +export { + isTextInput, + assignFocus, + dispatch, + xpathToElement, + getClassNames, + processElements, + operate, + before, + after +} diff --git a/package.json b/package.json index aed41677..0e9f648a 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,12 @@ }, "license": "MIT", "author": "Nathan Hopkins ", - "main": "./javascript/cable_ready.js", + "main": "./javascript/index.js", "scripts": { - "prettier-standard-check": "yarn run prettier-standard --check ./javascript/**/*.js" + "lint": "yarn run prettier-standard:check", + "format": "yarn run prettier-standard:format", + "prettier-standard:check": "yarn run prettier-standard --check ./javascript/**/*.js", + "prettier-standard:format": "yarn run prettier-standard ./javascript/**/*.js" }, "dependencies": { "morphdom": "^2.6.1" From 952838a278a8b263d11b961f813c0ea848663894 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Tue, 11 May 2021 19:12:03 +0200 Subject: [PATCH 2/4] make sure we don't define `StreamFromElement` multiple times --- javascript/stream_from_element.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/javascript/stream_from_element.js b/javascript/stream_from_element.js index 51b40526..a655b293 100644 --- a/javascript/stream_from_element.js +++ b/javascript/stream_from_element.js @@ -35,4 +35,6 @@ class StreamFromElement extends HTMLElement { } } -window.customElements.define('stream-from', StreamFromElement) +if (!window.customElements.get('stream-from')) { + window.customElements.define('stream-from', StreamFromElement) +} From a77f8535b6ebd468c0bd0a2a2c2702f61d13b325 Mon Sep 17 00:00:00 2001 From: leastbad Date: Tue, 11 May 2021 22:49:00 -0400 Subject: [PATCH 3/4] remove play_sound from config.rb --- lib/cable_ready/config.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/cable_ready/config.rb b/lib/cable_ready/config.rb index 81e46aa1..0ba08654 100644 --- a/lib/cable_ready/config.rb +++ b/lib/cable_ready/config.rb @@ -51,7 +51,6 @@ def default_operation_names morph notification outer_html - play_sound prepend push_state remove From f66c573e1d04b22fb65ff4e398c6dff8e504ee91 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Thu, 13 May 2021 14:37:50 +0200 Subject: [PATCH 4/4] emit warning if custom element is already defined and when it's not our `stream-from` element --- javascript/stream_from_element.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/javascript/stream_from_element.js b/javascript/stream_from_element.js index a655b293..327d4fc7 100644 --- a/javascript/stream_from_element.js +++ b/javascript/stream_from_element.js @@ -18,7 +18,7 @@ class StreamFromElement extends HTMLElement { ) } else { console.error( - `The stream_from helper cannot connect without an ActionCable consumer.\nPlease set 'CableReady.initialize({ consumer })' in your index.js.` + 'The `stream_from` helper cannot connect without an ActionCable consumer.\nPlease set `CableReady.initialize({ consumer })` in your `index.js`.' ) } } @@ -35,6 +35,16 @@ class StreamFromElement extends HTMLElement { } } -if (!window.customElements.get('stream-from')) { +const customElement = window.customElements.get('stream-from') + +if (customElement) { + if (customElement !== StreamFromElement) { + console.warn( + 'CableReady tried to register the HTML custom element `stream-from`, but `stream-from` is already registered and used by something else. Make sure that nothing else defines the custom element `stream-from`.' + ) + } else { + // CableReady has already registered the `stream-from` custom element and it's the right one + } +} else { window.customElements.define('stream-from', StreamFromElement) }