working draft
This commit is contained in:
parent
0b684bc42e
commit
40e07e7419
52
components/generic/Consumer.js
Normal file
52
components/generic/Consumer.js
Normal file
@ -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)
|
28
components/generic/ParentsIterator.js
Normal file
28
components/generic/ParentsIterator.js
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
53
components/generic/Provider.js
Normal file
53
components/generic/Provider.js
Normal file
@ -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)
|
43
components/rsvp-component.js
Normal file
43
components/rsvp-component.js
Normal file
@ -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 {
|
||||
constructor() {
|
||||
super()
|
||||
const shadow = this.attachShadow({ mode: 'open' })
|
||||
// const shadow = document.createElement('div')
|
||||
shadow.innerHTML = `
|
||||
<button action="prevSentence"><<</button>
|
||||
<button action="prevWord"><</button>
|
||||
<button action="stop">stop</button>
|
||||
<button action="playpause">start</button>
|
||||
<button action="nextWord">></button>
|
||||
<button action="nextSentence">>></button>
|
||||
<rsvp-button action="prevSentence"><<</rsvp-button>
|
||||
<rsvp-button action="prevWord"><</rsvp-button>
|
||||
<rsvp-button action="stop">stop</rsvp-button>
|
||||
<rsvp-button action="playpause">start</rsvp-button>
|
||||
<rsvp-button action="nextWord">></rsvp-button>
|
||||
<rsvp-button action="nextSentence">>></rsvp-button>
|
||||
`
|
||||
this._root = shadow
|
||||
// this.appendChild(shadow)
|
||||
@ -19,18 +39,22 @@ class RSVPControls extends HTMLElement {
|
||||
for (let button of this._root.querySelectorAll('button')) {
|
||||
button.addEventListener('click', e => {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
updateUI(chapter, player) {
|
||||
this.getButton('prevSentence').disabled = !chapter.hasPrevSentence()
|
||||
this.getButton('prevWord').disabled = !chapter.hasPrevWord()
|
||||
this.getButton('nextWord').disabled = !chapter.hasNextWord()
|
||||
this.getButton('nextSentence').disabled = !chapter.hasNextSentence()
|
||||
this.getButton('playpause').innerText = player.playing ? 'pause' : 'start'
|
||||
// this.getButton('prevSentence').disabled = !chapter.hasPrevSentence()
|
||||
// this.getButton('prevWord').disabled = !chapter.hasPrevWord()
|
||||
// this.getButton('nextWord').disabled = !chapter.hasNextWord()
|
||||
// this.getButton('nextSentence').disabled = !chapter.hasNextSentence()
|
||||
// this.getButton('playpause').innerText = player.playing ? 'pause' : 'start'
|
||||
}
|
||||
|
||||
getButton(name) {
|
||||
|
23
components/rsvp-provider.js
Normal file
23
components/rsvp-provider.js
Normal file
@ -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-controls.js'
|
||||
import './rsvp-word-markers.js'
|
||||
|
||||
import { Chapter } from '../src/Chapter.js'
|
||||
import { Player } from '../src/Player.js'
|
||||
import './rsvp-provider.js'
|
||||
|
||||
class RSVPReader extends HTMLElement {
|
||||
constructor() {
|
||||
super()
|
||||
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)
|
||||
marker.appendChild(word)
|
||||
shadow.appendChild(controls)
|
||||
|
||||
this._root = shadow
|
||||
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()
|
||||
shadow.innerHTML = `
|
||||
<rsvp-provider>
|
||||
<rsvp-word></rsvp-word>
|
||||
<rsvp-controls></rsvp-controls>
|
||||
</rsvp-provider>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { pivotize } from '../src/textProcessing/pivotize.js'
|
||||
import { RSVPComponent } from './rsvp-component.js'
|
||||
|
||||
class RSVPWord extends HTMLElement {
|
||||
class RSVPWord extends RSVPComponent {
|
||||
constructor() {
|
||||
super()
|
||||
const shadow = this.attachShadow({ mode: 'open' })
|
||||
const style = document.createElement('style')
|
||||
const word = document.createElement('div')
|
||||
const prefix = document.createElement('span')
|
||||
@ -21,15 +21,17 @@ class RSVPWord extends HTMLElement {
|
||||
word.appendChild(prefix)
|
||||
word.appendChild(pivot)
|
||||
word.appendChild(suffix)
|
||||
shadow.appendChild(style)
|
||||
shadow.appendChild(word)
|
||||
this._root.appendChild(style)
|
||||
this._root.appendChild(word)
|
||||
|
||||
this._root = shadow
|
||||
this.wordParts = { prefix, pivot, suffix }
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.updateDisplay()
|
||||
update({ chapter }) {
|
||||
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() {
|
||||
|
2
index.js
2
index.js
@ -9,4 +9,4 @@ function loadText() {
|
||||
}
|
||||
|
||||
loadBtn.addEventListener('click', loadText)
|
||||
loadText()
|
||||
// loadText()
|
||||
|
25
src/Book.js
Normal file
25
src/Book.js
Normal file
@ -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 {
|
||||
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.words = words
|
||||
this.sentences = sentences
|
||||
this.reset()
|
||||
this.resetIndex()
|
||||
}
|
||||
|
||||
reset() {
|
||||
resetIndex() {
|
||||
this.currentIdx = 0
|
||||
}
|
||||
|
||||
setText(text) {
|
||||
this._originalText = text
|
||||
this.initSegments()
|
||||
}
|
||||
|
||||
setMaxLength(maxLength) {
|
||||
this._maxLength = maxLength
|
||||
this.initSegments()
|
||||
}
|
||||
|
||||
get currentSegment() {
|
||||
return this.segments[this.currentIdx]
|
||||
}
|
||||
|
70
src/RSVPController.js
Normal file
70
src/RSVPController.js
Normal file
@ -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')
|
||||
}
|
||||
}
|
25
src/RSVPPlayer.js
Normal file
25
src/RSVPPlayer.js
Normal file
@ -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')
|
||||
}
|
||||
}
|
56
test-provider.html
Normal file
56
test-provider.html
Normal file
@ -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
Block a user