From 93a6dbe7d6126376d6fbfd4882eda82cb88b46a1 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 10 Apr 2020 15:07:08 -0400 Subject: [PATCH 1/2] Clean up public folder, move JS tools to frontEndSrc folder --- app/views/footer.php | 8 +- app/views/header.php | 6 +- frontEndSrc/build-js.js | 71 ++ {public/tools => frontEndSrc}/css.js | 34 +- frontEndSrc/css/auto.css | 3 + frontEndSrc/css/dark.css | 5 + frontEndSrc/css/light.css | 4 + .../css/src/-marx-.css | 0 .../css/src/components.css | 0 .../css/src/dark-override.css | 0 {public => frontEndSrc}/css/src/general.css | 0 .../css/src/responsive.css | 0 {public/tools => frontEndSrc}/cssfilter.js | 0 .../js/anime-client.js | 6 +- {public/js/src => frontEndSrc/js}/anime.js | 2 +- .../js/src/index.js => frontEndSrc/js/anon.js | 2 +- .../js/base/class-list.js | 0 .../js/base/sort-tables.js | 0 .../js/src/base => frontEndSrc/js}/events.js | 75 +- .../js/index.js | 2 +- {public/js/src => frontEndSrc/js}/manga.js | 2 +- .../js}/template-helpers.js | 4 +- {public => frontEndSrc}/package.json | 15 +- {public => frontEndSrc}/test/ajax.php | 0 {public => frontEndSrc}/test/index.html | 12 +- {public => frontEndSrc}/test/lib/mocha.css | 0 .../test/lib/testBundle.js | 0 .../test/tests/AnimeClient.js | 0 {public => frontEndSrc}/test/tests/ajax.js | 0 {public => frontEndSrc}/yarn.lock | 730 +++++++----------- public/css/auto.min.css | 1 + public/css/dark.min.css | 2 +- public/css/light.min.css | 1 + public/css/src/all.css | 4 - public/es/anon.min.js | 1 + public/es/scripts.min.js | 1 + public/js/anon.min.js | 14 + public/js/anon.min.js.map | 1 + public/js/scripts-authed.min.js | 26 - public/js/scripts-authed.min.js.map | 1 - public/js/scripts.min.js | 23 +- public/js/scripts.min.js.map | 2 +- public/js/tables.min.js | 2 +- public/js/tables.min.js.map | 2 +- public/tools/build-js.js | 44 -- 45 files changed, 518 insertions(+), 588 deletions(-) create mode 100644 frontEndSrc/build-js.js rename {public/tools => frontEndSrc}/css.js (50%) create mode 100644 frontEndSrc/css/auto.css create mode 100644 frontEndSrc/css/dark.css create mode 100644 frontEndSrc/css/light.css rename public/css/src/marx.css => frontEndSrc/css/src/-marx-.css (100%) rename {public => frontEndSrc}/css/src/components.css (100%) rename {public => frontEndSrc}/css/src/dark-override.css (100%) rename {public => frontEndSrc}/css/src/general.css (100%) rename {public => frontEndSrc}/css/src/responsive.css (100%) rename {public/tools => frontEndSrc}/cssfilter.js (100%) rename public/js/src/base/AnimeClient.js => frontEndSrc/js/anime-client.js (98%) rename {public/js/src => frontEndSrc/js}/anime.js (98%) rename public/js/src/index.js => frontEndSrc/js/anon.js (89%) rename public/js/src/base/classList.js => frontEndSrc/js/base/class-list.js (100%) rename public/js/src/base/sort_tables.js => frontEndSrc/js/base/sort-tables.js (100%) rename {public/js/src/base => frontEndSrc/js}/events.js (52%) rename public/js/src/index-authed.js => frontEndSrc/js/index.js (67%) rename {public/js/src => frontEndSrc/js}/manga.js (98%) rename {public/js/src => frontEndSrc/js}/template-helpers.js (98%) rename {public => frontEndSrc}/package.json (53%) rename {public => frontEndSrc}/test/ajax.php (100%) rename {public => frontEndSrc}/test/index.html (71%) rename {public => frontEndSrc}/test/lib/mocha.css (100%) rename {public => frontEndSrc}/test/lib/testBundle.js (100%) rename {public => frontEndSrc}/test/tests/AnimeClient.js (100%) rename {public => frontEndSrc}/test/tests/ajax.js (100%) rename {public => frontEndSrc}/yarn.lock (80%) create mode 100644 public/css/auto.min.css create mode 100644 public/css/light.min.css delete mode 100644 public/css/src/all.css create mode 100644 public/es/anon.min.js create mode 100644 public/es/scripts.min.js create mode 100644 public/js/anon.min.js create mode 100644 public/js/anon.min.js.map delete mode 100644 public/js/scripts-authed.min.js delete mode 100644 public/js/scripts-authed.min.js.map delete mode 100644 public/tools/build-js.js diff --git a/app/views/footer.php b/app/views/footer.php index c089893b..e2bdf90e 100644 --- a/app/views/footer.php +++ b/app/views/footer.php @@ -12,11 +12,11 @@ isAuthenticated()): ?> - - + + - - + + diff --git a/app/views/header.php b/app/views/header.php index 77eee069..25828f09 100644 --- a/app/views/header.php +++ b/app/views/header.php @@ -6,11 +6,7 @@ - get('theme') !== 'auto'): ?> - - get('theme') === 'auto'): ?> - - + diff --git a/frontEndSrc/build-js.js b/frontEndSrc/build-js.js new file mode 100644 index 00000000..a4cc6199 --- /dev/null +++ b/frontEndSrc/build-js.js @@ -0,0 +1,71 @@ +import compiler from '@ampproject/rollup-plugin-closure-compiler'; +import { terser } from 'rollup-plugin-terser'; + +const plugins = [ + compiler({ + assumeFunctionWrapper: true, + compilationLevel: 'WHITESPACE_ONLY', //'ADVANCED', + createSourceMap: true, + env: 'BROWSER', + languageIn: 'ECMASCRIPT_2018', + languageOut: 'ES3' + }) +]; + +const defaultOutput = { + format: 'iife', + sourcemap: true, +} + +const nonModules = [{ + input: './js/anon.js', + output: { + ...defaultOutput, + file: '../public/js/anon.min.js', + sourcemapFile: '../public/js/anon.min.js.map', + }, + plugins, +}, { + input: './js/index.js', + output: { + ...defaultOutput, + file: '../public/js/scripts.min.js', + sourcemapFile: '../public/js/scripts.min.js.map', + }, + plugins, +}, { + input: './js/base/sort-tables.js', + output: { + ...defaultOutput, + file: '../public/js/tables.min.js', + sourcemapFile: '../public/js/tables.min.js.map', + }, + plugins, +}]; + +const moduleOutput = { + format: 'es', + sourcemap: false, +} + +let modules = [{ + input: './js/anon.js', + output: { + ...moduleOutput, + file: '../public/es/anon.min.js', + }, + plugins: [terser()], +}, { + input: './js/index.js', + output: { + ...moduleOutput, + file: '../public/es/scripts.min.js', + }, + plugins: [terser()], +}]; + +// Return the config array for rollup +export default [ + ...nonModules, + ...modules, +]; \ No newline at end of file diff --git a/public/tools/css.js b/frontEndSrc/css.js similarity index 50% rename from public/tools/css.js rename to frontEndSrc/css.js index 7877a57a..4b02f04e 100644 --- a/public/tools/css.js +++ b/frontEndSrc/css.js @@ -7,8 +7,9 @@ const atImport = require('postcss-import'); const cssNext = require('postcss-preset-env'); const cssNano = require('cssnano'); -const css = fs.readFileSync('css/src/all.css', 'utf-8'); +const lightCss = fs.readFileSync('css/light.css', 'utf-8'); const darkCss = fs.readFileSync('css/src/dark-override.css', 'utf-8'); +const fullDarkCss = fs.readFileSync('css/dark.css', 'utf-8'); const minOptions = { autoprefixer: false, @@ -29,28 +30,37 @@ const processOptions = { (async () => { // Basic theme - const light = await postcss() + const lightMin = await postcss() .use(atImport()) .use(cssNext(processOptions)) .use(cssNano(minOptions)) - .process(css, { - from: 'css/src/all.css', - to: 'css/app.min.css', + .process(lightCss, { + from: 'css/light.css', + to: '/public/css/light.min.css', }); - fs.writeFileSync('css/app.min.css', light); + fs.writeFileSync('../public/css/light.min.css', lightMin); // Dark theme - const dark = await postcss() + const darkFullMin = await postcss() + .use(atImport()) + .use(cssNext(processOptions)) + .use(cssNano(minOptions)) + .process(fullDarkCss, { + from: 'css/dark.css', + to: '/public/css/dark.min.css', + }); + fs.writeFileSync('../public/css/dark.min.css', darkFullMin); + + // Dark override + const darkMin = await postcss() .use(atImport()) .use(cssNext(processOptions)) .use(cssNano(minOptions)) .process(darkCss, { from: 'css/dark-override.css', - to: 'css/dark.min.css', + to: '/public/css/dark.min.css', }); - fs.writeFileSync('css/dark.min.css', dark); - - const autoDarkCss = `${light} @media (prefers-color-scheme: dark) { ${dark} }` - fs.writeFileSync('css/dark-auto.min.css', autoDarkCss) + const autoDarkCss = `${lightMin} @media (prefers-color-scheme: dark) { ${darkMin} }` + fs.writeFileSync('../public/css/auto.min.css', autoDarkCss) })(); \ No newline at end of file diff --git a/frontEndSrc/css/auto.css b/frontEndSrc/css/auto.css new file mode 100644 index 00000000..37eca657 --- /dev/null +++ b/frontEndSrc/css/auto.css @@ -0,0 +1,3 @@ +@media (prefers-color-scheme: dark) { + @import "src/dark-override.css"; +} \ No newline at end of file diff --git a/frontEndSrc/css/dark.css b/frontEndSrc/css/dark.css new file mode 100644 index 00000000..3b4f7a82 --- /dev/null +++ b/frontEndSrc/css/dark.css @@ -0,0 +1,5 @@ +@import "src/-marx-.css"; +@import "src/general.css"; +@import "src/components.css"; +@import "src/responsive.css"; +@import "src/dark-override.css"; \ No newline at end of file diff --git a/frontEndSrc/css/light.css b/frontEndSrc/css/light.css new file mode 100644 index 00000000..6c328380 --- /dev/null +++ b/frontEndSrc/css/light.css @@ -0,0 +1,4 @@ +@import "src/-marx-.css"; +@import "src/general.css"; +@import "src/components.css"; +@import "src/responsive.css"; diff --git a/public/css/src/marx.css b/frontEndSrc/css/src/-marx-.css similarity index 100% rename from public/css/src/marx.css rename to frontEndSrc/css/src/-marx-.css diff --git a/public/css/src/components.css b/frontEndSrc/css/src/components.css similarity index 100% rename from public/css/src/components.css rename to frontEndSrc/css/src/components.css diff --git a/public/css/src/dark-override.css b/frontEndSrc/css/src/dark-override.css similarity index 100% rename from public/css/src/dark-override.css rename to frontEndSrc/css/src/dark-override.css diff --git a/public/css/src/general.css b/frontEndSrc/css/src/general.css similarity index 100% rename from public/css/src/general.css rename to frontEndSrc/css/src/general.css diff --git a/public/css/src/responsive.css b/frontEndSrc/css/src/responsive.css similarity index 100% rename from public/css/src/responsive.css rename to frontEndSrc/css/src/responsive.css diff --git a/public/tools/cssfilter.js b/frontEndSrc/cssfilter.js similarity index 100% rename from public/tools/cssfilter.js rename to frontEndSrc/cssfilter.js diff --git a/public/js/src/base/AnimeClient.js b/frontEndSrc/js/anime-client.js similarity index 98% rename from public/js/src/base/AnimeClient.js rename to frontEndSrc/js/anime-client.js index e77739af..04869dba 100644 --- a/public/js/src/base/AnimeClient.js +++ b/frontEndSrc/js/anime-client.js @@ -3,9 +3,9 @@ // ------------------------------------------------------------------------- const matches = (elm, selector) => { - let matches = (elm.document || elm.ownerDocument).querySelectorAll(selector), - i = matches.length; - while (--i >= 0 && matches.item(i) !== elm) {}; + let m = (elm.document || elm.ownerDocument).querySelectorAll(selector); + let i = matches.length; + while (--i >= 0 && m.item(i) !== elm) {}; return i > -1; } diff --git a/public/js/src/anime.js b/frontEndSrc/js/anime.js similarity index 98% rename from public/js/src/anime.js rename to frontEndSrc/js/anime.js index 27e1f94c..cdf1ac7e 100644 --- a/public/js/src/anime.js +++ b/frontEndSrc/js/anime.js @@ -1,4 +1,4 @@ -import _ from './base/AnimeClient.js' +import _ from './anime-client.js' import { renderAnimeSearchResults } from './template-helpers.js' const search = (query) => { diff --git a/public/js/src/index.js b/frontEndSrc/js/anon.js similarity index 89% rename from public/js/src/index.js rename to frontEndSrc/js/anon.js index 88d68d5b..89a7bdbc 100644 --- a/public/js/src/index.js +++ b/frontEndSrc/js/anon.js @@ -1,4 +1,4 @@ -import './base/events.js'; +import './events.js'; if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').then(reg => { diff --git a/public/js/src/base/classList.js b/frontEndSrc/js/base/class-list.js similarity index 100% rename from public/js/src/base/classList.js rename to frontEndSrc/js/base/class-list.js diff --git a/public/js/src/base/sort_tables.js b/frontEndSrc/js/base/sort-tables.js similarity index 100% rename from public/js/src/base/sort_tables.js rename to frontEndSrc/js/base/sort-tables.js diff --git a/public/js/src/base/events.js b/frontEndSrc/js/events.js similarity index 52% rename from public/js/src/base/events.js rename to frontEndSrc/js/events.js index 70182d37..2a8adfab 100644 --- a/public/js/src/base/events.js +++ b/frontEndSrc/js/events.js @@ -1,31 +1,61 @@ -import _ from './AnimeClient.js'; -/** - * Event handlers - */ -// Close event for messages -_.on('header', 'click', '.message', (e) => { - _.hide(e.target); -}); +import _ from './anime-client.js'; -// Confirm deleting of list or library items -_.on('form.js-delete', 'submit', (event) => { +// ---------------------------------------------------------------------------- +// Event subscriptions +// ---------------------------------------------------------------------------- +_.on('header', 'click', '.message', hide); +_.on('form.js-delete', 'submit', confirmDelete); +_.on('.js-clear-cache', 'click', clearAPICache); +_.on('.vertical-tabs input', 'change', scrollToSection); +_.on('.media-filter', 'input', filterMedia); + +// ---------------------------------------------------------------------------- +// Handler functions +// ---------------------------------------------------------------------------- + +/** + * Hide the html element attached to the event + * + * @param event + * @return void + */ +function hide (event) { + _.hide(event.target) +} + +/** + * Confirm deletion of an item + * + * @param event + * @return void + */ +function confirmDelete (event) { const proceed = confirm('Are you ABSOLUTELY SURE you want to delete this item?'); if (proceed === false) { event.preventDefault(); event.stopPropagation(); } -}); +} -// Clear the api cache -_.on('.js-clear-cache', 'click', () => { +/** + * Clear the API cache, and show a message if the cache is cleared + * + * @return void + */ +function clearAPICache () { _.get('/cache_purge', () => { _.showMessage('success', 'Successfully purged api cache'); }); -}); +} -// Alleviate some page jumping - _.on('.vertical-tabs input', 'change', (event) => { +/** + * Scroll to the accordion/vertical tab section just opened + * + * @param event + * @return void + */ +function scrollToSection (event) { const el = event.currentTarget.parentElement; const rect = el.getBoundingClientRect(); @@ -35,10 +65,15 @@ _.on('.js-clear-cache', 'click', () => { top, behavior: 'smooth', }); -}); +} -// Filter the current page (cover view) -_.on('.media-filter', 'input', (event) => { +/** + * Filter an anime or manga list + * + * @param event + * @return void + */ +function filterMedia (event) { const rawFilter = event.target.value; const filter = new RegExp(rawFilter, 'i'); @@ -72,4 +107,4 @@ _.on('.media-filter', 'input', (event) => { _.show('article.media'); _.show('table.media-wrap tbody tr'); } -}); +} diff --git a/public/js/src/index-authed.js b/frontEndSrc/js/index.js similarity index 67% rename from public/js/src/index-authed.js rename to frontEndSrc/js/index.js index 5d852c20..6cad73e4 100644 --- a/public/js/src/index-authed.js +++ b/frontEndSrc/js/index.js @@ -1,4 +1,4 @@ -import './index.js'; +import './anon.js'; import './anime.js'; import './manga.js'; diff --git a/public/js/src/manga.js b/frontEndSrc/js/manga.js similarity index 98% rename from public/js/src/manga.js rename to frontEndSrc/js/manga.js index 120b69e5..6af89f93 100644 --- a/public/js/src/manga.js +++ b/frontEndSrc/js/manga.js @@ -1,4 +1,4 @@ -import _ from './base/AnimeClient.js' +import _ from './anime-client.js' import { renderMangaSearchResults } from './template-helpers.js' const search = (query) => { diff --git a/public/js/src/template-helpers.js b/frontEndSrc/js/template-helpers.js similarity index 98% rename from public/js/src/template-helpers.js rename to frontEndSrc/js/template-helpers.js index 4de89e8b..d9038c22 100644 --- a/public/js/src/template-helpers.js +++ b/frontEndSrc/js/template-helpers.js @@ -1,4 +1,4 @@ -import _ from './base/AnimeClient.js'; +import _ from './anime-client.js'; // Click on hidden MAL checkbox so // that MAL id is passed @@ -27,7 +27,7 @@ export function renderAnimeSearchResults (data) { - + ${item.canonicalTitle}
${titles} diff --git a/public/package.json b/frontEndSrc/package.json similarity index 53% rename from public/package.json rename to frontEndSrc/package.json index 00d00c0a..7970de82 100644 --- a/public/package.json +++ b/frontEndSrc/package.json @@ -2,20 +2,21 @@ "license": "MIT", "scripts": { "build": "npm run build:css && npm run build:js", - "build:css": "node ./tools/css.js", - "build:js": "rollup -c ./tools/build-js.js", - "watch:css": "watch 'npm run build:css' --filter=./tools/cssfilter.js", + "build:css": "node ./css.js", + "build:js": "rollup -c ./build-js.js", + "watch:css": "watch 'npm run build:css' --filter=./cssfilter.js", "watch:js": "watch 'npm run build:js' ./js/src", "watch": "concurrently \"npm:watch:css\" \"npm:watch:js\" --kill-others" }, "devDependencies": { - "@ampproject/rollup-plugin-closure-compiler": "^0.9.0", - "concurrently": "^4.1.1", + "@ampproject/rollup-plugin-closure-compiler": "^0.24.0", + "concurrently": "^5.1.0", "cssnano": "^4.1.10", - "postcss": "^7.0.17", + "postcss": "^7.0.27", "postcss-import": "^12.0.1", "postcss-preset-env": "^6.7.0", - "rollup": "^1.16.7", + "rollup": "^2.4.0", + "rollup-plugin-terser": "^5.3.0", "watch": "^1.0.2" } } diff --git a/public/test/ajax.php b/frontEndSrc/test/ajax.php similarity index 100% rename from public/test/ajax.php rename to frontEndSrc/test/ajax.php diff --git a/public/test/index.html b/frontEndSrc/test/index.html similarity index 71% rename from public/test/index.html rename to frontEndSrc/test/index.html index 93cdb56c..7afcc145 100644 --- a/public/test/index.html +++ b/frontEndSrc/test/index.html @@ -3,7 +3,7 @@ Hummingbird AnimeClient Front-end Testsuite - +
@@ -20,8 +20,8 @@
    - - + + - + - - + + isAuthenticated()): ?> - + - + diff --git a/frontEndSrc/build-js.js b/frontEndSrc/build-js.js index a4cc6199..5a3c19de 100644 --- a/frontEndSrc/build-js.js +++ b/frontEndSrc/build-js.js @@ -1,5 +1,4 @@ import compiler from '@ampproject/rollup-plugin-closure-compiler'; -import { terser } from 'rollup-plugin-terser'; const plugins = [ compiler({ @@ -52,16 +51,14 @@ let modules = [{ input: './js/anon.js', output: { ...moduleOutput, - file: '../public/es/anon.min.js', + file: '../public/es/anon.js', }, - plugins: [terser()], }, { input: './js/index.js', output: { ...moduleOutput, - file: '../public/es/scripts.min.js', + file: '../public/es/scripts.js', }, - plugins: [terser()], }]; // Return the config array for rollup diff --git a/frontEndSrc/package.json b/frontEndSrc/package.json index 7970de82..34a8b9b2 100644 --- a/frontEndSrc/package.json +++ b/frontEndSrc/package.json @@ -16,7 +16,6 @@ "postcss-import": "^12.0.1", "postcss-preset-env": "^6.7.0", "rollup": "^2.4.0", - "rollup-plugin-terser": "^5.3.0", "watch": "^1.0.2" } } diff --git a/frontEndSrc/yarn.lock b/frontEndSrc/yarn.lock index 1a160fca..afcf2fbb 100644 --- a/frontEndSrc/yarn.lock +++ b/frontEndSrc/yarn.lock @@ -23,27 +23,6 @@ magic-string "0.25.7" uuid "7.0.2" -"@babel/code-frame@^7.5.5": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" - integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== - dependencies: - "@babel/highlight" "^7.8.3" - -"@babel/helper-validator-identifier@^7.9.0": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80" - integrity sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g== - -"@babel/highlight@^7.8.3": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079" - integrity sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ== - dependencies: - "@babel/helper-validator-identifier" "^7.9.0" - chalk "^2.0.0" - js-tokens "^4.0.0" - "@csstools/convert-colors@^1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" @@ -125,11 +104,6 @@ browserslist@^4.0.0, browserslist@^4.6.3, browserslist@^4.6.4: electron-to-chromium "^1.3.188" node-releases "^1.1.25" -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - caller-callsite@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" @@ -169,7 +143,7 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000980, caniuse-lite@^1.0.30000981: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000983.tgz#ab3c70061ca2a3467182a10ac75109b199b647f8" integrity sha512-/llD1bZ6qwNkt41AsvjsmwNOoA4ZB+8iqmf5LVyeSXuBODT/hAMFNVOh84NdUzoiYiSKqo5vQ3ZzeYHSi/olDQ== -chalk@2.x, chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: +chalk@2.x, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -253,11 +227,6 @@ color@^3.0.0: color-convert "^1.9.1" color-string "^1.5.2" -commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - concurrently@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-5.1.0.tgz#05523986ba7aaf4b58a49ddd658fab88fa783132" @@ -557,11 +526,6 @@ estree-walker@2.0.1: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.1.tgz#f8e030fb21cefa183b44b7ad516b747434e7a3e0" integrity sha512-tF0hv+Yi2Ot1cwj9eYHtxC0jB9bmjacjQs6ZBTj82H8JwUywFuc+7E83NWfNMwHXZc11mjfFcVXPe9gEP4B8dg== -estree-walker@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" - integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== - exec-sh@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36" @@ -780,19 +744,6 @@ isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= -jest-worker@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5" - integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw== - dependencies: - merge-stream "^2.0.0" - supports-color "^6.1.0" - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - js-yaml@^3.13.1: version "3.13.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" @@ -861,11 +812,6 @@ mdn-data@~1.1.0: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01" integrity sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA== -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - merge@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" @@ -1663,24 +1609,6 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= -rollup-plugin-terser@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-5.3.0.tgz#9c0dd33d5771df9630cd027d6a2559187f65885e" - integrity sha512-XGMJihTIO3eIBsVGq7jiNYOdDMb3pVxuzY0uhOE/FM4x/u9nQgr3+McsjzqBn3QfHIpNSZmFnpoKAwHBEcsT7g== - dependencies: - "@babel/code-frame" "^7.5.5" - jest-worker "^24.9.0" - rollup-pluginutils "^2.8.2" - serialize-javascript "^2.1.2" - terser "^4.6.2" - -rollup-pluginutils@^2.8.2: - version "2.8.2" - resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" - integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ== - dependencies: - estree-walker "^0.6.1" - rollup@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.4.0.tgz#b136a4d701d24dd79ec9551ee0330e7f632ee9d2" @@ -1710,11 +1638,6 @@ sax@~1.2.4: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== -serialize-javascript@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" - integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== - set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -1727,20 +1650,12 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" -source-map-support@~0.5.12: - version "0.5.16" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" - integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - source-map@^0.5.1, source-map@^0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: +source-map@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -1862,15 +1777,6 @@ svgo@^1.0.0: unquote "~1.1.1" util.promisify "~1.0.0" -terser@^4.6.2: - version "4.6.11" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.11.tgz#12ff99fdd62a26de2a82f508515407eb6ccd8a9f" - integrity sha512-76Ynm7OXUG5xhOpblhytE7X58oeNSmC8xnNhjWVo8CksHit0U0kO4hfNbPrrYwowLWFgM2n9L176VNx2QaHmtA== - dependencies: - commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" - timsort@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" diff --git a/public/es/anon.js b/public/es/anon.js new file mode 100644 index 00000000..266dc124 --- /dev/null +++ b/public/es/anon.js @@ -0,0 +1,460 @@ +// ------------------------------------------------------------------------- +// ! Base +// ------------------------------------------------------------------------- + +const matches = (elm, selector) => { + let m = (elm.document || elm.ownerDocument).querySelectorAll(selector); + let i = matches.length; + while (--i >= 0 && m.item(i) !== elm) {} return i > -1; +}; + +const AnimeClient = { + /** + * Placeholder function + */ + noop: () => {}, + /** + * DOM selector + * + * @param {string} selector - The dom selector string + * @param {object} [context] + * @return {[HTMLElement]} - array of dom elements + */ + $(selector, context = null) { + if (typeof selector !== 'string') { + return selector; + } + + context = (context !== null && context.nodeType === 1) + ? context + : document; + + let elements = []; + if (selector.match(/^#([\w]+$)/)) { + elements.push(document.getElementById(selector.split('#')[1])); + } else { + elements = [].slice.apply(context.querySelectorAll(selector)); + } + + return elements; + }, + /** + * Does the selector exist on the current page? + * + * @param {string} selector + * @returns {boolean} + */ + hasElement (selector) { + return AnimeClient.$(selector).length > 0; + }, + /** + * Scroll to the top of the Page + * + * @return {void} + */ + scrollToTop () { + const el = AnimeClient.$('header')[0]; + el.scrollIntoView(true); + }, + /** + * Hide the selected element + * + * @param {string|Element} sel - the selector of the element to hide + * @return {void} + */ + hide (sel) { + if (typeof sel === 'string') { + sel = AnimeClient.$(sel); + } + + if (Array.isArray(sel)) { + sel.forEach(el => el.setAttribute('hidden', 'hidden')); + } else { + sel.setAttribute('hidden', 'hidden'); + } + }, + /** + * UnHide the selected element + * + * @param {string|Element} sel - the selector of the element to hide + * @return {void} + */ + show (sel) { + if (typeof sel === 'string') { + sel = AnimeClient.$(sel); + } + + if (Array.isArray(sel)) { + sel.forEach(el => el.removeAttribute('hidden')); + } else { + sel.removeAttribute('hidden'); + } + }, + /** + * Display a message box + * + * @param {string} type - message type: info, error, success + * @param {string} message - the message itself + * @return {void} + */ + showMessage (type, message) { + let template = + `
    + + ${message} + +
    `; + + let sel = AnimeClient.$('.message'); + if (sel[0] !== undefined) { + sel[0].remove(); + } + + AnimeClient.$('header')[0].insertAdjacentHTML('beforeend', template); + }, + /** + * Finds the closest parent element matching the passed selector + * + * @param {HTMLElement} current - the current HTMLElement + * @param {string} parentSelector - selector for the parent element + * @return {HTMLElement|null} - the parent element + */ + closestParent (current, parentSelector) { + if (Element.prototype.closest !== undefined) { + return current.closest(parentSelector); + } + + while (current !== document.documentElement) { + if (matches(current, parentSelector)) { + return current; + } + + current = current.parentElement; + } + + return null; + }, + /** + * Generate a full url from a relative path + * + * @param {string} path - url path + * @return {string} - full url + */ + url (path) { + let uri = `//${document.location.host}`; + uri += (path.charAt(0) === '/') ? path : `/${path}`; + + return uri; + }, + /** + * Throttle execution of a function + * + * @see https://remysharp.com/2010/07/21/throttling-function-calls + * @see https://jsfiddle.net/jonathansampson/m7G64/ + * @param {Number} interval - the minimum throttle time in ms + * @param {Function} fn - the function to throttle + * @param {Object} [scope] - the 'this' object for the function + * @return {Function} + */ + throttle (interval, fn, scope) { + let wait = false; + return function (...args) { + const context = scope || this; + + if ( ! wait) { + fn.apply(context, args); + wait = true; + setTimeout(function() { + wait = false; + }, interval); + } + }; + }, +}; + +// ------------------------------------------------------------------------- +// ! Events +// ------------------------------------------------------------------------- + +function addEvent(sel, event, listener) { + // Recurse! + if (! event.match(/^([\w\-]+)$/)) { + event.split(' ').forEach((evt) => { + addEvent(sel, evt, listener); + }); + } + + sel.addEventListener(event, listener, false); +} + +function delegateEvent(sel, target, event, listener) { + // Attach the listener to the parent + addEvent(sel, event, (e) => { + // Get live version of the target selector + AnimeClient.$(target, sel).forEach((element) => { + if(e.target == element) { + listener.call(element, e); + e.stopPropagation(); + } + }); + }); +} + +/** + * Add an event listener + * + * @param {string|HTMLElement} sel - the parent selector to bind to + * @param {string} event - event name(s) to bind + * @param {string|HTMLElement|function} target - the element to directly bind the event to + * @param {function} [listener] - event listener callback + * @return {void} + */ +AnimeClient.on = (sel, event, target, listener) => { + if (listener === undefined) { + listener = target; + AnimeClient.$(sel).forEach((el) => { + addEvent(el, event, listener); + }); + } else { + AnimeClient.$(sel).forEach((el) => { + delegateEvent(el, target, event, listener); + }); + } +}; + +// ------------------------------------------------------------------------- +// ! Ajax +// ------------------------------------------------------------------------- + +/** + * Url encoding for non-get requests + * + * @param data + * @returns {string} + * @private + */ +function ajaxSerialize(data) { + let pairs = []; + + Object.keys(data).forEach((name) => { + let value = data[name].toString(); + + name = encodeURIComponent(name); + value = encodeURIComponent(value); + + pairs.push(`${name}=${value}`); + }); + + return pairs.join('&'); +} + +/** + * Make an ajax request + * + * Config:{ + * data: // data to send with the request + * type: // http verb of the request, defaults to GET + * success: // success callback + * error: // error callback + * } + * + * @param {string} url - the url to request + * @param {Object} config - the configuration object + * @return {void} + */ +AnimeClient.ajax = (url, config) => { + // Set some sane defaults + const defaultConfig = { + data: {}, + type: 'GET', + dataType: '', + success: AnimeClient.noop, + mimeType: 'application/x-www-form-urlencoded', + error: AnimeClient.noop + }; + + config = { + ...defaultConfig, + ...config, + }; + + let request = new XMLHttpRequest(); + let method = String(config.type).toUpperCase(); + + if (method === 'GET') { + url += (url.match(/\?/)) + ? ajaxSerialize(config.data) + : `?${ajaxSerialize(config.data)}`; + } + + request.open(method, url); + + request.onreadystatechange = () => { + if (request.readyState === 4) { + let responseText = ''; + + if (request.responseType === 'json') { + responseText = JSON.parse(request.responseText); + } else { + responseText = request.responseText; + } + + if (request.status > 299) { + config.error.call(null, request.status, responseText, request.response); + } else { + config.success.call(null, responseText, request.status); + } + } + }; + + if (config.dataType === 'json') { + config.data = JSON.stringify(config.data); + config.mimeType = 'application/json'; + } else { + config.data = ajaxSerialize(config.data); + } + + request.setRequestHeader('Content-Type', config.mimeType); + + if (method === 'GET') { + request.send(null); + } else { + request.send(config.data); + } +}; + +/** + * Do a get request + * + * @param {string} url + * @param {object|function} data + * @param {function} [callback] + */ +AnimeClient.get = (url, data, callback = null) => { + if (callback === null) { + callback = data; + data = {}; + } + + return AnimeClient.ajax(url, { + data, + success: callback + }); +}; + +// ---------------------------------------------------------------------------- +// Event subscriptions +// ---------------------------------------------------------------------------- +AnimeClient.on('header', 'click', '.message', hide); +AnimeClient.on('form.js-delete', 'submit', confirmDelete); +AnimeClient.on('.js-clear-cache', 'click', clearAPICache); +AnimeClient.on('.vertical-tabs input', 'change', scrollToSection); +AnimeClient.on('.media-filter', 'input', filterMedia); + +// ---------------------------------------------------------------------------- +// Handler functions +// ---------------------------------------------------------------------------- + +/** + * Hide the html element attached to the event + * + * @param event + * @return void + */ +function hide (event) { + AnimeClient.hide(event.target); +} + +/** + * Confirm deletion of an item + * + * @param event + * @return void + */ +function confirmDelete (event) { + const proceed = confirm('Are you ABSOLUTELY SURE you want to delete this item?'); + + if (proceed === false) { + event.preventDefault(); + event.stopPropagation(); + } +} + +/** + * Clear the API cache, and show a message if the cache is cleared + * + * @return void + */ +function clearAPICache () { + AnimeClient.get('/cache_purge', () => { + AnimeClient.showMessage('success', 'Successfully purged api cache'); + }); +} + +/** + * Scroll to the accordion/vertical tab section just opened + * + * @param event + * @return void + */ +function scrollToSection (event) { + const el = event.currentTarget.parentElement; + const rect = el.getBoundingClientRect(); + + const top = rect.top + window.pageYOffset; + + window.scrollTo({ + top, + behavior: 'smooth', + }); +} + +/** + * Filter an anime or manga list + * + * @param event + * @return void + */ +function filterMedia (event) { + const rawFilter = event.target.value; + const filter = new RegExp(rawFilter, 'i'); + + // console.log('Filtering items by: ', filter); + + if (rawFilter !== '') { + // Filter the cover view + AnimeClient.$('article.media').forEach(article => { + const titleLink = AnimeClient.$('.name a', article)[0]; + const title = String(titleLink.textContent).trim(); + if ( ! filter.test(title)) { + AnimeClient.hide(article); + } else { + AnimeClient.show(article); + } + }); + + // Filter the list view + AnimeClient.$('table.media-wrap tbody tr').forEach(tr => { + const titleCell = AnimeClient.$('td.align-left', tr)[0]; + const titleLink = AnimeClient.$('a', titleCell)[0]; + const linkTitle = String(titleLink.textContent).trim(); + const textTitle = String(titleCell.textContent).trim(); + if ( ! (filter.test(linkTitle) || filter.test(textTitle))) { + AnimeClient.hide(tr); + } else { + AnimeClient.show(tr); + } + }); + } else { + AnimeClient.show('article.media'); + AnimeClient.show('table.media-wrap tbody tr'); + } +} + +if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js').then(reg => { + console.log('Service worker registered', reg.scope); + }).catch(error => { + console.error('Failed to register service worker', error); + }); +} diff --git a/public/es/scripts.js b/public/es/scripts.js new file mode 100644 index 00000000..21b0ab5e --- /dev/null +++ b/public/es/scripts.js @@ -0,0 +1,721 @@ +// ------------------------------------------------------------------------- +// ! Base +// ------------------------------------------------------------------------- + +const matches = (elm, selector) => { + let m = (elm.document || elm.ownerDocument).querySelectorAll(selector); + let i = matches.length; + while (--i >= 0 && m.item(i) !== elm) {} return i > -1; +}; + +const AnimeClient = { + /** + * Placeholder function + */ + noop: () => {}, + /** + * DOM selector + * + * @param {string} selector - The dom selector string + * @param {object} [context] + * @return {[HTMLElement]} - array of dom elements + */ + $(selector, context = null) { + if (typeof selector !== 'string') { + return selector; + } + + context = (context !== null && context.nodeType === 1) + ? context + : document; + + let elements = []; + if (selector.match(/^#([\w]+$)/)) { + elements.push(document.getElementById(selector.split('#')[1])); + } else { + elements = [].slice.apply(context.querySelectorAll(selector)); + } + + return elements; + }, + /** + * Does the selector exist on the current page? + * + * @param {string} selector + * @returns {boolean} + */ + hasElement (selector) { + return AnimeClient.$(selector).length > 0; + }, + /** + * Scroll to the top of the Page + * + * @return {void} + */ + scrollToTop () { + const el = AnimeClient.$('header')[0]; + el.scrollIntoView(true); + }, + /** + * Hide the selected element + * + * @param {string|Element} sel - the selector of the element to hide + * @return {void} + */ + hide (sel) { + if (typeof sel === 'string') { + sel = AnimeClient.$(sel); + } + + if (Array.isArray(sel)) { + sel.forEach(el => el.setAttribute('hidden', 'hidden')); + } else { + sel.setAttribute('hidden', 'hidden'); + } + }, + /** + * UnHide the selected element + * + * @param {string|Element} sel - the selector of the element to hide + * @return {void} + */ + show (sel) { + if (typeof sel === 'string') { + sel = AnimeClient.$(sel); + } + + if (Array.isArray(sel)) { + sel.forEach(el => el.removeAttribute('hidden')); + } else { + sel.removeAttribute('hidden'); + } + }, + /** + * Display a message box + * + * @param {string} type - message type: info, error, success + * @param {string} message - the message itself + * @return {void} + */ + showMessage (type, message) { + let template = + `
    + + ${message} + +
    `; + + let sel = AnimeClient.$('.message'); + if (sel[0] !== undefined) { + sel[0].remove(); + } + + AnimeClient.$('header')[0].insertAdjacentHTML('beforeend', template); + }, + /** + * Finds the closest parent element matching the passed selector + * + * @param {HTMLElement} current - the current HTMLElement + * @param {string} parentSelector - selector for the parent element + * @return {HTMLElement|null} - the parent element + */ + closestParent (current, parentSelector) { + if (Element.prototype.closest !== undefined) { + return current.closest(parentSelector); + } + + while (current !== document.documentElement) { + if (matches(current, parentSelector)) { + return current; + } + + current = current.parentElement; + } + + return null; + }, + /** + * Generate a full url from a relative path + * + * @param {string} path - url path + * @return {string} - full url + */ + url (path) { + let uri = `//${document.location.host}`; + uri += (path.charAt(0) === '/') ? path : `/${path}`; + + return uri; + }, + /** + * Throttle execution of a function + * + * @see https://remysharp.com/2010/07/21/throttling-function-calls + * @see https://jsfiddle.net/jonathansampson/m7G64/ + * @param {Number} interval - the minimum throttle time in ms + * @param {Function} fn - the function to throttle + * @param {Object} [scope] - the 'this' object for the function + * @return {Function} + */ + throttle (interval, fn, scope) { + let wait = false; + return function (...args) { + const context = scope || this; + + if ( ! wait) { + fn.apply(context, args); + wait = true; + setTimeout(function() { + wait = false; + }, interval); + } + }; + }, +}; + +// ------------------------------------------------------------------------- +// ! Events +// ------------------------------------------------------------------------- + +function addEvent(sel, event, listener) { + // Recurse! + if (! event.match(/^([\w\-]+)$/)) { + event.split(' ').forEach((evt) => { + addEvent(sel, evt, listener); + }); + } + + sel.addEventListener(event, listener, false); +} + +function delegateEvent(sel, target, event, listener) { + // Attach the listener to the parent + addEvent(sel, event, (e) => { + // Get live version of the target selector + AnimeClient.$(target, sel).forEach((element) => { + if(e.target == element) { + listener.call(element, e); + e.stopPropagation(); + } + }); + }); +} + +/** + * Add an event listener + * + * @param {string|HTMLElement} sel - the parent selector to bind to + * @param {string} event - event name(s) to bind + * @param {string|HTMLElement|function} target - the element to directly bind the event to + * @param {function} [listener] - event listener callback + * @return {void} + */ +AnimeClient.on = (sel, event, target, listener) => { + if (listener === undefined) { + listener = target; + AnimeClient.$(sel).forEach((el) => { + addEvent(el, event, listener); + }); + } else { + AnimeClient.$(sel).forEach((el) => { + delegateEvent(el, target, event, listener); + }); + } +}; + +// ------------------------------------------------------------------------- +// ! Ajax +// ------------------------------------------------------------------------- + +/** + * Url encoding for non-get requests + * + * @param data + * @returns {string} + * @private + */ +function ajaxSerialize(data) { + let pairs = []; + + Object.keys(data).forEach((name) => { + let value = data[name].toString(); + + name = encodeURIComponent(name); + value = encodeURIComponent(value); + + pairs.push(`${name}=${value}`); + }); + + return pairs.join('&'); +} + +/** + * Make an ajax request + * + * Config:{ + * data: // data to send with the request + * type: // http verb of the request, defaults to GET + * success: // success callback + * error: // error callback + * } + * + * @param {string} url - the url to request + * @param {Object} config - the configuration object + * @return {void} + */ +AnimeClient.ajax = (url, config) => { + // Set some sane defaults + const defaultConfig = { + data: {}, + type: 'GET', + dataType: '', + success: AnimeClient.noop, + mimeType: 'application/x-www-form-urlencoded', + error: AnimeClient.noop + }; + + config = { + ...defaultConfig, + ...config, + }; + + let request = new XMLHttpRequest(); + let method = String(config.type).toUpperCase(); + + if (method === 'GET') { + url += (url.match(/\?/)) + ? ajaxSerialize(config.data) + : `?${ajaxSerialize(config.data)}`; + } + + request.open(method, url); + + request.onreadystatechange = () => { + if (request.readyState === 4) { + let responseText = ''; + + if (request.responseType === 'json') { + responseText = JSON.parse(request.responseText); + } else { + responseText = request.responseText; + } + + if (request.status > 299) { + config.error.call(null, request.status, responseText, request.response); + } else { + config.success.call(null, responseText, request.status); + } + } + }; + + if (config.dataType === 'json') { + config.data = JSON.stringify(config.data); + config.mimeType = 'application/json'; + } else { + config.data = ajaxSerialize(config.data); + } + + request.setRequestHeader('Content-Type', config.mimeType); + + if (method === 'GET') { + request.send(null); + } else { + request.send(config.data); + } +}; + +/** + * Do a get request + * + * @param {string} url + * @param {object|function} data + * @param {function} [callback] + */ +AnimeClient.get = (url, data, callback = null) => { + if (callback === null) { + callback = data; + data = {}; + } + + return AnimeClient.ajax(url, { + data, + success: callback + }); +}; + +// ---------------------------------------------------------------------------- +// Event subscriptions +// ---------------------------------------------------------------------------- +AnimeClient.on('header', 'click', '.message', hide); +AnimeClient.on('form.js-delete', 'submit', confirmDelete); +AnimeClient.on('.js-clear-cache', 'click', clearAPICache); +AnimeClient.on('.vertical-tabs input', 'change', scrollToSection); +AnimeClient.on('.media-filter', 'input', filterMedia); + +// ---------------------------------------------------------------------------- +// Handler functions +// ---------------------------------------------------------------------------- + +/** + * Hide the html element attached to the event + * + * @param event + * @return void + */ +function hide (event) { + AnimeClient.hide(event.target); +} + +/** + * Confirm deletion of an item + * + * @param event + * @return void + */ +function confirmDelete (event) { + const proceed = confirm('Are you ABSOLUTELY SURE you want to delete this item?'); + + if (proceed === false) { + event.preventDefault(); + event.stopPropagation(); + } +} + +/** + * Clear the API cache, and show a message if the cache is cleared + * + * @return void + */ +function clearAPICache () { + AnimeClient.get('/cache_purge', () => { + AnimeClient.showMessage('success', 'Successfully purged api cache'); + }); +} + +/** + * Scroll to the accordion/vertical tab section just opened + * + * @param event + * @return void + */ +function scrollToSection (event) { + const el = event.currentTarget.parentElement; + const rect = el.getBoundingClientRect(); + + const top = rect.top + window.pageYOffset; + + window.scrollTo({ + top, + behavior: 'smooth', + }); +} + +/** + * Filter an anime or manga list + * + * @param event + * @return void + */ +function filterMedia (event) { + const rawFilter = event.target.value; + const filter = new RegExp(rawFilter, 'i'); + + // console.log('Filtering items by: ', filter); + + if (rawFilter !== '') { + // Filter the cover view + AnimeClient.$('article.media').forEach(article => { + const titleLink = AnimeClient.$('.name a', article)[0]; + const title = String(titleLink.textContent).trim(); + if ( ! filter.test(title)) { + AnimeClient.hide(article); + } else { + AnimeClient.show(article); + } + }); + + // Filter the list view + AnimeClient.$('table.media-wrap tbody tr').forEach(tr => { + const titleCell = AnimeClient.$('td.align-left', tr)[0]; + const titleLink = AnimeClient.$('a', titleCell)[0]; + const linkTitle = String(titleLink.textContent).trim(); + const textTitle = String(titleCell.textContent).trim(); + if ( ! (filter.test(linkTitle) || filter.test(textTitle))) { + AnimeClient.hide(tr); + } else { + AnimeClient.show(tr); + } + }); + } else { + AnimeClient.show('article.media'); + AnimeClient.show('table.media-wrap tbody tr'); + } +} + +if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js').then(reg => { + console.log('Service worker registered', reg.scope); + }).catch(error => { + console.error('Failed to register service worker', error); + }); +} + +// Click on hidden MAL checkbox so +// that MAL id is passed +AnimeClient.on('main', 'change', '.big-check', (e) => { + const id = e.target.id; + document.getElementById(`mal_${id}`).checked = true; +}); + +function renderAnimeSearchResults (data) { + const results = []; + + data.forEach(x => { + const item = x.attributes; + const titles = item.titles.reduce((prev, current) => { + return prev + `${current}
    `; + }, []); + + results.push(` + + `); + }); + + return results.join(''); +} + +function renderMangaSearchResults (data) { + const results = []; + + data.forEach(x => { + const item = x.attributes; + const titles = item.titles.reduce((prev, current) => { + return prev + `${current}
    `; + }, []); + + results.push(` + + `); + }); + + return results.join(''); +} + +const search = (query) => { + // Show the loader + AnimeClient.show('.cssload-loader'); + + // Do the api search + AnimeClient.get(AnimeClient.url('/anime-collection/search'), { query }, (searchResults, status) => { + searchResults = JSON.parse(searchResults); + + // Hide the loader + AnimeClient.hide('.cssload-loader'); + + // Show the results + AnimeClient.$('#series-list')[ 0 ].innerHTML = renderAnimeSearchResults(searchResults.data); + }); +}; + +if (AnimeClient.hasElement('.anime #search')) { + AnimeClient.on('#search', 'input', AnimeClient.throttle(250, (e) => { + const query = encodeURIComponent(e.target.value); + if (query === '') { + return; + } + + search(query); + })); +} + +// Action to increment episode count +AnimeClient.on('body.anime.list', 'click', '.plus-one', (e) => { + let parentSel = AnimeClient.closestParent(e.target, 'article'); + let watchedCount = parseInt(AnimeClient.$('.completed_number', parentSel)[ 0 ].textContent, 10) || 0; + let totalCount = parseInt(AnimeClient.$('.total_number', parentSel)[ 0 ].textContent, 10); + let title = AnimeClient.$('.name a', parentSel)[ 0 ].textContent; + + // Setup the update data + let data = { + id: parentSel.dataset.kitsuId, + mal_id: parentSel.dataset.malId, + data: { + progress: watchedCount + 1 + } + }; + + // If the episode count is 0, and incremented, + // change status to currently watching + if (isNaN(watchedCount) || watchedCount === 0) { + data.data.status = 'current'; + } + + // If you increment at the last episode, mark as completed + if ((!isNaN(watchedCount)) && (watchedCount + 1) === totalCount) { + data.data.status = 'completed'; + } + + AnimeClient.show('#loading-shadow'); + + // okay, lets actually make some changes! + AnimeClient.ajax(AnimeClient.url('/anime/increment'), { + data, + dataType: 'json', + type: 'POST', + success: (res) => { + const resData = JSON.parse(res); + + if (resData.errors) { + AnimeClient.hide('#loading-shadow'); + AnimeClient.showMessage('error', `Failed to update ${title}. `); + AnimeClient.scrollToTop(); + return; + } + + if (resData.data.attributes.status === 'completed') { + AnimeClient.hide(parentSel); + } + + AnimeClient.hide('#loading-shadow'); + + AnimeClient.showMessage('success', `Successfully updated ${title}`); + AnimeClient.$('.completed_number', parentSel)[ 0 ].textContent = ++watchedCount; + AnimeClient.scrollToTop(); + }, + error: () => { + AnimeClient.hide('#loading-shadow'); + AnimeClient.showMessage('error', `Failed to update ${title}. `); + AnimeClient.scrollToTop(); + } + }); +}); + +const search$1 = (query) => { + AnimeClient.show('.cssload-loader'); + AnimeClient.get(AnimeClient.url('/manga/search'), { query }, (searchResults, status) => { + searchResults = JSON.parse(searchResults); + AnimeClient.hide('.cssload-loader'); + AnimeClient.$('#series-list')[ 0 ].innerHTML = renderMangaSearchResults(searchResults.data); + }); +}; + +if (AnimeClient.hasElement('.manga #search')) { + AnimeClient.on('#search', 'input', AnimeClient.throttle(250, (e) => { + let query = encodeURIComponent(e.target.value); + if (query === '') { + return; + } + + search$1(query); + })); +} + +/** + * Javascript for editing manga, if logged in + */ +AnimeClient.on('.manga.list', 'click', '.edit-buttons button', (e) => { + let thisSel = e.target; + let parentSel = AnimeClient.closestParent(e.target, 'article'); + let type = thisSel.classList.contains('plus-one-chapter') ? 'chapter' : 'volume'; + let completed = parseInt(AnimeClient.$(`.${type}s_read`, parentSel)[ 0 ].textContent, 10) || 0; + let total = parseInt(AnimeClient.$(`.${type}_count`, parentSel)[ 0 ].textContent, 10); + let mangaName = AnimeClient.$('.name', parentSel)[ 0 ].textContent; + + if (isNaN(completed)) { + completed = 0; + } + + // Setup the update data + let data = { + id: parentSel.dataset.kitsuId, + mal_id: parentSel.dataset.malId, + data: { + progress: completed + } + }; + + // If the episode count is 0, and incremented, + // change status to currently reading + if (isNaN(completed) || completed === 0) { + data.data.status = 'current'; + } + + // If you increment at the last chapter, mark as completed + if ((!isNaN(completed)) && (completed + 1) === total) { + data.data.status = 'completed'; + } + + // Update the total count + data.data.progress = ++completed; + + AnimeClient.show('#loading-shadow'); + + AnimeClient.ajax(AnimeClient.url('/manga/increment'), { + data, + dataType: 'json', + type: 'POST', + mimeType: 'application/json', + success: () => { + if (data.data.status === 'completed') { + AnimeClient.hide(parentSel); + } + + AnimeClient.hide('#loading-shadow'); + + AnimeClient.$(`.${type}s_read`, parentSel)[ 0 ].textContent = completed; + AnimeClient.showMessage('success', `Successfully updated ${mangaName}`); + AnimeClient.scrollToTop(); + }, + error: () => { + AnimeClient.hide('#loading-shadow'); + AnimeClient.showMessage('error', `Failed to update ${mangaName}`); + AnimeClient.scrollToTop(); + } + }); +});