working draft

context-store
Alfred Melch 5 years ago
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
}
}

@ -1,15 +1,35 @@
import { RSVPComponent } from './rsvp-component.js'
class RSVPButton extends RSVPComponent {
constructor() {
super()
const btn = document.createElement('button')
const slot = document.createElement('slot')
btn.addEventListener('click', this.handleClick.bind(this))
btn.appendChild(slot)
this._root.appendChild(btn)
}
handleClick(e) {
this.fireEvent(this.getAttribute('action'))
}
}
window.customElements.define('rsvp-button', RSVPButton)
class RSVPControls extends HTMLElement { class RSVPControls extends HTMLElement {
constructor() { constructor() {
super() super()
const shadow = this.attachShadow({ mode: 'open' }) const shadow = this.attachShadow({ mode: 'open' })
// const shadow = document.createElement('div') // const shadow = document.createElement('div')
shadow.innerHTML = ` shadow.innerHTML = `
<button action="prevSentence">&lt;&lt;</button> <rsvp-button action="prevSentence">&lt;&lt;</rsvp-button>
<button action="prevWord">&lt;</button> <rsvp-button action="prevWord">&lt;</rsvp-button>
<button action="stop">stop</button> <rsvp-button action="stop">stop</rsvp-button>
<button action="playpause">start</button> <rsvp-button action="playpause">start</rsvp-button>
<button action="nextWord">&gt;</button> <rsvp-button action="nextWord">&gt;</rsvp-button>
<button action="nextSentence">&gt;&gt;</button> <rsvp-button action="nextSentence">&gt;&gt;</rsvp-button>
` `
this._root = shadow this._root = shadow
// this.appendChild(shadow) // this.appendChild(shadow)
@ -19,18 +39,22 @@ class RSVPControls extends HTMLElement {
for (let button of this._root.querySelectorAll('button')) { for (let button of this._root.querySelectorAll('button')) {
button.addEventListener('click', e => { button.addEventListener('click', e => {
let action = e.target.getAttribute('action') let action = e.target.getAttribute('action')
let evt = new CustomEvent('control-event', { detail: { action } }) let evt = new CustomEvent('control-event', {
bubbles: true,
composed: true,
detail: { action }
})
this.dispatchEvent(evt) this.dispatchEvent(evt)
}) })
} }
} }
updateUI(chapter, player) { updateUI(chapter, player) {
this.getButton('prevSentence').disabled = !chapter.hasPrevSentence() // this.getButton('prevSentence').disabled = !chapter.hasPrevSentence()
this.getButton('prevWord').disabled = !chapter.hasPrevWord() // this.getButton('prevWord').disabled = !chapter.hasPrevWord()
this.getButton('nextWord').disabled = !chapter.hasNextWord() // this.getButton('nextWord').disabled = !chapter.hasNextWord()
this.getButton('nextSentence').disabled = !chapter.hasNextSentence() // this.getButton('nextSentence').disabled = !chapter.hasNextSentence()
this.getButton('playpause').innerText = player.playing ? 'pause' : 'start' // this.getButton('playpause').innerText = player.playing ? 'pause' : 'start'
} }
getButton(name) { getButton(name) {

@ -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)

@ -1,78 +1,19 @@
import './rsvp-word.js' import './rsvp-word.js'
import './rsvp-controls.js' import './rsvp-controls.js'
import './rsvp-word-markers.js' import './rsvp-word-markers.js'
import './rsvp-provider.js'
import { Chapter } from '../src/Chapter.js'
import { Player } from '../src/Player.js'
class RSVPReader extends HTMLElement { class RSVPReader extends HTMLElement {
constructor() { constructor() {
super() super()
const shadow = this.attachShadow({ mode: 'open' }) const shadow = this.attachShadow({ mode: 'open' })
const marker = document.createElement('rsvp-border-marker')
const word = document.createElement('rsvp-word')
const controls = document.createElement('rsvp-controls')
shadow.appendChild(marker) shadow.innerHTML = `
marker.appendChild(word) <rsvp-provider>
shadow.appendChild(controls) <rsvp-word></rsvp-word>
<rsvp-controls></rsvp-controls>
this._root = shadow </rsvp-provider>
this._rsvpWord = word `
this._rsvpControls = controls
this.player = new Player()
this.player.subscribe('main', this.tick.bind(this))
this.setText('Sample Text. Easy there.')
this._rsvpControls.addEventListener(
'control-event',
this.handleControlEvent.bind(this)
)
}
setText(text) {
this.chapter = new Chapter(text, 10)
this.updateChildrenUI()
}
tick() {
if (!this.chapter.hasNext()) {
this.player.stop()
} else {
this.chapter.next()
}
this.updateChildrenUI()
}
updateChildrenUI() {
this._rsvpWord.setAttribute('word', this.chapter.currentSegment)
this._rsvpControls.updateUI(this.chapter, this.player)
}
handleControlEvent(e) {
switch (e.detail.action) {
case 'prevSentence':
this.chapter.prevSentence()
break
case 'nextSentence':
this.chapter.nextSentence()
break
case 'prevWord':
this.chapter.prevWord()
break
case 'nextWord':
this.chapter.nextWord()
break
case 'playpause':
this.player.toggle()
break
case 'stop':
this.player.stop()
this.chapter.reset()
break
}
this.updateChildrenUI()
} }
} }

@ -1,9 +1,9 @@
import { pivotize } from '../src/textProcessing/pivotize.js' import { pivotize } from '../src/textProcessing/pivotize.js'
import { RSVPComponent } from './rsvp-component.js'
class RSVPWord extends HTMLElement { class RSVPWord extends RSVPComponent {
constructor() { constructor() {
super() super()
const shadow = this.attachShadow({ mode: 'open' })
const style = document.createElement('style') const style = document.createElement('style')
const word = document.createElement('div') const word = document.createElement('div')
const prefix = document.createElement('span') const prefix = document.createElement('span')
@ -21,15 +21,17 @@ class RSVPWord extends HTMLElement {
word.appendChild(prefix) word.appendChild(prefix)
word.appendChild(pivot) word.appendChild(pivot)
word.appendChild(suffix) word.appendChild(suffix)
shadow.appendChild(style) this._root.appendChild(style)
shadow.appendChild(word) this._root.appendChild(word)
this._root = shadow
this.wordParts = { prefix, pivot, suffix } this.wordParts = { prefix, pivot, suffix }
} }
connectedCallback() { update({ chapter }) {
this.updateDisplay() const [prefix, pivot, suffix] = pivotize(chapter.currentSegment)
this.wordParts.prefix.innerText = prefix
this.wordParts.pivot.innerText = pivot
this.wordParts.suffix.innerText = suffix
} }
static get observedAttributes() { static get observedAttributes() {

@ -9,4 +9,4 @@ function loadText() {
} }
loadBtn.addEventListener('click', loadText) loadBtn.addEventListener('click', loadText)
loadText() // loadText()

@ -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--
}
}
}

@ -2,17 +2,33 @@ import { parseText } from './textProcessing/parseText.js'
export class Chapter { export class Chapter {
constructor(text, maxLength = -1) { constructor(text, maxLength = -1) {
let { segments, words, sentences } = parseText(text, maxLength) this._originalText = text
this._maxLength = maxLength
this.initSegments()
}
initSegments() {
let { segments, words, sentences } = parseText(this._text, this._maxLength)
this.segments = segments this.segments = segments
this.words = words this.words = words
this.sentences = sentences this.sentences = sentences
this.reset() this.resetIndex()
} }
reset() { resetIndex() {
this.currentIdx = 0 this.currentIdx = 0
} }
setText(text) {
this._originalText = text
this.initSegments()
}
setMaxLength(maxLength) {
this._maxLength = maxLength
this.initSegments()
}
get currentSegment() { get currentSegment() {
return this.segments[this.currentIdx] return this.segments[this.currentIdx]
} }

@ -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…
Cancel
Save