From 2e1381acc605e94c211549018a61179665a3e260 Mon Sep 17 00:00:00 2001 From: "Phyks (Lucas Verney)" Date: Thu, 7 Jul 2016 23:23:18 +0200 Subject: [PATCH] Initial commit --- .babelrc | 3 + .eslintignore | 4 + .eslintrc.js | 41 + .gitignore | 1 + LICENSE | 21 + README.md | 42 + TODO | 16 + app/actions/APIActions.js | 77 + app/actions/auth.js | 173 + app/actions/index.js | 18 + app/assets/img/ampache-blue.png | Bin 0 -> 45726 bytes app/components/Album.jsx | 79 + app/components/Albums.jsx | 19 + app/components/Artist.jsx | 35 + app/components/Artists.jsx | 19 + app/components/Login.jsx | 142 + app/components/Songs.jsx | 110 + app/components/elements/FilterBar.jsx | 34 + app/components/elements/Grid.jsx | 232 + app/components/elements/Pagination.jsx | 138 + app/components/layouts/Sidebar.jsx | 60 + app/components/layouts/Simple.jsx | 11 + app/containers/App.jsx | 18 + app/containers/RequireAuthentication.js | 50 + app/containers/Root.jsx | 21 + app/dist/fix.ie9.js | 2 + app/dist/fix.ie9.js.map | 1 + app/dist/index.js | 20 + app/dist/index.js.map | 1 + app/middleware/api.js | 248 + app/reducers/auth.js | 74 + app/reducers/index.js | 34 + app/reducers/paginate.js | 46 + app/routes.js | 39 + app/store/configureStore.js | 19 + app/styles/ampache.css | 153 + app/styles/bootstrap/bootstrap-theme.css | 587 ++ app/styles/bootstrap/bootstrap-theme.css.map | 1 + app/styles/bootstrap/bootstrap-theme.min.css | 6 + .../bootstrap/bootstrap-theme.min.css.map | 1 + app/styles/bootstrap/bootstrap.css | 6760 +++++++++++++++++ app/styles/bootstrap/bootstrap.css.map | 1 + app/styles/bootstrap/bootstrap.min.css | 6 + app/styles/bootstrap/bootstrap.min.css.map | 1 + .../ie10-viewport-bug-workaround.css | 15 + .../fonts/glyphicons-halflings-regular.eot | Bin 0 -> 20127 bytes .../fonts/glyphicons-halflings-regular.svg | 288 + .../fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 45404 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 0 -> 23424 bytes .../fonts/glyphicons-halflings-regular.woff2 | Bin 0 -> 18028 bytes app/utils/index.js | 5 + app/utils/jquery.js | 20 + app/utils/misc.js | 26 + app/utils/reducers.js | 9 + app/utils/string.js | 16 + app/utils/url.js | 13 + app/views/AlbumPage.jsx | 40 + app/views/AlbumsPage.jsx | 43 + app/views/ArtistPage.jsx | 40 + app/views/ArtistsPage.jsx | 43 + app/views/BrowsePage.jsx | 11 + app/views/HomePage.jsx | 11 + app/views/LoginPage.jsx | 76 + app/views/LogoutPage.jsx | 26 + app/views/SongsPage.jsx | 43 + favicon.ico | Bin 0 -> 284274 bytes fix.ie9.js | 1 + hooks/pre-commit | 57 + index.html | 43 + index.js | 19 + package.json | 40 + vendor/bootstrap/bootstrap.min.js | 7 + .../bootstrap/ie10-viewport-bug-workaround.js | 23 + webpack.config.base.js | 43 + webpack.config.development.js | 6 + webpack.config.js | 1 + webpack.config.production.js | 32 + 77 files changed, 10361 insertions(+) create mode 100644 .babelrc create mode 100644 .eslintignore create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 TODO create mode 100644 app/actions/APIActions.js create mode 100644 app/actions/auth.js create mode 100644 app/actions/index.js create mode 100644 app/assets/img/ampache-blue.png create mode 100644 app/components/Album.jsx create mode 100644 app/components/Albums.jsx create mode 100644 app/components/Artist.jsx create mode 100644 app/components/Artists.jsx create mode 100644 app/components/Login.jsx create mode 100644 app/components/Songs.jsx create mode 100644 app/components/elements/FilterBar.jsx create mode 100644 app/components/elements/Grid.jsx create mode 100644 app/components/elements/Pagination.jsx create mode 100644 app/components/layouts/Sidebar.jsx create mode 100644 app/components/layouts/Simple.jsx create mode 100644 app/containers/App.jsx create mode 100644 app/containers/RequireAuthentication.js create mode 100644 app/containers/Root.jsx create mode 100644 app/dist/fix.ie9.js create mode 100644 app/dist/fix.ie9.js.map create mode 100644 app/dist/index.js create mode 100644 app/dist/index.js.map create mode 100644 app/middleware/api.js create mode 100644 app/reducers/auth.js create mode 100644 app/reducers/index.js create mode 100644 app/reducers/paginate.js create mode 100644 app/routes.js create mode 100644 app/store/configureStore.js create mode 100644 app/styles/ampache.css create mode 100644 app/styles/bootstrap/bootstrap-theme.css create mode 100644 app/styles/bootstrap/bootstrap-theme.css.map create mode 100644 app/styles/bootstrap/bootstrap-theme.min.css create mode 100644 app/styles/bootstrap/bootstrap-theme.min.css.map create mode 100644 app/styles/bootstrap/bootstrap.css create mode 100644 app/styles/bootstrap/bootstrap.css.map create mode 100644 app/styles/bootstrap/bootstrap.min.css create mode 100644 app/styles/bootstrap/bootstrap.min.css.map create mode 100644 app/styles/bootstrap/ie10-viewport-bug-workaround.css create mode 100644 app/styles/fonts/glyphicons-halflings-regular.eot create mode 100644 app/styles/fonts/glyphicons-halflings-regular.svg create mode 100644 app/styles/fonts/glyphicons-halflings-regular.ttf create mode 100644 app/styles/fonts/glyphicons-halflings-regular.woff create mode 100644 app/styles/fonts/glyphicons-halflings-regular.woff2 create mode 100644 app/utils/index.js create mode 100644 app/utils/jquery.js create mode 100644 app/utils/misc.js create mode 100644 app/utils/reducers.js create mode 100644 app/utils/string.js create mode 100644 app/utils/url.js create mode 100644 app/views/AlbumPage.jsx create mode 100644 app/views/AlbumsPage.jsx create mode 100644 app/views/ArtistPage.jsx create mode 100644 app/views/ArtistsPage.jsx create mode 100644 app/views/BrowsePage.jsx create mode 100644 app/views/HomePage.jsx create mode 100644 app/views/LoginPage.jsx create mode 100644 app/views/LogoutPage.jsx create mode 100644 app/views/SongsPage.jsx create mode 100644 favicon.ico create mode 100644 fix.ie9.js create mode 100755 hooks/pre-commit create mode 100644 index.html create mode 100644 index.js create mode 100644 package.json create mode 100644 vendor/bootstrap/bootstrap.min.js create mode 100644 vendor/bootstrap/ie10-viewport-bug-workaround.js create mode 100644 webpack.config.base.js create mode 100644 webpack.config.development.js create mode 100644 webpack.config.js create mode 100644 webpack.config.production.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..d4e5f8d --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015", "react"] +} diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..1dd702d --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +app/dist/* +node_modules/* +vendor/* +webpack.config.* diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..0526dde --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,41 @@ +module.exports = { + "env": { + "browser": true, + "es6": true + }, + "extends": "eslint:recommended", + "installedESLint": true, + "parserOptions": { + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + }, + "sourceType": "module" + }, + "plugins": [ + "react" + ], + "rules": { + "indent": [ + "error", + 4 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "double" + ], + "semi": [ + "error", + "always" + ], + "strict": [ + "error", + ], + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error" + } +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..de519c7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Phyks(Lucas Verney) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e1101c --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +Ampache React +============= + +This is an alternative web interface for +[Ampache](https://github.com/ampache/ampache/) built using Ampache XML API and +React. + +## Trying it out + +Just drop this repo in a location served by a webserver and head your browser +to the correct URL :) + + +## Support + +The supported browsers should be: + +* `IE >= 9` (previous versions of IE are no longer supported by Microsoft) +* Any recent version of any other browser. + +If you experience any issue, please report :) + + +## Building + +Building of this app relies on `webpack`. + +First do a `npm install` to install all the required dependencies. + +Then, to make a development build, just run `webpack` in the root folder. To +make a production build, just run `NODE_ENV=production webpack` in the root +folder. All files will be generated in the `app/dist` folder. + +Please use the Git hooks (in `hooks` folder) to automatically make a build +before comitting, as commit should always contain an up to date production +build. + +## License + +This code is distributed under an MIT license. + +Feel free to contribute and reuse. For more details, see `LICENSE` file. diff --git a/TODO b/TODO new file mode 100644 index 0000000..f24b16c --- /dev/null +++ b/TODO @@ -0,0 +1,16 @@ +5. Web player +6. Homepage +7. Settings +8. Search +9. Discover + + +## Global UI + * What happens when JS is off? + => https://www.allantatter.com/react-js-and-progressive-enhancement/ + +## Miscellaneous + * See TODOs in the code + * https://facebook.github.io/immutable-js/ ? + * Web workers? + * Accessibility and semantics diff --git a/app/actions/APIActions.js b/app/actions/APIActions.js new file mode 100644 index 0000000..bc2ad29 --- /dev/null +++ b/app/actions/APIActions.js @@ -0,0 +1,77 @@ +import humps from "humps"; + +import { CALL_API } from "../middleware/api"; +import { DEFAULT_LIMIT } from "../reducers/paginate"; + +export default function (action, requestType, successType, failureType) { + const itemName = action.rstrip("s"); + const fetchItemsSuccess = function (itemsList, itemsCount) { + return { + type: successType, + payload: { + items: itemsList, + total: itemsCount + } + }; + }; + const fetchItemsRequest = function () { + return { + type: requestType, + payload: { + } + }; + }; + const fetchItemsFailure = function (error) { + return { + type: failureType, + payload: { + error: error + } + }; + }; + const fetchItems = function (endpoint, username, passphrase, filter, offset, include = [], limit=DEFAULT_LIMIT) { + var extraParams = { + offset: offset, + limit: limit + }; + if (filter) { + extraParams.filter = filter; + } + if (include && include.length > 0) { + extraParams.include = include; + } + return { + type: CALL_API, + payload: { + endpoint: endpoint, + dispatch: [ + fetchItemsRequest, + jsonData => dispatch => { + dispatch(fetchItemsSuccess(jsonData[itemName], jsonData[action])); + }, + fetchItemsFailure + ], + action: action, + auth: passphrase, + username: username, + extraParams: extraParams + } + }; + }; + const loadItems = function({ pageNumber = 1, filter = null, include = [] } = {}) { + return (dispatch, getState) => { + const { auth } = getState(); + const offset = (pageNumber - 1) * DEFAULT_LIMIT; + dispatch(fetchItems(auth.endpoint, auth.username, auth.token.token, filter, offset, include)); + }; + }; + + const camelizedAction = humps.pascalize(action); + var returned = {}; + returned["fetch" + camelizedAction + "Success"] = fetchItemsSuccess; + returned["fetch" + camelizedAction + "Request"] = fetchItemsRequest; + returned["fetch" + camelizedAction + "Failure"] = fetchItemsFailure; + returned["fetch" + camelizedAction] = fetchItems; + returned["load" + camelizedAction] = loadItems; + return returned; +} diff --git a/app/actions/auth.js b/app/actions/auth.js new file mode 100644 index 0000000..ceb6ed3 --- /dev/null +++ b/app/actions/auth.js @@ -0,0 +1,173 @@ +import { push } from "react-router-redux"; +import jsSHA from "jssha"; +import Cookies from "js-cookie"; + +import { CALL_API } from "../middleware/api"; + +export const DEFAULT_SESSION_INTERVAL = 1800 * 1000; // 30 mins default + +function _cleanEndpoint (endpoint) { + // Handle endpoints of the form "ampache.example.com" + if ( + !endpoint.startsWith("//") && + !endpoint.startsWith("http://") && + !endpoint.startsWith("https://")) + { + endpoint = "http://" + endpoint; + } + // Remove trailing slash and store endpoint + endpoint = endpoint.replace(/\/$/, ""); + return endpoint; +} + +function _buildHMAC (password) { + // Handle Ampache HMAC generation + const time = Math.floor(Date.now() / 1000); + + var shaObj = new jsSHA("SHA-256", "TEXT"); + shaObj.update(password); + const key = shaObj.getHash("HEX"); + + shaObj = new jsSHA("SHA-256", "TEXT"); + shaObj.update(time + key); + + return { + time: time, + passphrase: shaObj.getHash("HEX") + }; +} + +export function loginKeepAlive(username, token, endpoint) { + return { + type: CALL_API, + payload: { + endpoint: endpoint, + dispatch: [ + null, + null, + error => dispatch => { + dispatch(loginUserFailure(error || "Your session expired… =(")); + } + ], + action: "ping", + auth: token, + username: username, + extraParams: {} + } + }; +} + +export const LOGIN_USER_SUCCESS = "LOGIN_USER_SUCCESS"; +export function loginUserSuccess(username, token, endpoint, rememberMe, timerID) { + return { + type: LOGIN_USER_SUCCESS, + payload: { + username: username, + token: token, + endpoint: endpoint, + rememberMe: rememberMe, + timerID: timerID + } + }; +} + +export const LOGIN_USER_FAILURE = "LOGIN_USER_FAILURE"; +export function loginUserFailure(error) { + Cookies.remove("username"); + Cookies.remove("token"); + Cookies.remove("endpoint"); + return { + type: LOGIN_USER_FAILURE, + payload: { + error: error + } + }; +} + +export const LOGIN_USER_REQUEST = "LOGIN_USER_REQUEST"; +export function loginUserRequest() { + return { + type: LOGIN_USER_REQUEST + }; +} + +export const LOGOUT_USER = "LOGOUT_USER"; +export function logout() { + return (dispatch, state) => { + const { auth } = state(); + if (auth.timerID) { + clearInterval(auth.timerID); + } + Cookies.remove("username"); + Cookies.remove("token"); + Cookies.remove("endpoint"); + dispatch({ + type: LOGOUT_USER + }); + }; +} + +export function logoutAndRedirect() { + return (dispatch) => { + dispatch(logout()); + dispatch(push("/login")); + }; +} + +export function loginUser(username, passwordOrToken, endpoint, rememberMe, redirect="/", isToken=false) { + endpoint = _cleanEndpoint(endpoint); + var time = 0; + var passphrase = passwordOrToken; + + if (!isToken) { + // Standard password connection + const HMAC = _buildHMAC(passwordOrToken); + time = HMAC.time; + passphrase = HMAC.passphrase; + } else { + // Remember me connection + if (passwordOrToken.expires < new Date()) { + // Token has expired + return loginUserFailure("Your session expired… =("); + } + time = Math.floor(Date.now() / 1000); + passphrase = passwordOrToken.token; + } + return { + type: CALL_API, + payload: { + endpoint: endpoint, + dispatch: [ + loginUserRequest, + jsonData => dispatch => { + if (!jsonData.auth || !jsonData.sessionExpire) { + return Promise.reject("API error."); + } + const token = { + token: jsonData.auth, + expires: new Date(jsonData.sessionExpire) + }; + // Dispatch success + const timerID = setInterval( + () => dispatch(loginKeepAlive(username, token.token, endpoint)), + DEFAULT_SESSION_INTERVAL + ); + if (rememberMe) { + const cookiesOption = { expires: token.expires }; + Cookies.set("username", username, cookiesOption); + Cookies.set("token", token, cookiesOption); + Cookies.set("endpoint", endpoint, cookiesOption); + } + dispatch(loginUserSuccess(username, token, endpoint, rememberMe, timerID)); + // Redirect + dispatch(push(redirect)); + }, + loginUserFailure + ], + action: "handshake", + auth: passphrase, + username: username, + extraParams: {timestamp: time} + } + }; +} diff --git a/app/actions/index.js b/app/actions/index.js new file mode 100644 index 0000000..7919dfb --- /dev/null +++ b/app/actions/index.js @@ -0,0 +1,18 @@ +export * from "./auth"; + +import APIAction from "./APIActions"; + +export const ARTISTS_SUCCESS = "ARTISTS_SUCCESS"; +export const ARTISTS_REQUEST = "ARTISTS_REQUEST"; +export const ARTISTS_FAILURE = "ARTISTS_FAILURE"; +export var { loadArtists } = APIAction("artists", ARTISTS_REQUEST, ARTISTS_SUCCESS, ARTISTS_FAILURE); + +export const ALBUMS_SUCCESS = "ALBUMS_SUCCESS"; +export const ALBUMS_REQUEST = "ALBUMS_REQUEST"; +export const ALBUMS_FAILURE = "ALBUMS_FAILURE"; +export var { loadAlbums } = APIAction("albums", ALBUMS_REQUEST, ALBUMS_SUCCESS, ALBUMS_FAILURE); + +export const SONGS_SUCCESS = "SONGS_SUCCESS"; +export const SONGS_REQUEST = "SONGS_REQUEST"; +export const SONGS_FAILURE = "SONGS_FAILURE"; +export var { loadSongs } = APIAction("songs", SONGS_REQUEST, SONGS_SUCCESS, SONGS_FAILURE); diff --git a/app/assets/img/ampache-blue.png b/app/assets/img/ampache-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..87840836de7fe02d2f37ac2e61f70b5a4a1ec3f1 GIT binary patch literal 45726 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 m0|1it--Z2a|N4?%|9=3Z#$dh + {this.props.track.track} + {this.props.track.name} + {length} + + ); + } +} + +AlbumTrackRow.propTypes = { + track: PropTypes.object.isRequired +}; + + +export class AlbumTracksTable extends Component { + render () { + var rows = []; + this.props.tracks.forEach(function (item) { + rows.push(); + }); + return ( + + + {rows} + +
+ ); + } +} + +AlbumTracksTable.propTypes = { + tracks: PropTypes.array.isRequired +}; + +export class AlbumRow extends Component { + render () { + return ( +
+
+
+

{this.props.album.name}

+
+
+
+
+

{this.props.album.name}

+
+
+ +
+
+
+ ); + } +} + +AlbumRow.propTypes = { + album: PropTypes.object.isRequired +}; + +export default class Album extends Component { + render () { + return ( + + ); + } +} + +Album.propTypes = { + album: PropTypes.object.isRequired +}; diff --git a/app/components/Albums.jsx b/app/components/Albums.jsx new file mode 100644 index 0000000..517def7 --- /dev/null +++ b/app/components/Albums.jsx @@ -0,0 +1,19 @@ +import React, { Component, PropTypes } from "react"; + +import FilterablePaginatedGrid from "./elements/Grid"; + +export default class Albums extends Component { + render () { + return ( + + ); + } +} + +Albums.propTypes = { + albums: PropTypes.array.isRequired, + albumsTotalCount: PropTypes.number.isRequired, + albumsPerPage: PropTypes.number.isRequired, + currentPage: PropTypes.number.isRequired, + location: PropTypes.object.isRequired +}; diff --git a/app/components/Artist.jsx b/app/components/Artist.jsx new file mode 100644 index 0000000..76a5b4c --- /dev/null +++ b/app/components/Artist.jsx @@ -0,0 +1,35 @@ +import React, { Component, PropTypes } from "react"; + +import { AlbumRow } from "./Album"; + +// TODO: Songs without associated album + +export default class Artist extends Component { + render () { + var albumsRows = []; + if (Array.isArray(this.props.artist.albums)) { + this.props.artist.albums.forEach(function (item) { + albumsRows.push(); + }); + } + return ( +
+
+
+

{this.props.artist.name}

+
+

{this.props.artist.summary}

+
+
+

{this.props.artist.name}/

+
+
+ { albumsRows } +
+ ); + } +} + +Artist.propTypes = { + artist: PropTypes.object.isRequired +}; diff --git a/app/components/Artists.jsx b/app/components/Artists.jsx new file mode 100644 index 0000000..1d2998c --- /dev/null +++ b/app/components/Artists.jsx @@ -0,0 +1,19 @@ +import React, { Component, PropTypes } from "react"; + +import FilterablePaginatedGrid from "./elements/Grid"; + +export default class Artists extends Component { + render () { + return ( + + ); + } +} + +Artists.propTypes = { + artists: PropTypes.array.isRequired, + artistsTotalCount: PropTypes.number.isRequired, + artistsPerPage: PropTypes.number.isRequired, + currentPage: PropTypes.number.isRequired, + location: PropTypes.object.isRequired +}; diff --git a/app/components/Login.jsx b/app/components/Login.jsx new file mode 100644 index 0000000..b9cebbb --- /dev/null +++ b/app/components/Login.jsx @@ -0,0 +1,142 @@ +import React, { Component, PropTypes } from "react"; +import $ from "jquery"; + +export class LoginForm extends Component { + constructor (props) { + super(props) + + this.handleSubmit = this.handleSubmit.bind(this) + } + + setError (formGroup, error) { + if (error) { + formGroup.classList.add("has-error"); + formGroup.classList.remove("has-success"); + return true; + } + formGroup.classList.remove("has-error"); + formGroup.classList.add("has-success"); + return false; + } + + handleSubmit (e) { + e.preventDefault(); + const username = this.refs.username.value.trim(); + const password = this.refs.password.value.trim(); + const endpoint = this.refs.endpoint.value.trim(); + const rememberMe = this.refs.rememberMe.checked; + + var hasError = this.setError(this.refs.usernameFormGroup, !username); + hasError |= this.setError(this.refs.passwordFormGroup, !password); + hasError |= this.setError(this.refs.endpointFormGroup, !endpoint); + + if (!hasError) { + this.props.onSubmit(username, password, endpoint, rememberMe) + } + } + + componentDidUpdate (prevProps) { + if (this.props.error) { + $(this.refs.loginForm).shake(3, 10, 300); + this.setError(this.refs.usernameFormGroup, this.props.error); + this.setError(this.refs.passwordFormGroup, this.props.error); + this.setError(this.refs.endpointFormGroup, this.props.error); + } + } + + render () { + return ( +
+ { + this.props.error ? +
+
+ { this.props.error } +
+
+ : null + } + { + this.props.info ? +
+
+ { this.props.info } +
+
+ : null + } +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+ ); + } +} + +LoginForm.propTypes = { + username: PropTypes.string, + endpoint: PropTypes.string, + rememberMe: PropTypes.bool, + onSubmit: PropTypes.func.isRequired, + isAuthenticating: PropTypes.bool, + error: PropTypes.string, + info: PropTypes.string +}; + + +export default class Login extends Component { + render () { + return ( +
+

Ampache

+
+

Welcome back on Ampache, let"s go!

+
+ +
+
+ ); + } +} + +Login.propTypes = { + username: PropTypes.string, + endpoint: PropTypes.string, + rememberMe: PropTypes.bool, + onSubmit: PropTypes.func.isRequired, + isAuthenticating: PropTypes.bool, + error: PropTypes.string, + info: PropTypes.string +}; diff --git a/app/components/Songs.jsx b/app/components/Songs.jsx new file mode 100644 index 0000000..536ae08 --- /dev/null +++ b/app/components/Songs.jsx @@ -0,0 +1,110 @@ +import React, { Component, PropTypes } from "react"; +import { Link} from "react-router"; +import Fuse from "fuse.js"; + +import FilterBar from "./elements/FilterBar"; +import Pagination from "./elements/Pagination"; +import { formatLength} from "../utils"; + +export class SongsTableRow extends Component { + render () { + const length = formatLength(this.props.song.length); + const linkToArtist = "/artist/" + this.props.song.artist.id; + const linkToAlbum = "/album/" + this.props.song.album.id; + return ( + + + {this.props.song.name} + {this.props.song.artist.name} + {this.props.song.album.name} + {this.props.song.genre} + {length} + + ); + } +} + +SongsTableRow.propTypes = { + song: PropTypes.object.isRequired +}; + + +export class SongsTable extends Component { + render () { + var displayedSongs = this.props.songs; + if (this.props.filterText) { + // Use Fuse for the filter + displayedSongs = new Fuse( + this.props.songs, + { + "keys": ["name"], + "threshold": 0.4, + "include": ["score"] + }).search(this.props.filterText); + // Keep only items in results + displayedSongs = displayedSongs.map(function (item) { return item.item; }); + } + + var rows = []; + displayedSongs.forEach(function (song) { + rows.push(); + }); + return ( + + + + + + + + + + + + {rows} +
TitleArtistAlbumGenreLength
+ ); + } +} + +SongsTable.propTypes = { + songs: PropTypes.array.isRequired, + filterText: PropTypes.string +}; + + +export default class FilterablePaginatedSongsTable extends Component { + constructor (props) { + super(props); + this.state = { + filterText: "" + }; + + this.handleUserInput = this.handleUserInput.bind(this); + } + + handleUserInput (filterText) { + this.setState({ + filterText: filterText.trim() + }); + } + + render () { + const nPages = Math.ceil(this.props.songsTotalCount / this.props.songsPerPage); + return ( +
+ + + +
+ ); + } +} + +FilterablePaginatedSongsTable.propTypes = { + songs: PropTypes.array.isRequired, + songsTotalCount: PropTypes.number.isRequired, + songsPerPage: PropTypes.number.isRequired, + currentPage: PropTypes.number.isRequired, + location: PropTypes.object.isRequired +}; diff --git a/app/components/elements/FilterBar.jsx b/app/components/elements/FilterBar.jsx new file mode 100644 index 0000000..d2a0b49 --- /dev/null +++ b/app/components/elements/FilterBar.jsx @@ -0,0 +1,34 @@ +import React, { Component, PropTypes } from "react"; + +export default class FilterBar extends Component { + constructor (props) { + super(props); + this.handleChange = this.handleChange.bind(this); + } + + handleChange (e) { + e.preventDefault(); + + this.props.onUserInput(this.refs.filterTextInput.value); + } + + render () { + return ( +
+

What are we listening to today?

+
+
+
+ +
+
+
+
+ ); + } +} + +FilterBar.propTypes = { + onUserInput: PropTypes.func, + filterText: PropTypes.string +}; diff --git a/app/components/elements/Grid.jsx b/app/components/elements/Grid.jsx new file mode 100644 index 0000000..0d3b73d --- /dev/null +++ b/app/components/elements/Grid.jsx @@ -0,0 +1,232 @@ +import React, { Component, PropTypes } from "react"; +import { Link} from "react-router"; +import imagesLoaded from "imagesloaded"; +import Isotope from "isotope-layout"; +import Fuse from "fuse.js"; +import _ from "lodash"; +import $ from "jquery"; + +import FilterBar from "./FilterBar"; +import Pagination from "./Pagination"; + +export class GridItem extends Component { + render () { + var nSubItems = this.props.item[this.props.subItemsType]; + if (Array.isArray(nSubItems)) { + nSubItems = nSubItems.length; + } + var subItemsLabel = this.props.subItemsType; + if (nSubItems < 2) { + subItemsLabel = subItemsLabel.rstrip("s"); + } + const to = "/" + this.props.itemsType.rstrip("s") + "/" + this.props.item.id; + const id = "grid-item-" + this.props.item.type + "/" + this.props.item.id; + return ( +
+
+ {this.props.item.name}/ +

{this.props.item.name}

+ {nSubItems} {subItemsLabel} +
+
+ ); + } +} + +GridItem.propTypes = { + item: PropTypes.object.isRequired, + itemsType: PropTypes.string.isRequired, + subItemsType: PropTypes.string.isRequired +}; + + +const ISOTOPE_OPTIONS = { /** Default options for Isotope grid layout. */ + getSortData: { + name: ".name", + nSubitems: ".sub-items .n-sub-items" + }, + transitionDuration: 0, + sortBy: "name", + itemSelector: ".grid-item", + percentPosition: true, + layoutMode: "fitRows", + filter: "*", + fitRows: { + gutter: 0 + } +}; + +export class Grid extends Component { + constructor (props) { + super(props); + + // Init grid data member + this.iso = null; + + this.handleFiltering = this.handleFiltering.bind(this); + } + + createIsotopeContainer () { + if (this.iso == null) { + this.iso = new Isotope(this.refs.grid, ISOTOPE_OPTIONS); + } + } + + handleFiltering (props) { + // If no query provided, drop any filter in use + if (props.filterText == "") { + return this.iso.arrange(ISOTOPE_OPTIONS); + } + // Use Fuse for the filter + var result = new Fuse( + props.items, + { + "keys": ["name"], + "threshold": 0.4, + "include": ["score"] + }).search(props.filterText); + + // Apply filter on grid + this.iso.arrange({ + filter: function () { + var name = $(this).find(".name").text(); + return result.find(function (item) { return item.item.name == name; }); + }, + transitionDuration: "0.4s", + getSortData: { + relevance: function (item) { + var name = $(item).find(".name").text(); + return result.reduce(function (p, c) { + if (c.item.name == name) { + return c.score + p; + } + return p; + }, 0); + } + }, + sortBy: "relevance" + }); + this.iso.updateSortData(); + this.iso.arrange(); + } + + shouldComponentUpdate(nextProps, nextState) { + return !_.isEqual(this.props, nextProps) || !_.isEqual(this.state, nextState); + } + + componentWillReceiveProps(nextProps) { + if (!_.isEqual(nextProps.filterText, this.props.filterText)) { + this.handleFiltering(nextProps); + } + } + + componentDidMount () { + // Setup grid + this.createIsotopeContainer(); + // Only arrange if there are elements to arrange + if (_.get(this, "props.items.length", 0) > 0) { + this.iso.arrange(); + } + } + + componentDidUpdate(prevProps) { + // The list of keys seen in the previous render + let currentKeys = _.map( + prevProps.items, + (n) => "grid-item-" + n.type + "/" + n.id); + + // The latest list of keys that have been rendered + let newKeys = _.map( + this.props.items, + (n) => "grid-item-" + n.type + "/" + n.id); + + // Find which keys are new between the current set of keys and any new children passed to this component + let addKeys = _.difference(newKeys, currentKeys); + + // Find which keys have been removed between the current set of keys and any new children passed to this component + let removeKeys = _.difference(currentKeys, newKeys); + + if (removeKeys.length > 0) { + _.each(removeKeys, removeKey => this.iso.remove(document.getElementById(removeKey))); + this.iso.arrange(); + } + if (addKeys.length > 0) { + this.iso.addItems(_.map(addKeys, (addKey) => document.getElementById(addKey))); + this.iso.arrange(); + } + + var iso = this.iso; + // Layout again after images are loaded + imagesLoaded(this.refs.grid).on("progress", function() { + // Layout after each image load, fix for responsive grid + if (!iso) { // Grid could have been destroyed in the meantime + return; + } + iso.layout(); + }); + } + + render () { + var gridItems = []; + const itemsType = this.props.itemsType; + const subItemsType = this.props.subItemsType; + this.props.items.forEach(function (item) { + gridItems.push(); + }); + return ( +
+
+ {/* Sizing element */} +
+ {/* Other items */} + { gridItems } +
+
+ ); + } +} + +Grid.propTypes = { + items: PropTypes.array.isRequired, + itemsType: PropTypes.string.isRequired, + subItemsType: PropTypes.string.isRequired, + filterText: PropTypes.string +}; + +export default class FilterablePaginatedGrid extends Component { + constructor (props) { + super(props); + this.state = { + filterText: "" + }; + + this.handleUserInput = this.handleUserInput.bind(this); + } + + handleUserInput (filterText) { + this.setState({ + filterText: filterText.trim() + }); + } + + render () { + const nPages = Math.ceil(this.props.itemsTotalCount / this.props.itemsPerPage); + return ( +
+ + + +
+ ); + } +} + +FilterablePaginatedGrid.propTypes = { + items: PropTypes.array.isRequired, + itemsTotalCount: PropTypes.number.isRequired, + itemsPerPage: PropTypes.number.isRequired, + currentPage: PropTypes.number.isRequired, + location: PropTypes.object.isRequired, + itemsType: PropTypes.string.isRequired, + subItemsType: PropTypes.string.isRequired +}; diff --git a/app/components/elements/Pagination.jsx b/app/components/elements/Pagination.jsx new file mode 100644 index 0000000..f1d9050 --- /dev/null +++ b/app/components/elements/Pagination.jsx @@ -0,0 +1,138 @@ +import React, { Component, PropTypes } from "react"; +import { Link, withRouter } from "react-router"; +import $ from "jquery"; + +export class Pagination extends Component { + constructor(props) { + super(props); + this.buildLinkTo.bind(this); + } + + computePaginationBounds(currentPage, nPages, maxNumberPagesShown=5) { + // Taken from http://stackoverflow.com/a/8608998/2626416 + var lowerLimit = currentPage; + var upperLimit = currentPage; + + for (var b = 1; b < maxNumberPagesShown && b < nPages;) { + if (lowerLimit > 1 ) { + lowerLimit--; + b++; + } + if (b < maxNumberPagesShown && upperLimit < nPages) { + upperLimit++; + b++; + } + } + + return { + lowerLimit: lowerLimit, + upperLimit: upperLimit + 1 // +1 to ease iteration in for with < + }; + } + + buildLinkTo(pageNumber) { + return { + pathname: this.props.location.pathname, + query: Object.assign({}, this.props.location.query, { page: pageNumber }) + }; + } + + goToPage() { + const pageNumber = parseInt(this.refs.pageInput.value); + $("#paginationModal").modal("hide"); + this.props.router.push(this.buildLinkTo(pageNumber)); + } + + render () { + const { lowerLimit, upperLimit } = this.computePaginationBounds(this.props.currentPage, this.props.nPages); + var pagesButton = []; + var key = 0; // key increment to ensure correct ordering + if (lowerLimit > 1) { + // Push first page + pagesButton.push( +
  • + 1 +
  • + ); + key++; + if (lowerLimit > 2) { + // Eventually push "…" + pagesButton.push( +
  • + $("#paginationModal").modal() }>… +
  • + ); + key++; + } + } + var i = 0; + for (i = lowerLimit; i < upperLimit; i++) { + var className = "page-item"; + if (this.props.currentPage == i) { + className += " active"; + } + pagesButton.push( +
  • + {i} +
  • + ); + key++; + } + if (i < this.props.nPages) { + if (i < this.props.nPages - 1) { + // Eventually push "…" + pagesButton.push( +
  • + $("#paginationModal").modal() }>… +
  • + ); + key++; + } + // Push last page + pagesButton.push( +
  • + {this.props.nPages} +
  • + ); + } + if (pagesButton.length > 1) { + return ( +
    + + +
    + ); + } + return null; + } +} + +Pagination.propTypes = { + currentPage: PropTypes.number.isRequired, + location: PropTypes.object.isRequired, + nPages: PropTypes.number.isRequired +}; + +export default withRouter(Pagination); diff --git a/app/components/layouts/Sidebar.jsx b/app/components/layouts/Sidebar.jsx new file mode 100644 index 0000000..ceab78b --- /dev/null +++ b/app/components/layouts/Sidebar.jsx @@ -0,0 +1,60 @@ +import React, { Component } from "react"; +import { IndexLink, Link} from "react-router"; + +export default class SidebarLayout extends Component { + render () { + return ( +
    +
    +

    Ampache

    + +
    + +
    + {this.props.children} +
    +
    + ); + } +} diff --git a/app/components/layouts/Simple.jsx b/app/components/layouts/Simple.jsx new file mode 100644 index 0000000..4430524 --- /dev/null +++ b/app/components/layouts/Simple.jsx @@ -0,0 +1,11 @@ +import React, { Component } from "react"; + +export default class SimpleLayout extends Component { + render () { + return ( +
    + {this.props.children} +
    + ); + } +} diff --git a/app/containers/App.jsx b/app/containers/App.jsx new file mode 100644 index 0000000..46f56e7 --- /dev/null +++ b/app/containers/App.jsx @@ -0,0 +1,18 @@ +import React, { Component, PropTypes } from "react"; + +export default class App extends Component { + render () { + return ( +
    + {this.props.children && React.cloneElement(this.props.children, { + error: this.props.error + })} +
    + ); + } +} + +App.propTypes = { + // Injected by React Router + children: PropTypes.node, +}; diff --git a/app/containers/RequireAuthentication.js b/app/containers/RequireAuthentication.js new file mode 100644 index 0000000..f00ffb7 --- /dev/null +++ b/app/containers/RequireAuthentication.js @@ -0,0 +1,50 @@ +import React, { Component, PropTypes } from "react"; +import { connect } from "react-redux"; + +export class RequireAuthentication extends Component { + componentWillMount () { + this.checkAuth(this.props.isAuthenticated); + } + + componentWillUpdate () { + this.checkAuth(this.props.isAuthenticated); + } + + checkAuth (isAuthenticated) { + if (!isAuthenticated) { + this.context.router.replace({ + pathname: "/login", + state: { + nextPathname: this.props.location.pathname, + nextQuery: this.props.location.query + } + }); + } + } + + render () { + return ( +
    + {this.props.isAuthenticated === true + ? this.props.children + : null + } +
    + ); + } +} + +RequireAuthentication.propTypes = { + // Injected by React Router + children: PropTypes.node +}; + +RequireAuthentication.contextTypes = { + router: PropTypes.object.isRequired +}; + +const mapStateToProps = (state) => ({ + isAuthenticated: state.auth.isAuthenticated +}); + +export default connect(mapStateToProps)(RequireAuthentication); diff --git a/app/containers/Root.jsx b/app/containers/Root.jsx new file mode 100644 index 0000000..37699c5 --- /dev/null +++ b/app/containers/Root.jsx @@ -0,0 +1,21 @@ +import React, { Component, PropTypes } from "react"; +import { Provider } from "react-redux"; +import { Router } from "react-router"; + +import routes from "../routes"; + +export default class Root extends Component { + render() { + const { store, history } = this.props; + return ( + + + + ); + } +} + +Root.propTypes = { + store: PropTypes.object.isRequired, + history: PropTypes.object.isRequired +}; diff --git a/app/dist/fix.ie9.js b/app/dist/fix.ie9.js new file mode 100644 index 0000000..7aa52db --- /dev/null +++ b/app/dist/fix.ie9.js @@ -0,0 +1,2 @@ +!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="/app/dist/",t(0)}({0:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(495);Object.keys(r).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},495:function(e,t){!function(t,n){function r(e,t){var n=e.createElement("p"),r=e.getElementsByTagName("head")[0]||e.documentElement;return n.innerHTML="x",r.insertBefore(n.lastChild,r.firstChild)}function a(){var e=b.elements;return"string"==typeof e?e.split(" "):e}function o(e,t){var n=b.elements;"string"!=typeof n&&(n=n.join(" ")),"string"!=typeof e&&(e=e.join(" ")),b.elements=n+" "+e,s(t)}function c(e){var t=E[e[v]];return t||(t={},y++,e[v]=y,E[y]=t),t}function i(e,t,r){if(t||(t=n),f)return t.createElement(e);r||(r=c(t));var a;return a=r.cache[e]?r.cache[e].cloneNode():g.test(e)?(r.cache[e]=r.createElem(e)).cloneNode():r.createElem(e),!a.canHaveChildren||p.test(e)||a.tagUrn?a:r.frag.appendChild(a)}function l(e,t){if(e||(e=n),f)return e.createDocumentFragment();t=t||c(e);for(var r=t.frag.cloneNode(),o=0,i=a(),l=i.length;o",d="hidden"in e,f=1==e.childNodes.length||function(){n.createElement("a");var e=n.createDocumentFragment();return"undefined"==typeof e.cloneNode||"undefined"==typeof e.createDocumentFragment||"undefined"==typeof e.createElement}()}catch(t){d=!0,f=!0}}();var b={elements:h.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:h.shivCSS!==!1,supportsUnknownElements:f,shivMethods:h.shivMethods!==!1,type:"default",shivDocument:s,createElement:i,createDocumentFragment:l,addElements:o};t.html5=b,s(n),"object"==typeof e&&e.exports&&(e.exports=b)}("undefined"!=typeof window?window:this,document)}}); +//# sourceMappingURL=fix.ie9.js.map \ No newline at end of file diff --git a/app/dist/fix.ie9.js.map b/app/dist/fix.ie9.js.map new file mode 100644 index 0000000..4d02e73 --- /dev/null +++ b/app/dist/fix.ie9.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///fix.ie9.js","webpack:///webpack/bootstrap 9adf20f87cfcad4a7983?2805","webpack:///./fix.ie9.js","webpack:///./~/html5shiv/dist/html5shiv.js"],"names":["modules","__webpack_require__","moduleId","installedModules","exports","module","id","loaded","call","m","c","p","0","Object","defineProperty","value","_html5shiv","keys","forEach","key","enumerable","get","495","window","document","addStyleSheet","ownerDocument","cssText","createElement","parent","getElementsByTagName","documentElement","innerHTML","insertBefore","lastChild","firstChild","getElements","elements","html5","split","addElements","newElements","join","shivDocument","getExpandoData","data","expandoData","expando","expanID","nodeName","supportsUnknownElements","node","cache","cloneNode","saveClones","test","createElem","canHaveChildren","reSkip","tagUrn","frag","appendChild","createDocumentFragment","clone","i","elems","l","length","shivMethods","createFrag","Function","replace","shivCSS","supportsHtml5Styles","hasCSS","version","options","a","childNodes","e","type","this"],"mappings":"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","file":"fix.ie9.js","sourcesContent":["/******/ (function(modules) { // webpackBootstrap\n/******/ \t// The module cache\n/******/ \tvar installedModules = {};\n/******/\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(installedModules[moduleId])\n/******/ \t\t\treturn installedModules[moduleId].exports;\n/******/\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = installedModules[moduleId] = {\n/******/ \t\t\texports: {},\n/******/ \t\t\tid: moduleId,\n/******/ \t\t\tloaded: false\n/******/ \t\t};\n/******/\n/******/ \t\t// Execute the module function\n/******/ \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n/******/\n/******/ \t\t// Flag the module as loaded\n/******/ \t\tmodule.loaded = true;\n/******/\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/\n/******/\n/******/ \t// expose the modules object (__webpack_modules__)\n/******/ \t__webpack_require__.m = modules;\n/******/\n/******/ \t// expose the module cache\n/******/ \t__webpack_require__.c = installedModules;\n/******/\n/******/ \t// __webpack_public_path__\n/******/ \t__webpack_require__.p = \"/app/dist/\";\n/******/\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(0);\n/******/ })\n/************************************************************************/\n/******/ ({\n\n/***/ 0:\n/***/ function(module, exports, __webpack_require__) {\n\n\t\"use strict\";\n\t\n\tObject.defineProperty(exports, \"__esModule\", {\n\t value: true\n\t});\n\t\n\tvar _html5shiv = __webpack_require__(495);\n\t\n\tObject.keys(_html5shiv).forEach(function (key) {\n\t if (key === \"default\") return;\n\t Object.defineProperty(exports, key, {\n\t enumerable: true,\n\t get: function get() {\n\t return _html5shiv[key];\n\t }\n\t });\n\t});\n\n/***/ },\n\n/***/ 495:\n/***/ function(module, exports) {\n\n\t/**\n\t* @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed\n\t*/\n\t;(function(window, document) {\n\t/*jshint evil:true */\n\t /** version */\n\t var version = '3.7.3-pre';\n\t\n\t /** Preset options */\n\t var options = window.html5 || {};\n\t\n\t /** Used to skip problem elements */\n\t var reSkip = /^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i;\n\t\n\t /** Not all elements can be cloned in IE **/\n\t var saveClones = /^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i;\n\t\n\t /** Detect whether the browser supports default html5 styles */\n\t var supportsHtml5Styles;\n\t\n\t /** Name of the expando, to work with multiple documents or to re-shiv one document */\n\t var expando = '_html5shiv';\n\t\n\t /** The id for the the documents expando */\n\t var expanID = 0;\n\t\n\t /** Cached data for each document */\n\t var expandoData = {};\n\t\n\t /** Detect whether the browser supports unknown elements */\n\t var supportsUnknownElements;\n\t\n\t (function() {\n\t try {\n\t var a = document.createElement('a');\n\t a.innerHTML = '';\n\t //if the hidden property is implemented we can assume, that the browser supports basic HTML5 Styles\n\t supportsHtml5Styles = ('hidden' in a);\n\t\n\t supportsUnknownElements = a.childNodes.length == 1 || (function() {\n\t // assign a false positive if unable to shiv\n\t (document.createElement)('a');\n\t var frag = document.createDocumentFragment();\n\t return (\n\t typeof frag.cloneNode == 'undefined' ||\n\t typeof frag.createDocumentFragment == 'undefined' ||\n\t typeof frag.createElement == 'undefined'\n\t );\n\t }());\n\t } catch(e) {\n\t // assign a false positive if detection fails => unable to shiv\n\t supportsHtml5Styles = true;\n\t supportsUnknownElements = true;\n\t }\n\t\n\t }());\n\t\n\t /*--------------------------------------------------------------------------*/\n\t\n\t /**\n\t * Creates a style sheet with the given CSS text and adds it to the document.\n\t * @private\n\t * @param {Document} ownerDocument The document.\n\t * @param {String} cssText The CSS text.\n\t * @returns {StyleSheet} The style element.\n\t */\n\t function addStyleSheet(ownerDocument, cssText) {\n\t var p = ownerDocument.createElement('p'),\n\t parent = ownerDocument.getElementsByTagName('head')[0] || ownerDocument.documentElement;\n\t\n\t p.innerHTML = 'x';\n\t return parent.insertBefore(p.lastChild, parent.firstChild);\n\t }\n\t\n\t /**\n\t * Returns the value of `html5.elements` as an array.\n\t * @private\n\t * @returns {Array} An array of shived element node names.\n\t */\n\t function getElements() {\n\t var elements = html5.elements;\n\t return typeof elements == 'string' ? elements.split(' ') : elements;\n\t }\n\t\n\t /**\n\t * Extends the built-in list of html5 elements\n\t * @memberOf html5\n\t * @param {String|Array} newElements whitespace separated list or array of new element names to shiv\n\t * @param {Document} ownerDocument The context document.\n\t */\n\t function addElements(newElements, ownerDocument) {\n\t var elements = html5.elements;\n\t if(typeof elements != 'string'){\n\t elements = elements.join(' ');\n\t }\n\t if(typeof newElements != 'string'){\n\t newElements = newElements.join(' ');\n\t }\n\t html5.elements = elements +' '+ newElements;\n\t shivDocument(ownerDocument);\n\t }\n\t\n\t /**\n\t * Returns the data associated to the given document\n\t * @private\n\t * @param {Document} ownerDocument The document.\n\t * @returns {Object} An object of data.\n\t */\n\t function getExpandoData(ownerDocument) {\n\t var data = expandoData[ownerDocument[expando]];\n\t if (!data) {\n\t data = {};\n\t expanID++;\n\t ownerDocument[expando] = expanID;\n\t expandoData[expanID] = data;\n\t }\n\t return data;\n\t }\n\t\n\t /**\n\t * returns a shived element for the given nodeName and document\n\t * @memberOf html5\n\t * @param {String} nodeName name of the element\n\t * @param {Document} ownerDocument The context document.\n\t * @returns {Object} The shived element.\n\t */\n\t function createElement(nodeName, ownerDocument, data){\n\t if (!ownerDocument) {\n\t ownerDocument = document;\n\t }\n\t if(supportsUnknownElements){\n\t return ownerDocument.createElement(nodeName);\n\t }\n\t if (!data) {\n\t data = getExpandoData(ownerDocument);\n\t }\n\t var node;\n\t\n\t if (data.cache[nodeName]) {\n\t node = data.cache[nodeName].cloneNode();\n\t } else if (saveClones.test(nodeName)) {\n\t node = (data.cache[nodeName] = data.createElem(nodeName)).cloneNode();\n\t } else {\n\t node = data.createElem(nodeName);\n\t }\n\t\n\t // Avoid adding some elements to fragments in IE < 9 because\n\t // * Attributes like `name` or `type` cannot be set/changed once an element\n\t // is inserted into a document/fragment\n\t // * Link elements with `src` attributes that are inaccessible, as with\n\t // a 403 response, will cause the tab/window to crash\n\t // * Script elements appended to fragments will execute when their `src`\n\t // or `text` property is set\n\t return node.canHaveChildren && !reSkip.test(nodeName) && !node.tagUrn ? data.frag.appendChild(node) : node;\n\t }\n\t\n\t /**\n\t * returns a shived DocumentFragment for the given document\n\t * @memberOf html5\n\t * @param {Document} ownerDocument The context document.\n\t * @returns {Object} The shived DocumentFragment.\n\t */\n\t function createDocumentFragment(ownerDocument, data){\n\t if (!ownerDocument) {\n\t ownerDocument = document;\n\t }\n\t if(supportsUnknownElements){\n\t return ownerDocument.createDocumentFragment();\n\t }\n\t data = data || getExpandoData(ownerDocument);\n\t var clone = data.frag.cloneNode(),\n\t i = 0,\n\t elems = getElements(),\n\t l = elems.length;\n\t for(;i