Skip to content
This repository has been archived by the owner on Apr 13, 2022. It is now read-only.

Commit

Permalink
Add initial preact components
Browse files Browse the repository at this point in the history
  • Loading branch information
westonruter committed Jan 2, 2018
1 parent 2f31632 commit b5195e3
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 10 deletions.
2 changes: 2 additions & 0 deletions .babelrc
@@ -1,5 +1,7 @@
{
"presets": [
"es2015",
"react",
[ "env", {
"targets": {
"browsers": [
Expand Down
21 changes: 19 additions & 2 deletions .eslintrc.json
@@ -1,12 +1,23 @@
{
"extends": "wordpress",
"extends": [
"wordpress",
"plugin:react/recommended",
"plugin:jsx-a11y/recommended"
],
"env": {
"es6": true
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"plugins": [
"react",
"jsx-a11y"
],
"rules": {
"array-bracket-spacing": [ "error", "always" ],
"brace-style": [ "error", "1tbs" ],
Expand All @@ -21,6 +32,11 @@
"eqeqeq": "error",
"func-call-spacing": "error",
"indent": [ "error", "tab", { "SwitchCase": 1 } ],
"jsx-a11y/label-has-for": [ 2, {
"required": {
"every": [ "id" ]
}
}],
"key-spacing": "error",
"keyword-spacing": "error",
"lines-around-comment": "off",
Expand Down Expand Up @@ -117,6 +133,7 @@
"padded-blocks": [ "error", "never" ],
"prefer-const": "error",
"quote-props": [ "error", "as-needed" ],
"react/prop-types": "off",
"semi": "error",
"semi-spacing": "error",
"space-before-blocks": [ "error", "always" ],
Expand Down
9 changes: 8 additions & 1 deletion package.json
Expand Up @@ -25,10 +25,17 @@
"babel-preset-env": "^1.6.1",
"eslint": "^4.13.1",
"eslint-config-wordpress": "^2.0.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "^7.5.1",
"source-map-loader": "^0.2.3",
"webpack": "^3.10.0"
},
"dependencies": {
"event-emitter": "^0.3.5"
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"event-emitter": "^0.3.5",
"preact": "^8.2.7",
"preact-compat": "^3.17.0",
"prop-types": "^15.6.0"
}
}
24 changes: 24 additions & 0 deletions src/components/PlaybackButton.js
@@ -0,0 +1,24 @@
import React, { Component } from 'preact-compat';
import PropTypes from 'prop-types';

export default class PlaybackButton extends Component {
render() {
const style = {
fontFamily: '"Apple Color Emoji","Segoe UI Emoji","NotoColorEmoji","Segoe UI Symbol","Android Emoji","EmojiSymbols"',
background: 'none',
border: 'none',
cursor: 'pointer',
};
return (
<button type="button" aria-label={ this.props.label } style={ style } onClick={ this.props.onClick }>
{ this.props.icon }
</button>
);
}
}

PlaybackButton.propTypes = {
label: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
};
90 changes: 90 additions & 0 deletions src/components/PlaybackControls.js
@@ -0,0 +1,90 @@

import React, { Component } from 'preact-compat';
import PropTypes from 'prop-types';
import PlaybackButton from './PlaybackButton';
import { uniqueId } from '../helpers';

export default class PlaybackControls extends Component {
constructor() {
super();
this.state = {
dialogOpen: false,
};
}

componentWillMount() {
this.idPrefix = `input${ uniqueId() }-`;
}

componentDidMount() {
this.updateDialogState();
this.dialog.addEventListener( 'cancel', ( event ) => {
event.preventDefault();
this.setState( { dialogOpen: false } );
} );
}

componentDidUpdate() {
this.updateDialogState();
}

updateDialogState() {
if ( ! this.state.dialogOpen && this.dialog.open ) {
this.dialog.close();
if ( this.previousActiveElement ) {
this.previousActiveElement.focus();
}
} else if ( this.state.dialogOpen && ! this.dialog.open ) {
this.previousActiveElement = document.activeElement;
this.dialog.showModal();
}
}

render() {
const showDialog = () => {
this.setState( { dialogOpen: true } );
};
const hideDialog = () => {
this.setState( { dialogOpen: false } );
};
const saveDialogRef = ( dialog ) => {
this.dialog = dialog;
};

return (
<fieldset>
<legend>Playback</legend>
<PlaybackButton icon="▶" label="Play" onClick={ this.props.play } />
<PlaybackButton icon="⏹" label="Stop" onClick={ this.props.stop } />
<PlaybackButton icon="⏪" label="Previous" onClick={ this.props.previous } />
<PlaybackButton icon="⏩" label="Forward" onClick={ this.props.next } />
<PlaybackButton icon="⚙" label="Settings" onClick={ showDialog } />

<dialog ref={ saveDialogRef }>
<p>
<label htmlFor={ this.idPrefix + 'rate' }>Rate:</label>
<input id={ this.idPrefix + 'rate' } type="number" defaultValue={1.0} />
</p>
<p>
<label htmlFor={ this.idPrefix + 'pitch' }>Pitch:</label>
<input id={ this.idPrefix + 'pitch' } type="number" defaultValue={1.0} />
</p>
<p>
<label htmlFor={ this.idPrefix + 'voice[en]' }>English Voice:</label>
<select id={ this.idPrefix + 'voice[en]' }>
<option>Alex</option>
</select>
</p>
<button onClick={ hideDialog }>Close</button>
</dialog>
</fieldset>
);
}
}

PlaybackControls.propTypes = {
play: PropTypes.func.isRequired,
stop: PropTypes.func.isRequired,
previous: PropTypes.func.isRequired,
next: PropTypes.func.isRequired,
};
12 changes: 12 additions & 0 deletions src/helpers.js
Expand Up @@ -14,3 +14,15 @@ export function equalRanges( range1, range2 ) {
range1.endOffset === range2.endOffset
);
}

let lastId = 0;

/**
* Generate unique (auto-incremented) ID.
*
* @return {number} ID.
*/
export function uniqueId() {
lastId++;
return lastId;
}
18 changes: 17 additions & 1 deletion src/speech.js
@@ -1,9 +1,11 @@

import React, { render } from 'preact-compat';
import EventEmitter from 'event-emitter';

import chunkify from './chunkify';
import * as voices from './voices';
import { equalRanges } from './helpers';
import PlaybackControls from './components/PlaybackControls';

/**
* A segment of text nodes that are to be read by the TTS engine.
Expand All @@ -27,7 +29,6 @@ const CHUNK_BEGINNING_OFFSET_THRESHOLD = 10;
* @augments EventEmitter
*/
export default class Speech {

/**
* Construct.
*
Expand Down Expand Up @@ -176,6 +177,13 @@ export default class Speech {
* Setup controls.
*/
setupControls() {



return;

// PlaybackControls

const container = document.createElement( 'fieldset' );

const legend = document.createElement( 'legend' );
Expand Down Expand Up @@ -252,7 +260,15 @@ export default class Speech {
* Inject controls into content.
*/
injectControls() {
this.controlsElement = document.createElement( 'div' );
this.rootElement.insertBefore( this.controlsElement, this.rootElement.firstChild );
const props = {
play: this.play.bind( this ),
stop: this.stop.bind( this ),
next: this.next.bind( this ),
previous: this.previous.bind( this ),
};
render( <PlaybackControls { ...props } />, this.controlsElement );
}

/**
Expand Down
22 changes: 16 additions & 6 deletions webpack.config.js
@@ -1,3 +1,5 @@
/* eslint-env node */

const path = require( 'path' );

module.exports = {
Expand All @@ -6,22 +8,30 @@ module.exports = {
path: path.resolve( __dirname, 'dist' ),
filename: 'app.js',
libraryTarget: 'window',
library: 'spokenWord'
library: 'spokenWord',
},
devtool: 'source-map',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
loader: 'babel-loader',
},
{
test: /\.js$/,
exclude: /node_modules/,
use: [ 'source-map-loader' ],
enforce: 'pre'
}
]
}
enforce: 'pre',
},
],
},
resolve: {
alias: {
react: 'preact-compat',
'react-dom': 'preact-compat',
// Not necessary unless you consume a module using `createClass`.
'create-react-class': 'preact-compat/lib/create-react-class',
},
},
};

0 comments on commit b5195e3

Please sign in to comment.