Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce OperationStore to add operations on-the-fly #124

Merged
merged 4 commits into from
May 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/prettier-standard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/
2 changes: 1 addition & 1 deletion bin/standardize
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

bundle exec magic_frozen_string_literal
bundle exec standardrb --fix
yarn run prettier-standard ./javascript/*.js
yarn format
44 changes: 11 additions & 33 deletions javascript/cable_ready.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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 }
27 changes: 27 additions & 0 deletions javascript/index.js
Original file line number Diff line number Diff line change
@@ -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
}
}
13 changes: 11 additions & 2 deletions javascript/morph_callbacks.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
26 changes: 26 additions & 0 deletions javascript/operation_store.js
Original file line number Diff line number Diff line change
@@ -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
}
}
24 changes: 0 additions & 24 deletions javascript/operations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
16 changes: 14 additions & 2 deletions javascript/stream_from_element.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`.'
)
}
}
Expand All @@ -35,4 +35,16 @@ class StreamFromElement extends HTMLElement {
}
}

window.customElements.define('stream-from', StreamFromElement)
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)
}
marcoroth marked this conversation as resolved.
Show resolved Hide resolved
32 changes: 23 additions & 9 deletions javascript/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ 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]
}

// Assigns focus to the appropriate element... preferring the explicitly passed selector
//
// * 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
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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('')
Expand All @@ -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
Expand All @@ -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
}
1 change: 0 additions & 1 deletion lib/cable_ready/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ def default_operation_names
morph
notification
outer_html
play_sound
prepend
push_state
remove
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@
},
"license": "MIT",
"author": "Nathan Hopkins <[email protected]>",
"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"
Expand Down