working draft
parent
0b684bc42e
commit
40e07e7419
@ -0,0 +1,52 @@
|
||||
import { createParentsIterator } from './ParentsIterator.js'
|
||||
import { StateProvider } from './Provider.js'
|
||||
|
||||
class StateConsumer extends HTMLElement {
|
||||
constructor() {
|
||||
super()
|
||||
this.provider = null
|
||||
this.appendChild(document.createElement('slot'))
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.getAttribute('name')
|
||||
}
|
||||
|
||||
/** Returns the value of the corresponding provider */
|
||||
get value() {
|
||||
return this.provider ? this.provider.value : null
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.provider = this.findProvider()
|
||||
if (this.provider) {
|
||||
this.provider.addObserver(this)
|
||||
}
|
||||
this.update(this.provider)
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.provider.removeObserver(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses tree upwards until a Provider with the same name is found
|
||||
* Returns the closest provider or null if none is existent
|
||||
*/
|
||||
findProvider() {
|
||||
for (let node of createParentsIterator(this)) {
|
||||
if (node instanceof StateProvider && node.name === this.name) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
update({ value }) {
|
||||
if (typeof this.onChange === 'function') {
|
||||
this.onChange(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('state-consumer', StateConsumer)
|
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Returns the parent of a node.
|
||||
* Breaks through shadow roots.
|
||||
* Returns null if node has no parent.
|
||||
* @param {Node} node
|
||||
*/
|
||||
function getParent(node) {
|
||||
let parent = node.parentNode
|
||||
if (!parent) return null
|
||||
return parent.host ? parent.host : parent
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an Iterator that walks the DOM tree upwards.
|
||||
* @param {Node} node
|
||||
*/
|
||||
export function createParentsIterator(node) {
|
||||
let curNode = node
|
||||
return {
|
||||
next: function() {
|
||||
curNode = getParent(curNode)
|
||||
return curNode ? { value: curNode } : { done: true }
|
||||
},
|
||||
[Symbol.iterator]: function() {
|
||||
return this
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
export class StateProvider extends HTMLElement {
|
||||
constructor() {
|
||||
super()
|
||||
this.appendChild(document.createElement('slot'))
|
||||
this.observers = []
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.value = this.getAttribute('value')
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.getAttribute('name')
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._val
|
||||
}
|
||||
|
||||
set value(newValue) {
|
||||
// if (this.value !== newValue) {
|
||||
this._val = newValue
|
||||
this.notifyObservers()
|
||||
// }
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['value']
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
// console.log('attrChange', name, oldValue, newValue)
|
||||
if (name === 'value') {
|
||||
this.value = newValue
|
||||
}
|
||||
}
|
||||
|
||||
notifyObservers() {
|
||||
for (let obs of this.observers) {
|
||||
obs.update(this)
|
||||
}
|
||||
}
|
||||
|
||||
addObserver(obs) {
|
||||
this.observers.push(obs)
|
||||
}
|
||||
|
||||
removeObserver(obs) {
|
||||
this.observers = this.observers.filter(el => el !== obs)
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('state-provider', StateProvider)
|
@ -0,0 +1,43 @@
|
||||
import { RSVPProvider } from './rsvp-provider.js'
|
||||
|
||||
export class RSVPComponent extends HTMLElement {
|
||||
constructor() {
|
||||
super()
|
||||
this._root = this.attachShadow({ mode: 'open' })
|
||||
const provider = this.findProvider()
|
||||
provider.addObserver(this)
|
||||
}
|
||||
|
||||
connectedCallback() {}
|
||||
|
||||
render() {}
|
||||
|
||||
update(state) {}
|
||||
|
||||
fireEvent(action, payload) {
|
||||
let evt = new CustomEvent('rsvp-event', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: { action, payload }
|
||||
})
|
||||
this.dispatchEvent(evt)
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses tree upwards until a RSVPProvider node is found
|
||||
* Returns the closest provider or null if none is existent
|
||||
*/
|
||||
findProvider() {
|
||||
let curNode = this
|
||||
while (curNode.parentNode) {
|
||||
curNode = curNode.parentNode
|
||||
if (curNode.host) {
|
||||
curNode = curNode.host
|
||||
}
|
||||
if (curNode instanceof RSVPProvider) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return curNode instanceof RSVPProvider ? curNode : null
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { RSVPController } from '../src/RSVPController.js'
|
||||
|
||||
export class RSVPProvider extends HTMLElement {
|
||||
constructor() {
|
||||
super()
|
||||
const shadow = this.attachShadow({ mode: 'open' })
|
||||
const provider = document.createElement('state-provider')
|
||||
const slot = document.createElement('slot')
|
||||
provider.setAttribute('name', 'rsvp-controller')
|
||||
provider.appendChild(slot)
|
||||
shadow.appendChild(provider)
|
||||
|
||||
const controller = new RSVPController()
|
||||
controller.onChange = this.provider.notifyObservers()
|
||||
|
||||
this.addEventListener('rsvp-event', function(e) {
|
||||
let { action, payload } = e.detail
|
||||
controller.handleAction(action, payload)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('rsvp-provider', RSVPProvider)
|
@ -0,0 +1,25 @@
|
||||
import { Chapter } from './Chapter.js'
|
||||
import { Player } from './Player.js'
|
||||
|
||||
export class Book {
|
||||
constructor() {
|
||||
this.chapters = [new Chapter('')]
|
||||
this.curIdx = 0
|
||||
this.player = new Player()
|
||||
}
|
||||
|
||||
get currentChapter() {
|
||||
return this.chapters(curIdx)
|
||||
}
|
||||
|
||||
addChapter(chapter) {
|
||||
this.chapters.push(chapter)
|
||||
}
|
||||
|
||||
removeChapter(chapter) {
|
||||
this.chapters.filter(el => el !== chapter)
|
||||
if (this.curIdx > 0) {
|
||||
this.curIdx--
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
import { RSVPPlayer } from './RSVPPlayer.js'
|
||||
|
||||
const initialOptions = {
|
||||
maxLength: 10,
|
||||
autoPlayNextChapter: false,
|
||||
wordsPerMinute: 200,
|
||||
pivotMethod: 'default',
|
||||
pauseOnPunctuation: true
|
||||
}
|
||||
|
||||
export class RSVPController {
|
||||
constructor(options = {}) {
|
||||
this.options = initialOptions
|
||||
this.setOptions(options)
|
||||
|
||||
this.rsvpPlayer = new RSVPPlayer()
|
||||
this.rsvpPlayer.onTick = this.onChange
|
||||
|
||||
this.applyOptions()
|
||||
}
|
||||
|
||||
setOptions(options) {
|
||||
this.options = { ...this.options, ...options }
|
||||
}
|
||||
|
||||
applyOptions() {
|
||||
let { options } = this
|
||||
let { chapter, player } = this.rsvpPlayer
|
||||
chapter.setMaxLength = options.maxLength
|
||||
player.updateInterval(1000 / options.wordsPerMinute)
|
||||
}
|
||||
|
||||
updateOptions(options) {
|
||||
this.setOptions(options)
|
||||
this.applyOptions()
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
processAction(action, payload) {
|
||||
let { chapter, player } = this.rsvpPlayer
|
||||
switch (action) {
|
||||
case 'prevSentence':
|
||||
chapter.prevSentence()
|
||||
break
|
||||
case 'nextSentence':
|
||||
chapter.nextSentence()
|
||||
break
|
||||
case 'prevWord':
|
||||
chapter.prevWord()
|
||||
break
|
||||
case 'nextWord':
|
||||
chapter.nextWord()
|
||||
break
|
||||
case 'playpause':
|
||||
player.toggle()
|
||||
break
|
||||
case 'stop':
|
||||
player.stop()
|
||||
chapter.reset()
|
||||
break
|
||||
case 'load':
|
||||
chapter = new Chapter(payload, 10)
|
||||
}
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
onChange() {
|
||||
console.warn('RSVPController: onChange not set')
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { Player } from './Player.js'
|
||||
import { Book } from './Book.js'
|
||||
import { Chapter } from './Chapter.js'
|
||||
|
||||
export class RSVPPlayer {
|
||||
constructor(maxLength) {
|
||||
this.chapter = new Chapter(maxLength)
|
||||
this.player = new Player()
|
||||
|
||||
this.player.subscribe('main', this.tick.bind(this))
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (!this.chapter.hasNext()) {
|
||||
this.player.stop()
|
||||
} else {
|
||||
this.chapter.next()
|
||||
}
|
||||
this.onTick()
|
||||
}
|
||||
|
||||
onTick() {
|
||||
console.warn('RSVPPlayer: onTick not set')
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>Document</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<state-provider value="1" id="prov1" name="state1">
|
||||
<b>lalal</b>
|
||||
<div id="1">
|
||||
Hello World
|
||||
<state-provider value="2" id="prov2" name="state2">
|
||||
<span id="2">
|
||||
<state-consumer id="cons1" name="state1"></state-consumer>
|
||||
</span>
|
||||
</state-provider>
|
||||
</div>
|
||||
</state-provider>
|
||||
<div id="root"></div>
|
||||
<script type="module">
|
||||
import './components/generic/Provider.js'
|
||||
import './components/generic/Consumer.js'
|
||||
|
||||
const prov1 = document.getElementById('prov1')
|
||||
const cons1 = document.getElementById('cons1')
|
||||
|
||||
cons1.onChange = console.log
|
||||
|
||||
prov1.setAttribute('value', 'change1')
|
||||
prov1.value = 'change2'
|
||||
prov1.value = { val: 'change3', test: 'testing objects' }
|
||||
|
||||
console.log(prov1.observers.length, 'should be 1')
|
||||
cons1.remove()
|
||||
console.log(prov1.observers.length, 'should be 0')
|
||||
</script>
|
||||
|
||||
<script>
|
||||
const root = document.getElementById('root')
|
||||
|
||||
root.innerHTML = `
|
||||
<state-provider id="prov3" value="initial">
|
||||
<state-consumer id="cons3" onChange=${console.log}></state-consumer>
|
||||
</state-provider>
|
||||
`
|
||||
|
||||
const prov3 = document.getElementById('prov3')
|
||||
const cons3 = document.getElementById('cons3')
|
||||
|
||||
console.log(prov3)
|
||||
console.log(typeof cons3.getAttribute('onChange'))
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue