working draft

This commit is contained in:
Alfred Melch 2019-12-08 09:16:12 +01:00
parent 0b684bc42e
commit 40e07e7419
14 changed files with 447 additions and 89 deletions

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

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

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

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

View File

@ -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">&lt;&lt;</button>
<button action="prevWord">&lt;</button>
<button action="stop">stop</button>
<button action="playpause">start</button>
<button action="nextWord">&gt;</button>
<button action="nextSentence">&gt;&gt;</button>
<rsvp-button action="prevSentence">&lt;&lt;</rsvp-button>
<rsvp-button action="prevWord">&lt;</rsvp-button>
<rsvp-button action="stop">stop</rsvp-button>
<rsvp-button action="playpause">start</rsvp-button>
<rsvp-button action="nextWord">&gt;</rsvp-button>
<rsvp-button action="nextSentence">&gt;&gt;</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) {

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

View File

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

View File

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

View File

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

25
src/Book.js Normal file
View 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--
}
}
}

View File

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