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 + + + + + +
+
+
+ + + + +