diff --git a/components/generic/Consumer.js b/components/generic/Consumer.js
new file mode 100644
index 0000000..2edc832
--- /dev/null
+++ b/components/generic/Consumer.js
@@ -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)
diff --git a/components/generic/ParentsIterator.js b/components/generic/ParentsIterator.js
new file mode 100644
index 0000000..e780a2f
--- /dev/null
+++ b/components/generic/ParentsIterator.js
@@ -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
+ }
+ }
+}
diff --git a/components/generic/Provider.js b/components/generic/Provider.js
new file mode 100644
index 0000000..cde22b8
--- /dev/null
+++ b/components/generic/Provider.js
@@ -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)
diff --git a/components/rsvp-component.js b/components/rsvp-component.js
new file mode 100644
index 0000000..5bffcd2
--- /dev/null
+++ b/components/rsvp-component.js
@@ -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
+ }
+}
diff --git a/components/rsvp-controls.js b/components/rsvp-controls.js
index 2584ea1..cf4e1f7 100644
--- a/components/rsvp-controls.js
+++ b/components/rsvp-controls.js
@@ -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 = `
-
-
-
-
-
-
+ <<
+ <
+ stop
+ start
+ >
+ >>
`
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) {
diff --git a/components/rsvp-provider.js b/components/rsvp-provider.js
new file mode 100644
index 0000000..7479d9a
--- /dev/null
+++ b/components/rsvp-provider.js
@@ -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)
diff --git a/components/rsvp-reader.js b/components/rsvp-reader.js
index 4c49c75..198be35 100644
--- a/components/rsvp-reader.js
+++ b/components/rsvp-reader.js
@@ -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 = `
+
+
+
+
+ `
}
}
diff --git a/components/rsvp-word.js b/components/rsvp-word.js
index 9667479..82d0557 100644
--- a/components/rsvp-word.js
+++ b/components/rsvp-word.js
@@ -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() {
diff --git a/index.js b/index.js
index b513f75..0772b66 100644
--- a/index.js
+++ b/index.js
@@ -9,4 +9,4 @@ function loadText() {
}
loadBtn.addEventListener('click', loadText)
-loadText()
+// loadText()
diff --git a/src/Book.js b/src/Book.js
new file mode 100644
index 0000000..ad8e73d
--- /dev/null
+++ b/src/Book.js
@@ -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--
+ }
+ }
+}
diff --git a/src/Chapter.js b/src/Chapter.js
index a6abd41..8adcbfb 100644
--- a/src/Chapter.js
+++ b/src/Chapter.js
@@ -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]
}
diff --git a/src/RSVPController.js b/src/RSVPController.js
new file mode 100644
index 0000000..189935c
--- /dev/null
+++ b/src/RSVPController.js
@@ -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')
+ }
+}
diff --git a/src/RSVPPlayer.js b/src/RSVPPlayer.js
new file mode 100644
index 0000000..caa8fee
--- /dev/null
+++ b/src/RSVPPlayer.js
@@ -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')
+ }
+}
diff --git a/test-provider.html b/test-provider.html
new file mode 100644
index 0000000..f689e5c
--- /dev/null
+++ b/test-provider.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+ Document
+
+
+
+
+ lalal
+
+ Hello World
+
+
+
+
+
+
+
+
+
+
+
+
+