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