From f1102a9548a31aacf37da9c4ad94b09b7de27ef7 Mon Sep 17 00:00:00 2001 From: harshgupta2125 Date: Mon, 6 Apr 2026 10:34:07 +0530 Subject: [PATCH 01/15] Draft: Hybrid linting (Biome for JS, ESLint for Vue) - Added Biome to handle all JS files for much faster linting. - Kept ESLint strictly for .vue files to avoid breaking anything. - Auto-formatted 158 JS files to match our current style rules. - Put it all in one single commit to make git-blame tracking easier. --- .eslintrc.json | 17 +- biome.json | 27 + conf/svgo.config.js | 26 +- .../BarcodeScanner/utils/classes.js | 162 +- .../components/BulkSearch/utils/classes.js | 459 +++--- .../components/BulkSearch/utils/samples.js | 43 +- .../BulkSearch/utils/searchUtils.js | 72 +- .../IdentifiersInput/utils/utils.js | 147 +- .../components/LibraryExplorer/utils.js | 102 +- .../components/LibraryExplorer/utils/lcc.js | 61 +- openlibrary/components/MergeUI/utils.js | 398 ++--- .../ObservationForm/ObservationService.js | 44 +- .../components/ObservationForm/Utils.js | 12 +- openlibrary/components/configs.js | 41 +- openlibrary/components/dev/serve-component.js | 2 +- openlibrary/components/dev/vite.config.js | 9 +- openlibrary/components/lit/OLChip.js | 88 +- openlibrary/components/lit/OLChipGroup.js | 34 +- openlibrary/components/lit/OLReadMore.js | 169 +- openlibrary/components/lit/OlPagination.js | 499 +++--- openlibrary/components/lit/OlPopover.js | 1032 ++++++------ openlibrary/components/lit/index.js | 5 +- openlibrary/components/rollupInputCore.js | 26 +- openlibrary/components/vite-lit.config.mjs | 69 +- openlibrary/components/vite.config.mjs | 66 +- openlibrary/plugins/openlibrary/js/Browser.js | 56 +- .../plugins/openlibrary/js/SearchBar.js | 782 ++++----- .../plugins/openlibrary/js/SearchPage.js | 30 +- .../plugins/openlibrary/js/SearchUtils.js | 195 ++- openlibrary/plugins/openlibrary/js/Toast.js | 121 +- .../plugins/openlibrary/js/add-book.js | 307 ++-- .../plugins/openlibrary/js/add_provider.js | 72 +- openlibrary/plugins/openlibrary/js/admin.js | 60 +- .../plugins/openlibrary/js/affiliate-links.js | 116 +- .../plugins/openlibrary/js/autocomplete.js | 623 ++++---- .../plugins/openlibrary/js/banner/index.js | 51 +- .../plugins/openlibrary/js/book-page-lists.js | 128 +- .../openlibrary/js/breadcrumb_select/index.js | 38 +- .../openlibrary/js/bulk-tagger/BulkTagger.js | 1179 +++++++------- .../js/bulk-tagger/BulkTagger/MenuOption.js | 305 ++-- .../BulkTagger/SortedMenuOptionContainer.js | 229 +-- .../openlibrary/js/bulk-tagger/index.js | 4 +- .../openlibrary/js/bulk-tagger/models/Tag.js | 185 +-- .../openlibrary/js/carousel/Carousel.js | 318 ++-- .../plugins/openlibrary/js/carousel/index.js | 22 +- .../plugins/openlibrary/js/clampers.js | 37 +- .../openlibrary/js/compact-title/index.js | 82 +- openlibrary/plugins/openlibrary/js/covers.js | 298 ++-- openlibrary/plugins/openlibrary/js/dialog.js | 155 +- .../plugins/openlibrary/js/dropper/Dropper.js | 216 +-- .../plugins/openlibrary/js/dropper/index.js | 82 +- openlibrary/plugins/openlibrary/js/edit.js | 898 ++++++----- .../js/edition-nav-bar/EditionNavBar.js | 250 +-- .../openlibrary/js/edition-nav-bar/index.js | 16 +- .../openlibrary/js/editions-table/index.js | 193 +-- .../plugins/openlibrary/js/following.js | 66 +- .../js/fulltext-search-suggestion.js | 103 +- .../plugins/openlibrary/js/go-back-links.js | 20 +- .../openlibrary/js/goodreads_import.js | 327 ++-- .../plugins/openlibrary/js/graphs/index.js | 42 +- .../plugins/openlibrary/js/graphs/options.js | 90 +- .../plugins/openlibrary/js/graphs/plot.js | 473 +++--- openlibrary/plugins/openlibrary/js/i18n.js | 18 +- .../openlibrary/js/ia_thirdparty_logins.js | 41 +- .../plugins/openlibrary/js/idValidation.js | 117 +- .../plugins/openlibrary/js/ile/index.js | 151 +- .../SelectionManager/SelectionManager.js | 948 +++++------ .../plugins/openlibrary/js/ile/utils/ol.js | 60 +- openlibrary/plugins/openlibrary/js/index.js | 1253 ++++++++------- .../plugins/openlibrary/js/interstitial.js | 36 +- .../plugins/openlibrary/js/isbnOverride.js | 16 +- .../plugins/openlibrary/js/jquery.repeat.js | 182 +-- openlibrary/plugins/openlibrary/js/jsdef.js | 196 ++- .../plugins/openlibrary/js/lazy-carousel.js | 136 +- .../openlibrary/js/lazy-thing-preview.js | 222 +-- .../js/librarian-dashboard/index.js | 142 +- .../plugins/openlibrary/js/list_books.js | 62 +- .../openlibrary/js/lists/ListService.js | 65 +- .../openlibrary/js/lists/ListViewBody.js | 217 ++- .../openlibrary/js/lists/ShowcaseItem.js | 383 ++--- .../openlibrary/js/markdown-editor/index.js | 24 +- .../MergeRequestService.js | 97 +- .../merge-request-table/MergeRequestTable.js | 77 +- .../MergeRequestTable/TableHeader.js | 215 +-- .../MergeRequestTable/TableRow.js | 470 +++--- .../js/merge-request-table/index.js | 4 +- openlibrary/plugins/openlibrary/js/merge.js | 162 +- .../plugins/openlibrary/js/modals/index.js | 575 +++---- .../openlibrary/js/my-books/CreateListForm.js | 204 +-- .../openlibrary/js/my-books/MyBooksDropper.js | 359 +++-- .../MyBooksDropper/CheckInComponents.js | 1392 +++++++++-------- .../my-books/MyBooksDropper/ReadingLists.js | 666 ++++---- .../MyBooksDropper/ReadingLogForms.js | 457 +++--- .../plugins/openlibrary/js/my-books/index.js | 179 ++- .../openlibrary/js/my-books/store/index.js | 124 +- .../openlibrary/js/native-dialog/index.js | 51 +- .../plugins/openlibrary/js/nonjquery_utils.js | 32 +- .../plugins/openlibrary/js/offline-banner.js | 9 +- .../plugins/openlibrary/js/ol.analytics.js | 99 +- openlibrary/plugins/openlibrary/js/ol.js | 73 +- .../plugins/openlibrary/js/partner_ol_lib.js | 86 +- .../plugins/openlibrary/js/password-toggle.js | 21 +- .../plugins/openlibrary/js/patron_exports.js | 16 +- .../plugins/openlibrary/js/private-button.js | 19 +- openlibrary/plugins/openlibrary/js/python.js | 36 +- .../openlibrary/js/reading-goals/index.js | 271 ++-- .../openlibrary/js/readinglog_stats.js | 351 +++-- .../openlibrary/js/return-form/index.js | 16 +- openlibrary/plugins/openlibrary/js/search.js | 212 +-- .../openlibrary/js/service-worker-init.js | 29 +- .../openlibrary/js/service-worker-matchers.js | 35 +- .../plugins/openlibrary/js/service-worker.js | 176 ++- openlibrary/plugins/openlibrary/js/signup.js | 444 +++--- .../openlibrary/js/star-ratings/index.js | 123 +- .../plugins/openlibrary/js/stats/index.js | 29 +- openlibrary/plugins/openlibrary/js/tabs.js | 8 +- openlibrary/plugins/openlibrary/js/team.js | 610 ++++---- .../plugins/openlibrary/js/template.js | 61 +- .../plugins/openlibrary/js/type_changer.js | 20 +- openlibrary/plugins/openlibrary/js/utils.js | 83 +- .../plugins/openlibrary/js/waitlist.js | 22 +- package-lock.json | 164 ++ package.json | 1 + scripts/gh_scripts/new_pr_labeler.mjs | 162 +- scripts/gh_scripts/weekly_status_report.mjs | 655 ++++---- scripts/solr_restarter/index.js | 217 +-- static/bookmarklets/import_webbook.js | 16 +- stories/.storybook/main.js | 23 +- stories/.storybook/preview.js | 5 +- stories/Button.stories.js | 89 +- tests/unit/js/Browser.test.js | 87 +- tests/unit/js/SearchBar.test.js | 552 +++---- tests/unit/js/SearchUtils.test.js | 156 +- tests/unit/js/SelectionManager.test.js | 121 +- tests/unit/js/autocomplete.test.js | 105 +- tests/unit/js/droppers.test.js | 631 ++++---- .../js/editionEditPageClassification.test.js | 74 +- tests/unit/js/editionsEditPage.test.js | 388 ++--- tests/unit/js/html-test-data.js | 4 +- tests/unit/js/idValidation.test.js | 262 ++-- tests/unit/js/jquery.repeat.test.js | 62 +- tests/unit/js/jsdef.test.js | 77 +- tests/unit/js/lists.test.js | 452 +++--- tests/unit/js/my-books.test.js | 288 ++-- tests/unit/js/nonjquery_utils.test.js | 94 +- tests/unit/js/python.test.js | 66 +- .../unit/js/sample-html/checkIns-test-data.js | 2 +- .../unit/js/sample-html/dropper-test-data.js | 30 +- tests/unit/js/sample-html/lists-test-data.js | 139 +- tests/unit/js/sample-html/utils-test-data.js | 6 +- tests/unit/js/search.test.js | 282 ++-- tests/unit/js/service-worker-matchers.test.js | 200 ++- tests/unit/js/setup.js | 3 +- tests/unit/js/signup.test.js | 315 ++-- tests/unit/js/utils.test.js | 106 +- vue.config.js | 4 +- webpack.config.css.js | 142 +- webpack.config.js | 194 +-- 158 files changed, 15956 insertions(+), 14179 deletions(-) create mode 100644 biome.json diff --git a/.eslintrc.json b/.eslintrc.json index 377b5015e4f..807d8b11fc0 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -45,5 +45,20 @@ "vue/multi-word-component-names": ["error", { "ignores": ["Bookshelf", "Shelf"] }] - } + }, + "overrides": [ + { + "files": ["*.js", "*.mjs", "*.cjs"], + "rules": { + "semi": "off", + "quotes": "off", + "indent": "off", + "no-extra-semi": "off", + "no-mixed-spaces-and-tabs": "off", + "no-trailing-spaces": "off", + "keyword-spacing": "off", + "key-spacing": "off" + } + } + ] } diff --git a/biome.json b/biome.json new file mode 100644 index 00000000000..5401bfba31a --- /dev/null +++ b/biome.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json", + "files": { + "includes": ["**/*.js", "**/*.mjs", "**/*.cjs"], + "ignoreUnknown": true + }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80 + }, + "javascript": { + "formatter": { + "semicolons": "always", + "quoteStyle": "single", + "bracketSpacing": true + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + } +} diff --git a/conf/svgo.config.js b/conf/svgo.config.js index e9fd1302661..14d04dc52c9 100644 --- a/conf/svgo.config.js +++ b/conf/svgo.config.js @@ -1,15 +1,15 @@ /* eslint-env node, es6 */ -const {extendDefaultPlugins} = require('svgo'); +const { extendDefaultPlugins } = require('svgo'); module.exports = { - plugins: extendDefaultPlugins([ - // Disable plugins enabled by default - {name: 'removeXMLProcInst', active: false}, - {name: 'collapseGroups', active: false}, - {name: 'mergePaths', active: false}, - {name: 'cleanupIDs', active: false}, - {name: 'convertPathData', active: false}, - {name: 'removeDesc', active: false}, - {name: 'removeTitle', active: false}, - {name: 'removeViewBox', active: false} - ]) -} + plugins: extendDefaultPlugins([ + // Disable plugins enabled by default + { name: 'removeXMLProcInst', active: false }, + { name: 'collapseGroups', active: false }, + { name: 'mergePaths', active: false }, + { name: 'cleanupIDs', active: false }, + { name: 'convertPathData', active: false }, + { name: 'removeDesc', active: false }, + { name: 'removeTitle', active: false }, + { name: 'removeViewBox', active: false }, + ]), +}; diff --git a/openlibrary/components/BarcodeScanner/utils/classes.js b/openlibrary/components/BarcodeScanner/utils/classes.js index 2428734ef3e..a708f39385c 100644 --- a/openlibrary/components/BarcodeScanner/utils/classes.js +++ b/openlibrary/components/BarcodeScanner/utils/classes.js @@ -1,99 +1,107 @@ // @ts-check /* eslint-disable no-console */ -import { createWorker, createScheduler } from 'tesseract.js'; +import { createScheduler, createWorker } from 'tesseract.js'; export class OCRScanner { - constructor() { - this.scheduler = createScheduler(); - /** @type {number | null} */ - this.timerId = null; + constructor() { + this.scheduler = createScheduler(); + /** @type {number | null} */ + this.timerId = null; - this.listeners = { - /** @type {Array<(isbn: string) => void>} */ - onISBNDetected: [] - } - } - - /** @param {(isbn: string) => void} callback */ - onISBNDetected(callback) { - this.listeners.onISBNDetected.push(callback); - } + this.listeners = { + /** @type {Array<(isbn: string) => void>} */ + onISBNDetected: [], + }; + } - async init() { - this._initPromise = this._initPromise || this._init(); - await this._initPromise; - } + /** @param {(isbn: string) => void} callback */ + onISBNDetected(callback) { + this.listeners.onISBNDetected.push(callback); + } - async _init() { - console.log('Initializing Tesseract.js'); - for (let i = 0; i < 1; i++) { - const worker = await createWorker(); - await worker.load(); - await worker.loadLanguage('eng'); - await worker.initialize('eng'); - this.scheduler.addWorker(worker); - console.log(`Loaded worker ${i}`); - } + async init() { + this._initPromise = this._initPromise || this._init(); + await this._initPromise; + } - console.log('Tesseract.js initialized'); + async _init() { + console.log('Initializing Tesseract.js'); + for (let i = 0; i < 1; i++) { + const worker = await createWorker(); + await worker.load(); + await worker.loadLanguage('eng'); + await worker.initialize('eng'); + this.scheduler.addWorker(worker); + console.log(`Loaded worker ${i}`); } - /** - * @param {HTMLCanvasElement} canvas - */ - async doOCR(canvas) { - const { data: { lines } } = await this.scheduler.addJob('recognize', canvas); - const textLines = lines.map(l => l.text.trim()).filter(line => line); - console.log(textLines.join('\n')); - for (const line of textLines) { - const sanitizedLine = line.replace(/[\s-'.–—]/g, ''); - if (!/\d{2}/.test(sanitizedLine)) continue; - console.log(sanitizedLine); - if (sanitizedLine.includes('isbn') || /97[0-9]{10}[0-9x]/i.test(sanitizedLine) || /[0-9]{9}[0-9x]/i.test(sanitizedLine)) { - const isbn = sanitizedLine.match(/(97[0-9]{10}[0-9x]|[0-9]{9}[0-9x])/i)[0]; - console.log(`ISBN detected: ${isbn}`); - this.listeners.onISBNDetected.forEach(callback => callback(isbn)); - } - } + console.log('Tesseract.js initialized'); + } + + /** + * @param {HTMLCanvasElement} canvas + */ + async doOCR(canvas) { + const { + data: { lines }, + } = await this.scheduler.addJob('recognize', canvas); + const textLines = lines.map((l) => l.text.trim()).filter((line) => line); + console.log(textLines.join('\n')); + for (const line of textLines) { + const sanitizedLine = line.replace(/[\s-'.–—]/g, ''); + if (!/\d{2}/.test(sanitizedLine)) continue; + console.log(sanitizedLine); + if ( + sanitizedLine.includes('isbn') || + /97[0-9]{10}[0-9x]/i.test(sanitizedLine) || + /[0-9]{9}[0-9x]/i.test(sanitizedLine) + ) { + const isbn = sanitizedLine.match( + /(97[0-9]{10}[0-9x]|[0-9]{9}[0-9x])/i, + )[0]; + console.log(`ISBN detected: ${isbn}`); + this.listeners.onISBNDetected.forEach((callback) => callback(isbn)); + } } + } } /** * @template {(...args: any) => void} TFunc */ export class ThrottleGrouping { - /** - * @param {object} param0 - * @param {TFunc} param0.func - * @param {function(Parameters[]): Parameters} param0.reducer - * @param {number} param0.wait - */ - constructor({func, reducer, wait=100}) { - this.func = func; - this.reducer = reducer; - this.wait = wait; - /** @type {Parameters[]} */ - this.curGroup = []; - this.timeout = null; - } + /** + * @param {object} param0 + * @param {TFunc} param0.func + * @param {function(Parameters[]): Parameters} param0.reducer + * @param {number} param0.wait + */ + constructor({ func, reducer, wait = 100 }) { + this.func = func; + this.reducer = reducer; + this.wait = wait; + /** @type {Parameters[]} */ + this.curGroup = []; + this.timeout = null; + } - submitGroup() { - this.timeout = null; - this.func(...this.reducer(this.curGroup)); - this.curGroup = []; - } + submitGroup() { + this.timeout = null; + this.func(...this.reducer(this.curGroup)); + this.curGroup = []; + } - /** - * @param {Parameters} args - */ - takeNext(...args) { - this.curGroup.push(args); - if (!this.timeout) { - this.timeout = setTimeout(this.submitGroup.bind(this), this.wait); - } + /** + * @param {Parameters} args + */ + takeNext(...args) { + this.curGroup.push(args); + if (!this.timeout) { + this.timeout = setTimeout(this.submitGroup.bind(this), this.wait); } + } - asFunction() { - return this.takeNext.bind(this); - } + asFunction() { + return this.takeNext.bind(this); + } } diff --git a/openlibrary/components/BulkSearch/utils/classes.js b/openlibrary/components/BulkSearch/utils/classes.js index e1d4cb32bae..b52aed77991 100644 --- a/openlibrary/components/BulkSearch/utils/classes.js +++ b/openlibrary/components/BulkSearch/utils/classes.js @@ -1,248 +1,273 @@ //@ts-check export class ExtractedBook { - constructor(title = '', author = '', isbn = '') { - /** @type {string} */ - this.title = title; - /**@type {string} */ - this.author = author; - /**@type {string} */ - this.isbn = isbn; - } + constructor(title = '', author = '', isbn = '') { + /** @type {string} */ + this.title = title; + /**@type {string} */ + this.author = author; + /**@type {string} */ + this.isbn = isbn; + } } class AbstractExtractor { - - /** - * @param {string} label - */ - constructor(label) { - /** @type {string} */ - this.label = label - } - /** - * @param {ExtractionOptions} _extractOptions - * @param {string} _text - * @returns {Promise} - */ - async run(_extractOptions, _text) { //eslint-disable-line no-unused-vars - throw new Error('Not Implemented Error') - } + /** + * @param {string} label + */ + constructor(label) { + /** @type {string} */ + this.label = label; + } + /** + * @param {ExtractionOptions} _extractOptions + * @param {string} _text + * @returns {Promise} + */ + async run(_extractOptions, _text) { + //eslint-disable-line no-unused-vars + throw new Error('Not Implemented Error'); + } } export class RegexExtractor extends AbstractExtractor { + name = 'regex_extractor'; + /** + * + * @param {string} label + * @param {string} pattern + */ + constructor(label, pattern) { + super(label); + /** @type {RegExp} */ + this.pattern = new RegExp(pattern, 'gmu'); + } - name = 'regex_extractor' - /** - * - * @param {string} label - * @param {string} pattern - */ - constructor(label, pattern){ - super(label) - /** @type {RegExp} */ - this.pattern = new RegExp(pattern, 'gmu'); - } - - /** - * @param {ExtractionOptions} _extractOptions - * @param {string} text - * @returns {Promise} - */ - async run(_extractOptions, text) { - const data = [...text.matchAll(this.pattern)] - const extractedBooks = data.map((entry) => new ExtractedBook(entry.groups?.title, entry.groups?.author, entry.groups?.isbn)) - const matchedBooks = extractedBooks.map((entry) => new BookMatch(entry, [])) - return matchedBooks - } + /** + * @param {ExtractionOptions} _extractOptions + * @param {string} text + * @returns {Promise} + */ + async run(_extractOptions, text) { + const data = [...text.matchAll(this.pattern)]; + const extractedBooks = data.map( + (entry) => + new ExtractedBook( + entry.groups?.title, + entry.groups?.author, + entry.groups?.isbn, + ), + ); + const matchedBooks = extractedBooks.map( + (entry) => new BookMatch(entry, []), + ); + return matchedBooks; + } } -export class AiExtractor extends AbstractExtractor{ - - name = 'ai_extractor' - /** - * @param {string} label - * @param {string} model - */ - constructor(label, model) { - super(label) - /** @type {string} */ - this.model = model - } - - /** - * - * @param {ExtractionOptions} extractOptions - * @param {string} text - * @returns {Promise} - */ - async run(extractOptions, text) { - const request = { - - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${extractOptions.openaiApiKey}` - }, - body: JSON.stringify({ - model: this.model, - response_format: { type: 'json_object' }, - messages: [ - { - role: 'system', - content: 'You are a book extraction system. You will be given a free form passage of text containing references to books, and you will need to extract the book titles, author, and optionally ISBN in a JSON array.' - }, - { - role: 'user', - content: `Please extract the books from the following text:\n\n${text}`, - } - ], - }) - - } - try { - const resp = await fetch('https://api.openai.com/v1/chat/completions', request) +export class AiExtractor extends AbstractExtractor { + name = 'ai_extractor'; + /** + * @param {string} label + * @param {string} model + */ + constructor(label, model) { + super(label); + /** @type {string} */ + this.model = model; + } - if (!resp.ok) { - const status = resp.status - let errorMessage = 'Network response was not okay.' - if (status === 401) { + /** + * + * @param {ExtractionOptions} extractOptions + * @param {string} text + * @returns {Promise} + */ + async run(extractOptions, text) { + const request = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${extractOptions.openaiApiKey}`, + }, + body: JSON.stringify({ + model: this.model, + response_format: { type: 'json_object' }, + messages: [ + { + role: 'system', + content: + 'You are a book extraction system. You will be given a free form passage of text containing references to books, and you will need to extract the book titles, author, and optionally ISBN in a JSON array.', + }, + { + role: 'user', + content: `Please extract the books from the following text:\n\n${text}`, + }, + ], + }), + }; + try { + const resp = await fetch( + 'https://api.openai.com/v1/chat/completions', + request, + ); - errorMessage = `${errorMessage} Error: Incorrect Authorization key.` - } - throw new Error(errorMessage) - } - const data = await resp.json() - return JSON.parse(data.choices[0].message.content)['books'] - .map((entry) => - new BookMatch(new ExtractedBook(entry?.title, entry?.author, entry?.isbn), {}) - ) - } - catch (error) { - return [] + if (!resp.ok) { + const status = resp.status; + let errorMessage = 'Network response was not okay.'; + if (status === 401) { + errorMessage = `${errorMessage} Error: Incorrect Authorization key.`; } - - + throw new Error(errorMessage); + } + const data = await resp.json(); + return JSON.parse(data.choices[0].message.content)['books'].map( + (entry) => + new BookMatch( + new ExtractedBook(entry?.title, entry?.author, entry?.isbn), + {}, + ), + ); + } catch (error) { + return []; } + } } -export class TableExtractor extends AbstractExtractor{ +export class TableExtractor extends AbstractExtractor { + name = 'table_extractor'; + /** + * + * @param {string} label + */ + constructor(label) { + super(label); + /** @type {string} */ + this.authorColumn = 'author'; + /** @type {string} */ + this.titleColumn = 'title'; + } - name = 'table_extractor' - /** - * - * @param {string} label - */ - constructor(label) { - super(label) - /** @type {string} */ - this.authorColumn = 'author' - /** @type {string} */ - this.titleColumn = 'title' - } - - /** - * @param {ExtractionOptions} extractionOptions - * @param {string} text - * @return {Promise} - */ - async run(extractionOptions, text){ - - /** @type {string[]} */ - const lines = text.split('\n') - /** @type {string[][]} */ - const cells = lines.map(line => line.split('\t')) - /** @type {{columns: String[], rows: {columnName: string}[]}} */ - const tableData = { - columns: cells[0], - rows: [] - } - for (let i=1; i< cells.length; i++){ - const row = {} - for (let j = 0; j < tableData.columns.length; j++){ - row[tableData.columns[j].trim().toLowerCase()] = cells[i][j] - } - // @ts-ignore - tableData.rows.push(row) - } - return tableData.rows.map( - row => new BookMatch( - new ExtractedBook( - row[this.titleColumn] || '', row[this.authorColumn] || '', row['isbn'] || ''), - {}) - ) + /** + * @param {ExtractionOptions} extractionOptions + * @param {string} text + * @return {Promise} + */ + async run(extractionOptions, text) { + /** @type {string[]} */ + const lines = text.split('\n'); + /** @type {string[][]} */ + const cells = lines.map((line) => line.split('\t')); + /** @type {{columns: String[], rows: {columnName: string}[]}} */ + const tableData = { + columns: cells[0], + rows: [], + }; + for (let i = 1; i < cells.length; i++) { + const row = {}; + for (let j = 0; j < tableData.columns.length; j++) { + row[tableData.columns[j].trim().toLowerCase()] = cells[i][j]; + } + // @ts-expect-error + tableData.rows.push(row); } + return tableData.rows.map( + (row) => + new BookMatch( + new ExtractedBook( + row[this.titleColumn] || '', + row[this.authorColumn] || '', + row['isbn'] || '', + ), + {}, + ), + ); + } } class ExtractionOptions { - constructor() { - /** @type {string} */ - this.openaiApiKey = '' - } + constructor() { + /** @type {string} */ + this.openaiApiKey = ''; + } } -class MatchOptions { - constructor (){ - /** @type {boolean} */ - this.includeAuthor = true; - } +class MatchOptions { + constructor() { + /** @type {boolean} */ + this.includeAuthor = true; + } } export class BookMatch { - - /** - * - * @param {ExtractedBook} extractedBook - * @param {*} solrDocs - */ - constructor(extractedBook, solrDocs){ - /** @type {ExtractedBook} */ - this.extractedBook = extractedBook; - this.solrDocs = solrDocs - } + /** + * + * @param {ExtractedBook} extractedBook + * @param {*} solrDocs + */ + constructor(extractedBook, solrDocs) { + /** @type {ExtractedBook} */ + this.extractedBook = extractedBook; + this.solrDocs = solrDocs; + } } +const BASE_LIST_URL = '/account/lists/add?seeds='; -const BASE_LIST_URL = '/account/lists/add?seeds=' - -export class BulkSearchState{ - constructor(){ - /** @type {string} */ - this.inputText= ''; - /** @type {BookMatch[]} */ - this.matchedBooks = []; - /** @type {MatchOptions} */ - this.matchOptions = new MatchOptions() - /** @type {ExtractionOptions} */ - this.extractionOptions = new ExtractionOptions(); - /** @type {AbstractExtractor[]} */ - this.extractors = [ - new RegexExtractor('Pattern: Title by Author', '(^|>)(?[A-Za-z][\\p{L}0-9\\- ,]{1,250})\\s+(by|[-\u2013\u2014\\t])\\s+(?<author>[\\p{L}][\\p{L}\\.\\- ]{3,70})( \\(.*)?($|<\\/)'), - new RegexExtractor('Pattern: Author - Title', '(^|>)(?<author>[A-Za-z][\\p{L}0-9\\- ,]{1,250})\\s+[,-\u2013\u2014\\t]\\s+(?<title>[\\p{L}][\\p{L}\\.\\- ]{3,70})( \\(.*)?($|<\\/)'), - new RegexExtractor('Pattern: Title - Author', '(^|>)(?<title>[A-Za-z][\\p{L}0-9\\- ,]{1,250})\\s+[,-\u2013\u2014\\t]\\s+(?<author>[\\p{L}][\\p{L}\\.\\- ]{3,70})( \\(.*)?($|<\\/)'), - new RegexExtractor('Pattern: Title (Author)', '^(?<title>[\\p{L}].{1,250})\\s\\(?<author>(.{3,70})\\)$$'), - new RegexExtractor('Wikipedia Citation Pattern: (e.g. Baum, Frank L. (1994). The Wizard of Oz)', '^(?<author>[^.()]+).*?\\)\\. (?<title>[^.]+)'), - new AiExtractor('✨ AI Extraction (Beta)', 'gpt-4o-mini'), - new TableExtractor('Extract from a Table/Spreadsheet') - ] - /** @type {Number} */ - this._activeExtractorIndex = 0 - } +export class BulkSearchState { + constructor() { + /** @type {string} */ + this.inputText = ''; + /** @type {BookMatch[]} */ + this.matchedBooks = []; + /** @type {MatchOptions} */ + this.matchOptions = new MatchOptions(); + /** @type {ExtractionOptions} */ + this.extractionOptions = new ExtractionOptions(); + /** @type {AbstractExtractor[]} */ + this.extractors = [ + new RegexExtractor( + 'Pattern: Title by Author', + '(^|>)(?<title>[A-Za-z][\\p{L}0-9\\- ,]{1,250})\\s+(by|[-\u2013\u2014\\t])\\s+(?<author>[\\p{L}][\\p{L}\\.\\- ]{3,70})( \\(.*)?($|<\\/)', + ), + new RegexExtractor( + 'Pattern: Author - Title', + '(^|>)(?<author>[A-Za-z][\\p{L}0-9\\- ,]{1,250})\\s+[,-\u2013\u2014\\t]\\s+(?<title>[\\p{L}][\\p{L}\\.\\- ]{3,70})( \\(.*)?($|<\\/)', + ), + new RegexExtractor( + 'Pattern: Title - Author', + '(^|>)(?<title>[A-Za-z][\\p{L}0-9\\- ,]{1,250})\\s+[,-\u2013\u2014\\t]\\s+(?<author>[\\p{L}][\\p{L}\\.\\- ]{3,70})( \\(.*)?($|<\\/)', + ), + new RegexExtractor( + 'Pattern: Title (Author)', + '^(?<title>[\\p{L}].{1,250})\\s\\(?<author>(.{3,70})\\)$$', + ), + new RegexExtractor( + 'Wikipedia Citation Pattern: (e.g. Baum, Frank L. (1994). The Wizard of Oz)', + '^(?<author>[^.()]+).*?\\)\\. (?<title>[^.]+)', + ), + new AiExtractor('✨ AI Extraction (Beta)', 'gpt-4o-mini'), + new TableExtractor('Extract from a Table/Spreadsheet'), + ]; + /** @type {Number} */ + this._activeExtractorIndex = 0; + } - /**@type {AbstractExtractor} */ - get activeExtractor() { - return this.extractors[this._activeExtractorIndex] - } - /**@type {String} */ - get listUrl() { - return BASE_LIST_URL + this.matchedBooks - .map(bm => bm.solrDocs?.docs?.[0]?.key.split('/')[2]) - .filter(key => key); - } - /**@type {String} */ - get listString(){ - return `${this.matchedBooks - .map(bm => bm.solrDocs?.docs?.[0]?.key.split('/')[2]) - .filter(key => key)}`; - } + /**@type {AbstractExtractor} */ + get activeExtractor() { + return this.extractors[this._activeExtractorIndex]; + } + /**@type {String} */ + get listUrl() { + return ( + BASE_LIST_URL + + this.matchedBooks + .map((bm) => bm.solrDocs?.docs?.[0]?.key.split('/')[2]) + .filter((key) => key) + ); + } + /**@type {String} */ + get listString() { + return `${this.matchedBooks + .map((bm) => bm.solrDocs?.docs?.[0]?.key.split('/')[2]) + .filter((key) => key)}`; + } } - - diff --git a/openlibrary/components/BulkSearch/utils/samples.js b/openlibrary/components/BulkSearch/utils/samples.js index e9c08adddf5..075e2a94372 100644 --- a/openlibrary/components/BulkSearch/utils/samples.js +++ b/openlibrary/components/BulkSearch/utils/samples.js @@ -1,22 +1,23 @@ export const sampleData = [ - { - name: 'Try a Sample...', - source: '', - text: '', - }, - { - name: '1927 Books', - source: 'https://en.wikipedia.org/wiki/1927_in_literature#New_books', - text: 'Djamaluddin Adinegoro - Darah Muda (Young Blood)\nIon Agârbiceanu - Legea minții\nAnthony Berkeley - Cicely Disappears\nArthur Bernède - Belphégor\nTjoe Hong Bok - Setangan Berloemoer Darah (A Glove Covered in Blood)\nJames Boyd - Marching On\nLynn Brock - The Kink\nEdgar Rice Burroughs - The Outlaw of Torn\nJames Branch Cabell - Something About Eve\nWilla Cather - Death Comes for the Archbishop\nBlaise Cendrars - La Confession de Dan Yack\nAgatha Christie - The Big Four\nJ.J. Connington Murder in the Maze Tragedy at Ravensthorpe\nJaime de Angulo - The Lariat\nMazo de la Roche - Jalna\nWarwick Deeping - Kitty', - }, - { - name: '2023 Public Domain Day', - source: 'https://web.law.duke.edu/cspd/publicdomainday/2023/', - text: 'To the Lighthouse - Virginia Woolf\nThe Case-Book of Sherlock Holmes - Arthur Conan Doyle\nDeath Comes for the Archbishop - Willa Cather\nCopper Sun - Countee Cullen\nNow We Are Six - illustrations by E. H. Shepard - A. A. Milne\nThe Bridge of San Luis Rey - Thornton Wilder\nMen Without Women - Ernest Hemingway\nMosquitoes - William Faulkner\nThe Big Four - Agatha Christie\nTwilight Sleep - Edith Wharton\nThe Gangs of New York - Herbert Asbury\nThe Tower Treasure - Franklin W. Dixon (pseudonym)\nDer Steppenwolf - Hermann Hesse\nAmerika - Franz Kafka', - }, - { - name: 'Holocaust Wikipedia citations', - source: 'https://en.wikipedia.org/wiki/Bibliography_of_the_Holocaust#Historical_studies', - text: 'Bauer, Yehuda (1994). Jews for Sale? Nazi-Jewish Negotiations 1933-1945. Yale University Press. ISBN 0-300-06852-2.\nBerenbaum, Michael (1990). A Mosaic of Victims: Non-Jews Persecuted and Murdered by the Nazis.\nBergen, Doris (2009). War and Genocide: Concise History of the Holocaust.\nBlack, Edwin (2010). The Farhud: The Arab-Nazi Alliance in the Holocaust. Dialog Press. ISBN 0-914153-14-5.\nBraham, Randolph (1994) [1981]. The Politics of Genocide: The Holocaust in Hungary. Columbia University Press. ISBN 0-88033-247-6.\nBraham, Randolph (2011). The Auschwitz Reports and the Holocaust in Hungary. Columbia University Press.\nChalmers, Beverley (2015). Birth, Sex and Abuse: Women\'s Voices Under Nazi Rule. Grosvenor House Publishing Ltd. ISBN 1781483531.\nDavies, Norman; Lukas, Richard C. (2001) [1996]. Forgotten Holocaust: The Poles Under German Occupation.\nDean, Martin (2008). Robbing the Jews: The Confiscation of Jewish Property in the Holocaust. Cambridge University Press.\nEvans, Suzanne (2004). Forgotten Crimes: The Holocaust and People with Disabilities.\nFriedländer, Saul (1998). The Years of Persecution: Nazi Germany and the Jews, 1933-1939. Vol. 1.\nGrau, Gunter; Shoppmann, Claudia (1995). The Hidden Holocaust?: Gay and Lesbian Persecution in Germany 1933-45.\nHedgepeth, Sonja; Saidel, Rochelle (2010). Sexual Violence against Jewish Women during the Holocaust.\nPeukert, Detlev (1994). "The Genesis of the \'Final Solution\' from the Spirit of Science". In Thomas Childers; Jane Caplan (eds.). Reevaluating the Third Reich. New York: Holmes & Meier. pp. 234–252. ISBN 0-8419-1178-9.\nRees, Laurence (2017). The Holocaust: A New History. London: Viking Press. ISBN 978-1610398442.\nAméry, Jean (1980). At the Mind\'s Limits: Contemplations by a Survivor on Auschwitz and its Realities.\nBlitz Konig, Nanette (2018). Holocaust Memoirs of a Bergen-Belsen Survivor and Classmate of Anne Frank. Amsterdam Publishers. ISBN 9789492371614.\nDittman, Anita (2005). Trapped in Hitler\'s Hell. ISBN 0-9721512-8-1.\nGerrard, Mady. Full Circle. KLPM. ISBN 978-0955865008.\nKogon, Eugen (1974). Der SS-Staat. Das System der deutschen Konzentrationslager (in German).\nRittner, Carol; Roth, John K. (1998). Different Voices: Women and the Holocaust.\nStojka, Ceija (1988). We Live in Seclusion: The Memories of a Romni.\nSteinberg, Manny (2014). Outcry: Holocaust Memoirs. Amsterdam Publishers. ISBN 978-9-082103137.\nWetzler, Alfréd (2007). Escape from Hell: The True Story of the Auschwitz Protocol. Berghahn Books.\nWinter, Walter (2004). Winter Time: Memoirs of a German Sinto who Survived Auschwitz.\nCzech, Danuta (1999). Auschwitz Chronicle: 1939-1945.\nDean, Martin (1999). Collaboration in the Holocaust: Crimes of the Local Police in Belorussia and Ukraine.\nFings, Karola; Kenrick, Donald, eds. (1999). The Gypsies During the Second World War.\nHogan, David J.; Aretha, David, eds. (2000). The Holocaust Chronicle: A History in Words and Pictures. Lincolnwood, IL: Publications International.\nPressac, Jean-Claude (1989). Auschwitz: Technique and operation of the gas chambers.\nAgamben, Giorgio (1999). Remnants of Auschwitz: The Witness and the Archive.\nBloxham, Donald (2009). The Final Solution: A Genocide.\nLawson, Tom (2010). Debates on the Holocaust. University of Manchester Press.\nLeff, Laurel (2005). Buried By The Times: The Holocaust And America\'s Most Important Newspaper. Cambridge University Press. ISBN 0-521-81287-9.\nMason, Timothy. "Intention and Explanation: A Current Controversy about the Interpretation of National Socialism". In Marrus, Michael R. (ed.). The Nazi Holocaust Part 3, The "Final Solution": The Implementation of Mass Murder. Vol. 1. Westpoint, CT: Mecler. pp. 3–20.\nNiewyk, Donald L. (1992). Holocaust: Problems & Perspective of Interpretation.' - } -] + { + name: 'Try a Sample...', + source: '', + text: '', + }, + { + name: '1927 Books', + source: 'https://en.wikipedia.org/wiki/1927_in_literature#New_books', + text: 'Djamaluddin Adinegoro - Darah Muda (Young Blood)\nIon Agârbiceanu - Legea minții\nAnthony Berkeley - Cicely Disappears\nArthur Bernède - Belphégor\nTjoe Hong Bok - Setangan Berloemoer Darah (A Glove Covered in Blood)\nJames Boyd - Marching On\nLynn Brock - The Kink\nEdgar Rice Burroughs - The Outlaw of Torn\nJames Branch Cabell - Something About Eve\nWilla Cather - Death Comes for the Archbishop\nBlaise Cendrars - La Confession de Dan Yack\nAgatha Christie - The Big Four\nJ.J. Connington Murder in the Maze Tragedy at Ravensthorpe\nJaime de Angulo - The Lariat\nMazo de la Roche - Jalna\nWarwick Deeping - Kitty', + }, + { + name: '2023 Public Domain Day', + source: 'https://web.law.duke.edu/cspd/publicdomainday/2023/', + text: 'To the Lighthouse - Virginia Woolf\nThe Case-Book of Sherlock Holmes - Arthur Conan Doyle\nDeath Comes for the Archbishop - Willa Cather\nCopper Sun - Countee Cullen\nNow We Are Six - illustrations by E. H. Shepard - A. A. Milne\nThe Bridge of San Luis Rey - Thornton Wilder\nMen Without Women - Ernest Hemingway\nMosquitoes - William Faulkner\nThe Big Four - Agatha Christie\nTwilight Sleep - Edith Wharton\nThe Gangs of New York - Herbert Asbury\nThe Tower Treasure - Franklin W. Dixon (pseudonym)\nDer Steppenwolf - Hermann Hesse\nAmerika - Franz Kafka', + }, + { + name: 'Holocaust Wikipedia citations', + source: + 'https://en.wikipedia.org/wiki/Bibliography_of_the_Holocaust#Historical_studies', + text: 'Bauer, Yehuda (1994). Jews for Sale? Nazi-Jewish Negotiations 1933-1945. Yale University Press. ISBN 0-300-06852-2.\nBerenbaum, Michael (1990). A Mosaic of Victims: Non-Jews Persecuted and Murdered by the Nazis.\nBergen, Doris (2009). War and Genocide: Concise History of the Holocaust.\nBlack, Edwin (2010). The Farhud: The Arab-Nazi Alliance in the Holocaust. Dialog Press. ISBN 0-914153-14-5.\nBraham, Randolph (1994) [1981]. The Politics of Genocide: The Holocaust in Hungary. Columbia University Press. ISBN 0-88033-247-6.\nBraham, Randolph (2011). The Auschwitz Reports and the Holocaust in Hungary. Columbia University Press.\nChalmers, Beverley (2015). Birth, Sex and Abuse: Women\'s Voices Under Nazi Rule. Grosvenor House Publishing Ltd. ISBN 1781483531.\nDavies, Norman; Lukas, Richard C. (2001) [1996]. Forgotten Holocaust: The Poles Under German Occupation.\nDean, Martin (2008). Robbing the Jews: The Confiscation of Jewish Property in the Holocaust. Cambridge University Press.\nEvans, Suzanne (2004). Forgotten Crimes: The Holocaust and People with Disabilities.\nFriedländer, Saul (1998). The Years of Persecution: Nazi Germany and the Jews, 1933-1939. Vol. 1.\nGrau, Gunter; Shoppmann, Claudia (1995). The Hidden Holocaust?: Gay and Lesbian Persecution in Germany 1933-45.\nHedgepeth, Sonja; Saidel, Rochelle (2010). Sexual Violence against Jewish Women during the Holocaust.\nPeukert, Detlev (1994). "The Genesis of the \'Final Solution\' from the Spirit of Science". In Thomas Childers; Jane Caplan (eds.). Reevaluating the Third Reich. New York: Holmes & Meier. pp. 234–252. ISBN 0-8419-1178-9.\nRees, Laurence (2017). The Holocaust: A New History. London: Viking Press. ISBN 978-1610398442.\nAméry, Jean (1980). At the Mind\'s Limits: Contemplations by a Survivor on Auschwitz and its Realities.\nBlitz Konig, Nanette (2018). Holocaust Memoirs of a Bergen-Belsen Survivor and Classmate of Anne Frank. Amsterdam Publishers. ISBN 9789492371614.\nDittman, Anita (2005). Trapped in Hitler\'s Hell. ISBN 0-9721512-8-1.\nGerrard, Mady. Full Circle. KLPM. ISBN 978-0955865008.\nKogon, Eugen (1974). Der SS-Staat. Das System der deutschen Konzentrationslager (in German).\nRittner, Carol; Roth, John K. (1998). Different Voices: Women and the Holocaust.\nStojka, Ceija (1988). We Live in Seclusion: The Memories of a Romni.\nSteinberg, Manny (2014). Outcry: Holocaust Memoirs. Amsterdam Publishers. ISBN 978-9-082103137.\nWetzler, Alfréd (2007). Escape from Hell: The True Story of the Auschwitz Protocol. Berghahn Books.\nWinter, Walter (2004). Winter Time: Memoirs of a German Sinto who Survived Auschwitz.\nCzech, Danuta (1999). Auschwitz Chronicle: 1939-1945.\nDean, Martin (1999). Collaboration in the Holocaust: Crimes of the Local Police in Belorussia and Ukraine.\nFings, Karola; Kenrick, Donald, eds. (1999). The Gypsies During the Second World War.\nHogan, David J.; Aretha, David, eds. (2000). The Holocaust Chronicle: A History in Words and Pictures. Lincolnwood, IL: Publications International.\nPressac, Jean-Claude (1989). Auschwitz: Technique and operation of the gas chambers.\nAgamben, Giorgio (1999). Remnants of Auschwitz: The Witness and the Archive.\nBloxham, Donald (2009). The Final Solution: A Genocide.\nLawson, Tom (2010). Debates on the Holocaust. University of Manchester Press.\nLeff, Laurel (2005). Buried By The Times: The Holocaust And America\'s Most Important Newspaper. Cambridge University Press. ISBN 0-521-81287-9.\nMason, Timothy. "Intention and Explanation: A Current Controversy about the Interpretation of National Socialism". In Marrus, Michael R. (ed.). The Nazi Holocaust Part 3, The "Final Solution": The Implementation of Mass Murder. Vol. 1. Westpoint, CT: Mecler. pp. 3–20.\nNiewyk, Donald L. (1992). Holocaust: Problems & Perspective of Interpretation.', + }, +]; diff --git a/openlibrary/components/BulkSearch/utils/searchUtils.js b/openlibrary/components/BulkSearch/utils/searchUtils.js index 58947a9d07a..c4f35c80158 100644 --- a/openlibrary/components/BulkSearch/utils/searchUtils.js +++ b/openlibrary/components/BulkSearch/utils/searchUtils.js @@ -1,43 +1,57 @@ - /** @typedef {import('./classes.js').ExtractedBook} ExtractedBook */ /** @typedef {import('./classes.js').MatchOptions} MatchOptions */ /** @typedef {import('./classes.js').BookMatch} BookMatch */ -const OL_SEARCH_BASE = 'openlibrary.org' +const OL_SEARCH_BASE = 'openlibrary.org'; /** * @param {ExtractedBook} extractedBook * @param {MatchOptions} matchOptions */ export function buildSearchUrl(extractedBook, matchOptions, json = true) { - let title = extractedBook.title?.split(/[:(?]/)[0].replace(/’/g, '\''); - const author = extractedBook.author - // Remove leading articles from title; these can sometimes be missing from OL records, - // and will hence cause a failed match. - // Taken from https://github.com/internetarchive/openlibrary/blob/4d880c1bf3e2391dd001c7818052fd639d38ff58/conf/solr/conf/managed-schema.xml#L526 - title = title.replace(/^(an? |the |l[aeo]s? |l'|de la |el |il |un[ae]? |du |de[imrst]? |das |ein |eine[mnrs]? |bir )/i, '').trim(); - const query = []; + let title = extractedBook.title?.split(/[:(?]/)[0].replace(/’/g, "'"); + const author = extractedBook.author; + // Remove leading articles from title; these can sometimes be missing from OL records, + // and will hence cause a failed match. + // Taken from https://github.com/internetarchive/openlibrary/blob/4d880c1bf3e2391dd001c7818052fd639d38ff58/conf/solr/conf/managed-schema.xml#L526 + title = title + .replace( + /^(an? |the |l[aeo]s? |l'|de la |el |il |un[ae]? |du |de[imrst]? |das |ein |eine[mnrs]? |bir )/i, + '', + ) + .trim(); + const query = []; - if (title) { - query.push(`title:"${title}"`); - } - if (matchOptions.includeAuthor && author && author.toLowerCase() !== 'null' && author.toLowerCase() !== 'unknown') { - const authorParts = author.replace(/^\S+\./, '').trim().split(/\s/); - const authorLastName = author.includes(',') ? author.replace(/,.*/, '') : authorParts[authorParts.length - 1]; - query.push(`author:${authorLastName}`); - } + if (title) { + query.push(`title:"${title}"`); + } + if ( + matchOptions.includeAuthor && + author && + author.toLowerCase() !== 'null' && + author.toLowerCase() !== 'unknown' + ) { + const authorParts = author + .replace(/^\S+\./, '') + .trim() + .split(/\s/); + const authorLastName = author.includes(',') + ? author.replace(/,.*/, '') + : authorParts[authorParts.length - 1]; + query.push(`author:${authorLastName}`); + } - if (extractedBook.isbn) { - query.push(`isbn:${extractedBook.isbn}`); - } + if (extractedBook.isbn) { + query.push(`isbn:${extractedBook.isbn}`); + } - let path = `https://${OL_SEARCH_BASE}/search`; - if (json) path += '.json'; - const url = `${path}?${new URLSearchParams({ - q: query.join(' '), - mode: 'everything', - fields: 'key,title,author_name,cover_i,first_publish_year,edition_count,ebook_access', - })}`; - return url; + let path = `https://${OL_SEARCH_BASE}/search`; + if (json) path += '.json'; + const url = `${path}?${new URLSearchParams({ + q: query.join(' '), + mode: 'everything', + fields: + 'key,title,author_name,cover_i,first_publish_year,edition_count,ebook_access', + })}`; + return url; } - diff --git a/openlibrary/components/IdentifiersInput/utils/utils.js b/openlibrary/components/IdentifiersInput/utils/utils.js index fcb2fd796ad..98b18458806 100644 --- a/openlibrary/components/IdentifiersInput/utils/utils.js +++ b/openlibrary/components/IdentifiersInput/utils/utils.js @@ -1,87 +1,100 @@ import { - parseIsbn, - parseLccn, - isChecksumValidIsbn10, - isChecksumValidIsbn13, - isFormatValidIsbn10, - isFormatValidIsbn13, - isValidLccn, + isChecksumValidIsbn10, + isChecksumValidIsbn13, + isFormatValidIsbn10, + isFormatValidIsbn13, + isValidLccn, + parseIsbn, + parseLccn, } from '../../../plugins/openlibrary/js/idValidation.js'; export function errorDisplay(message, error_output) { - let errorSelector; - if (error_output === '#hiddenAuthorIdentifiers') { - errorSelector = document.querySelector('#id-errors-author') - } else if (error_output === '#hiddenWorkIdentifiers') { - errorSelector = document.querySelector('#id-errors-work') - } else if (error_output === '#hiddenEditionIdentifiers') { - errorSelector = document.querySelector('#id-errors-edition') - } - if (message) { - errorSelector.style.display = ''; - errorSelector.innerHTML = `<div>${message}</div>`; - } else { - errorSelector.style.display = 'none'; - errorSelector.innerHTML = ''; - } - + let errorSelector; + if (error_output === '#hiddenAuthorIdentifiers') { + errorSelector = document.querySelector('#id-errors-author'); + } else if (error_output === '#hiddenWorkIdentifiers') { + errorSelector = document.querySelector('#id-errors-work'); + } else if (error_output === '#hiddenEditionIdentifiers') { + errorSelector = document.querySelector('#id-errors-edition'); + } + if (message) { + errorSelector.style.display = ''; + errorSelector.innerHTML = `<div>${message}</div>`; + } else { + errorSelector.style.display = 'none'; + errorSelector.innerHTML = ''; + } } function validateIsbn10(value) { - const isbn10_value = parseIsbn(value); - if (!isFormatValidIsbn10(isbn10_value)) { - errorDisplay('ID must be exactly 10 characters [0-9] or X.', '#hiddenEditionIdentifiers'); - return false; - } else if ( - isFormatValidIsbn10(isbn10_value) && !isChecksumValidIsbn10(isbn10_value) - ) { - errorDisplay(`ISBN ${isbn10_value} may be invalid. Please confirm if you'd like to add it before saving all changes`, '#hiddenEditionIdentifiers'); - } - return true; + const isbn10_value = parseIsbn(value); + if (!isFormatValidIsbn10(isbn10_value)) { + errorDisplay( + 'ID must be exactly 10 characters [0-9] or X.', + '#hiddenEditionIdentifiers', + ); + return false; + } else if ( + isFormatValidIsbn10(isbn10_value) && + !isChecksumValidIsbn10(isbn10_value) + ) { + errorDisplay( + `ISBN ${isbn10_value} may be invalid. Please confirm if you'd like to add it before saving all changes`, + '#hiddenEditionIdentifiers', + ); + } + return true; } function validateIsbn13(value) { - const isbn13_value = parseIsbn(value); + const isbn13_value = parseIsbn(value); - if (!isFormatValidIsbn13(isbn13_value)) { - errorDisplay('ID must be exactly 13 digits [0-9]. For example: 978-1-56619-909-4', '#hiddenEditionIdentifiers'); - return false; - } else if ( - isFormatValidIsbn13(isbn13_value) && !isChecksumValidIsbn13(isbn13_value) - ) { - errorDisplay(`ISBN ${isbn13_value} may be invalid. Please confirm if you'd like to add it before saving all changes`, '#hiddenEditionIdentifiers'); - } - return true; + if (!isFormatValidIsbn13(isbn13_value)) { + errorDisplay( + 'ID must be exactly 13 digits [0-9]. For example: 978-1-56619-909-4', + '#hiddenEditionIdentifiers', + ); + return false; + } else if ( + isFormatValidIsbn13(isbn13_value) && + !isChecksumValidIsbn13(isbn13_value) + ) { + errorDisplay( + `ISBN ${isbn13_value} may be invalid. Please confirm if you'd like to add it before saving all changes`, + '#hiddenEditionIdentifiers', + ); + } + return true; } function validateLccn(value) { - const lccn_value = parseLccn(value); + const lccn_value = parseLccn(value); - if (!isValidLccn(lccn_value)) { - errorDisplay('Invalid ID format', '#hiddenEditionIdentifiers'); - return false; - } - return true; + if (!isValidLccn(lccn_value)) { + errorDisplay('Invalid ID format', '#hiddenEditionIdentifiers'); + return false; + } + return true; } export function validateIdentifiers(name, value, entries, error_output) { - let validId = true; - errorDisplay('', error_output); - if (name === '' || name === '---') { + let validId = true; + errorDisplay('', error_output); + if (name === '' || name === '---') { // if somehow an invalid identifier is passed through - errorDisplay('Invalid identifier', error_output); - return false; - } - if (name === 'isbn_10') { - validId = validateIsbn10(value); - } else if (name === 'isbn_13') { - validId = validateIsbn13(value); - } else if (name === 'lccn') { - validId = validateLccn(value); - } - if (Array.from(entries).some(entry => entry === value) === true) { - validId = false; - errorDisplay('That ID already exists for an identifier.', error_output); - } - return validId; + errorDisplay('Invalid identifier', error_output); + return false; + } + if (name === 'isbn_10') { + validId = validateIsbn10(value); + } else if (name === 'isbn_13') { + validId = validateIsbn13(value); + } else if (name === 'lccn') { + validId = validateLccn(value); + } + if (Array.from(entries).some((entry) => entry === value) === true) { + validId = false; + errorDisplay('That ID already exists for an identifier.', error_output); + } + return validId; } diff --git a/openlibrary/components/LibraryExplorer/utils.js b/openlibrary/components/LibraryExplorer/utils.js index 404d76b3558..2712c5cfb9c 100644 --- a/openlibrary/components/LibraryExplorer/utils.js +++ b/openlibrary/components/LibraryExplorer/utils.js @@ -6,13 +6,13 @@ * @param {(node: T) => void} fn */ export function recurForEach(node, fn) { - if (!node) return; - fn(node); - if (!node.children) return; - for (const child of node.children) { - recurForEach(child, fn); - } - return node; + if (!node) return; + fn(node); + if (!node.children) return; + for (const child of node.children) { + recurForEach(child, fn); + } + return node; } /** @@ -20,11 +20,11 @@ export function recurForEach(node, fn) { * @param {string} str */ export function hashCode(str) { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - } - return hash; + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + return hash; } /* @@ -35,12 +35,12 @@ export function hashCode(str) { * @returns {T[]} */ export function hierarchyFind(node, predicate) { - if (!predicate(node)) return []; - for (const child of (node.children || [])) { - const childResult = hierarchyFind(child, predicate); - if (childResult.length) return [node, ...childResult]; - } - return [node]; + if (!predicate(node)) return []; + for (const child of node.children || []) { + const childResult = hierarchyFind(child, predicate); + if (childResult.length) return [node, ...childResult]; + } + return [node]; } /** @@ -51,14 +51,14 @@ export function hierarchyFind(node, predicate) { * @param {string} string */ export function testLuceneSyntax(pattern, string) { - if (pattern.endsWith('*')) { - return string.startsWith(pattern.slice(0, -1)); - } else if (pattern.endsWith(']')) { - const [lo, hi] = pattern.slice(1, -1).split(' TO '); - return string >= lo && string <= hi; - } else { - throw new Error(`Unsupported lucene syntax: ${pattern}`); - } + if (pattern.endsWith('*')) { + return string.startsWith(pattern.slice(0, -1)); + } else if (pattern.endsWith(']')) { + const [lo, hi] = pattern.slice(1, -1).split(' TO '); + return string >= lo && string <= hi; + } else { + throw new Error(`Unsupported lucene syntax: ${pattern}`); + } } /** @@ -66,20 +66,30 @@ export function testLuceneSyntax(pattern, string) { * while keeping it solr-query safe. * @param {string} string */ -export function decrementStringSolr(string, caseSensitive=true, numeric=false) { - const lastChar = caseSensitive ? string[string.length - 1] : string[string.length - 1].toUpperCase(); - // Anything < '.' will likely cause query issues, so assume it's - // the end of the that prefix. - // Also append Z; this is the equivalent of going back one, and then expanding (e.g. 0.123 decremented is not 0.122, it's 0.12999999) - const maxTail = (numeric ? '9' : 'z').repeat(5); - const newLastChar = ( - lastChar === '.' ? '' : - lastChar === '0' ? `.${maxTail}` : - lastChar === 'A' ? `9${maxTail}` : - lastChar === 'a' ? `Z${maxTail}` : - `${String.fromCharCode(lastChar.charCodeAt(0) - 1)}${maxTail}`); +export function decrementStringSolr( + string, + caseSensitive = true, + numeric = false, +) { + const lastChar = caseSensitive + ? string[string.length - 1] + : string[string.length - 1].toUpperCase(); + // Anything < '.' will likely cause query issues, so assume it's + // the end of the that prefix. + // Also append Z; this is the equivalent of going back one, and then expanding (e.g. 0.123 decremented is not 0.122, it's 0.12999999) + const maxTail = (numeric ? '9' : 'z').repeat(5); + const newLastChar = + lastChar === '.' + ? '' + : lastChar === '0' + ? `.${maxTail}` + : lastChar === 'A' + ? `9${maxTail}` + : lastChar === 'a' + ? `Z${maxTail}` + : `${String.fromCharCode(lastChar.charCodeAt(0) - 1)}${maxTail}`; - return string.slice(0, -1) + newLastChar; + return string.slice(0, -1) + newLastChar; } /** @@ -90,13 +100,13 @@ export function decrementStringSolr(string, caseSensitive=true, numeric=false) { * @returns {Promise<T | undefined>} - A promise that resolves to the truthy value returned by the function, or undefined if the timeout is reached. */ export async function pollUntilTruthy(fn, { timeout = 1000, step = 100 } = {}) { - const start = Date.now(); - while (Date.now() - start <= timeout) { - const val = fn(); - if (val) return val; - await new Promise(resolve => setTimeout(resolve, step)); - } - return undefined; + const start = Date.now(); + while (Date.now() - start <= timeout) { + const val = fn(); + if (val) return val; + await new Promise((resolve) => setTimeout(resolve, step)); + } + return undefined; } /** diff --git a/openlibrary/components/LibraryExplorer/utils/lcc.js b/openlibrary/components/LibraryExplorer/utils/lcc.js index 06c9bed4538..67a9ed81802 100644 --- a/openlibrary/components/LibraryExplorer/utils/lcc.js +++ b/openlibrary/components/LibraryExplorer/utils/lcc.js @@ -4,7 +4,7 @@ */ const LCC_PARTS_RE = new RegExp( - String.raw` + String.raw` ^ (?<letters>[A-HJ-NP-VWZ][A-Z-]{0,2}) \s? @@ -12,50 +12,55 @@ const LCC_PARTS_RE = new RegExp( (?<cutter1>\s*\.\s*[^\d\s\[]{1,3}\d*\S*)? (?<rest>\s.*)? $`.replace(/\s/g, ''), - 'i'); + 'i', +); export function short_lcc_to_sortable_lcc(lcc) { - const m = clean_raw_lcc(lcc).match(LCC_PARTS_RE); - if (!m) return null + const m = clean_raw_lcc(lcc).match(LCC_PARTS_RE); + if (!m) return null; - const letters = m.groups.letters.toUpperCase().padEnd(3, '-'); - const number = parseFloat(m.groups.number || 0); - const cutter1 = m.groups.cutter1 ? `.${m.groups.cutter1.replace(/^[ .]+/, '')}` : ''; - const rest = m.groups.rest ? ` ${m.groups.rest}` : ''; + const letters = m.groups.letters.toUpperCase().padEnd(3, '-'); + const number = parseFloat(m.groups.number || 0); + const cutter1 = m.groups.cutter1 + ? `.${m.groups.cutter1.replace(/^[ .]+/, '')}` + : ''; + const rest = m.groups.rest ? ` ${m.groups.rest}` : ''; - // There will often be a CPB Box No (whatever that is) in the LCC field; - // E.g. "CPB Box no. 1516 vol. 17" - // Although this might be useful to search by, it's not really an LCC, - // so considering it invalid here. - if (letters === 'CPB') return null; + // There will often be a CPB Box No (whatever that is) in the LCC field; + // E.g. "CPB Box no. 1516 vol. 17" + // Although this might be useful to search by, it's not really an LCC, + // so considering it invalid here. + if (letters === 'CPB') return null; - return `${letters}${number.toFixed(8).padStart(13, '0')}${cutter1}${rest}`; + return `${letters}${number.toFixed(8).padStart(13, '0')}${cutter1}${rest}`; } /** * @param {string} lcc */ export function sortable_lcc_to_short_lcc(lcc) { - const m = lcc.match(LCC_PARTS_RE); - const parts = { - letters: m.groups.letters.replace(/-+/, ''), - number: parseFloat(m.groups.number), - cutter1: m.groups.cutter1 ? m.groups.cutter1.trim() : '', - rest: m.groups.rest ? ` ${m.groups.rest}` : '' - } - return `${parts.letters}${parts.number}${parts.cutter1}${parts.rest}`; + const m = lcc.match(LCC_PARTS_RE); + const parts = { + letters: m.groups.letters.replace(/-+/, ''), + number: parseFloat(m.groups.number), + cutter1: m.groups.cutter1 ? m.groups.cutter1.trim() : '', + rest: m.groups.rest ? ` ${m.groups.rest}` : '', + }; + return `${parts.letters}${parts.number}${parts.cutter1}${parts.rest}`; } - /** * Remove noise in lcc before matching to LCC_PARTS_RE * @param {string} raw_lcc * @return {string} */ export function clean_raw_lcc(raw_lcc) { - let lcc = raw_lcc.replace(/\\/g, ' ').trim(); - if ((lcc.startsWith('[') && lcc.endsWith(']')) || (lcc.startsWith('(') && lcc.endsWith(')'))) { - lcc = lcc.slice(1, -1); - } - return lcc + let lcc = raw_lcc.replace(/\\/g, ' ').trim(); + if ( + (lcc.startsWith('[') && lcc.endsWith(']')) || + (lcc.startsWith('(') && lcc.endsWith(')')) + ) { + lcc = lcc.slice(1, -1); + } + return lcc; } diff --git a/openlibrary/components/MergeUI/utils.js b/openlibrary/components/MergeUI/utils.js index a02ea5b7d61..71968fac397 100644 --- a/openlibrary/components/MergeUI/utils.js +++ b/openlibrary/components/MergeUI/utils.js @@ -1,41 +1,51 @@ /* eslint no-console: 0 */ import _ from 'lodash'; -import { approveRequest, declineRequest, createRequest, REQUEST_TYPES } from '../../plugins/openlibrary/js/merge-request-table/MergeRequestService' +import { + approveRequest, + createRequest, + declineRequest, + REQUEST_TYPES, +} from '../../plugins/openlibrary/js/merge-request-table/MergeRequestService'; import CONFIGS from '../configs.js'; -const collator = new Intl.Collator('en-US', {numeric: true}) -export const DEFAULT_EDITION_LIMIT = 200 +const collator = new Intl.Collator('en-US', { numeric: true }); +export const DEFAULT_EDITION_LIMIT = 200; /** * @param {string | URL | Request} input * @param {RequestInit?} init * @returns {Promise<Response>} */ -export async function fetchWithRetry(input, init = {}, maxRetries = 5, initialDelay = 2000) { - let lastError = null; - for (let attempt = 0; attempt < maxRetries; attempt++) { - try { - const response = await fetch(input, init); - if (response.status !== 429) { - return response; - } - } catch (error) { - // This block catches network errors (e.g., DNS, connection refused) and the server errors we threw above. - // 429s come here if there is a cors issue (like on localhost) - lastError = error; - } - - const backoff = Math.pow(2, attempt) * initialDelay; - const jitter = Math.random() * 2000; - const delay = backoff + jitter; - await new Promise(resolve => setTimeout(resolve, delay)); +export async function fetchWithRetry( + input, + init = {}, + maxRetries = 5, + initialDelay = 2000, +) { + let lastError = null; + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + const response = await fetch(input, init); + if (response.status !== 429) { + return response; + } + } catch (error) { + // This block catches network errors (e.g., DNS, connection refused) and the server errors we threw above. + // 429s come here if there is a cors issue (like on localhost) + lastError = error; } - if (lastError) { - throw lastError; - } else { - throw new Error('Max retries exceeded for request'); - } + const backoff = 2 ** attempt * initialDelay; + const jitter = Math.random() * 2000; + const delay = backoff + jitter; + await new Promise((resolve) => setTimeout(resolve, delay)); + } + + if (lastError) { + throw lastError; + } else { + throw new Error('Max retries exceeded for request'); + } } /** @@ -45,11 +55,12 @@ export async function fetchWithRetry(input, init = {}, maxRetries = 5, initialDe * @return {string} */ function hash_subel(field, value) { - switch (field) { - case 'authors': - // Handle the two possible formats for authors in works and editions - const authorKey = value.author ? value.author.key : value.key; - return (value.type.key || value.type) + authorKey; + switch (field) { + case 'authors': { + // Handle the two possible formats for authors in works and editions + const authorKey = value.author ? value.author.key : value.key; + return (value.type.key || value.type) + authorKey; + } case 'covers': case 'subjects': case 'subject_people': @@ -57,8 +68,8 @@ function hash_subel(field, value) { case 'subject_times': case 'excerpts': default: - return JSON.stringify(value); - } + return JSON.stringify(value); + } } /** @@ -67,131 +78,141 @@ function hash_subel(field, value) { * @param {Object} dupes */ export function merge(master, dupes) { - const result = _.cloneDeep(master); - result.latest_revision++; - result.revision = result.latest_revision; - result.last_modified.value = (new Date()).toISOString().slice(0, -1); - /** @type {{[field: string]: String}} field -> key where it came from */ - const sources = {}; - const subsources = {}; // for array elements - - for (const field in result) { - sources[field] = [master.key]; - if (result[field] instanceof Array) { - for (const el of result[field]) { - subsources[field] = { - [hash_subel(field, el)]: [master.key] - }; - } - } + const result = _.cloneDeep(master); + result.latest_revision++; + result.revision = result.latest_revision; + result.last_modified.value = new Date().toISOString().slice(0, -1); + /** @type {{[field: string]: String}} field -> key where it came from */ + const sources = {}; + const subsources = {}; // for array elements + + for (const field in result) { + sources[field] = [master.key]; + if (result[field] instanceof Array) { + for (const el of result[field]) { + subsources[field] = { + [hash_subel(field, el)]: [master.key], + }; + } } - - for (const dupe of dupes) { - for (const field in dupe) { - if (!(field in result) && field !== 'subtitle') { - result[field] = dupe[field]; - sources[field] = [dupe.key]; - } else if (result[field] instanceof Array) { - result[field] = result[field].concat(dupe[field]) - sources[field].push(dupe.key); - } - } + } + + for (const dupe of dupes) { + for (const field in dupe) { + if (!(field in result) && field !== 'subtitle') { + result[field] = dupe[field]; + sources[field] = [dupe.key]; + } else if (result[field] instanceof Array) { + result[field] = result[field].concat(dupe[field]); + sources[field].push(dupe.key); + } } - - // dedup - for (const key in result) { - if (!(result[key] instanceof Array)) - continue; - switch (key) { - case 'authors': - const authors = _.cloneDeep(result.authors); - authors - .filter(a => typeof a.type === 'string') - .forEach(a => a.type = { key: a.type }); - result.authors = _.uniqWith(authors, _.isEqual); - break; - case 'covers': - case 'subjects': - case 'subject_people': - case 'subject_places': - case 'subject_times': - case 'excerpts': - default: - result[key] = _.uniqWith(result[key], _.isEqual); - break; - } + } + + // dedup + for (const key in result) { + if (!(result[key] instanceof Array)) continue; + switch (key) { + case 'authors': { + const authors = _.cloneDeep(result.authors); + authors + .filter((a) => typeof a.type === 'string') + .forEach((a) => (a.type = { key: a.type })); + result.authors = _.uniqWith(authors, _.isEqual); + break; + } + case 'covers': + case 'subjects': + case 'subject_people': + case 'subject_places': + case 'subject_times': + case 'excerpts': + default: + result[key] = _.uniqWith(result[key], _.isEqual); + break; } + } - return [result, sources]; + return [result, sources]; } export async function do_merge(merged_record, dupes, editions, mrid) { - editions.forEach(ed => ed.works = [{key: merged_record.key}]); - const edits = [ - merged_record, - ...dupes.map(dupe => make_redirect(merged_record.key, dupe)), - ...editions - ]; - - let comment = 'Merge works' - if (mrid) { - comment += ` (MRID: ${mrid})` - } - - return await save_many( - edits, - comment, - 'merge-works', - { - master: merged_record.key, - duplicates: dupes.map(dupe => dupe.key), - mrid: mrid, - }, - ); + editions.forEach((ed) => (ed.works = [{ key: merged_record.key }])); + const edits = [ + merged_record, + ...dupes.map((dupe) => make_redirect(merged_record.key, dupe)), + ...editions, + ]; + + let comment = 'Merge works'; + if (mrid) { + comment += ` (MRID: ${mrid})`; + } + + return await save_many(edits, comment, 'merge-works', { + master: merged_record.key, + duplicates: dupes.map((dupe) => dupe.key), + mrid: mrid, + }); } export function make_redirect(master_key, dupe) { - return { - location: master_key, - key: dupe.key, - type: { key: '/type/redirect' } - }; + return { + location: master_key, + key: dupe.key, + type: { key: '/type/redirect' }, + }; } export function get_editions(work_key) { - const endpoint = `${work_key}/editions.json`; - let base = ''; - if (CONFIGS.OL_BASE_BOOKS) { - base = CONFIGS.OL_BASE_BOOKS; - } else { - // FIXME Fetch from prod openlibrary.org, otherwise it's outdated - base = location.host.endsWith('.openlibrary.org') ? 'https://openlibrary.org' : ''; - } - return fetchWithRetry(`${base}${endpoint}?${new URLSearchParams({limit: DEFAULT_EDITION_LIMIT})}`).then(r => { - if (r.ok) return r.json(); - if (confirm(`Network error; failed to load editions for ${work_key}. Click OK to reload.`)) location.reload(); - }); + const endpoint = `${work_key}/editions.json`; + let base = ''; + if (CONFIGS.OL_BASE_BOOKS) { + base = CONFIGS.OL_BASE_BOOKS; + } else { + // FIXME Fetch from prod openlibrary.org, otherwise it's outdated + base = location.host.endsWith('.openlibrary.org') + ? 'https://openlibrary.org' + : ''; + } + return fetchWithRetry( + `${base}${endpoint}?${new URLSearchParams({ limit: DEFAULT_EDITION_LIMIT })}`, + ).then((r) => { + if (r.ok) return r.json(); + if ( + confirm( + `Network error; failed to load editions for ${work_key}. Click OK to reload.`, + ) + ) + location.reload(); + }); } -export function get_lists(key, limit=10) { - return fetchWithRetry(`${CONFIGS.OL_BASE_BOOKS}${key}/lists.json?${new URLSearchParams({ limit })}`).then(r => { - if (r.ok) return r.json(); - return {error: true}; - }); +export function get_lists(key, limit = 10) { + return fetchWithRetry( + `${CONFIGS.OL_BASE_BOOKS}${key}/lists.json?${new URLSearchParams({ limit })}`, + ).then((r) => { + if (r.ok) return r.json(); + return { error: true }; + }); } export function get_bookshelves(key) { - return fetchWithRetry(`${CONFIGS.OL_BASE_BOOKS}${key}/bookshelves.json`).then(r => { - if (r.ok) return r.json(); - return {error: true}; - }); + return fetchWithRetry(`${CONFIGS.OL_BASE_BOOKS}${key}/bookshelves.json`).then( + (r) => { + if (r.ok) return r.json(); + return { error: true }; + }, + ); } export function get_ratings(key) { - return fetchWithRetry(`${CONFIGS.OL_BASE_BOOKS}${key}/ratings.json`).then(r => { - if (r.ok) return r.json(); - return {error: true}; - }); + return fetchWithRetry(`${CONFIGS.OL_BASE_BOOKS}${key}/ratings.json`).then( + (r) => { + if (r.ok) return r.json(); + return { error: true }; + }, + ); } /** @@ -204,12 +225,11 @@ export function get_ratings(key) { * @returns {Promise<Response>} A response to the request */ export function update_merge_request(mrid, action, comment) { - if (action === 'approve') { - return approveRequest(mrid, comment) - } - else if (action === 'decline') { - return declineRequest(mrid, comment) - } + if (action === 'approve') { + return approveRequest(mrid, comment); + } else if (action === 'decline') { + return declineRequest(mrid, comment); + } } /** @@ -222,9 +242,20 @@ export function update_merge_request(mrid, action, comment) { * * @returns {Promise<Response>} */ -export function createMergeRequest(workIds, primaryRecord, action = 'create-merged', comment = null) { - const normalizedIds = prepareIds(workIds).join(',') - return createRequest(normalizedIds, action, REQUEST_TYPES['WORK_MERGE'], comment, primaryRecord) +export function createMergeRequest( + workIds, + primaryRecord, + action = 'create-merged', + comment = null, +) { + const normalizedIds = prepareIds(workIds).join(','); + return createRequest( + normalizedIds, + action, + REQUEST_TYPES['WORK_MERGE'], + comment, + primaryRecord, + ); } /** @@ -236,10 +267,10 @@ export function createMergeRequest(workIds, primaryRecord, action = 'create-merg * @returns {Array<string>} Noralized and sorted array of OLIDs */ function prepareIds(workIds) { - return Array.from(workIds, id => { - const splitArr = id.split('/') - return splitArr[splitArr.length - 1] - }).sort(collator.compare) + return Array.from(workIds, (id) => { + const splitArr = id.split('/'); + return splitArr[splitArr.length - 1]; + }).sort(collator.compare); } /** @@ -250,18 +281,18 @@ function prepareIds(workIds) { * @param {Object} data */ function save_many(items, comment, action, data) { - const headers = { - Opt: '"http://openlibrary.org/dev/docs/api"; ns=42', - '42-comment': comment, - '42-action': action, - '42-data': JSON.stringify(data), - }; - - return fetchWithRetry(`${CONFIGS.OL_BASE_SAVES}/api/save_many`, { - method: 'POST', - headers, - body: JSON.stringify(items) - }); + const headers = { + Opt: '"http://openlibrary.org/dev/docs/api"; ns=42', + '42-comment': comment, + '42-action': action, + '42-data': JSON.stringify(data), + }; + + return fetchWithRetry(`${CONFIGS.OL_BASE_SAVES}/api/save_many`, { + method: 'POST', + headers, + body: JSON.stringify(items), + }); } /** @@ -270,32 +301,35 @@ function save_many(items, comment, action, data) { * @returns {Promise<Record<string,object>} A response to the request */ export async function get_author_names(works) { - const authorIds = _.uniq(works).flatMap(record => - (record.authors || []) - .map(authorEntry => authorEntry.author?.key ?? authorEntry.key) - ) + const authorIds = _.uniq(works).flatMap((record) => + (record.authors || []).map( + (authorEntry) => authorEntry.author?.key ?? authorEntry.key, + ), + ); - if (!authorIds.length) return {}; + if (!authorIds.length) return {}; - const queryParams = new URLSearchParams({ - q: `key:(${authorIds.join(' OR ')})`, - mode: 'everything', - fields: 'key,name', - }) + const queryParams = new URLSearchParams({ + q: `key:(${authorIds.join(' OR ')})`, + mode: 'everything', + fields: 'key,name', + }); - const response = await fetchWithRetry(`${CONFIGS.OL_BASE_SEARCH}/search/authors.json?${queryParams}`) + const response = await fetchWithRetry( + `${CONFIGS.OL_BASE_SEARCH}/search/authors.json?${queryParams}`, + ); - if (!response.ok) { - throw new Error('Failed to fetch author data'); - } + if (!response.ok) { + throw new Error('Failed to fetch author data'); + } - const results = await response.json() + const results = await response.json(); - const authorDirectory = {} + const authorDirectory = {}; - for (const doc of results.docs) { - authorDirectory[doc.key] = doc.name; - } + for (const doc of results.docs) { + authorDirectory[doc.key] = doc.name; + } - return authorDirectory + return authorDirectory; } diff --git a/openlibrary/components/ObservationForm/ObservationService.js b/openlibrary/components/ObservationForm/ObservationService.js index 8b00a3c59c9..899ce97238e 100644 --- a/openlibrary/components/ObservationForm/ObservationService.js +++ b/openlibrary/components/ObservationForm/ObservationService.js @@ -10,23 +10,23 @@ * @returns A Promise representing the state of the POST request. */ export function updateObservation(action, type, value, workKey, username) { - const data = constructDataObject(type, value, username, action) + const data = constructDataObject(type, value, username, action); - return fetch(`${workKey}/observations`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) + return fetch(`${workKey}/observations`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }) + .then((response) => { + if (!response.ok) { + throw new Error('Server response was not ok'); + } }) - .then(response => { - if (!response.ok) { - throw new Error('Server response was not ok') - } - }) - .catch(error => { - throw error - }) + .catch((error) => { + throw error; + }); } /** @@ -48,13 +48,13 @@ export function updateObservation(action, type, value, workKey, username) { * @returns An object that represents the observation update that will be made. */ function constructDataObject(type, value, username, action) { - const data = { - username: username, - action: action, - observation: {} - } + const data = { + username: username, + action: action, + observation: {}, + }; - data.observation[type] = value; + data.observation[type] = value; - return data; + return data; } diff --git a/openlibrary/components/ObservationForm/Utils.js b/openlibrary/components/ObservationForm/Utils.js index e0c3a32c1cd..19a48afb01e 100644 --- a/openlibrary/components/ObservationForm/Utils.js +++ b/openlibrary/components/ObservationForm/Utils.js @@ -5,7 +5,7 @@ * @returns A JavaScript object */ export function decodeAndParseJSON(str) { - return JSON.parse(decodeURIComponent(str)); + return JSON.parse(decodeURIComponent(str)); } /* @@ -13,7 +13,11 @@ export function decodeAndParseJSON(str) { window.$.colorbox is a jQuery plugin */ export function resizeColorbox() { - if (window.$ && window.$.colorbox && typeof window.$.colorbox.resize === 'function') { - window.$.colorbox.resize(); - } + if ( + window.$ && + window.$.colorbox && + typeof window.$.colorbox.resize === 'function' + ) { + window.$.colorbox.resize(); + } } diff --git a/openlibrary/components/configs.js b/openlibrary/components/configs.js index 486802064d6..ae7514394f7 100644 --- a/openlibrary/components/configs.js +++ b/openlibrary/components/configs.js @@ -3,27 +3,34 @@ const urlParams = new URLSearchParams(location.search); -const IS_VUE_APP = document.title === 'Vue App'; -const OL_BASE_DEFAULT = urlParams.get('ol_base') || (IS_VUE_APP ? 'openlibrary.org' : ''); +const IS_VUE_APP = document.title === 'Vue App'; +const OL_BASE_DEFAULT = + urlParams.get('ol_base') || (IS_VUE_APP ? 'openlibrary.org' : ''); const CONFIGS = { - OL_BASE_COVERS: urlParams.get('ol_base_covers') || 'covers.openlibrary.org', - OL_BASE_SEARCH: urlParams.get('ol_base_search') || OL_BASE_DEFAULT || '', - OL_BASE_BOOKS: urlParams.get('ol_base_books') || OL_BASE_DEFAULT || '', - OL_BASE_LANGS: urlParams.get('ol_base_langs') || OL_BASE_DEFAULT || '', - // Make the save location explicitly different from ol_base to avoid - // accidentally triggering saves to prod (which shouldn't work anyways - // due to cookies, but just in case!) - OL_BASE_SAVES: urlParams.get('ol_base_saves') || '', - OL_BASE_PUBLIC: urlParams.get('ol_base') || 'openlibrary.org', - DEBUG_MODE: urlParams.get('debug') === 'true', - LANG: urlParams.get('lang'), + OL_BASE_COVERS: urlParams.get('ol_base_covers') || 'covers.openlibrary.org', + OL_BASE_SEARCH: urlParams.get('ol_base_search') || OL_BASE_DEFAULT || '', + OL_BASE_BOOKS: urlParams.get('ol_base_books') || OL_BASE_DEFAULT || '', + OL_BASE_LANGS: urlParams.get('ol_base_langs') || OL_BASE_DEFAULT || '', + // Make the save location explicitly different from ol_base to avoid + // accidentally triggering saves to prod (which shouldn't work anyways + // due to cookies, but just in case!) + OL_BASE_SAVES: urlParams.get('ol_base_saves') || '', + OL_BASE_PUBLIC: urlParams.get('ol_base') || 'openlibrary.org', + DEBUG_MODE: urlParams.get('debug') === 'true', + LANG: urlParams.get('lang'), }; -for (const key of ['OL_BASE_COVERS', 'OL_BASE_SEARCH', 'OL_BASE_BOOKS', 'OL_BASE_LANGS', 'OL_BASE_SAVES']) { - if (CONFIGS[key] && !CONFIGS[key].startsWith('http')) { - CONFIGS[key] = `https://${CONFIGS[key]}`; - } +for (const key of [ + 'OL_BASE_COVERS', + 'OL_BASE_SEARCH', + 'OL_BASE_BOOKS', + 'OL_BASE_LANGS', + 'OL_BASE_SAVES', +]) { + if (CONFIGS[key] && !CONFIGS[key].startsWith('http')) { + CONFIGS[key] = `https://${CONFIGS[key]}`; + } } export default CONFIGS; diff --git a/openlibrary/components/dev/serve-component.js b/openlibrary/components/dev/serve-component.js index a041c2aff9a..817c1761693 100644 --- a/openlibrary/components/dev/serve-component.js +++ b/openlibrary/components/dev/serve-component.js @@ -11,7 +11,7 @@ import { createApp } from 'vue' import HelloWorld from '../HelloWorld.vue' createApp(HelloWorld).mount('#app') -` +`; const result = data.replace(/HelloWorld/g, componentName); fs.writeFileSync('openlibrary/components/dev/_dev.js', result); diff --git a/openlibrary/components/dev/vite.config.js b/openlibrary/components/dev/vite.config.js index ea39d477e95..3e70f22e767 100644 --- a/openlibrary/components/dev/vite.config.js +++ b/openlibrary/components/dev/vite.config.js @@ -2,10 +2,11 @@ This is the config used for the dev server ala `npm run serve` This does not effect production builds */ -import { defineConfig } from 'vite' -import vue from '@vitejs/plugin-vue' + +import vue from '@vitejs/plugin-vue'; +import { defineConfig } from 'vite'; // https://vite.dev/config/ export default defineConfig({ - plugins: [vue()], -}) + plugins: [vue()], +}); diff --git a/openlibrary/components/lit/OLChip.js b/openlibrary/components/lit/OLChip.js index 7fa2e5c56d9..7511ed60d3e 100644 --- a/openlibrary/components/lit/OLChip.js +++ b/openlibrary/components/lit/OLChip.js @@ -1,4 +1,4 @@ -import { LitElement, html, css, nothing } from 'lit'; +import { css, html, LitElement, nothing } from 'lit'; /** * OLChip - A pill-shaped interactive chip web component @@ -24,15 +24,15 @@ import { LitElement, html, css, nothing } from 'lit'; * <ol-chip size="small" count="76" href="/subjects/fiction">Fiction</ol-chip> */ export class OLChip extends LitElement { - static properties = { - selected: { type: Boolean, reflect: true }, - size: { type: String, reflect: true }, - href: { type: String }, - count: { type: String }, - accessibleLabel: { type: String, attribute: 'accessible-label' }, - }; - - static styles = css` + static properties = { + selected: { type: Boolean, reflect: true }, + size: { type: String, reflect: true }, + href: { type: String }, + count: { type: String }, + accessibleLabel: { type: String, attribute: 'accessible-label' }, + }; + + static styles = css` :host { --chip-padding-block: 6px; --chip-padding-inline: 12px; @@ -127,27 +127,29 @@ export class OLChip extends LitElement { } `; - constructor() { - super(); - this.selected = false; - this.size = 'medium'; - this.href = null; - this.count = null; - this.accessibleLabel = null; - } - - _handleClick() { - this.dispatchEvent(new CustomEvent('ol-chip-select', { - bubbles: true, - composed: true, - detail: { selected: !this.selected }, - })); - } - - _renderIcons() { - if (!this.selected) return nothing; - - return html` + constructor() { + super(); + this.selected = false; + this.size = 'medium'; + this.href = null; + this.count = null; + this.accessibleLabel = null; + } + + _handleClick() { + this.dispatchEvent( + new CustomEvent('ol-chip-select', { + bubbles: true, + composed: true, + detail: { selected: !this.selected }, + }), + ); + } + + _renderIcons() { + if (!this.selected) return nothing; + + return html` <span class="icon-slot"> <svg class="icon" @@ -163,32 +165,32 @@ export class OLChip extends LitElement { </svg> </span> `; - } + } - _renderCount() { - if (this.count === null) return nothing; + _renderCount() { + if (this.count === null) return nothing; - return html`<span class="count">${this.count}</span>`; - } + return html`<span class="count">${this.count}</span>`; + } - render() { - const content = html` + render() { + const content = html` ${this._renderIcons()} <slot></slot> ${this._renderCount()} `; - if (this.href) { - return html` + if (this.href) { + return html` <a class="chip" href=${this.href} aria-label=${this.accessibleLabel || nothing} @click=${this._handleClick}> ${content} </a> `; - } + } - return html` + return html` <button class="chip" type="button" aria-label=${this.accessibleLabel || nothing} aria-pressed=${this.selected} @@ -196,7 +198,7 @@ export class OLChip extends LitElement { ${content} </button> `; - } + } } customElements.define('ol-chip', OLChip); diff --git a/openlibrary/components/lit/OLChipGroup.js b/openlibrary/components/lit/OLChipGroup.js index 10ca82828b8..94fc4becf55 100644 --- a/openlibrary/components/lit/OLChipGroup.js +++ b/openlibrary/components/lit/OLChipGroup.js @@ -1,4 +1,4 @@ -import { LitElement, html, css } from 'lit'; +import { css, html, LitElement } from 'lit'; /** * OLChipGroup - A flex-wrap container for ol-chip components @@ -15,11 +15,11 @@ import { LitElement, html, css } from 'lit'; * </ol-chip-group> */ export class OLChipGroup extends LitElement { - static properties = { - gap: { type: String, reflect: true }, - }; + static properties = { + gap: { type: String, reflect: true }, + }; - static styles = css` + static styles = css` :host { display: flex; flex-wrap: wrap; @@ -35,21 +35,21 @@ export class OLChipGroup extends LitElement { } `; - constructor() { - super(); - this.gap = 'medium'; - } + constructor() { + super(); + this.gap = 'medium'; + } - connectedCallback() { - super.connectedCallback(); - if (!this.hasAttribute('role')) { - this.setAttribute('role', 'group'); - } + connectedCallback() { + super.connectedCallback(); + if (!this.hasAttribute('role')) { + this.setAttribute('role', 'group'); } + } - render() { - return html`<slot></slot>`; - } + render() { + return html`<slot></slot>`; + } } customElements.define('ol-chip-group', OLChipGroup); diff --git a/openlibrary/components/lit/OLReadMore.js b/openlibrary/components/lit/OLReadMore.js index 33cc01de31e..c304705d449 100644 --- a/openlibrary/components/lit/OLReadMore.js +++ b/openlibrary/components/lit/OLReadMore.js @@ -1,4 +1,4 @@ -import { LitElement, html, css } from 'lit'; +import { css, html, LitElement } from 'lit'; /** * OLReadMore - A web component for expandable/collapsible content @@ -25,18 +25,18 @@ import { LitElement, html, css } from 'lit'; * </ol-read-more> */ export class OLReadMore extends LitElement { - static properties = { - maxHeight: { type: String, attribute: 'max-height' }, - moreText: { type: String, attribute: 'more-text' }, - lessText: { type: String, attribute: 'less-text' }, - backgroundColor: { type: String, attribute: 'background-color' }, - labelSize: { type: String, attribute: 'label-size' }, - // Internal state - _expanded: { type: Boolean, state: true }, - _unnecessary: { type: Boolean, state: true }, - }; - - static styles = css` + static properties = { + maxHeight: { type: String, attribute: 'max-height' }, + moreText: { type: String, attribute: 'more-text' }, + lessText: { type: String, attribute: 'less-text' }, + backgroundColor: { type: String, attribute: 'background-color' }, + labelSize: { type: String, attribute: 'label-size' }, + // Internal state + _expanded: { type: Boolean, state: true }, + _unnecessary: { type: Boolean, state: true }, + }; + + static styles = css` :host { display: block; position: relative; @@ -113,88 +113,91 @@ export class OLReadMore extends LitElement { } `; - constructor() { - super(); - this.maxHeight = '80px'; - this.moreText = 'Read More'; - this.lessText = 'Read Less'; - this.backgroundColor = null; - this.labelSize = 'medium'; - this._expanded = false; - this._unnecessary = false; + constructor() { + super(); + this.maxHeight = '80px'; + this.moreText = 'Read More'; + this.lessText = 'Read Less'; + this.backgroundColor = null; + this.labelSize = 'medium'; + this._expanded = false; + this._unnecessary = false; + } + + firstUpdated() { + this._checkIfTruncationNeeded(); + this._updateBackgroundColor(); + // Remove styles that were used to prevent layout shift + // Now that the component has rendered, it can size naturally + this.style.minHeight = 'auto'; + this.style.visibility = 'visible'; + this.style.overflow = 'visible'; + } + + updated(changedProperties) { + if (changedProperties.has('backgroundColor')) { + this._updateBackgroundColor(); } - - firstUpdated() { - this._checkIfTruncationNeeded(); - this._updateBackgroundColor(); - // Remove styles that were used to prevent layout shift - // Now that the component has rendered, it can size naturally - this.style.minHeight = 'auto'; - this.style.visibility = 'visible'; - this.style.overflow = 'visible'; - } - - updated(changedProperties) { - if (changedProperties.has('backgroundColor')) { - this._updateBackgroundColor(); - } + } + + _updateBackgroundColor() { + if (this.backgroundColor) { + this.style.setProperty( + '--ol-readmore-gradient-color', + this.backgroundColor, + ); } + } - _updateBackgroundColor() { - if (this.backgroundColor) { - this.style.setProperty('--ol-readmore-gradient-color', this.backgroundColor); - } - } + _checkIfTruncationNeeded() { + const content = this.shadowRoot.querySelector('.content-wrapper'); + if (!content) return; - _checkIfTruncationNeeded() { - const content = this.shadowRoot.querySelector('.content-wrapper'); - if (!content) return; + const isOverflowing = content.scrollHeight > content.clientHeight; + this._unnecessary = !isOverflowing; - const isOverflowing = content.scrollHeight > content.clientHeight; - this._unnecessary = !isOverflowing; - - if (this._unnecessary) { - this._expanded = true; - } + if (this._unnecessary) { + this._expanded = true; } - - _handleMoreClick() { - if (this._unnecessary) return; - this._expanded = true; + } + + _handleMoreClick() { + if (this._unnecessary) return; + this._expanded = true; + } + + _handleLessClick() { + if (this._unnecessary) return; + this._expanded = false; + + // Scroll back to top when collapsing if component is off-screen + const rect = this.getBoundingClientRect(); + if (rect.top < 0) { + this.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); } + } - _handleLessClick() { - if (this._unnecessary) return; - this._expanded = false; - - // Scroll back to top when collapsing if component is off-screen - const rect = this.getBoundingClientRect(); - if (rect.top < 0) { - this.scrollIntoView({ - behavior: 'smooth', - block: 'start', - }); - } + _getContentStyle() { + if (this._expanded) { + return ''; } - _getContentStyle() { - if (this._expanded) { - return ''; - } - - if (this.maxHeight) { - return `max-height: ${this.maxHeight}`; - } - - return ''; + if (this.maxHeight) { + return `max-height: ${this.maxHeight}`; } - render() { - const showMoreBtn = !this._expanded && !this._unnecessary; - const showLessBtn = this._expanded && !this._unnecessary; - const sizeClass = this.labelSize === 'small' ? 'small' : ''; + return ''; + } - return html` + render() { + const showMoreBtn = !this._expanded && !this._unnecessary; + const showLessBtn = this._expanded && !this._unnecessary; + const sizeClass = this.labelSize === 'small' ? 'small' : ''; + + return html` <div class="content-wrapper ${this._expanded ? 'expanded' : ''}" style="${this._getContentStyle()}" @@ -220,7 +223,7 @@ export class OLReadMore extends LitElement { <svg class="chevron up" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg> </button> `; - } + } } customElements.define('ol-read-more', OLReadMore); diff --git a/openlibrary/components/lit/OlPagination.js b/openlibrary/components/lit/OlPagination.js index 298152caa33..faa3c5049ad 100644 --- a/openlibrary/components/lit/OlPagination.js +++ b/openlibrary/components/lit/OlPagination.js @@ -1,4 +1,4 @@ -import { LitElement, html, css } from 'lit'; +import { css, html, LitElement } from 'lit'; /** * A pagination component that displays page numbers with navigation controls. @@ -56,21 +56,21 @@ import { LitElement, html, css } from 'lit'; * ></ol-pagination> */ export class OlPagination extends LitElement { - static properties = { - mode: { type: String }, - totalPages: { type: Number, attribute: 'total-pages' }, - currentPage: { type: Number, attribute: 'current-page' }, - hasNextPage: { type: Boolean, attribute: 'has-next-page' }, - baseUrl: { type: String, attribute: 'base-url' }, - labelPreviousPage: { type: String, attribute: 'label-previous-page' }, - labelNextPage: { type: String, attribute: 'label-next-page' }, - labelGoToPage: { type: String, attribute: 'label-go-to-page' }, - labelCurrentPage: { type: String, attribute: 'label-current-page' }, - labelPagination: { type: String, attribute: 'label-pagination' }, - _focusedIndex: { type: Number, state: true } - }; - - static styles = css` + static properties = { + mode: { type: String }, + totalPages: { type: Number, attribute: 'total-pages' }, + currentPage: { type: Number, attribute: 'current-page' }, + hasNextPage: { type: Boolean, attribute: 'has-next-page' }, + baseUrl: { type: String, attribute: 'base-url' }, + labelPreviousPage: { type: String, attribute: 'label-previous-page' }, + labelNextPage: { type: String, attribute: 'label-next-page' }, + labelGoToPage: { type: String, attribute: 'label-go-to-page' }, + labelCurrentPage: { type: String, attribute: 'label-current-page' }, + labelPagination: { type: String, attribute: 'label-pagination' }, + _focusedIndex: { type: Number, state: true }, + }; + + static styles = css` :host { display: block; font-family: var(--font-family-body); @@ -141,181 +141,194 @@ export class OlPagination extends LitElement { } `; - /** Left chevron arrow icon */ - static _leftArrowIcon = html`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>`; - - /** Right chevron arrow icon */ - static _rightArrowIcon = html`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>`; - - constructor() { - super(); - this.mode = 'full'; - this.totalPages = 1; - this.currentPage = 1; - this.hasNextPage = false; - this.baseUrl = ''; - this._focusedIndex = -1; - - // Translatable label defaults (English) - this.labelPreviousPage = 'Go to previous page'; - this.labelNextPage = 'Go to next page'; - this.labelGoToPage = 'Go to page {page}'; - this.labelCurrentPage = 'Page {page}, current page'; - this.labelPagination = 'Pagination'; + /** Left chevron arrow icon */ + static _leftArrowIcon = + html`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>`; + + /** Right chevron arrow icon */ + static _rightArrowIcon = + html`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>`; + + constructor() { + super(); + this.mode = 'full'; + this.totalPages = 1; + this.currentPage = 1; + this.hasNextPage = false; + this.baseUrl = ''; + this._focusedIndex = -1; + + // Translatable label defaults (English) + this.labelPreviousPage = 'Go to previous page'; + this.labelNextPage = 'Go to next page'; + this.labelGoToPage = 'Go to page {page}'; + this.labelCurrentPage = 'Page {page}, current page'; + this.labelPagination = 'Pagination'; + } + + /** + * Interpolate a label template by replacing {key} placeholders with values. + * @param {String} template - The label template (e.g., "Go to page {page}") + * @param {Object} values - Key-value pairs to substitute (e.g., { page: 5 }) + * @returns {String} The interpolated string + */ + _interpolateLabel(template, values) { + return template.replace(/\{(\w+)\}/g, (_, key) => values[key] ?? ''); + } + + /** + * Build URL for a specific page number. + * Uses baseUrl if provided, otherwise falls back to the current window location. + * This preserves all existing query parameters (like changequery() does). + * @param {Number} page - The page number + * @returns {String|null} The URL for the page + */ + _getPageUrl(page) { + try { + const base = this.baseUrl || window.location.href; + const url = new URL(base, window.location.origin); + if (page === 1) { + url.searchParams.delete('page'); + } else { + url.searchParams.set('page', page); + } + return url.pathname + url.search; + } catch { + return null; } - - /** - * Interpolate a label template by replacing {key} placeholders with values. - * @param {String} template - The label template (e.g., "Go to page {page}") - * @param {Object} values - Key-value pairs to substitute (e.g., { page: 5 }) - * @returns {String} The interpolated string - */ - _interpolateLabel(template, values) { - return template.replace(/\{(\w+)\}/g, (_, key) => values[key] ?? ''); - } - - /** - * Build URL for a specific page number. - * Uses baseUrl if provided, otherwise falls back to the current window location. - * This preserves all existing query parameters (like changequery() does). - * @param {Number} page - The page number - * @returns {String|null} The URL for the page - */ - _getPageUrl(page) { - try { - const base = this.baseUrl || window.location.href; - const url = new URL(base, window.location.origin); - if (page === 1) { - url.searchParams.delete('page'); - } else { - url.searchParams.set('page', page); - } - return url.pathname + url.search; - } catch { - return null; + } + + /** + * Calculate which page numbers to display based on current page and total pages. + * Always shows exactly 5 page numbers max, adjusting position based on current page: + * - Near start: 1, 2, 3, 4 ... last (5 total) + * - Middle: 1 ... current-1, current, current+1 ... last (5 total) + * - Near end: 1 ... last-3, last-2, last-1, last (5 total) + * @returns {Array} Array of page numbers and 'ellipsis' markers + */ + _getVisiblePages() { + const total = this.totalPages; + const current = this.currentPage; + + if (total <= 5) return [...Array(total)].map((_, i) => i + 1); + if (current <= 3) return [1, 2, 3, 4, 'ellipsis-right', total]; + if (current >= total - 2) + return [1, 'ellipsis-left', total - 3, total - 2, total - 1, total]; + + return [ + 1, + 'ellipsis-left', + current - 1, + current, + current + 1, + 'ellipsis-right', + total, + ]; + } + + /** + * Get all focusable elements in the pagination + * @returns {Array} Array of focusable elements (buttons or anchors) + */ + _getFocusableElements() { + return Array.from( + this.shadowRoot.querySelectorAll( + '.pagination-item:not([aria-disabled="true"])', + ), + ); + } + + /** + * Handle keyboard navigation within the pagination + * @param {KeyboardEvent} e + */ + _handleKeyDown(e) { + const focusable = this._getFocusableElements(); + const currentIndex = focusable.indexOf(this.shadowRoot.activeElement); + + switch (e.key) { + case 'ArrowLeft': + e.preventDefault(); + if (currentIndex > 0) { + focusable[currentIndex - 1].focus(); } - } - - /** - * Calculate which page numbers to display based on current page and total pages. - * Always shows exactly 5 page numbers max, adjusting position based on current page: - * - Near start: 1, 2, 3, 4 ... last (5 total) - * - Middle: 1 ... current-1, current, current+1 ... last (5 total) - * - Near end: 1 ... last-3, last-2, last-1, last (5 total) - * @returns {Array} Array of page numbers and 'ellipsis' markers - */ - _getVisiblePages() { - const total = this.totalPages; - const current = this.currentPage; - - if (total <= 5) return [...Array(total)].map((_, i) => i + 1); - if (current <= 3) return [1, 2, 3, 4, 'ellipsis-right', total]; - if (current >= total - 2) return [1, 'ellipsis-left', total - 3, total - 2, total - 1, total]; - - return [1, 'ellipsis-left', current - 1, current, current + 1, 'ellipsis-right', total]; - } - - /** - * Get all focusable elements in the pagination - * @returns {Array} Array of focusable elements (buttons or anchors) - */ - _getFocusableElements() { - return Array.from( - this.shadowRoot.querySelectorAll('.pagination-item:not([aria-disabled="true"])') - ); - } - - /** - * Handle keyboard navigation within the pagination - * @param {KeyboardEvent} e - */ - _handleKeyDown(e) { - const focusable = this._getFocusableElements(); - const currentIndex = focusable.indexOf(this.shadowRoot.activeElement); - - switch (e.key) { - case 'ArrowLeft': - e.preventDefault(); - if (currentIndex > 0) { - focusable[currentIndex - 1].focus(); - } - break; - case 'ArrowRight': - e.preventDefault(); - if (currentIndex < focusable.length - 1) { - focusable[currentIndex + 1].focus(); - } - break; - case 'Home': - e.preventDefault(); - focusable[0]?.focus(); - break; - case 'End': - e.preventDefault(); - focusable[focusable.length - 1]?.focus(); - break; + break; + case 'ArrowRight': + e.preventDefault(); + if (currentIndex < focusable.length - 1) { + focusable[currentIndex + 1].focus(); } + break; + case 'Home': + e.preventDefault(); + focusable[0]?.focus(); + break; + case 'End': + e.preventDefault(); + focusable[focusable.length - 1]?.focus(); + break; } - - /** - * Navigate to a specific page - * @param {Number} page - The page number to navigate to - */ - _goToPage(page) { - const maxPage = this.mode === 'arrows' ? Infinity : this.totalPages; - if (page < 1 || page > maxPage || page === this.currentPage) { - return; - } - - const event = new CustomEvent('ol-pagination-change', { - detail: { page }, - bubbles: true, - composed: true, - cancelable: true, - }); - this.dispatchEvent(event); - if (event.defaultPrevented) return; - - this.currentPage = page; + } + + /** + * Navigate to a specific page + * @param {Number} page - The page number to navigate to + */ + _goToPage(page) { + const maxPage = this.mode === 'arrows' ? Infinity : this.totalPages; + if (page < 1 || page > maxPage || page === this.currentPage) { + return; } - /** - * Handle click on anchor-based page links. - * Dispatches the ol-pagination-change event to allow interception. - * If the event is cancelled via preventDefault(), anchor navigation is also prevented. - * @param {Event} e - Click event - * @param {Number} page - The page number - */ - _handlePageClick(e, page) { - const event = new CustomEvent('ol-pagination-change', { - detail: { page }, - bubbles: true, - composed: true, - cancelable: true, - }); - this.dispatchEvent(event); - if (event.defaultPrevented) { - e.preventDefault(); - this.currentPage = page; - } + const event = new CustomEvent('ol-pagination-change', { + detail: { page }, + bubbles: true, + composed: true, + cancelable: true, + }); + this.dispatchEvent(event); + if (event.defaultPrevented) return; + + this.currentPage = page; + } + + /** + * Handle click on anchor-based page links. + * Dispatches the ol-pagination-change event to allow interception. + * If the event is cancelled via preventDefault(), anchor navigation is also prevented. + * @param {Event} e - Click event + * @param {Number} page - The page number + */ + _handlePageClick(e, page) { + const event = new CustomEvent('ol-pagination-change', { + detail: { page }, + bubbles: true, + composed: true, + cancelable: true, + }); + this.dispatchEvent(event); + if (event.defaultPrevented) { + e.preventDefault(); + this.currentPage = page; } - - /** - * Render a pagination item (button or anchor based on URL mode) - * @param {Object} options - Render options - * @param {Number} options.page - Target page number - * @param {String} options.label - Aria label for the item - * @param {String} options.className - Additional CSS class - * @param {TemplateResult} options.content - Content to render inside the item - * @returns {TemplateResult} Lit template for the button or anchor - */ - _renderPaginationItem({ page, label, className = '', content }) { - const url = this._getPageUrl(page); - const isCurrent = page === this.currentPage; - const ariaCurrent = isCurrent ? 'page' : 'false'; - - if (url) { - return html` + } + + /** + * Render a pagination item (button or anchor based on URL mode) + * @param {Object} options - Render options + * @param {Number} options.page - Target page number + * @param {String} options.label - Aria label for the item + * @param {String} options.className - Additional CSS class + * @param {TemplateResult} options.content - Content to render inside the item + * @returns {TemplateResult} Lit template for the button or anchor + */ + _renderPaginationItem({ page, label, className = '', content }) { + const url = this._getPageUrl(page); + const isCurrent = page === this.currentPage; + const ariaCurrent = isCurrent ? 'page' : 'false'; + + if (url) { + return html` <a href=${url} class="pagination-item ${className}" @@ -324,9 +337,9 @@ export class OlPagination extends LitElement { @click=${(e) => this._handlePageClick(e, page)} >${content}</a> `; - } + } - return html` + return html` <button class="pagination-item ${className}" aria-label=${label} @@ -334,68 +347,74 @@ export class OlPagination extends LitElement { @click=${() => this._goToPage(page)} >${content}</button> `; + } + + /** + * Render a single page button/link or ellipsis + * @param {Number|String} page - Page number or 'ellipsis-left'/'ellipsis-right' + * @returns {TemplateResult} Lit template for the button or anchor + */ + _renderPageButton(page) { + if (typeof page === 'string' && page.startsWith('ellipsis')) { + return html`<span class="ellipsis" aria-hidden="true">•••</span>`; } - /** - * Render a single page button/link or ellipsis - * @param {Number|String} page - Page number or 'ellipsis-left'/'ellipsis-right' - * @returns {TemplateResult} Lit template for the button or anchor - */ - _renderPageButton(page) { - if (typeof page === 'string' && page.startsWith('ellipsis')) { - return html`<span class="ellipsis" aria-hidden="true">•••</span>`; - } - - const isCurrent = page === this.currentPage; - const label = isCurrent - ? this._interpolateLabel(this.labelCurrentPage, { page }) - : this._interpolateLabel(this.labelGoToPage, { page }); - - return this._renderPaginationItem({ page, label, content: page }); - } - - /** - * Render a navigation arrow (previous or next) - * @param {String} direction - 'prev' or 'next' - * @returns {TemplateResult} Lit template for the arrow - */ - _renderNavArrow(direction) { - const isPrev = direction === 'prev'; - const isDisabled = isPrev - ? this.currentPage === 1 - : this.mode === 'arrows' ? !this.hasNextPage : this.currentPage === this.totalPages; - - if (isDisabled && this.mode !== 'arrows') return html``; - - if (isDisabled) { - const label = isPrev ? this.labelPreviousPage : this.labelNextPage; - const icon = isPrev ? OlPagination._leftArrowIcon : OlPagination._rightArrowIcon; - return html` + const isCurrent = page === this.currentPage; + const label = isCurrent + ? this._interpolateLabel(this.labelCurrentPage, { page }) + : this._interpolateLabel(this.labelGoToPage, { page }); + + return this._renderPaginationItem({ page, label, content: page }); + } + + /** + * Render a navigation arrow (previous or next) + * @param {String} direction - 'prev' or 'next' + * @returns {TemplateResult} Lit template for the arrow + */ + _renderNavArrow(direction) { + const isPrev = direction === 'prev'; + const isDisabled = isPrev + ? this.currentPage === 1 + : this.mode === 'arrows' + ? !this.hasNextPage + : this.currentPage === this.totalPages; + + if (isDisabled && this.mode !== 'arrows') return html``; + + if (isDisabled) { + const label = isPrev ? this.labelPreviousPage : this.labelNextPage; + const icon = isPrev + ? OlPagination._leftArrowIcon + : OlPagination._rightArrowIcon; + return html` <span class="pagination-item pagination-arrow" aria-disabled="true" aria-label=${label} >${icon}</span> `; - } - - const page = isPrev ? this.currentPage - 1 : this.currentPage + 1; - const label = isPrev ? this.labelPreviousPage : this.labelNextPage; - const icon = isPrev ? OlPagination._leftArrowIcon : OlPagination._rightArrowIcon; - - return this._renderPaginationItem({ - page, - label, - className: 'pagination-arrow', - content: icon - }); } - render() { - const isArrows = this.mode === 'arrows'; - const visiblePages = isArrows ? [] : this._getVisiblePages(); - - return html` + const page = isPrev ? this.currentPage - 1 : this.currentPage + 1; + const label = isPrev ? this.labelPreviousPage : this.labelNextPage; + const icon = isPrev + ? OlPagination._leftArrowIcon + : OlPagination._rightArrowIcon; + + return this._renderPaginationItem({ + page, + label, + className: 'pagination-arrow', + content: icon, + }); + } + + render() { + const isArrows = this.mode === 'arrows'; + const visiblePages = isArrows ? [] : this._getVisiblePages(); + + return html` <nav class="pagination" role="navigation" @@ -403,11 +422,11 @@ export class OlPagination extends LitElement { @keydown=${this._handleKeyDown} > ${this._renderNavArrow('prev')} - ${visiblePages.map(page => this._renderPageButton(page))} + ${visiblePages.map((page) => this._renderPageButton(page))} ${this._renderNavArrow('next')} </nav> `; - } + } } customElements.define('ol-pagination', OlPagination); diff --git a/openlibrary/components/lit/OlPopover.js b/openlibrary/components/lit/OlPopover.js index be7e1f05fcf..dc303035d9c 100644 --- a/openlibrary/components/lit/OlPopover.js +++ b/openlibrary/components/lit/OlPopover.js @@ -1,9 +1,10 @@ -import { LitElement, html, css, nothing } from 'lit'; +import { css, html, LitElement, nothing } from 'lit'; import { ifDefined } from 'lit/directives/if-defined.js'; let _idCounter = 0; -const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; +const FOCUSABLE = + 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; /** * A reusable popover component that anchors to a trigger element. @@ -45,24 +46,24 @@ const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), selec * </ol-popover> */ export class OlPopover extends LitElement { - static properties = { - open: { type: Boolean, reflect: true }, - placement: { type: String }, - offset: { type: Number }, - accessibleLabel: { type: String, attribute: 'accessible-label' }, - autoClose: { type: Boolean, attribute: 'auto-close' }, - _position: { state: true }, - _transformOrigin: { state: true }, - _animState: { state: true }, - _mobile: { state: true }, - }; - - // Animation states: closed → preparing → entering → open → exiting → closed - // "preparing" renders the panel in the DOM at its start position (opacity 0, - // scale 0.95) without a transition so the browser paints it. We measure the - // panel here for collision detection, then move to "entering". - - static styles = css` + static properties = { + open: { type: Boolean, reflect: true }, + placement: { type: String }, + offset: { type: Number }, + accessibleLabel: { type: String, attribute: 'accessible-label' }, + autoClose: { type: Boolean, attribute: 'auto-close' }, + _position: { state: true }, + _transformOrigin: { state: true }, + _animState: { state: true }, + _mobile: { state: true }, + }; + + // Animation states: closed → preparing → entering → open → exiting → closed + // "preparing" renders the panel in the DOM at its start position (opacity 0, + // scale 0.95) without a transition so the browser paints it. We measure the + // panel here for collision detection, then move to "entering". + + static styles = css` :host { display: inline-flex; align-items: center; @@ -237,49 +238,55 @@ export class OlPopover extends LitElement { } `; - constructor() { - super(); - this.open = false; - this.placement = 'bottom-center'; - this.offset = 4; - this.accessibleLabel = ''; - this.autoClose = true; - this._position = { top: 0, left: 0 }; - this._transformOrigin = 'top left'; - this._animState = 'closed'; - this._mobile = false; - this._panelId = `ol-popover-${++_idCounter}`; - this._prevFocus = null; - this._rafId = null; - this._savedOverflow = null; - - // Touch drag state - this._touchStartY = 0; - this._touchStartTime = 0; - this._isDragging = false; - this._isHandleDrag = false; - this._lastDragY = 0; - - this._onOutsideClick = this._onOutsideClick.bind(this); - this._onKeydownGlobal = this._onKeydownGlobal.bind(this); - this._onScrollResize = this._onScrollResize.bind(this); - this._onTouchStart = this._onTouchStart.bind(this); - this._onTouchMove = this._onTouchMove.bind(this); - this._onTouchEnd = this._onTouchEnd.bind(this); - } - - render() { - const showPanel = this._animState !== 'closed'; - return html` + constructor() { + super(); + this.open = false; + this.placement = 'bottom-center'; + this.offset = 4; + this.accessibleLabel = ''; + this.autoClose = true; + this._position = { top: 0, left: 0 }; + this._transformOrigin = 'top left'; + this._animState = 'closed'; + this._mobile = false; + this._panelId = `ol-popover-${++_idCounter}`; + this._prevFocus = null; + this._rafId = null; + this._savedOverflow = null; + + // Touch drag state + this._touchStartY = 0; + this._touchStartTime = 0; + this._isDragging = false; + this._isHandleDrag = false; + this._lastDragY = 0; + + this._onOutsideClick = this._onOutsideClick.bind(this); + this._onKeydownGlobal = this._onKeydownGlobal.bind(this); + this._onScrollResize = this._onScrollResize.bind(this); + this._onTouchStart = this._onTouchStart.bind(this); + this._onTouchMove = this._onTouchMove.bind(this); + this._onTouchEnd = this._onTouchEnd.bind(this); + } + + render() { + const showPanel = this._animState !== 'closed'; + return html` <slot name="trigger"></slot> - ${showPanel ? html` - ${this._mobile ? html` + ${ + showPanel + ? html` + ${ + this._mobile + ? html` <div class="backdrop" data-state="${this._animState}" @click="${this._onBackdropClick}" ></div> - ` : nothing} + ` + : nothing + } <div id="${this._panelId}" class="panel ${this._mobile ? 'tray' : ''}" @@ -288,11 +295,15 @@ export class OlPopover extends LitElement { aria-modal="true" aria-label="${ifDefined(this.accessibleLabel || undefined)}" tabindex="-1" - style="${this._mobile ? '' : ` + style="${ + this._mobile + ? '' + : ` top: ${this._position.top}px; left: ${this._position.left}px; transform-origin: ${this._transformOrigin}; - `}" + ` + }" @transitionend="${this._onTransitionEnd}" > <span @@ -302,11 +313,15 @@ export class OlPopover extends LitElement { data-edge="start" @focus="${this._onSentinelFocus}" ></span> - ${this._mobile ? html` + ${ + this._mobile + ? html` <div class="tray-handle" aria-hidden="true"> <div class="tray-handle-bar"></div> </div> - ` : nothing} + ` + : nothing + } <slot></slot> <span class="focus-sentinel" @@ -316,489 +331,526 @@ export class OlPopover extends LitElement { @focus="${this._onSentinelFocus}" ></span> </div> - ` : nothing} - `; - } - - firstUpdated() { - const triggerSlot = this.shadowRoot.querySelector('slot[name="trigger"]'); - triggerSlot?.addEventListener('slotchange', () => this._syncTriggerAria()); - } - - updated(changed) { - if (changed.has('open')) { - this._syncTriggerAria(); - if (this.open) { - this._show(); - } else if (changed.get('open') === true) { - this._hide(); + ` + : nothing } - } + `; + } + + firstUpdated() { + const triggerSlot = this.shadowRoot.querySelector('slot[name="trigger"]'); + triggerSlot?.addEventListener('slotchange', () => this._syncTriggerAria()); + } + + updated(changed) { + if (changed.has('open')) { + this._syncTriggerAria(); + if (this.open) { + this._show(); + } else if (changed.get('open') === true) { + this._hide(); + } } + } - // ── Show / Hide ───────────────────────────────────────────── + // ── Show / Hide ───────────────────────────────────────────── - _show() { - this._prevFocus = document.activeElement; + _show() { + this._prevFocus = document.activeElement; - document.addEventListener('click', this._onOutsideClick, true); - document.addEventListener('keydown', this._onKeydownGlobal); + document.addEventListener('click', this._onOutsideClick, true); + document.addEventListener('keydown', this._onKeydownGlobal); - this._mobile = window.matchMedia('(max-width: 767px)').matches; - const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + this._mobile = window.matchMedia('(max-width: 767px)').matches; + const reducedMotion = window.matchMedia( + '(prefers-reduced-motion: reduce)', + ).matches; - if (this._mobile) { - this._lockBodyScroll(); - } - - // On desktop, render panel off-screen first so we can measure it. - // On mobile, CSS positions the tray at the bottom automatically. - if (!this._mobile) { - this._position = { top: -9999, left: -9999 }; - } - this._animState = reducedMotion ? 'open' : 'preparing'; - - this.updateComplete.then(() => { - const panel = this.shadowRoot.querySelector('.panel'); - if (!panel) return; - - // Desktop: measure and position relative to trigger. - // Use offsetWidth/Height — getBoundingClientRect includes the - // scale(0.95) transform from the preparing state, under-reporting - // the true layout size by 5%. - if (!this._mobile) { - this._computePosition(panel.offsetWidth, panel.offsetHeight); - } - - // Add scroll/resize listeners for repositioning (desktop) - this._addScrollResizeListeners(); - - // Add touch listeners for swipe-to-dismiss (mobile) - if (this._mobile) { - panel.addEventListener('touchstart', this._onTouchStart, { passive: true }); - panel.addEventListener('touchmove', this._onTouchMove, { passive: false }); - panel.addEventListener('touchend', this._onTouchEnd, { passive: true }); - } - - // Focus the panel for screen reader context - panel.focus({ preventScroll: true }); - - if (reducedMotion) { - this.dispatchEvent(new CustomEvent('ol-popover-open', { - bubbles: true, composed: true, - detail: { placement: this.placement }, - })); - return; - } - - // Force reflow so the browser paints the start position - panel.getBoundingClientRect(); + if (this._mobile) { + this._lockBodyScroll(); + } - this._animState = 'entering'; - this.dispatchEvent(new CustomEvent('ol-popover-open', { - bubbles: true, composed: true, - detail: { placement: this.placement }, - })); + // On desktop, render panel off-screen first so we can measure it. + // On mobile, CSS positions the tray at the bottom automatically. + if (!this._mobile) { + this._position = { top: -9999, left: -9999 }; + } + this._animState = reducedMotion ? 'open' : 'preparing'; + + this.updateComplete.then(() => { + const panel = this.shadowRoot.querySelector('.panel'); + if (!panel) return; + + // Desktop: measure and position relative to trigger. + // Use offsetWidth/Height — getBoundingClientRect includes the + // scale(0.95) transform from the preparing state, under-reporting + // the true layout size by 5%. + if (!this._mobile) { + this._computePosition(panel.offsetWidth, panel.offsetHeight); + } + + // Add scroll/resize listeners for repositioning (desktop) + this._addScrollResizeListeners(); + + // Add touch listeners for swipe-to-dismiss (mobile) + if (this._mobile) { + panel.addEventListener('touchstart', this._onTouchStart, { + passive: true, + }); + panel.addEventListener('touchmove', this._onTouchMove, { + passive: false, }); + panel.addEventListener('touchend', this._onTouchEnd, { passive: true }); + } + + // Focus the panel for screen reader context + panel.focus({ preventScroll: true }); + + if (reducedMotion) { + this.dispatchEvent( + new CustomEvent('ol-popover-open', { + bubbles: true, + composed: true, + detail: { placement: this.placement }, + }), + ); + return; + } + + // Force reflow so the browser paints the start position + panel.getBoundingClientRect(); + + this._animState = 'entering'; + this.dispatchEvent( + new CustomEvent('ol-popover-open', { + bubbles: true, + composed: true, + detail: { placement: this.placement }, + }), + ); + }); + } + + _hide() { + if (this._animState === 'closed') return; + + const reducedMotion = window.matchMedia( + '(prefers-reduced-motion: reduce)', + ).matches; + if (reducedMotion) { + this._animState = 'closed'; + this._cleanup(); + return; } - _hide() { - if (this._animState === 'closed') return; + this._animState = 'exiting'; + } - const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - if (reducedMotion) { - this._animState = 'closed'; - this._cleanup(); - return; - } + _onTransitionEnd(e) { + if (e.target !== e.currentTarget) return; - this._animState = 'exiting'; + if (this._animState === 'entering') { + this._animState = 'open'; + } else if (this._animState === 'exiting') { + this._animState = 'closed'; + this._cleanup(); } - - _onTransitionEnd(e) { - if (e.target !== e.currentTarget) return; - - if (this._animState === 'entering') { - this._animState = 'open'; - } else if (this._animState === 'exiting') { - this._animState = 'closed'; - this._cleanup(); - } + } + + /** + * Central cleanup called when the popover finishes closing. + * Removes all global listeners, unlocks scroll, and restores focus. + */ + _cleanup() { + this._removeListeners(); + this._unlockBodyScroll(); + this._restoreFocus(); + } + + _restoreFocus() { + if (this._prevFocus && typeof this._prevFocus.focus === 'function') { + this._prevFocus.focus({ preventScroll: true }); } - - /** - * Central cleanup called when the popover finishes closing. - * Removes all global listeners, unlocks scroll, and restores focus. - */ - _cleanup() { - this._removeListeners(); - this._unlockBodyScroll(); - this._restoreFocus(); + this._prevFocus = null; + } + + // ── Trigger ARIA ──────────────────────────────────────────── + + _syncTriggerAria() { + const trigger = this._triggerEl; + if (!trigger) return; + trigger.setAttribute('aria-haspopup', 'dialog'); + trigger.setAttribute('aria-expanded', String(this.open)); + if (this.open) { + trigger.setAttribute('aria-controls', this._panelId); + } else { + trigger.removeAttribute('aria-controls'); } + } - _restoreFocus() { - if (this._prevFocus && typeof this._prevFocus.focus === 'function') { - this._prevFocus.focus({ preventScroll: true }); - } - this._prevFocus = null; - } + // ── Focus trap ────────────────────────────────────────────── - // ── Trigger ARIA ──────────────────────────────────────────── - - _syncTriggerAria() { - const trigger = this._triggerEl; - if (!trigger) return; - trigger.setAttribute('aria-haspopup', 'dialog'); - trigger.setAttribute('aria-expanded', String(this.open)); - if (this.open) { - trigger.setAttribute('aria-controls', this._panelId); - } else { - trigger.removeAttribute('aria-controls'); - } + _getFocusableElements() { + const slot = this.shadowRoot?.querySelector('.panel slot:not([name])'); + if (!slot) return []; + const elements = []; + for (const node of slot.assignedElements({ flatten: true })) { + if (node.matches?.(FOCUSABLE)) elements.push(node); + elements.push(...node.querySelectorAll(FOCUSABLE)); } - - // ── Focus trap ────────────────────────────────────────────── - - _getFocusableElements() { - const slot = this.shadowRoot?.querySelector('.panel slot:not([name])'); - if (!slot) return []; - const elements = []; - for (const node of slot.assignedElements({ flatten: true })) { - if (node.matches?.(FOCUSABLE)) elements.push(node); - elements.push(...node.querySelectorAll(FOCUSABLE)); - } - return elements; + return elements; + } + + _onSentinelFocus(e) { + const edge = e.target.dataset.edge; + const focusable = this._getFocusableElements(); + if (focusable.length === 0) { + // No focusable children — keep focus on the panel itself + this.shadowRoot.querySelector('.panel')?.focus({ preventScroll: true }); + return; } - - _onSentinelFocus(e) { - const edge = e.target.dataset.edge; - const focusable = this._getFocusableElements(); - if (focusable.length === 0) { - // No focusable children — keep focus on the panel itself - this.shadowRoot.querySelector('.panel')?.focus({ preventScroll: true }); - return; - } - if (edge === 'start') { - focusable[focusable.length - 1].focus({ preventScroll: true }); - } else { - focusable[0].focus({ preventScroll: true }); - } + if (edge === 'start') { + focusable[focusable.length - 1].focus({ preventScroll: true }); + } else { + focusable[0].focus({ preventScroll: true }); } - - // ── Positioning ───────────────────────────────────────────── - - /** - * Compute the final position of the popover panel, flipping and shifting - * as needed to keep it within the viewport. - */ - _computePosition(panelW, panelH) { - const trigger = this._triggerEl; - if (!trigger) return; - - const anchor = trigger.getBoundingClientRect(); - const gap = this.offset; - const viewW = window.innerWidth; - const viewH = window.innerHeight; - const pad = 8; // minimum distance from viewport edge - - // Parse requested placement - const [reqSide, reqAlign] = this._parsePlacement(this.placement); - - // Determine side (top or bottom), flipping if it would overflow - let side = reqSide; - const spaceBelow = viewH - anchor.bottom - gap; - const spaceAbove = anchor.top - gap; - - if (side === 'bottom' && panelH > spaceBelow && spaceAbove > spaceBelow) { - side = 'top'; - } else if (side === 'top' && panelH > spaceAbove && spaceBelow > spaceAbove) { - side = 'bottom'; - } - - // Vertical position - let top; - if (side === 'bottom') { - top = anchor.bottom + gap; - } else { - top = anchor.top - gap - panelH; - } - - // Horizontal position based on alignment - let left; - const anchorCenter = anchor.left + anchor.width / 2; - - switch (reqAlign) { - case 'center': - left = anchorCenter - panelW / 2; - break; - case 'end': - left = anchor.right - panelW; - break; - case 'start': - default: - left = anchor.left; - break; - } - - // Shift horizontally to keep within viewport - if (left + panelW > viewW - pad) { - left = viewW - pad - panelW; - } - if (left < pad) { - left = pad; - } - - // Shift vertically to keep within viewport - if (top + panelH > viewH - pad) { - top = viewH - pad - panelH; - } - if (top < pad) { - top = pad; - } - - // Compute transform-origin so the animation radiates from the trigger. - // The origin is expressed relative to the panel's top-left corner. - const originY = side === 'bottom' ? 'top' : 'bottom'; - - // Find where the anchor center falls within the panel horizontally - const anchorCenterInPanel = anchorCenter - left; - const originX = `${anchorCenterInPanel}px`; - - this._position = { top, left }; - this._transformOrigin = `${originX} ${originY}`; + } + + // ── Positioning ───────────────────────────────────────────── + + /** + * Compute the final position of the popover panel, flipping and shifting + * as needed to keep it within the viewport. + */ + _computePosition(panelW, panelH) { + const trigger = this._triggerEl; + if (!trigger) return; + + const anchor = trigger.getBoundingClientRect(); + const gap = this.offset; + const viewW = window.innerWidth; + const viewH = window.innerHeight; + const pad = 8; // minimum distance from viewport edge + + // Parse requested placement + const [reqSide, reqAlign] = this._parsePlacement(this.placement); + + // Determine side (top or bottom), flipping if it would overflow + let side = reqSide; + const spaceBelow = viewH - anchor.bottom - gap; + const spaceAbove = anchor.top - gap; + + if (side === 'bottom' && panelH > spaceBelow && spaceAbove > spaceBelow) { + side = 'top'; + } else if ( + side === 'top' && + panelH > spaceAbove && + spaceBelow > spaceAbove + ) { + side = 'bottom'; } - _parsePlacement(placement) { - const parts = (placement || 'bottom-center').split('-'); - const side = parts[0] === 'top' ? 'top' : 'bottom'; - const align = ['start', 'center', 'end'].includes(parts[1]) ? parts[1] : 'center'; - return [side, align]; + // Vertical position + let top; + if (side === 'bottom') { + top = anchor.bottom + gap; + } else { + top = anchor.top - gap - panelH; } - get _triggerEl() { - const slot = this.shadowRoot?.querySelector('slot[name="trigger"]'); - return slot?.assignedElements()[0] ?? null; + // Horizontal position based on alignment + let left; + const anchorCenter = anchor.left + anchor.width / 2; + + switch (reqAlign) { + case 'center': + left = anchorCenter - panelW / 2; + break; + case 'end': + left = anchor.right - panelW; + break; + case 'start': + default: + left = anchor.left; + break; } - // ── Scroll / resize repositioning ─────────────────────────── - - _addScrollResizeListeners() { - window.addEventListener('scroll', this._onScrollResize, { capture: true, passive: true }); - window.addEventListener('resize', this._onScrollResize, { passive: true }); + // Shift horizontally to keep within viewport + if (left + panelW > viewW - pad) { + left = viewW - pad - panelW; } - - _removeScrollResizeListeners() { - window.removeEventListener('scroll', this._onScrollResize, { capture: true }); - window.removeEventListener('resize', this._onScrollResize); - if (this._rafId) { - cancelAnimationFrame(this._rafId); - this._rafId = null; - } + if (left < pad) { + left = pad; } - _onScrollResize() { - if (this._rafId) return; - this._rafId = requestAnimationFrame(() => { - this._rafId = null; - if (this._mobile) return; - if (this._animState !== 'open' && this._animState !== 'entering') return; - const panel = this.shadowRoot?.querySelector('.panel'); - if (panel) { - this._computePosition(panel.offsetWidth, panel.offsetHeight); - } - }); + // Shift vertically to keep within viewport + if (top + panelH > viewH - pad) { + top = viewH - pad - panelH; } - - // ── Outside click / keyboard ──────────────────────────────── - - _onOutsideClick(e) { - if (!this.autoClose) return; - if (this._animState === 'closed' || this._animState === 'exiting') return; - const path = e.composedPath(); - if (!path.includes(this)) { - this._requestClose('outside-click'); - } + if (top < pad) { + top = pad; } - _onBackdropClick() { - if (this.autoClose) { - this._requestClose('outside-click'); - } + // Compute transform-origin so the animation radiates from the trigger. + // The origin is expressed relative to the panel's top-left corner. + const originY = side === 'bottom' ? 'top' : 'bottom'; + + // Find where the anchor center falls within the panel horizontally + const anchorCenterInPanel = anchorCenter - left; + const originX = `${anchorCenterInPanel}px`; + + this._position = { top, left }; + this._transformOrigin = `${originX} ${originY}`; + } + + _parsePlacement(placement) { + const parts = (placement || 'bottom-center').split('-'); + const side = parts[0] === 'top' ? 'top' : 'bottom'; + const align = ['start', 'center', 'end'].includes(parts[1]) + ? parts[1] + : 'center'; + return [side, align]; + } + + get _triggerEl() { + const slot = this.shadowRoot?.querySelector('slot[name="trigger"]'); + return slot?.assignedElements()[0] ?? null; + } + + // ── Scroll / resize repositioning ─────────────────────────── + + _addScrollResizeListeners() { + window.addEventListener('scroll', this._onScrollResize, { + capture: true, + passive: true, + }); + window.addEventListener('resize', this._onScrollResize, { passive: true }); + } + + _removeScrollResizeListeners() { + window.removeEventListener('scroll', this._onScrollResize, { + capture: true, + }); + window.removeEventListener('resize', this._onScrollResize); + if (this._rafId) { + cancelAnimationFrame(this._rafId); + this._rafId = null; } - - _onKeydownGlobal(e) { - if (e.key === 'Escape' && this.open) { - e.preventDefault(); - this._requestClose('escape'); - } + } + + _onScrollResize() { + if (this._rafId) return; + this._rafId = requestAnimationFrame(() => { + this._rafId = null; + if (this._mobile) return; + if (this._animState !== 'open' && this._animState !== 'entering') return; + const panel = this.shadowRoot?.querySelector('.panel'); + if (panel) { + this._computePosition(panel.offsetWidth, panel.offsetHeight); + } + }); + } + + // ── Outside click / keyboard ──────────────────────────────── + + _onOutsideClick(e) { + if (!this.autoClose) return; + if (this._animState === 'closed' || this._animState === 'exiting') return; + const path = e.composedPath(); + if (!path.includes(this)) { + this._requestClose('outside-click'); } + } - _requestClose(reason) { - this.dispatchEvent(new CustomEvent('ol-popover-close', { - bubbles: true, composed: true, - detail: { reason }, - })); + _onBackdropClick() { + if (this.autoClose) { + this._requestClose('outside-click'); } + } - // ── Mobile touch / swipe-to-dismiss ───────────────────────── - - _onTouchStart(e) { - const handle = this.shadowRoot.querySelector('.tray-handle'); - const panel = this.shadowRoot.querySelector('.panel'); - const touch = e.touches[0]; - - this._touchStartY = touch.clientY; - this._touchStartTime = Date.now(); - this._isDragging = false; - this._lastDragY = 0; - this._isHandleDrag = !!(handle && e.composedPath().includes(handle)); - this._touchScrollTop = panel?.scrollTop ?? 0; + _onKeydownGlobal(e) { + if (e.key === 'Escape' && this.open) { + e.preventDefault(); + this._requestClose('escape'); } - - _onTouchMove(e) { - const touch = e.touches[0]; - const deltaY = touch.clientY - this._touchStartY; - - if (!this._isDragging) { - // Start drag if touching handle, or at scroll-top and swiping down - if (this._isHandleDrag || (this._touchScrollTop <= 0 && deltaY > 5)) { - this._isDragging = true; - } else { - return; // Let normal scroll happen - } - } - - const dragY = Math.max(0, deltaY); - this._lastDragY = dragY; - e.preventDefault(); - - const panel = this.shadowRoot.querySelector('.panel'); - if (panel) { - panel.style.transform = `translateY(${dragY}px)`; - panel.style.transition = 'none'; - } - - const backdrop = this.shadowRoot.querySelector('.backdrop'); - if (backdrop) { - const progress = Math.min(dragY / 300, 1); - backdrop.style.opacity = String(1 - progress); - backdrop.style.transition = 'none'; - } + } + + _requestClose(reason) { + this.dispatchEvent( + new CustomEvent('ol-popover-close', { + bubbles: true, + composed: true, + detail: { reason }, + }), + ); + } + + // ── Mobile touch / swipe-to-dismiss ───────────────────────── + + _onTouchStart(e) { + const handle = this.shadowRoot.querySelector('.tray-handle'); + const panel = this.shadowRoot.querySelector('.panel'); + const touch = e.touches[0]; + + this._touchStartY = touch.clientY; + this._touchStartTime = Date.now(); + this._isDragging = false; + this._lastDragY = 0; + this._isHandleDrag = !!(handle && e.composedPath().includes(handle)); + this._touchScrollTop = panel?.scrollTop ?? 0; + } + + _onTouchMove(e) { + const touch = e.touches[0]; + const deltaY = touch.clientY - this._touchStartY; + + if (!this._isDragging) { + // Start drag if touching handle, or at scroll-top and swiping down + if (this._isHandleDrag || (this._touchScrollTop <= 0 && deltaY > 5)) { + this._isDragging = true; + } else { + return; // Let normal scroll happen + } } - _onTouchEnd() { - if (!this._isDragging) return; + const dragY = Math.max(0, deltaY); + this._lastDragY = dragY; + e.preventDefault(); - const dragY = this._lastDragY; - const elapsed = Date.now() - this._touchStartTime; - const velocity = dragY / Math.max(elapsed, 1); - - this._isDragging = false; - this._lastDragY = 0; - - const panel = this.shadowRoot.querySelector('.panel'); - const backdrop = this.shadowRoot.querySelector('.backdrop'); - - const DISMISS_THRESHOLD = 80; - const VELOCITY_THRESHOLD = 0.5; - - if (dragY > DISMISS_THRESHOLD || velocity > VELOCITY_THRESHOLD) { - // Swipe dismiss — animate to off-screen, then close - if (panel) { - panel.style.transition = 'transform 200ms cubic-bezier(0.23, 1, 0.32, 1)'; - panel.style.transform = 'translateY(100%)'; - } - if (backdrop) { - backdrop.style.transition = 'opacity 200ms cubic-bezier(0.23, 1, 0.32, 1)'; - backdrop.style.opacity = '0'; - } - - const onDone = () => { - panel?.removeEventListener('transitionend', onDone); - this._clearDragStyles(); - this._animState = 'closed'; - this._cleanup(); - this.dispatchEvent(new CustomEvent('ol-popover-close', { - bubbles: true, composed: true, - detail: { reason: 'swipe' }, - })); - }; - - if (panel) { - panel.addEventListener('transitionend', onDone, { once: true }); - } else { - onDone(); - } - } else { - // Snap back to open position - if (panel) { - panel.style.transition = 'transform 200ms cubic-bezier(0.23, 1, 0.32, 1)'; - panel.style.transform = ''; - } - if (backdrop) { - backdrop.style.transition = 'opacity 200ms cubic-bezier(0.23, 1, 0.32, 1)'; - backdrop.style.opacity = ''; - } - - const onDone = () => { - panel?.removeEventListener('transitionend', onDone); - this._clearDragStyles(); - }; - - if (panel) { - panel.addEventListener('transitionend', onDone, { once: true }); - } - } + const panel = this.shadowRoot.querySelector('.panel'); + if (panel) { + panel.style.transform = `translateY(${dragY}px)`; + panel.style.transition = 'none'; } - _clearDragStyles() { - const panel = this.shadowRoot?.querySelector('.panel'); - const backdrop = this.shadowRoot?.querySelector('.backdrop'); - if (panel) { - panel.style.transition = ''; - panel.style.transform = ''; - } - if (backdrop) { - backdrop.style.transition = ''; - backdrop.style.opacity = ''; - } + const backdrop = this.shadowRoot.querySelector('.backdrop'); + if (backdrop) { + const progress = Math.min(dragY / 300, 1); + backdrop.style.opacity = String(1 - progress); + backdrop.style.transition = 'none'; + } + } + + _onTouchEnd() { + if (!this._isDragging) return; + + const dragY = this._lastDragY; + const elapsed = Date.now() - this._touchStartTime; + const velocity = dragY / Math.max(elapsed, 1); + + this._isDragging = false; + this._lastDragY = 0; + + const panel = this.shadowRoot.querySelector('.panel'); + const backdrop = this.shadowRoot.querySelector('.backdrop'); + + const DISMISS_THRESHOLD = 80; + const VELOCITY_THRESHOLD = 0.5; + + if (dragY > DISMISS_THRESHOLD || velocity > VELOCITY_THRESHOLD) { + // Swipe dismiss — animate to off-screen, then close + if (panel) { + panel.style.transition = + 'transform 200ms cubic-bezier(0.23, 1, 0.32, 1)'; + panel.style.transform = 'translateY(100%)'; + } + if (backdrop) { + backdrop.style.transition = + 'opacity 200ms cubic-bezier(0.23, 1, 0.32, 1)'; + backdrop.style.opacity = '0'; + } + + const onDone = () => { + panel?.removeEventListener('transitionend', onDone); + this._clearDragStyles(); + this._animState = 'closed'; + this._cleanup(); + this.dispatchEvent( + new CustomEvent('ol-popover-close', { + bubbles: true, + composed: true, + detail: { reason: 'swipe' }, + }), + ); + }; + + if (panel) { + panel.addEventListener('transitionend', onDone, { once: true }); + } else { + onDone(); + } + } else { + // Snap back to open position + if (panel) { + panel.style.transition = + 'transform 200ms cubic-bezier(0.23, 1, 0.32, 1)'; + panel.style.transform = ''; + } + if (backdrop) { + backdrop.style.transition = + 'opacity 200ms cubic-bezier(0.23, 1, 0.32, 1)'; + backdrop.style.opacity = ''; + } + + const onDone = () => { + panel?.removeEventListener('transitionend', onDone); + this._clearDragStyles(); + }; + + if (panel) { + panel.addEventListener('transitionend', onDone, { once: true }); + } } + } + + _clearDragStyles() { + const panel = this.shadowRoot?.querySelector('.panel'); + const backdrop = this.shadowRoot?.querySelector('.backdrop'); + if (panel) { + panel.style.transition = ''; + panel.style.transform = ''; + } + if (backdrop) { + backdrop.style.transition = ''; + backdrop.style.opacity = ''; + } + } - // ── Body scroll lock ──────────────────────────────────────── + // ── Body scroll lock ──────────────────────────────────────── - _lockBodyScroll() { - this._savedOverflow = document.body.style.overflow; - document.body.style.overflow = 'hidden'; - } + _lockBodyScroll() { + this._savedOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + } - _unlockBodyScroll() { - if (this._savedOverflow !== null) { - document.body.style.overflow = this._savedOverflow; - this._savedOverflow = null; - } + _unlockBodyScroll() { + if (this._savedOverflow !== null) { + document.body.style.overflow = this._savedOverflow; + this._savedOverflow = null; } + } - // ── Listener management ───────────────────────────────────── + // ── Listener management ───────────────────────────────────── - _removeListeners() { - document.removeEventListener('click', this._onOutsideClick, true); - document.removeEventListener('keydown', this._onKeydownGlobal); - this._removeScrollResizeListeners(); + _removeListeners() { + document.removeEventListener('click', this._onOutsideClick, true); + document.removeEventListener('keydown', this._onKeydownGlobal); + this._removeScrollResizeListeners(); - // Remove touch listeners from panel - const panel = this.shadowRoot?.querySelector('.panel'); - if (panel) { - panel.removeEventListener('touchstart', this._onTouchStart); - panel.removeEventListener('touchmove', this._onTouchMove); - panel.removeEventListener('touchend', this._onTouchEnd); - } + // Remove touch listeners from panel + const panel = this.shadowRoot?.querySelector('.panel'); + if (panel) { + panel.removeEventListener('touchstart', this._onTouchStart); + panel.removeEventListener('touchmove', this._onTouchMove); + panel.removeEventListener('touchend', this._onTouchEnd); } + } - disconnectedCallback() { - super.disconnectedCallback(); - this._removeListeners(); - this._unlockBodyScroll(); - } + disconnectedCallback() { + super.disconnectedCallback(); + this._removeListeners(); + this._unlockBodyScroll(); + } } customElements.define('ol-popover', OlPopover); diff --git a/openlibrary/components/lit/index.js b/openlibrary/components/lit/index.js index 7ce73a4dd3e..079ca16b678 100644 --- a/openlibrary/components/lit/index.js +++ b/openlibrary/components/lit/index.js @@ -5,10 +5,9 @@ * Components are bundled together via Vite for production use. */ +export { OLChip } from './OLChip.js'; +export { OLChipGroup } from './OLChipGroup.js'; // Export components (importing also registers them as custom elements) export { OLReadMore } from './OLReadMore.js'; export { OlPagination } from './OlPagination.js'; export { OlPopover } from './OlPopover.js'; -export { OLChip } from './OLChip.js'; -export { OLChipGroup } from './OLChipGroup.js'; - diff --git a/openlibrary/components/rollupInputCore.js b/openlibrary/components/rollupInputCore.js index 169a3481bd4..da169be020d 100644 --- a/openlibrary/components/rollupInputCore.js +++ b/openlibrary/components/rollupInputCore.js @@ -1,20 +1,20 @@ +import { kebabCase } from 'lodash'; import { defineCustomElement } from 'vue'; import AsyncComputed from 'vue-async-computed'; -import { kebabCase } from 'lodash'; export const createWebComponentSimple = (rootComponent, name) => { - // This is the name we use in the DOM like: <ol-barcode-scanner></ol-barcode-scanner> - const elementName = `ol-${kebabCase(name)}`; + // This is the name we use in the DOM like: <ol-barcode-scanner></ol-barcode-scanner> + const elementName = `ol-${kebabCase(name)}`; - const WebComponent = defineCustomElement(rootComponent, { - configureApp(app) { - if (elementName === 'ol-merge-ui') { - app.use(AsyncComputed); - } - }, - }); + const WebComponent = defineCustomElement(rootComponent, { + configureApp(app) { + if (elementName === 'ol-merge-ui') { + app.use(AsyncComputed); + } + }, + }); - if (!customElements.get(elementName)) { - customElements.define(elementName, WebComponent); - } + if (!customElements.get(elementName)) { + customElements.define(elementName, WebComponent); + } }; diff --git a/openlibrary/components/vite-lit.config.mjs b/openlibrary/components/vite-lit.config.mjs index 6b83eb88d11..585f8f70a6d 100644 --- a/openlibrary/components/vite-lit.config.mjs +++ b/openlibrary/components/vite-lit.config.mjs @@ -10,45 +10,44 @@ * - ol-components-legacy.js (transpiled for older browsers) */ -import { defineConfig } from 'vite'; import legacy from '@vitejs/plugin-legacy'; import { join } from 'path'; +import { defineConfig } from 'vite'; const BUILD_DIR = process.env.BUILD_DIR || 'static/build/components'; export default defineConfig({ - plugins: [ - // Provides legacy browser support - // Creates both modern and legacy builds - legacy({ - targets: ['defaults', 'not IE 11'], - // Generate polyfills for older browsers - modernPolyfills: true - }) - ], - build: { - // Output directory for built files - outDir: join(BUILD_DIR, '/production'), - - // Rollup-specific options - rollupOptions: { - input: { - 'ol-components': 'openlibrary/components/lit/index.js' - }, - output: { - // Output filename pattern - entryFileNames: '[name].js', - - // Ensure we're building for browsers, not Node.js - format: 'es' - } - }, - - // Minify the output - minify: 'terser', - - // Generate source maps for debugging - sourcemap: true - } + plugins: [ + // Provides legacy browser support + // Creates both modern and legacy builds + legacy({ + targets: ['defaults', 'not IE 11'], + // Generate polyfills for older browsers + modernPolyfills: true, + }), + ], + build: { + // Output directory for built files + outDir: join(BUILD_DIR, '/production'), + + // Rollup-specific options + rollupOptions: { + input: { + 'ol-components': 'openlibrary/components/lit/index.js', + }, + output: { + // Output filename pattern + entryFileNames: '[name].js', + + // Ensure we're building for browsers, not Node.js + format: 'es', + }, + }, + + // Minify the output + minify: 'terser', + + // Generate source maps for debugging + sourcemap: true, + }, }); - diff --git a/openlibrary/components/vite.config.mjs b/openlibrary/components/vite.config.mjs index eeb6d17fa12..55900b98750 100644 --- a/openlibrary/components/vite.config.mjs +++ b/openlibrary/components/vite.config.mjs @@ -2,36 +2,38 @@ /** * Vite config for Vue components. (for Lit components see vite-lit.config.mjs) */ -import { defineConfig } from 'vite'; -import vue from '@vitejs/plugin-vue'; + import legacy from '@vitejs/plugin-legacy'; -import { writeFileSync, readdirSync } from 'fs'; +import vue from '@vitejs/plugin-vue'; +import { readdirSync, writeFileSync } from 'fs'; import { join } from 'path'; - +import { defineConfig } from 'vite'; const BUILD_DIR = process.env.BUILD_DIR || 'static/build/components'; const componentNames = getComponentNames(); -componentNames.forEach(generateViteEntryFile) +componentNames.forEach(generateViteEntryFile); const buildInput = {}; -componentNames.forEach(name => { buildInput[name] = getTemporaryVueInputPath(name) }); +componentNames.forEach((name) => { + buildInput[name] = getTemporaryVueInputPath(name); +}); export default defineConfig({ - plugins: [ - vue({ customElement: true }), - legacy({ targets: ['defaults', 'not IE 11'] }) - ], - build: { - outDir: join(BUILD_DIR, '/production'), - rollupOptions: { - input: buildInput, - output: { - entryFileNames: 'ol-[name].js', - format: 'es' // for browser only builds (not NodeJS) - }, - }, + plugins: [ + vue({ customElement: true }), + legacy({ targets: ['defaults', 'not IE 11'] }), + ], + build: { + outDir: join(BUILD_DIR, '/production'), + rollupOptions: { + input: buildInput, + output: { + entryFileNames: 'ol-[name].js', + format: 'es', // for browser only builds (not NodeJS) + }, }, + }, }); /** @@ -42,12 +44,14 @@ export default defineConfig({ * @returns {string[]} An array of component names, e.g., ['BarcodeScanner', 'BulkSearch']. */ function getComponentNames() { - const files = readdirSync('./openlibrary/components'); - return files.filter(name => name.includes('.vue')).map(name => name.replace('.vue', '')); + const files = readdirSync('./openlibrary/components'); + return files + .filter((name) => name.includes('.vue')) + .map((name) => name.replace('.vue', '')); } function getTemporaryVueInputPath(componentName) { - return join(BUILD_DIR, `tmp-${componentName}.js`) + return join(BUILD_DIR, `tmp-${componentName}.js`); } /** @@ -56,17 +60,17 @@ function getTemporaryVueInputPath(componentName) { * @throws {Error} If file creation fails */ function generateViteEntryFile(componentName) { - const componentsPath = '../../../openlibrary/components'; - const template = ` + const componentsPath = '../../../openlibrary/components'; + const template = ` import { createWebComponentSimple } from '${componentsPath}/rollupInputCore.js'; import rootComponent from '${componentsPath}/${componentName}.vue'; createWebComponentSimple(rootComponent, '${componentName}');`; - try { - writeFileSync(getTemporaryVueInputPath(componentName), template); - } catch (error) { - // eslint-disable-next-line no-console - console.error(`Failed to generate Vite entry file: ${error.message}`); - process.exit(1); - } + try { + writeFileSync(getTemporaryVueInputPath(componentName), template); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to generate Vite entry file: ${error.message}`); + process.exit(1); + } } diff --git a/openlibrary/plugins/openlibrary/js/Browser.js b/openlibrary/plugins/openlibrary/js/Browser.js index 5827d28af9c..41d464b914d 100644 --- a/openlibrary/plugins/openlibrary/js/Browser.js +++ b/openlibrary/plugins/openlibrary/js/Browser.js @@ -6,15 +6,15 @@ * @returns {UrlParams} */ export function getJsonFromUrl(urlSearch) { - const query = urlSearch.substr(1); - const result = {}; - if (query) { - query.split('&').forEach(part => { - const item = part.split('='); - result[item[0]] = decodeURIComponent(item[1]); - }); - } - return result; + const query = urlSearch.substr(1); + const result = {}; + if (query) { + query.split('&').forEach((part) => { + const item = part.split('='); + result[item[0]] = decodeURIComponent(item[1]); + }); + } + return result; } /** @@ -23,25 +23,25 @@ export function getJsonFromUrl(urlSearch) { * @returns {String} */ export function removeURLParameter(url, parameter) { - var urlparts = url.split('?'); - var prefix = urlparts[0]; - var query, paramPrefix, params, i; - if (urlparts.length >= 2) { - query = urlparts[1]; - paramPrefix = `${encodeURIComponent(parameter)}=`; - params = query.split(/[&;]/g); - - //reverse iteration as may be destructive - for (i = params.length; i-- > 0;) { - //idiom for string.startsWith - if (params[i].lastIndexOf(paramPrefix, 0) !== -1) { - params.splice(i, 1); - } - } + var urlparts = url.split('?'); + var prefix = urlparts[0]; + var query, paramPrefix, params, i; + if (urlparts.length >= 2) { + query = urlparts[1]; + paramPrefix = `${encodeURIComponent(parameter)}=`; + params = query.split(/[&;]/g); - url = prefix + (params.length > 0 ? `?${params.join('&')}` : ''); - return url; - } else { - return url; + //reverse iteration as may be destructive + for (i = params.length; i-- > 0; ) { + //idiom for string.startsWith + if (params[i].lastIndexOf(paramPrefix, 0) !== -1) { + params.splice(i, 1); + } } + + url = prefix + (params.length > 0 ? `?${params.join('&')}` : ''); + return url; + } else { + return url; + } } diff --git a/openlibrary/plugins/openlibrary/js/SearchBar.js b/openlibrary/plugins/openlibrary/js/SearchBar.js index cc32927ef19..c7dcbbc831d 100644 --- a/openlibrary/plugins/openlibrary/js/SearchBar.js +++ b/openlibrary/plugins/openlibrary/js/SearchBar.js @@ -1,40 +1,40 @@ +import $ from 'jquery'; +import { websafe } from './jsdef'; import { debounce } from './nonjquery_utils.js'; import * as SearchUtils from './SearchUtils'; import { PersistentValue } from './SearchUtils'; -import $ from 'jquery'; -import { websafe } from './jsdef' /** Mapping of search bar facets to search endpoints */ const FACET_TO_ENDPOINT = { - title: '/search', - author: '/search/authors', - lists: '/search/lists', - subject: '/search/subjects', - all: '/search', - text: '/search/inside', + title: '/search', + author: '/search/authors', + lists: '/search/lists', + subject: '/search/subjects', + all: '/search', + text: '/search/inside', }; const DEFAULT_FACET = 'all'; const DEFAULT_JSON_FIELDS = [ - 'key', - 'cover_i', - 'title', - 'subtitle', - 'author_name', - 'editions', - // This is for authors autocomplete; we mix them all up here for simplicity - 'name', + 'key', + 'cover_i', + 'title', + 'subtitle', + 'author_name', + 'editions', + // This is for authors autocomplete; we mix them all up here for simplicity + 'name', ]; /** Functions that render autocomplete results */ const RENDER_AUTOCOMPLETE_RESULT = { - ['/search'](work) { - const book = work.editions?.docs?.[0] || work; - const author_name = work.author_name ? work.author_name[0] : ''; - // See _get_safepath_re in openlibrary/core/helpers.py - let link = `${work.key}/${encodeURIComponent(work.title.replace(/[;/?:@&=+$,\s<>#%"{}|\\^[\]`]+/g, '_'))}`; - if (book !== work) { - link += `?edition=key:${book.key}`; - } - return ` + ['/search'](work) { + const book = work.editions?.docs?.[0] || work; + const author_name = work.author_name ? work.author_name[0] : ''; + // See _get_safepath_re in openlibrary/core/helpers.py + let link = `${work.key}/${encodeURIComponent(work.title.replace(/[;/?:@&=+$,\s<>#%"{}|\\^[\]`]+/g, '_'))}`; + if (book !== work) { + link += `?edition=key:${book.key}`; + } + return ` <li tabindex=0> <a href="${link}"> <img @@ -49,9 +49,9 @@ const RENDER_AUTOCOMPLETE_RESULT = { </span> </a> </li>`; - }, - ['/search/authors'](author) { - return ` + }, + ['/search/authors'](author) { + return ` <li> <a href="/authors/${author.key}"> <img @@ -62,372 +62,416 @@ const RENDER_AUTOCOMPLETE_RESULT = { <span class="author-desc"><div class="author-name">${websafe(author.name)}</div></span> </a> </li>`; - } -} + }, +}; /** * Manages the interactions associated with the search bar in the header */ export class SearchBar { - /** - * @param {JQuery} $component - * @param {Object?} urlParams - */ - constructor($component, urlParams={}) { - /** UI Elements */ - this.$component = $component; - this.$form = this.$component.find('form.search-bar-input'); - this.$input = this.$form.find('input[type="text"]'); - this.$results = this.$component.find('ul.search-results'); - this.$facetSelect = this.$component.find('.search-facet-selector select'); - this.$barcodeScanner = this.$component.find('#barcode_scanner_link'); - this.$searchSubmit = this.$component.find('.search-bar-submit') - - /** State */ - /** Whether the bar is in collapsible mode */ - this.inCollapsibleMode = false; - /** Whether the search bar is currently collapsed */ - this.collapsed = false; - /** Selected facet (persisted) */ - this.facet = new PersistentValue('facet', { - default: DEFAULT_FACET, - initValidation(val) { return val in FACET_TO_ENDPOINT; } - }); - - this.initFromUrlParams(urlParams); - this.initCollapsibleMode(); - // Stop renderAutoCompletionResults from firing when ESC is pressed in results list - this.escapeInput = false; - - // Bind to changes in the search state - SearchUtils.mode.sync(this.handleSearchModeChange.bind(this)); - this.facet.sync(this.handleFacetValueChange.bind(this)); - this.$facetSelect.on('change', this.handleFacetSelectChange.bind(this)); - this.$form.on('submit', this.submitForm.bind(this)); - - // Shift + Tabbing out of the search facet to clear results list - this.$facetSelect.on('keydown', (e) => { - if (e.key === 'Tab' && e.shiftKey) { - this.clearAutocompletionResults(); - } - }) - - this.$input.on('keydown', (e) => { - if (e.key === 'ArrowUp') { - this.$results.children().last().trigger('focus'); - return false; - } else if (e.key === 'ArrowDown') { - this.$results.children().first().trigger('focus'); - return false; - } else if (e.key === 'Escape') { - this.clearAutocompletionResults(); - } - }) - - this.$barcodeScanner.on('keydown', (e) => { - if (e.key === 'Tab') { - this.clearAutocompletionResults(); - } - }) - - this.$results.on('keydown', (e) => { - if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { - // On arrow keys focus on the next item unless there is none, then focus on input - const direction = e.key === 'ArrowUp' ? 'previousElementSibling' : 'nextElementSibling'; - if (!e.target[direction]) { - this.$input.trigger('focus'); - return false; - } else { - $(e.target[direction]).trigger('focus'); - return false; - } - } else if (e.key === 'Tab') { - // On tab, always go to the next selector (instead of next result), like wikipedia - this.clearAutocompletionResults(); - if (e.shiftKey) { - this.$facetSelect.trigger('focus'); - return false; - } else { - this.$searchSubmit.trigger('focus'); - return false; - } - } else if (e.key === 'Enter') { - e.target.firstElementChild.click(); - } else if (e.key === 'Escape') { - this.$input.trigger('focus'); - this.escapeInput = true; - this.clearAutocompletionResults(); - } - }) - - this.$form.on('keydown', (e) => { - if (e.key === 'Tab') { - this.clearAutocompletionResults(); - } - }); - - this.initAutocompletionLogic(); - } - - /** @type {String} The endpoint of the active facet */ - get facetEndpoint() { - return FACET_TO_ENDPOINT[this.facet.read()]; - } - - /** - * Update internal state from url parameters - * @param {Object} urlParams - */ - initFromUrlParams(urlParams) { - if (urlParams.facet in FACET_TO_ENDPOINT) { - this.facet.write(urlParams.facet); - } - - if (urlParams.q && this.getCurUrl().pathname.match(/^\/search/)) { - let q = urlParams.q.replace(/\+/g, ' '); - if (this.facet.read() === 'title' && q.indexOf('title:') !== -1) { - const parts = q.split('"'); - if (parts.length === 3) { - q = parts[1]; - } - } - this.$input.val(q); - } - } - - submitForm() { - if (this.facet.read() === 'title') { - const q = this.$input.val(); - this.$input.val(SearchBar.marshalBookSearchQuery(q)); - } - this.$form.attr('action', SearchBar.composeSearchUrl(this.facetEndpoint, this.$input.val())); - SearchUtils.addModeInputsToForm(this.$form, SearchUtils.mode.read()); - } - - /** Initialize event handlers that allow the form to collapse for small screens */ - initCollapsibleMode() { - this.toggleCollapsibleModeForSmallScreens($(window).width()); - $(window).on('resize', debounce(() => { - this.toggleCollapsibleModeForSmallScreens($(window).width()); - }, 50)); - - const expandAndFocusSearch = (event) => { - if (this.inCollapsibleMode && this.collapsed) { - event.preventDefault(); - this.toggleCollapse(); - this.$input.trigger('focus'); - } - } - const expandSelectors = ['.search-component', 'a[href="/search"]']; - - // When clicking on the search bar or a link to /search, expand search if it isn't already. - // If clicking elsewhere, collapse search. - $(document).on('submit', '.in-collapsible-mode', event => expandAndFocusSearch(event)); - $(document).on('click', event => { - const shouldExpand = (item) => $(event.target).closest(item).length === 1; - if (expandSelectors.some(shouldExpand)) { - expandAndFocusSearch(event); - } else { - if (!this.collapsed) this.toggleCollapse(); - } - }); - } - - /** - * Enables/disables CollapsibleMode depending on screen size - * @param {Number} windowWidth - */ - toggleCollapsibleModeForSmallScreens(windowWidth) { - if (windowWidth < 568) { - if (!this.inCollapsibleMode) { - this.enableCollapsibleMode(); - this.collapse(); - } - this.clearAutocompletionResults(); + /** + * @param {JQuery} $component + * @param {Object?} urlParams + */ + constructor($component, urlParams = {}) { + /** UI Elements */ + this.$component = $component; + this.$form = this.$component.find('form.search-bar-input'); + this.$input = this.$form.find('input[type="text"]'); + this.$results = this.$component.find('ul.search-results'); + this.$facetSelect = this.$component.find('.search-facet-selector select'); + this.$barcodeScanner = this.$component.find('#barcode_scanner_link'); + this.$searchSubmit = this.$component.find('.search-bar-submit'); + + /** State */ + /** Whether the bar is in collapsible mode */ + this.inCollapsibleMode = false; + /** Whether the search bar is currently collapsed */ + this.collapsed = false; + /** Selected facet (persisted) */ + this.facet = new PersistentValue('facet', { + default: DEFAULT_FACET, + initValidation(val) { + return val in FACET_TO_ENDPOINT; + }, + }); + + this.initFromUrlParams(urlParams); + this.initCollapsibleMode(); + // Stop renderAutoCompletionResults from firing when ESC is pressed in results list + this.escapeInput = false; + + // Bind to changes in the search state + SearchUtils.mode.sync(this.handleSearchModeChange.bind(this)); + this.facet.sync(this.handleFacetValueChange.bind(this)); + this.$facetSelect.on('change', this.handleFacetSelectChange.bind(this)); + this.$form.on('submit', this.submitForm.bind(this)); + + // Shift + Tabbing out of the search facet to clear results list + this.$facetSelect.on('keydown', (e) => { + if (e.key === 'Tab' && e.shiftKey) { + this.clearAutocompletionResults(); + } + }); + + this.$input.on('keydown', (e) => { + if (e.key === 'ArrowUp') { + this.$results.children().last().trigger('focus'); + return false; + } else if (e.key === 'ArrowDown') { + this.$results.children().first().trigger('focus'); + return false; + } else if (e.key === 'Escape') { + this.clearAutocompletionResults(); + } + }); + + this.$barcodeScanner.on('keydown', (e) => { + if (e.key === 'Tab') { + this.clearAutocompletionResults(); + } + }); + + this.$results.on('keydown', (e) => { + if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { + // On arrow keys focus on the next item unless there is none, then focus on input + const direction = + e.key === 'ArrowUp' ? 'previousElementSibling' : 'nextElementSibling'; + if (!e.target[direction]) { + this.$input.trigger('focus'); + return false; } else { - if (this.inCollapsibleMode) { - this.disableCollapsibleMode(); - } + $(e.target[direction]).trigger('focus'); + return false; } - } - - /** Collapses or expands the searchbar */ - toggleCollapse() { - if (this.collapsed) { - this.expand(); + } else if (e.key === 'Tab') { + // On tab, always go to the next selector (instead of next result), like wikipedia + this.clearAutocompletionResults(); + if (e.shiftKey) { + this.$facetSelect.trigger('focus'); + return false; } else { - this.collapse(); + this.$searchSubmit.trigger('focus'); + return false; } + } else if (e.key === 'Enter') { + e.target.firstElementChild.click(); + } else if (e.key === 'Escape') { + this.$input.trigger('focus'); + this.escapeInput = true; + this.clearAutocompletionResults(); + } + }); + + this.$form.on('keydown', (e) => { + if (e.key === 'Tab') { + this.clearAutocompletionResults(); + } + }); + + this.initAutocompletionLogic(); + } + + /** @type {String} The endpoint of the active facet */ + get facetEndpoint() { + return FACET_TO_ENDPOINT[this.facet.read()]; + } + + /** + * Update internal state from url parameters + * @param {Object} urlParams + */ + initFromUrlParams(urlParams) { + if (urlParams.facet in FACET_TO_ENDPOINT) { + this.facet.write(urlParams.facet); } - collapse() { - $('header#header-bar .logo-component').removeClass('hidden'); - this.$component.removeClass('expanded'); - this.collapsed = true; - } - - expand() { - $('header#header-bar .logo-component').addClass('hidden'); - this.$component.addClass('expanded'); - this.collapsed = false; + if (urlParams.q && this.getCurUrl().pathname.match(/^\/search/)) { + let q = urlParams.q.replace(/\+/g, ' '); + if (this.facet.read() === 'title' && q.indexOf('title:') !== -1) { + const parts = q.split('"'); + if (parts.length === 3) { + q = parts[1]; + } + } + this.$input.val(q); } + } - enableCollapsibleMode() { - this.$form.addClass('in-collapsible-mode'); - this.inCollapsibleMode = true; + submitForm() { + if (this.facet.read() === 'title') { + const q = this.$input.val(); + this.$input.val(SearchBar.marshalBookSearchQuery(q)); } - - disableCollapsibleMode() { + this.$form.attr( + 'action', + SearchBar.composeSearchUrl(this.facetEndpoint, this.$input.val()), + ); + SearchUtils.addModeInputsToForm(this.$form, SearchUtils.mode.read()); + } + + /** Initialize event handlers that allow the form to collapse for small screens */ + initCollapsibleMode() { + this.toggleCollapsibleModeForSmallScreens($(window).width()); + $(window).on( + 'resize', + debounce(() => { + this.toggleCollapsibleModeForSmallScreens($(window).width()); + }, 50), + ); + + const expandAndFocusSearch = (event) => { + if (this.inCollapsibleMode && this.collapsed) { + event.preventDefault(); + this.toggleCollapse(); + this.$input.trigger('focus'); + } + }; + const expandSelectors = ['.search-component', 'a[href="/search"]']; + + // When clicking on the search bar or a link to /search, expand search if it isn't already. + // If clicking elsewhere, collapse search. + $(document).on('submit', '.in-collapsible-mode', (event) => + expandAndFocusSearch(event), + ); + $(document).on('click', (event) => { + const shouldExpand = (item) => $(event.target).closest(item).length === 1; + if (expandSelectors.some(shouldExpand)) { + expandAndFocusSearch(event); + } else { + if (!this.collapsed) this.toggleCollapse(); + } + }); + } + + /** + * Enables/disables CollapsibleMode depending on screen size + * @param {Number} windowWidth + */ + toggleCollapsibleModeForSmallScreens(windowWidth) { + if (windowWidth < 568) { + if (!this.inCollapsibleMode) { + this.enableCollapsibleMode(); this.collapse(); - this.$form.removeClass('in-collapsible-mode'); - this.inCollapsibleMode = false; + } + this.clearAutocompletionResults(); + } else { + if (this.inCollapsibleMode) { + this.disableCollapsibleMode(); + } } - - /** - * Converts an already processed query into a search url - * @param {String} facetEndpoint - * @param {String} q query that's ready to get passed to the search endpoint - * @param {Boolean} [json] whether to hit the JSON endpoint - * @param {Number} [limit] how many items to get - * @param {String[]} [fields] the Solr fields to fetch (if using JSON) - */ - static composeSearchUrl(facetEndpoint, q, json=false, limit=null, fields=null) { - let url = facetEndpoint; - if (json) { - url += `.json?q=${q}&_spellcheck_count=0`; - } else { - url += `?q=${q}`; - } - - if (limit) url += `&limit=${limit}`; - if (fields) url += `&fields=${fields.map(encodeURIComponent).join(',')}`; - url += `&mode=${SearchUtils.mode.read()}`; - return url; + } + + /** Collapses or expands the searchbar */ + toggleCollapse() { + if (this.collapsed) { + this.expand(); + } else { + this.collapse(); } - - /** - * Prepare an unprocessed query for book searching - * @param {String} q - * @return {String} - */ - static marshalBookSearchQuery(q) { - if (q && q.indexOf(':') === -1 && q.indexOf('"') === -1) { - q = `title: "${q}"`; - } - return q; + } + + collapse() { + $('header#header-bar .logo-component').removeClass('hidden'); + this.$component.removeClass('expanded'); + this.collapsed = true; + } + + expand() { + $('header#header-bar .logo-component').addClass('hidden'); + this.$component.addClass('expanded'); + this.collapsed = false; + } + + enableCollapsibleMode() { + this.$form.addClass('in-collapsible-mode'); + this.inCollapsibleMode = true; + } + + disableCollapsibleMode() { + this.collapse(); + this.$form.removeClass('in-collapsible-mode'); + this.inCollapsibleMode = false; + } + + /** + * Converts an already processed query into a search url + * @param {String} facetEndpoint + * @param {String} q query that's ready to get passed to the search endpoint + * @param {Boolean} [json] whether to hit the JSON endpoint + * @param {Number} [limit] how many items to get + * @param {String[]} [fields] the Solr fields to fetch (if using JSON) + */ + static composeSearchUrl( + facetEndpoint, + q, + json = false, + limit = null, + fields = null, + ) { + let url = facetEndpoint; + if (json) { + url += `.json?q=${q}&_spellcheck_count=0`; + } else { + url += `?q=${q}`; } - /** Setup event listeners for autocompletion */ - initAutocompletionLogic() { - // searches should be cancelled if you click anywhere in the page - $(document.body).on('click', this.clearAutocompletionResults.bind(this)); - // but clicking search input should not empty search results. - this.$input.on('click', false); - - this.$input.on('keyup', debounce(event => { - // ignore directional keys, enter, escape, and shift for callback - if (![13,16,27,37,38,39,40].includes(event.keyCode)) { - this.renderAutocompletionResults(); - } - }, 500, false)); - - this.$input.on('focus', debounce(event => { - event.stopPropagation(); - // don't render on focus if there are already results showing, avoid flashing - const resultsAreRendered = this.$results.children().length > 0; - if (this.escapeInput || resultsAreRendered) { - return; - } - this.renderAutocompletionResults(); - }, 300, false)); + if (limit) url += `&limit=${limit}`; + if (fields) url += `&fields=${fields.map(encodeURIComponent).join(',')}`; + url += `&mode=${SearchUtils.mode.read()}`; + return url; + } + + /** + * Prepare an unprocessed query for book searching + * @param {String} q + * @return {String} + */ + static marshalBookSearchQuery(q) { + if (q && q.indexOf(':') === -1 && q.indexOf('"') === -1) { + q = `title: "${q}"`; } - - /** - * @async - * Awkwardly fetches the the results as well as renders them :/ - * Cleans up and performs the query, then update the autocomplete results - * @returns {JQuery.jqXHR} - **/ - renderAutocompletionResults() { - let q = this.$input.val().trim(); - if (q.length < 3 || q.toLowerCase() === 'the' || !(this.facetEndpoint in RENDER_AUTOCOMPLETE_RESULT)) { + return q; + } + + /** Setup event listeners for autocompletion */ + initAutocompletionLogic() { + // searches should be cancelled if you click anywhere in the page + $(document.body).on('click', this.clearAutocompletionResults.bind(this)); + // but clicking search input should not empty search results. + this.$input.on('click', false); + + this.$input.on( + 'keyup', + debounce( + (event) => { + // ignore directional keys, enter, escape, and shift for callback + if (![13, 16, 27, 37, 38, 39, 40].includes(event.keyCode)) { + this.renderAutocompletionResults(); + } + }, + 500, + false, + ), + ); + + this.$input.on( + 'focus', + debounce( + (event) => { + event.stopPropagation(); + // don't render on focus if there are already results showing, avoid flashing + const resultsAreRendered = this.$results.children().length > 0; + if (this.escapeInput || resultsAreRendered) { return; - } - if (this.facet.read() === 'title') { - q = SearchBar.marshalBookSearchQuery(q); - } - - this.$results.css('opacity', 0.5); - return $.getJSON(SearchBar.composeSearchUrl(this.facetEndpoint, q, true, 10, DEFAULT_JSON_FIELDS), data => { - const renderer = RENDER_AUTOCOMPLETE_RESULT[this.facetEndpoint]; - this.$results.css('opacity', 1); - this.clearAutocompletionResults(); - for (const d in data.docs) { - this.$results.append(renderer(data.docs[d])); - } - }); + } + this.renderAutocompletionResults(); + }, + 300, + false, + ), + ); + } + + /** + * @async + * Awkwardly fetches the the results as well as renders them :/ + * Cleans up and performs the query, then update the autocomplete results + * @returns {JQuery.jqXHR} + **/ + renderAutocompletionResults() { + let q = this.$input.val().trim(); + if ( + q.length < 3 || + q.toLowerCase() === 'the' || + !(this.facetEndpoint in RENDER_AUTOCOMPLETE_RESULT) + ) { + return; } - - clearAutocompletionResults() { - this.$results.empty(); + if (this.facet.read() === 'title') { + q = SearchBar.marshalBookSearchQuery(q); } - /** - * Updates the UI to match after the facet is changed - * @param {String} newFacet - */ - handleFacetValueChange(newFacet) { - // update the UI - this.$facetSelect.val(newFacet); - const text = this.$facetSelect.find('option:selected').text(); - $('header#header-bar .search-facet-value').html(text); - - // Add immediate refresh when input has value and focus is on the facet selector - if (this.$input.val() && this.$facetSelect.is(':focus')) { - this.renderAutocompletionResults(); + this.$results.css('opacity', 0.5); + return $.getJSON( + SearchBar.composeSearchUrl( + this.facetEndpoint, + q, + true, + 10, + DEFAULT_JSON_FIELDS, + ), + (data) => { + const renderer = RENDER_AUTOCOMPLETE_RESULT[this.facetEndpoint]; + this.$results.css('opacity', 1); + this.clearAutocompletionResults(); + for (const d in data.docs) { + this.$results.append(renderer(data.docs[d])); } + }, + ); + } + + clearAutocompletionResults() { + this.$results.empty(); + } + + /** + * Updates the UI to match after the facet is changed + * @param {String} newFacet + */ + handleFacetValueChange(newFacet) { + // update the UI + this.$facetSelect.val(newFacet); + const text = this.$facetSelect.find('option:selected').text(); + $('header#header-bar .search-facet-value').html(text); + + // Add immediate refresh when input has value and focus is on the facet selector + if (this.$input.val() && this.$facetSelect.is(':focus')) { + this.renderAutocompletionResults(); } - - /** - * Handles changes to the facet from the UI - * @param {JQuery.Event} event - */ - handleFacetSelectChange(event) { - const newFacet = event.target.value; - // We don't want to persist advanced becaues it behaves like a button - if (newFacet === 'advanced') { - event.preventDefault(); - this.navigateTo('/advancedsearch'); - } else { - this.facet.write(newFacet); - } - } - - /** - * For testing purposes, wraps window.location - * @returns {URL} The current URL - */ - getCurUrl() { - return window.location; - } - - /** - * Just so we can stub/test this - * @param {String} path - */ - navigateTo(path) { - window.location.assign(path); - } - - /** - * Makes changes to the UI after a change occurs to the mode - * Parts of this might be dead code; I don't really understand why - * this is necessary, so opting to leave it alone for now. - * @param {String} newMode - */ - handleSearchModeChange(newMode) { - $('.instantsearch-mode').val(newMode); - $(`input[name=mode][value=${newMode}]`).prop('checked', true); - SearchUtils.addModeInputsToForm(this.$form, newMode); + } + + /** + * Handles changes to the facet from the UI + * @param {JQuery.Event} event + */ + handleFacetSelectChange(event) { + const newFacet = event.target.value; + // We don't want to persist advanced becaues it behaves like a button + if (newFacet === 'advanced') { + event.preventDefault(); + this.navigateTo('/advancedsearch'); + } else { + this.facet.write(newFacet); } + } + + /** + * For testing purposes, wraps window.location + * @returns {URL} The current URL + */ + getCurUrl() { + return window.location; + } + + /** + * Just so we can stub/test this + * @param {String} path + */ + navigateTo(path) { + window.location.assign(path); + } + + /** + * Makes changes to the UI after a change occurs to the mode + * Parts of this might be dead code; I don't really understand why + * this is necessary, so opting to leave it alone for now. + * @param {String} newMode + */ + handleSearchModeChange(newMode) { + $('.instantsearch-mode').val(newMode); + $(`input[name=mode][value=${newMode}]`).prop('checked', true); + SearchUtils.addModeInputsToForm(this.$form, newMode); + } } diff --git a/openlibrary/plugins/openlibrary/js/SearchPage.js b/openlibrary/plugins/openlibrary/js/SearchPage.js index 98abc635308..b0b869f03e9 100644 --- a/openlibrary/plugins/openlibrary/js/SearchPage.js +++ b/openlibrary/plugins/openlibrary/js/SearchPage.js @@ -1,23 +1,23 @@ -import { addModeInputsToForm, mode as searchMode } from './SearchUtils'; import $ from 'jquery'; +import { addModeInputsToForm, mode as searchMode } from './SearchUtils'; /** @typedef {import('./SearchUtils').SearchModeSelector} SearchModeSelector */ /** Manages some (PROBABLY VERY FEW) of the interactions on the search page */ export class SearchPage { - /** - * @param {HTMLFormElement|JQuery} form the .olform search form - * @param {SearchModeSelector} searchModeSelector - */ - constructor(form, searchModeSelector) { - this.$form = $(form); - searchMode.sync(this.updateModeInputs.bind(this)); - this.$form.on('submit', this.updateModeInputs.bind(this)); - searchModeSelector.change(() => this.$form.trigger('submit')); - } + /** + * @param {HTMLFormElement|JQuery} form the .olform search form + * @param {SearchModeSelector} searchModeSelector + */ + constructor(form, searchModeSelector) { + this.$form = $(form); + searchMode.sync(this.updateModeInputs.bind(this)); + this.$form.on('submit', this.updateModeInputs.bind(this)); + searchModeSelector.change(() => this.$form.trigger('submit')); + } - /** Convenience wrapper of {@link addModeInputsToForm} */ - updateModeInputs() { - addModeInputsToForm(this.$form, searchMode.read()); - } + /** Convenience wrapper of {@link addModeInputsToForm} */ + updateModeInputs() { + addModeInputsToForm(this.$form, searchMode.read()); + } } diff --git a/openlibrary/plugins/openlibrary/js/SearchUtils.js b/openlibrary/plugins/openlibrary/js/SearchUtils.js index 18da91076be..7481b8f3e9e 100644 --- a/openlibrary/plugins/openlibrary/js/SearchUtils.js +++ b/openlibrary/plugins/openlibrary/js/SearchUtils.js @@ -1,5 +1,5 @@ -import { removeURLParameter } from './Browser'; import $ from 'jquery'; +import { removeURLParameter } from './Browser'; /** * Adds hidden input elements/modifes the action of the form to match the given search mode @@ -8,23 +8,22 @@ import $ from 'jquery'; * @param {String} searchMode */ export function addModeInputsToForm($form, searchMode) { - $('input[name=\'has_fulltext\']').remove(); + $("input[name='has_fulltext']").remove(); - let url = $form.attr('action'); - if (url) { - url = removeURLParameter(url, 'm'); - url = removeURLParameter(url, 'has_fulltext'); - url = removeURLParameter(url, 'subject_facet'); + let url = $form.attr('action'); + if (url) { + url = removeURLParameter(url, 'm'); + url = removeURLParameter(url, 'has_fulltext'); + url = removeURLParameter(url, 'subject_facet'); - if (searchMode !== 'everything') { - $form.append('<input type="hidden" name="has_fulltext" value="true"/>'); - url = `${url + (url.indexOf('?') > -1 ? '&' : '?')}has_fulltext=true`; - } - - $form.attr('action', url); + if (searchMode !== 'everything') { + $form.append('<input type="hidden" name="has_fulltext" value="true"/>'); + url = `${url + (url.indexOf('?') > -1 ? '&' : '?')}has_fulltext=true`; } -} + $form.attr('action', url); + } +} /** * @typedef {Object} PersistentValue.Options @@ -36,108 +35,108 @@ export function addModeInputsToForm($form, searchMode) { /** String value that's persisted to localstorage */ export class PersistentValue { - /** - * @param {String} key - * @param {PersistentValue.Options} options - */ - constructor(key, options={}) { - this.key = key; - this.options = Object.assign({}, PersistentValue.DEFAULT_OPTIONS, options); - this._listeners = []; - - const noValue = this.read() === null; - const isValid = () => !this.options.initValidation || this.options.initValidation(this.read()); - if (noValue || !isValid()) { - this.write(this.options.default); - } + /** + * @param {String} key + * @param {PersistentValue.Options} options + */ + constructor(key, options = {}) { + this.key = key; + this.options = Object.assign({}, PersistentValue.DEFAULT_OPTIONS, options); + this._listeners = []; + + const noValue = this.read() === null; + const isValid = () => + !this.options.initValidation || this.options.initValidation(this.read()); + if (noValue || !isValid()) { + this.write(this.options.default); } - - /** - * Read the stored value - * @return {String} - */ - read() { - return localStorage.getItem(this.key); + } + + /** + * Read the stored value + * @return {String} + */ + read() { + return localStorage.getItem(this.key); + } + + /** + * Update the stored value + * @param {String} newValue + */ + write(newValue) { + const oldValue = this.read(); + let toWrite = newValue; + if (this.options.writeTransformation) { + toWrite = this.options.writeTransformation(newValue, oldValue); } - /** - * Update the stored value - * @param {String} newValue - */ - write(newValue) { - const oldValue = this.read(); - let toWrite = newValue; - if (this.options.writeTransformation) { - toWrite = this.options.writeTransformation(newValue, oldValue); - } - - if (toWrite === null) { - localStorage.removeItem(this.key); - } else { - localStorage.setItem(this.key, toWrite); - } - - if (oldValue !== toWrite) { - this._emit(toWrite); - } + if (toWrite === null) { + localStorage.removeItem(this.key); + } else { + localStorage.setItem(this.key, toWrite); } - /** - * Listen to updates to this value - * @param {Function} listener - * @param {Boolean} callAtStart whether to call the listener right now with the current value - */ - sync(listener, callAtStart=true) { - this._listeners.push(listener); - if (callAtStart) listener(this.read()); - } - - /** - * @private - * Notify listeners of an update - * @param {String} newValue - */ - _emit(newValue) { - this._listeners.forEach(listener => listener(newValue)); + if (oldValue !== toWrite) { + this._emit(toWrite); } + } + + /** + * Listen to updates to this value + * @param {Function} listener + * @param {Boolean} callAtStart whether to call the listener right now with the current value + */ + sync(listener, callAtStart = true) { + this._listeners.push(listener); + if (callAtStart) listener(this.read()); + } + + /** + * @private + * Notify listeners of an update + * @param {String} newValue + */ + _emit(newValue) { + this._listeners.forEach((listener) => listener(newValue)); + } } /** @type {PersistentValue.Options} */ PersistentValue.DEFAULT_OPTIONS = { - default: null, - initValidation: null, - writeTransformation: null, + default: null, + initValidation: null, + writeTransformation: null, }; - const MODES = ['everything', 'ebooks']; const DEFAULT_MODE = 'everything'; /** Search mode; {@see MODES} */ export const mode = new PersistentValue('mode', { - default: DEFAULT_MODE, - initValidation: mode => MODES.indexOf(mode) !== -1, - writeTransformation(newValue, oldValue) { - const mode = (newValue && newValue.toLowerCase()) || oldValue; - const isValidMode = MODES.indexOf(mode) !== -1; - return isValidMode ? mode : DEFAULT_MODE; - } + default: DEFAULT_MODE, + initValidation: (mode) => MODES.indexOf(mode) !== -1, + writeTransformation(newValue, oldValue) { + const mode = (newValue && newValue.toLowerCase()) || oldValue; + const isValidMode = MODES.indexOf(mode) !== -1; + return isValidMode ? mode : DEFAULT_MODE; + }, }); /** Manages interactions of the search mode radio buttons */ export class SearchModeSelector { - /** - * @param {JQuery} radioButtons - */ - constructor(radioButtons) { - this.$radioButtons = radioButtons; - this.change(newMode => mode.write(newMode)); - } - - /** - * Listen for changes - * @param {Function} handler - */ - change(handler) { - this.$radioButtons.on('change', event => handler($(event.target).val())); - } + /** + * @param {JQuery} radioButtons + */ + constructor(radioButtons) { + this.$radioButtons = radioButtons; + this.change((newMode) => mode.write(newMode)); + } + + /** + * Listen for changes + * @param {Function} handler + */ + change(handler) { + this.$radioButtons.on('change', (event) => handler($(event.target).val())); + } } diff --git a/openlibrary/plugins/openlibrary/js/Toast.js b/openlibrary/plugins/openlibrary/js/Toast.js index 5b81a53f462..a32f2fc53e6 100644 --- a/openlibrary/plugins/openlibrary/js/Toast.js +++ b/openlibrary/plugins/openlibrary/js/Toast.js @@ -8,89 +8,86 @@ import '../../../../static/css/components/toast.css'; const DEFAULT_TIMEOUT = 2500; export class Toast { - /** - * @param {JQuery} $toast The element containing the appropriate parts - * @param {JQuery|HTMLElement} containerParent where to add the toast bar - */ - constructor($toast, containerParent=document.body) { - const $parent = $(containerParent); - if (!$parent.has('.toast-container').length) { - $parent.prepend('<div class="toast-container"></div>') - } - if ($toast.data('toast-trigger')) { - $($toast.data('toast-trigger')).on('click', () => this.show()); - } - /** The toast bar that the toast will be added to. */ - this.$container = $parent.children('.toast-container').first(); - this.$toast = $toast; + /** + * @param {JQuery} $toast The element containing the appropriate parts + * @param {JQuery|HTMLElement} containerParent where to add the toast bar + */ + constructor($toast, containerParent = document.body) { + const $parent = $(containerParent); + if (!$parent.has('.toast-container').length) { + $parent.prepend('<div class="toast-container"></div>'); } - - /** Displays the toast component on the page. */ - show() { - this.$toast - .appendTo(this.$container) - .fadeIn(); - this.$toast.find('.toast__close') - .one('click', () => this.close()); + if ($toast.data('toast-trigger')) { + $($toast.data('toast-trigger')).on('click', () => this.show()); } + /** The toast bar that the toast will be added to. */ + this.$container = $parent.children('.toast-container').first(); + this.$toast = $toast; + } - /** Hides the toast component and removes it from the DOM. */ - close() { - this.$toast.fadeOut('slow', () => this.$toast.remove()); - } + /** Displays the toast component on the page. */ + show() { + this.$toast.appendTo(this.$container).fadeIn(); + this.$toast.find('.toast__close').one('click', () => this.close()); + } + + /** Hides the toast component and removes it from the DOM. */ + close() { + this.$toast.fadeOut('slow', () => this.$toast.remove()); + } } /** * Creates a small pop-up message that closes after some amount of time. */ export class FadingToast extends Toast { - /** - * Creates a new toast component, adds a close listener to the component, and adds the component - * as the first child of the given parent element. - * - * @param {string} message Message that will be displayed in the toast component - * @param {JQuery} [$parent] Designates where the toast component will be attached - * @param {number} [timeout] Amount of time, in milliseconds, that the component will be visible - */ - constructor(message, $parent=null, timeout=DEFAULT_TIMEOUT) { - // TODO(i18n-js) - const $toast = $(`<div class="toast"> + /** + * Creates a new toast component, adds a close listener to the component, and adds the component + * as the first child of the given parent element. + * + * @param {string} message Message that will be displayed in the toast component + * @param {JQuery} [$parent] Designates where the toast component will be attached + * @param {number} [timeout] Amount of time, in milliseconds, that the component will be visible + */ + constructor(message, $parent = null, timeout = DEFAULT_TIMEOUT) { + // TODO(i18n-js) + const $toast = $(`<div class="toast"> <span class="toast__body">${message}</span> <a class="toast__close">×<span class="shift">Close</span></a> - </div>`) + </div>`); - // Prevent sending null parent: - if ($parent) { - super($toast, $parent); - } else { - super($toast); - } - this.timeout = timeout; + // Prevent sending null parent: + if ($parent) { + super($toast, $parent); + } else { + super($toast); } + this.timeout = timeout; + } - /** @override */ - show() { - super.show(); + /** @override */ + show() { + super.show(); - setTimeout(() => { - this.close(); - }, this.timeout); - } + setTimeout(() => { + this.close(); + }, this.timeout); + } } /** * Creates a small pop-up message that must be closed by the viewer. */ export class PersistentToast extends Toast { - /** - * @param {string} message String that will be displayed within the toast component - * @param {string} classes Additional classes to add to the toast component - */ - constructor(message, classes='') { - const $toast = $(`<div class="toast ${classes}"> + /** + * @param {string} message String that will be displayed within the toast component + * @param {string} classes Additional classes to add to the toast component + */ + constructor(message, classes = '') { + const $toast = $(`<div class="toast ${classes}"> <span class="toast__body">${message}</span> <a class="toast__close">×<span class="shift">Close</span> - </div>`) - super($toast) - } + </div>`); + super($toast); + } } diff --git a/openlibrary/plugins/openlibrary/js/add-book.js b/openlibrary/plugins/openlibrary/js/add-book.js index bf92fb27713..c42702d2e0c 100644 --- a/openlibrary/plugins/openlibrary/js/add-book.js +++ b/openlibrary/plugins/openlibrary/js/add-book.js @@ -1,14 +1,14 @@ import { - parseIsbn, - parseLccn, - parseOclc, - isChecksumValidIsbn10, - isChecksumValidIsbn13, - isFormatValidIsbn10, - isFormatValidIsbn13, - isValidLccn, - isValidOclc -} from './idValidation.js' + isChecksumValidIsbn10, + isChecksumValidIsbn13, + isFormatValidIsbn10, + isFormatValidIsbn13, + isValidLccn, + isValidOclc, + parseIsbn, + parseLccn, + parseOclc, +} from './idValidation.js'; import { trimInputValues } from './utils.js'; let invalidChecksum; @@ -18,188 +18,185 @@ let invalidLccn; let invalidOclc; let emptyId; -const i18nStrings = JSON.parse(document.querySelector('form[name=edit]').dataset.i18n); +const i18nStrings = JSON.parse( + document.querySelector('form[name=edit]').dataset.i18n, +); const addBookForm = $('form#addbook'); -export function initAddBookImport () { - $('.list-books a').on('click', function() { - var li = $(this).parents('li').first(); - $('input#work').val(`/works/${li.attr('id')}`); - addBookForm.trigger('submit'); - }); - $('#bookAddCont').on('click', function() { - $('input#work').val('none-of-these'); - addBookForm.trigger('submit'); - }); - - invalidChecksum = i18nStrings.invalid_checksum; - invalidIsbn10 = i18nStrings.invalid_isbn10; - invalidIsbn13 = i18nStrings.invalid_isbn13; - invalidLccn = i18nStrings.invalid_lccn; - invalidOclc = i18nStrings.invalid_oclc; - emptyId = i18nStrings.empty_id; - - $('#id_value').on('change',autoCompleteIdName); - $('#addbook').on('submit', parseAndValidateId); - $('#id_value').on('input', clearErrors); - $('#id_name').on('change', clearErrors); - - $('#publish_date').on('blur', validatePublishDate); - - trimInputValues('input') - - // Prevents submission if the publish date is > 1 year in the future - addBookForm.on('submit', function() { - if ($('#publish-date-errors').hasClass('hidden')) { - return true; - } else return false; - }) +export function initAddBookImport() { + $('.list-books a').on('click', function () { + var li = $(this).parents('li').first(); + $('input#work').val(`/works/${li.attr('id')}`); + addBookForm.trigger('submit'); + }); + $('#bookAddCont').on('click', () => { + $('input#work').val('none-of-these'); + addBookForm.trigger('submit'); + }); + + invalidChecksum = i18nStrings.invalid_checksum; + invalidIsbn10 = i18nStrings.invalid_isbn10; + invalidIsbn13 = i18nStrings.invalid_isbn13; + invalidLccn = i18nStrings.invalid_lccn; + invalidOclc = i18nStrings.invalid_oclc; + emptyId = i18nStrings.empty_id; + + $('#id_value').on('change', autoCompleteIdName); + $('#addbook').on('submit', parseAndValidateId); + $('#id_value').on('input', clearErrors); + $('#id_name').on('change', clearErrors); + + $('#publish_date').on('blur', validatePublishDate); + + trimInputValues('input'); + + // Prevents submission if the publish date is > 1 year in the future + addBookForm.on('submit', () => { + if ($('#publish-date-errors').hasClass('hidden')) { + return true; + } else return false; + }); } // a flag to make raiseIsbnError perform differently upon subsequent calls let addBookWithIsbnErrors = false; function displayIsbnError(event, errorMessage) { - if (!addBookWithIsbnErrors) { - addBookWithIsbnErrors = true; - const errorDiv = document.getElementById('id-errors'); - errorDiv.classList.remove('hidden'); - errorDiv.textContent = errorMessage; - const confirm = document.getElementById('confirm-add'); - confirm.classList.remove('hidden'); - const isbnInput = document.getElementById('id_value'); - isbnInput.focus({focusVisible: true}); - event.preventDefault(); - return; - } - // parsing potentially invalid ISBN - document.getElementById('id_value').value = parseIsbn(document.getElementById('id_value').value); -} - -function displayIdentifierError(event, errorMessage) { + if (!addBookWithIsbnErrors) { + addBookWithIsbnErrors = true; const errorDiv = document.getElementById('id-errors'); errorDiv.classList.remove('hidden'); errorDiv.textContent = errorMessage; + const confirm = document.getElementById('confirm-add'); + confirm.classList.remove('hidden'); + const isbnInput = document.getElementById('id_value'); + isbnInput.focus({ focusVisible: true }); event.preventDefault(); return; + } + // parsing potentially invalid ISBN + document.getElementById('id_value').value = parseIsbn( + document.getElementById('id_value').value, + ); +} + +function displayIdentifierError(event, errorMessage) { + const errorDiv = document.getElementById('id-errors'); + errorDiv.classList.remove('hidden'); + errorDiv.textContent = errorMessage; + event.preventDefault(); + return; } function clearErrors() { - addBookWithIsbnErrors = false; - const errorDiv = document.getElementById('id-errors'); - errorDiv.classList.add('hidden'); - const confirm = document.getElementById('confirm-add'); - confirm.classList.add('hidden'); + addBookWithIsbnErrors = false; + const errorDiv = document.getElementById('id-errors'); + errorDiv.classList.add('hidden'); + const confirm = document.getElementById('confirm-add'); + confirm.classList.add('hidden'); } function parseAndValidateId(event) { - const fieldName = document.getElementById('id_name').value; - const idValue = document.getElementById('id_value').value; - - if (fieldName === 'isbn_10') { - parseAndValidateIsbn10(event, idValue); - } - else if (fieldName === 'isbn_13') { - parseAndValidateIsbn13(event, idValue); - } - else if (fieldName === 'lccn') { - parseAndValidateLccn(event, idValue); - } - else if (fieldName === 'oclc_numbers') { - parseAndValidateOclc(event, idValue); - } - else if (!fieldName || !isEmptyId(event, idValue)) { - document.getElementById('id_value').value = idValue.trim(); - } + const fieldName = document.getElementById('id_name').value; + const idValue = document.getElementById('id_value').value; + + if (fieldName === 'isbn_10') { + parseAndValidateIsbn10(event, idValue); + } else if (fieldName === 'isbn_13') { + parseAndValidateIsbn13(event, idValue); + } else if (fieldName === 'lccn') { + parseAndValidateLccn(event, idValue); + } else if (fieldName === 'oclc_numbers') { + parseAndValidateOclc(event, idValue); + } else if (!fieldName || !isEmptyId(event, idValue)) { + document.getElementById('id_value').value = idValue.trim(); + } } function isEmptyId(event, idValue) { - if (!idValue.trim()) { - const errorDiv = document.getElementById('id-errors'); - errorDiv.classList.remove('hidden'); - errorDiv.textContent = emptyId; - event.preventDefault(); - return true; - } - return false; + if (!idValue.trim()) { + const errorDiv = document.getElementById('id-errors'); + errorDiv.classList.remove('hidden'); + errorDiv.textContent = emptyId; + event.preventDefault(); + return true; + } + return false; } function parseAndValidateIsbn10(event, idValue) { - // parsing valid ISBN that passes checks - idValue = parseIsbn(idValue); - if (!isFormatValidIsbn10(idValue)) { - return displayIsbnError(event, invalidIsbn10); - } - if (!isChecksumValidIsbn10(idValue)) { - return displayIsbnError(event, invalidChecksum); - } - document.getElementById('id_value').value = idValue; + // parsing valid ISBN that passes checks + idValue = parseIsbn(idValue); + if (!isFormatValidIsbn10(idValue)) { + return displayIsbnError(event, invalidIsbn10); + } + if (!isChecksumValidIsbn10(idValue)) { + return displayIsbnError(event, invalidChecksum); + } + document.getElementById('id_value').value = idValue; } function parseAndValidateIsbn13(event, idValue) { - idValue = parseIsbn(idValue); - if (!isFormatValidIsbn13(idValue)) { - return displayIsbnError(event, invalidIsbn13); - } - if (!isChecksumValidIsbn13(idValue)) { - return displayIsbnError(event, invalidChecksum); - } - document.getElementById('id_value').value = idValue; + idValue = parseIsbn(idValue); + if (!isFormatValidIsbn13(idValue)) { + return displayIsbnError(event, invalidIsbn13); + } + if (!isChecksumValidIsbn13(idValue)) { + return displayIsbnError(event, invalidChecksum); + } + document.getElementById('id_value').value = idValue; } function parseAndValidateLccn(event, idValue) { - idValue = parseLccn(idValue); - if (!isValidLccn(idValue)) { - return displayIdentifierError(event, invalidLccn); - } - document.getElementById('id_value').value = idValue; + idValue = parseLccn(idValue); + if (!isValidLccn(idValue)) { + return displayIdentifierError(event, invalidLccn); + } + document.getElementById('id_value').value = idValue; } function parseAndValidateOclc(event, idValue) { - idValue = parseOclc(idValue); - if (!isValidOclc(idValue)) { - return displayIdentifierError(event, invalidOclc); - } - document.getElementById('id_value').value = idValue; + idValue = parseOclc(idValue); + if (!isValidOclc(idValue)) { + return displayIdentifierError(event, invalidOclc); + } + document.getElementById('id_value').value = idValue; } -function autoCompleteIdName(){ - const idValue = document.querySelector('input#id_value').value.trim(); - const idValueIsbn = parseIsbn(idValue); - const currentSelection = document.getElementById('id_name').value; - - if (isFormatValidIsbn10(idValueIsbn) && isChecksumValidIsbn10(idValueIsbn)){ - document.getElementById('id_name').value = 'isbn_10'; - } - - else if (isFormatValidIsbn13(idValueIsbn) && isChecksumValidIsbn13(idValueIsbn)){ - document.getElementById('id_name').value = 'isbn_13'; - } - - else if ((isValidLccn(parseLccn(idValue)))){ - document.getElementById('id_name').value = 'lccn'; - } - - else { - document.getElementById('id_name').value = currentSelection || ''; - } +function autoCompleteIdName() { + const idValue = document.querySelector('input#id_value').value.trim(); + const idValueIsbn = parseIsbn(idValue); + const currentSelection = document.getElementById('id_name').value; + + if (isFormatValidIsbn10(idValueIsbn) && isChecksumValidIsbn10(idValueIsbn)) { + document.getElementById('id_name').value = 'isbn_10'; + } else if ( + isFormatValidIsbn13(idValueIsbn) && + isChecksumValidIsbn13(idValueIsbn) + ) { + document.getElementById('id_name').value = 'isbn_13'; + } else if (isValidLccn(parseLccn(idValue))) { + document.getElementById('id_name').value = 'lccn'; + } else { + document.getElementById('id_name').value = currentSelection || ''; + } } function validatePublishDate() { - // validate publish-date to make sure the date is not in future - // used in templates/books/add.html - const publish_date = this.value; - // if it doesn't have even three digits then it can't be a future date - const tokens = /(\d{3,})/.exec(publish_date); - const year = new Date().getFullYear(); - const isValidDate = tokens && tokens[1] && parseInt(tokens[1]) <= year + 1; // allow one year in future. - - const errorDiv = document.getElementById('publish-date-errors'); - - if (!isValidDate) { - errorDiv.classList.remove('hidden'); - errorDiv.textContent = i18nStrings['invalid_publish_date']; - } else { - errorDiv.classList.add('hidden'); - } + // validate publish-date to make sure the date is not in future + // used in templates/books/add.html + const publish_date = this.value; + // if it doesn't have even three digits then it can't be a future date + const tokens = /(\d{3,})/.exec(publish_date); + const year = new Date().getFullYear(); + const isValidDate = tokens && tokens[1] && parseInt(tokens[1]) <= year + 1; // allow one year in future. + + const errorDiv = document.getElementById('publish-date-errors'); + + if (!isValidDate) { + errorDiv.classList.remove('hidden'); + errorDiv.textContent = i18nStrings['invalid_publish_date']; + } else { + errorDiv.classList.add('hidden'); + } } diff --git a/openlibrary/plugins/openlibrary/js/add_provider.js b/openlibrary/plugins/openlibrary/js/add_provider.js index 11cd5c13cc5..b6a9601a5c3 100644 --- a/openlibrary/plugins/openlibrary/js/add_provider.js +++ b/openlibrary/plugins/openlibrary/js/add_provider.js @@ -1,60 +1,60 @@ export function initAddProviderRowLink(elem) { - elem.addEventListener('click', function() { - let index = Number(elem.dataset.index) - const tbody = document.querySelector('#provider-table-body') - tbody.appendChild(createProviderRow(index)) - if (index === 0) { - document.querySelector('#provider-table').classList.remove('hidden') - } - this.dataset.index = ++index - }) + elem.addEventListener('click', function () { + let index = Number(elem.dataset.index); + const tbody = document.querySelector('#provider-table-body'); + tbody.appendChild(createProviderRow(index)); + if (index === 0) { + document.querySelector('#provider-table').classList.remove('hidden'); + } + this.dataset.index = ++index; + }); } function createProviderRow(index) { - const tr = document.createElement('tr') + const tr = document.createElement('tr'); - const innerHtml = `${createTextInputDataCell(index, 'url')} + const innerHtml = `${createTextInputDataCell(index, 'url')} ${createSelectDataCell(index, 'access', accessTypeValues)} ${createSelectDataCell(index, 'format', formatValues)} - ${createTextInputDataCell(index, 'provider_name')}` + ${createTextInputDataCell(index, 'provider_name')}`; - tr.innerHTML = innerHtml - return tr + tr.innerHTML = innerHtml; + return tr; } function createTextInputDataCell(index, type) { - const id = `edition--providers--${index}--${type}` - return `<td><input name="${id}" id="${id}" ${type === 'url' ? 'type="url"' : ''}></td>` + const id = `edition--providers--${index}--${type}`; + return `<td><input name="${id}" id="${id}" ${type === 'url' ? 'type="url"' : ''}></td>`; } function createSelectDataCell(index, type, values) { - const id = `edition--providers--${index}--${type}` - return `<td> + const id = `edition--providers--${index}--${type}`; + return `<td> <select name="${id}" id="${id}"> ${createSelectOptions(values)} </select> - </td>` + </td>`; } const accessTypeValues = [ - {value: '', text: ''}, - {value: 'read', text: 'Read'}, - {value: 'listen', text: 'Listen'}, - {value: 'buy', text: 'Buy'}, - {value: 'borrow', text: 'Borrow'}, - {value: 'preview', text: 'Preview'} -] + { value: '', text: '' }, + { value: 'read', text: 'Read' }, + { value: 'listen', text: 'Listen' }, + { value: 'buy', text: 'Buy' }, + { value: 'borrow', text: 'Borrow' }, + { value: 'preview', text: 'Preview' }, +]; const formatValues = [ - {value: '', text: ''}, - {value: 'web', text: 'Web'}, - {value: 'epub', text: 'ePub'}, - {value: 'pdf', text: 'PDF'} -] + { value: '', text: '' }, + { value: 'web', text: 'Web' }, + { value: 'epub', text: 'ePub' }, + { value: 'pdf', text: 'PDF' }, +]; function createSelectOptions(values) { - let html = '' - for (const value of values) { - html += `<option value="${value.value}">${value.text}</option>\n` - } - return html + let html = ''; + for (const value of values) { + html += `<option value="${value.value}">${value.text}</option>\n`; + } + return html; } diff --git a/openlibrary/plugins/openlibrary/js/admin.js b/openlibrary/plugins/openlibrary/js/admin.js index 93e9cfef789..e5c2b8e8aea 100644 --- a/openlibrary/plugins/openlibrary/js/admin.js +++ b/openlibrary/plugins/openlibrary/js/admin.js @@ -3,34 +3,34 @@ */ export function initAdmin() { - // admin/people/view - $('a.tag').on('click', function () { - var action; - var tag; + // admin/people/view + $('a.tag').on('click', function () { + var action; + var tag; - $(this).toggleClass('active'); - action = $(this).hasClass('active') ? 'add_tag': 'remove_tag'; - tag = $(this).text(); - $.post(window.location.href, { - action: action, - tag: tag - }); + $(this).toggleClass('active'); + action = $(this).hasClass('active') ? 'add_tag' : 'remove_tag'; + tag = $(this).text(); + $.post(window.location.href, { + action: action, + tag: tag, }); + }); - // admin/people/edits - $('#checkall').on('click', function () { - $('form.olform').find(':checkbox').prop('checked', this.checked); - }); + // admin/people/edits + $('#checkall').on('click', function () { + $('form.olform').find(':checkbox').prop('checked', this.checked); + }); } export function initAnonymizationButton(button) { - const displayName = button.dataset.displayName; - const confirmMessage = `Really anonymize ${displayName}'s account? This will delete ${displayName}'s profile page and booknotes, and anonymize ${displayName}'s reading log, reviews, star ratings, and merge request submissions.`; - button.addEventListener('click', function(event) { - if (!confirm(confirmMessage)) { - event.preventDefault(); - } - }) + const displayName = button.dataset.displayName; + const confirmMessage = `Really anonymize ${displayName}'s account? This will delete ${displayName}'s profile page and booknotes, and anonymize ${displayName}'s reading log, reviews, star ratings, and merge request submissions.`; + button.addEventListener('click', (event) => { + if (!confirm(confirmMessage)) { + event.preventDefault(); + } + }); } /** @@ -40,12 +40,12 @@ export function initAnonymizationButton(button) { * @param {NodeList<HTMLButtonElement>} buttons */ export function initConfirmationButtons(buttons) { - const confirmMessage = 'Are you sure?' - for (const button of buttons) { - button.addEventListener('click', function(event) { - if (!confirm(confirmMessage)) { - event.preventDefault(); - } - }) - } + const confirmMessage = 'Are you sure?'; + for (const button of buttons) { + button.addEventListener('click', (event) => { + if (!confirm(confirmMessage)) { + event.preventDefault(); + } + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/affiliate-links.js b/openlibrary/plugins/openlibrary/js/affiliate-links.js index c31e7247428..622004a6cfa 100644 --- a/openlibrary/plugins/openlibrary/js/affiliate-links.js +++ b/openlibrary/plugins/openlibrary/js/affiliate-links.js @@ -1,4 +1,4 @@ -import { buildPartialsUrl } from './utils' +import { buildPartialsUrl } from './utils'; /** * Adds functionality to fetch affialite links asyncronously. @@ -10,17 +10,17 @@ import { buildPartialsUrl } from './utils' * @param {NodeList<HTMLElement>} affiliateLinksSections Collection of each affiliate links section that is on the page */ export function initAffiliateLinks(affiliateLinksSections) { - const isLoading = showLoadingIndicators(affiliateLinksSections) - if (isLoading) { - // Replace loading indicators with fetched partials + const isLoading = showLoadingIndicators(affiliateLinksSections); + if (isLoading) { + // Replace loading indicators with fetched partials - const title = affiliateLinksSections[0].dataset.title - const opts = JSON.parse(affiliateLinksSections[0].dataset.opts) - const args = [title, opts] - const d = {args: args} + const title = affiliateLinksSections[0].dataset.title; + const opts = JSON.parse(affiliateLinksSections[0].dataset.opts); + const args = [title, opts]; + const d = { args: args }; - getPartials(d, affiliateLinksSections) - } + getPartials(d, affiliateLinksSections); + } } /** @@ -31,15 +31,15 @@ export function initAffiliateLinks(affiliateLinksSections) { * @returns {boolean} `true` if a loading indicator is displayed on the screen */ function showLoadingIndicators(linkSections) { - let isLoading = false - for (const section of linkSections) { - const loadingIndicator = section.querySelector('.loadingIndicator') - if (loadingIndicator) { - isLoading = true - loadingIndicator.classList.remove('hidden') - } + let isLoading = false; + for (const section of linkSections) { + const loadingIndicator = section.querySelector('.loadingIndicator'); + if (loadingIndicator) { + isLoading = true; + loadingIndicator.classList.remove('hidden'); } - return isLoading + } + return isLoading; } /** @@ -50,44 +50,50 @@ function showLoadingIndicators(linkSections) { * @returns {Promise} */ async function getPartials(data, affiliateLinksSections) { - const dataString = JSON.stringify(data) + const dataString = JSON.stringify(data); - return fetch(buildPartialsUrl('AffiliateLinks', {data: dataString})) - .then((resp) => { - if (resp.status !== 200) { - throw new Error(`Failed to fetch partials. Status code: ${resp.status}`) - } - return resp.json() - }) - .then((data) => { - const span = document.createElement('span') - span.innerHTML = data['partials'] - const links = span.firstElementChild - for (const section of affiliateLinksSections) { - section.replaceWith(links.cloneNode(true)) - } - }) - .catch(() => { - // XXX : Handle errors sensibly - for (const section of affiliateLinksSections) { - const loadingIndicator = section.querySelector('.loadingIndicator') - if (loadingIndicator) { - loadingIndicator.classList.add('hidden') - } + return fetch(buildPartialsUrl('AffiliateLinks', { data: dataString })) + .then((resp) => { + if (resp.status !== 200) { + throw new Error( + `Failed to fetch partials. Status code: ${resp.status}`, + ); + } + return resp.json(); + }) + .then((data) => { + const span = document.createElement('span'); + span.innerHTML = data['partials']; + const links = span.firstElementChild; + for (const section of affiliateLinksSections) { + section.replaceWith(links.cloneNode(true)); + } + }) + .catch(() => { + // XXX : Handle errors sensibly + for (const section of affiliateLinksSections) { + const loadingIndicator = section.querySelector('.loadingIndicator'); + if (loadingIndicator) { + loadingIndicator.classList.add('hidden'); + } - const existingRetryAffordance = section.querySelector('.affiliate-links-section__retry') - if (existingRetryAffordance) { - existingRetryAffordance.classList.remove('hidden') - } else { - section.insertAdjacentHTML('afterbegin', renderRetryLink()) - const retryAffordance = section.querySelector('.affiliate-links-section__retry') - retryAffordance.addEventListener('click', () => { - retryAffordance.classList.add('hidden') - getPartials(data, affiliateLinksSections) - }) - } - } - }) + const existingRetryAffordance = section.querySelector( + '.affiliate-links-section__retry', + ); + if (existingRetryAffordance) { + existingRetryAffordance.classList.remove('hidden'); + } else { + section.insertAdjacentHTML('afterbegin', renderRetryLink()); + const retryAffordance = section.querySelector( + '.affiliate-links-section__retry', + ); + retryAffordance.addEventListener('click', () => { + retryAffordance.classList.add('hidden'); + getPartials(data, affiliateLinksSections); + }); + } + } + }); } /** @@ -96,5 +102,5 @@ async function getPartials(data, affiliateLinksSections) { * @returns {string} HTML for a retry link. */ function renderRetryLink() { - return '<span class="affiliate-links-section__retry">Failed to fetch affiliate links. <a href="javascript:;">Retry?</a></span>' + return '<span class="affiliate-links-section__retry">Failed to fetch affiliate links. <a href="javascript:;">Retry?</a></span>'; } diff --git a/openlibrary/plugins/openlibrary/js/autocomplete.js b/openlibrary/plugins/openlibrary/js/autocomplete.js index e2677599fd8..6cbc376592b 100644 --- a/openlibrary/plugins/openlibrary/js/autocomplete.js +++ b/openlibrary/plugins/openlibrary/js/autocomplete.js @@ -12,10 +12,13 @@ import 'jquery-ui-touch-punch'; // this makes drag-to-reorder work on touch devi * @return {string} */ export function highlight(value, term) { - return value.replace( - new RegExp(`(?![^&;]+;)(?!<[^<>]*)(${term.replace(/([\^$()[]\{\}\*\.\+\?\|\\])/gi, '$1')})(?![^<>]*>)(?![^&;]+;)`, 'gi'), - '<strong>$1</strong>' - ); + return value.replace( + new RegExp( + `(?![^&;]+;)(?!<[^<>]*)(${term.replace(/([\^$()[]\{\}\*\.\+\?\|\\])/gi, '$1')})(?![^<>]*>)(?![^&;]+;)`, + 'gi', + ), + '<strong>$1</strong>', + ); } /** @@ -27,334 +30,358 @@ export function highlight(value, term) { * creating a new entry will be added * @return {array} of modified results that are compatible with the jquery autocomplete search suggestions */ -export const mapApiResultsToAutocompleteSuggestions = (results, labelFormatter, addNewFieldTerm) => { - const mapAPIResultToSuggestedItem = (r) => ({ - key: r.key, - label: labelFormatter(r), - value: r.name - }); +export const mapApiResultsToAutocompleteSuggestions = ( + results, + labelFormatter, + addNewFieldTerm, +) => { + const mapAPIResultToSuggestedItem = (r) => ({ + key: r.key, + label: labelFormatter(r), + value: r.name, + }); - // When no results if callback is defined, append a create new entry - if (addNewFieldTerm) { - results.push( - { - name: addNewFieldTerm, - key: '__new__', - value: addNewFieldTerm - } - ); - } - return results.map(mapAPIResultToSuggestedItem); + // When no results if callback is defined, append a create new entry + if (addNewFieldTerm) { + results.push({ + name: addNewFieldTerm, + key: '__new__', + value: addNewFieldTerm, + }); + } + return results.map(mapAPIResultToSuggestedItem); }; export function init() { - /** - * Some extra options for when creating an autocomplete input field - * @typedef {Object} OpenLibraryAutocompleteOptions - * @property {string} endpoint - url to hit for autocomplete results - * @property{(boolean|Function)} [addnew] - when (or whether) to display a "Create new record" - * element in the autocomplete list. The function takes the query and should return a boolean. - * a boolean. - * @property{string} [new_name] - name to display when __new__ selected. Defaults to the query - * @property {boolean} [allow_empty] - whether to allow empty list. Only applies to multi-select - * @property {boolean} [sortable=false] - */ + /** + * Some extra options for when creating an autocomplete input field + * @typedef {Object} OpenLibraryAutocompleteOptions + * @property {string} endpoint - url to hit for autocomplete results + * @property{(boolean|Function)} [addnew] - when (or whether) to display a "Create new record" + * element in the autocomplete list. The function takes the query and should return a boolean. + * a boolean. + * @property{string} [new_name] - name to display when __new__ selected. Defaults to the query + * @property {boolean} [allow_empty] - whether to allow empty list. Only applies to multi-select + * @property {boolean} [sortable=false] + */ - /** - * @private - * @param{HTMLInputElement} _this - input element that will become autocompleting. - * @param{OpenLibraryAutocompleteOptions} ol_ac_opts - * @param{Object} ac_opts - options passed to $.autocomplete; see that. - * @param {Function} ac_opts.formatItem - optional item formatter. Returns a string of HTML for rendering as an item. - * @param {Function} ac_opts.termPreprocessor - optional hook for processing the search term before doing the search - */ - function setup_autocomplete(_this, ol_ac_opts, ac_opts) { - var default_ac_opts = { - minChars: 2, - autoFill: true, - formatItem: item => item.name, - /** - * Adds the ac_over class to the selected autocomplete item - * - * @param {Event} _event (unused) - * @param {Object} ui containing item key - */ - focus: function (_event, ui) { - const $list = $(_this).data('list'); - if ($list) { - $list.find('li') - .removeClass('ac_over') - .filter((_, el) => $(el).data('ui-autocomplete-item').key === ui.item.key) - .addClass('ac_over'); - } - return ac_opts.autoFill; - }, - select: function (_event, ui) { - var item = ui.item; - var $this = $(this); - $this.closest('.ac-input').find('.ac-input__value').val(item.key); - const $preview = $this.closest('.ac-input').find('.ac-input__preview'); - if ($preview.length) { - $preview.html(item.label); - } - setTimeout(function() { - $this.addClass('accept'); - }, 0); - }, - mustMatch: true, - formatMatch: function(item) { return item.name; }, - termPreprocessor: function(term) { return term; } - }; + /** + * @private + * @param{HTMLInputElement} _this - input element that will become autocompleting. + * @param{OpenLibraryAutocompleteOptions} ol_ac_opts + * @param{Object} ac_opts - options passed to $.autocomplete; see that. + * @param {Function} ac_opts.formatItem - optional item formatter. Returns a string of HTML for rendering as an item. + * @param {Function} ac_opts.termPreprocessor - optional hook for processing the search term before doing the search + */ + function setup_autocomplete(_this, ol_ac_opts, ac_opts) { + var default_ac_opts = { + minChars: 2, + autoFill: true, + formatItem: (item) => item.name, + /** + * Adds the ac_over class to the selected autocomplete item + * + * @param {Event} _event (unused) + * @param {Object} ui containing item key + */ + focus: (_event, ui) => { + const $list = $(_this).data('list'); + if ($list) { + $list + .find('li') + .removeClass('ac_over') + .filter( + (_, el) => $(el).data('ui-autocomplete-item').key === ui.item.key, + ) + .addClass('ac_over'); + } + return ac_opts.autoFill; + }, + select: function (_event, ui) { + var item = ui.item; + var $this = $(this); + $this.closest('.ac-input').find('.ac-input__value').val(item.key); + const $preview = $this.closest('.ac-input').find('.ac-input__preview'); + if ($preview.length) { + $preview.html(item.label); + } + setTimeout(() => { + $this.addClass('accept'); + }, 0); + }, + mustMatch: true, + formatMatch: (item) => item.name, + termPreprocessor: (term) => term, + }; - $.widget('custom.autocompleteHTML', $.ui.autocomplete, { - _renderMenu($ul, items) { - $ul.addClass('ac_results').attr('id', this.ulRef); - items.forEach((item) => { - $('<li>') - .data('ui-autocomplete-item', item) - .attr('aria-label', item.value) - .html(item.label) - .appendTo($ul); - }); - // store list so we can add ac_over hover effect in `focus` event - $(_this).data('list', $ul); - } + $.widget('custom.autocompleteHTML', $.ui.autocomplete, { + _renderMenu($ul, items) { + $ul.addClass('ac_results').attr('id', this.ulRef); + items.forEach((item) => { + $('<li>') + .data('ui-autocomplete-item', item) + .attr('aria-label', item.value) + .html(item.label) + .appendTo($ul); }); - const options = $.extend(default_ac_opts, ac_opts); - options.source = function (q, response) { - const term = options.termPreprocessor(q.term); - const params = { - q: term, - limit: options.max - }; - if (location.search.indexOf('lang=') !== -1) { - params.lang = new URLSearchParams(location.search).get('lang'); - } - if (params.q.length < options.minChars) return; - return $.ajax({ - url: ol_ac_opts.endpoint, - data: params - }).then((results) => { - response( - mapApiResultsToAutocompleteSuggestions( - results, - (r) => highlight(options.formatItem(r), term), - ol_ac_opts.addnew === true || - (ol_ac_opts.addnew && ol_ac_opts.addnew(term)) ? (ol_ac_opts.new_name || term) : null - ) - ); - }); - }; - $(_this) - .autocompleteHTML(options) - .on('keypress', function() { - $(this).removeClass('accept').removeClass('reject'); - }); - } + // store list so we can add ac_over hover effect in `focus` event + $(_this).data('list', $ul); + }, + }); + const options = $.extend(default_ac_opts, ac_opts); + options.source = (q, response) => { + const term = options.termPreprocessor(q.term); + const params = { + q: term, + limit: options.max, + }; + if (location.search.indexOf('lang=') !== -1) { + params.lang = new URLSearchParams(location.search).get('lang'); + } + if (params.q.length < options.minChars) return; + return $.ajax({ + url: ol_ac_opts.endpoint, + data: params, + }).then((results) => { + response( + mapApiResultsToAutocompleteSuggestions( + results, + (r) => highlight(options.formatItem(r), term), + ol_ac_opts.addnew === true || + (ol_ac_opts.addnew && ol_ac_opts.addnew(term)) + ? ol_ac_opts.new_name || term + : null, + ), + ); + }); + }; + $(_this) + .autocompleteHTML(options) + .on('keypress', function () { + $(this).removeClass('accept').removeClass('reject'); + }); + } - /** - * @this HTMLElement - the element that contains the different inputs. - * Expects an html structure like: - * <div class="multi-input-autocomplete"> - * <div class="ac-input mia__input"> - * <div class="mia__reorder">≡</div> - * <input class="ac-input__visible" type="text" name="fake_name--0" value="Author 1" /> - * <input class="ac-input__value" type="hidden" name="author--0" value="/authors/OL1234A" /> - * <a class="mia__remove" href="javascript:;">[x]</a> - * </div> - * ... - * < /div> - * @param {Function} input_renderer - ((index, item) -> html_string) render the ith .input. - * @param {OpenLibraryAutocompleteOptions} ol_ac_opts - * @param {Object} ac_opts - options given to override defaults of $.autocomplete; see that. - */ - $.fn.setup_multi_input_autocomplete = function(input_renderer, ol_ac_opts, ac_opts) { - /** @type {JQuery<HTMLElement>} */ - var container = $(this); + /** + * @this HTMLElement - the element that contains the different inputs. + * Expects an html structure like: + * <div class="multi-input-autocomplete"> + * <div class="ac-input mia__input"> + * <div class="mia__reorder">≡</div> + * <input class="ac-input__visible" type="text" name="fake_name--0" value="Author 1" /> + * <input class="ac-input__value" type="hidden" name="author--0" value="/authors/OL1234A" /> + * <a class="mia__remove" href="javascript:;">[x]</a> + * </div> + * ... + * < /div> + * @param {Function} input_renderer - ((index, item) -> html_string) render the ith .input. + * @param {OpenLibraryAutocompleteOptions} ol_ac_opts + * @param {Object} ac_opts - options given to override defaults of $.autocomplete; see that. + */ + $.fn.setup_multi_input_autocomplete = function ( + input_renderer, + ol_ac_opts, + ac_opts, + ) { + /** @type {JQuery<HTMLElement>} */ + var container = $(this); - // first let's init any pre-existing inputs - container.find('.ac-input__visible').each(function() { - setup_autocomplete(this, ol_ac_opts, ac_opts); - }); - const allow_empty = ol_ac_opts.allow_empty; + // first let's init any pre-existing inputs + container.find('.ac-input__visible').each(function () { + setup_autocomplete(this, ol_ac_opts, ac_opts); + }); + const allow_empty = ol_ac_opts.allow_empty; + + function update_visible() { + if (allow_empty || container.find('.mia__input').length > 1) { + container.find('.mia__remove').show(); + } else { + container.find('.mia__remove').hide(); + } + } - function update_visible() { - if (allow_empty || container.find('.mia__input').length > 1) { - container.find('.mia__remove').show(); + function update_indices() { + container.find('.mia__input').each(function (index) { + $(this) + .find('.mia__index') + .each(function () { + $(this).text( + $(this) + .text() + .replace(/\d+/, index + 1), + ); + }); + $(this) + .find('[name]') + .each(function () { + // this won't behave nicely with nested numeric things, if that ever happens + if ($(this).attr('name').match(/\d+/)?.length > 1) { + throw new Error('nested numeric names not supported'); } - else { - container.find('.mia__remove').hide(); + $(this).attr('name', $(this).attr('name').replace(/\d+/, index)); + if ($(this).attr('id')) { + $(this).attr('id', $(this).attr('id').replace(/\d+/, index)); } - } + }); + }); + } - function update_indices() { - container.find('.mia__input').each(function(index) { - $(this).find('.mia__index').each(function () { - $(this).text($(this).text().replace(/\d+/, index + 1)); - }); - $(this).find('[name]').each(function() { - // this won't behave nicely with nested numeric things, if that ever happens - if ($(this).attr('name').match(/\d+/)?.length > 1) { - throw new Error('nested numeric names not supported'); - } - $(this).attr('name', $(this).attr('name').replace(/\d+/, index)); - if ($(this).attr('id')) { - $(this).attr('id', $(this).attr('id').replace(/\d+/, index)); - } - }); - }); - } + update_visible(); + if (ol_ac_opts.sortable) { + container.sortable({ + handle: '.mia__reorder', + items: '.mia__input', + update: update_indices, + cancel: '.mia__move, .mia__remove', + }); + } + + container.on('click', '.mia__remove', function () { + if (allow_empty || container.find('.mia__input').length > 1) { + $(this).closest('.mia__input').remove(); update_visible(); + update_indices(); + } + }); - if (ol_ac_opts.sortable) { - container.sortable({ - handle: '.mia__reorder', - items: '.mia__input', - update: update_indices, - cancel: '.mia__move, .mia__remove' - }); - } + // Add move button functionality + container.on('click', '.mia__move', function (event) { + event.preventDefault(); + const $currentItem = $(this).closest('.mia__input'); + const $allItems = container.find('.mia__input'); + const currentIndex = $allItems.index($currentItem); + const currentPosition = currentIndex + 1; // 1-based position for user display + const totalItems = $allItems.length; - container.on('click', '.mia__remove', function() { - if (allow_empty || container.find('.mia__input').length > 1) { - $(this).closest('.mia__input').remove(); - update_visible(); - update_indices(); - } - }); + // Create a clear message showing current position + const message = `Enter the new position (1-${totalItems}):`; - // Add move button functionality - container.on('click', '.mia__move', function(event) { - event.preventDefault(); - const $currentItem = $(this).closest('.mia__input'); - const $allItems = container.find('.mia__input'); - const currentIndex = $allItems.index($currentItem); - const currentPosition = currentIndex + 1; // 1-based position for user display - const totalItems = $allItems.length; + const userInput = prompt(message); - // Create a clear message showing current position - const message = `Enter the new position (1-${totalItems}):`; + // Handle cancellation + if (userInput === null) { + return; + } - const userInput = prompt(message); + const newPosition = parseFloat(userInput.trim()); - // Handle cancellation - if (userInput === null) { - return; - } + // Validate the input + if (isNaN(newPosition) || newPosition < 1 || newPosition > totalItems) { + alert(`Please enter a valid number between 1 and ${totalItems}.`); + return; + } - const newPosition = parseFloat(userInput.trim()); + // Check if it's the same position + if (newPosition === currentPosition) { + alert('Item is already at that position.'); + return; + } - // Validate the input - if (isNaN(newPosition) || newPosition < 1 || newPosition > totalItems) { - alert(`Please enter a valid number between 1 and ${totalItems}.`); - return; - } + // Perform the move + const newIndex = newPosition - 1; // Convert to 0-based index - // Check if it's the same position - if (newPosition === currentPosition) { - alert('Item is already at that position.'); - return; - } + if (newIndex < currentIndex) { + $currentItem.insertBefore($allItems.eq(newIndex)); + } else { + $currentItem.insertAfter($allItems.eq(newIndex)); + } - // Perform the move - const newIndex = newPosition - 1; // Convert to 0-based index + // Update indices after move + update_indices(); + }); - if (newIndex < currentIndex) { - $currentItem.insertBefore($allItems.eq(newIndex)); - } else { - $currentItem.insertAfter($allItems.eq(newIndex)); - } + container.on('click', '.mia__add', (event) => { + var next_index, new_input; + event.preventDefault(); - // Update indices after move - update_indices(); - }); + next_index = container.find('.mia__input').length; + new_input = $(input_renderer(next_index, { key: '', name: '' })); + new_input.insertBefore(container.find('.mia__add')); + setup_autocomplete( + new_input.find('.ac-input__visible')[0], + ol_ac_opts, + ac_opts, + ); + update_visible(); + }); + }; - container.on('click', '.mia__add', function(event) { - var next_index, new_input; - event.preventDefault(); - - next_index = container.find('.mia__input').length; - new_input = $(input_renderer(next_index, {key: '', name: ''})); - new_input.insertBefore(container.find('.mia__add')); - setup_autocomplete( - new_input.find('.ac-input__visible')[0], - ol_ac_opts, - ac_opts); - update_visible(); - }); - }; + /** + * @this HTMLElement - the element that contains the input. + * @param {string} autocomplete_selector - selector to find the input element use for autocomplete. + * @param {OpenLibraryAutocompleteOptions} ol_ac_opts + * @param {Object} ac_opts - options given to override defaults of $.autocomplete; see that. + */ + $.fn.setup_csv_autocomplete = function ( + autocomplete_selector, + ol_ac_opts, + ac_opts, + ) { + const container = $(this); + const dataConfig = JSON.parse(container[0].dataset.config); /** - * @this HTMLElement - the element that contains the input. - * @param {string} autocomplete_selector - selector to find the input element use for autocomplete. - * @param {OpenLibraryAutocompleteOptions} ol_ac_opts - * @param {Object} ac_opts - options given to override defaults of $.autocomplete; see that. + * Converts a csv string to an array of strings + * + * Eg + * - "a, b, c" -> ["a", "b", "c"] + * - 'a, "b, b", c' -> ["a", "b, b", "c"] + * @param {string} val + * @returns {string[]} */ - $.fn.setup_csv_autocomplete = function(autocomplete_selector, ol_ac_opts, ac_opts) { - const container = $(this); - const dataConfig = JSON.parse(container[0].dataset.config); - - /** - * Converts a csv string to an array of strings - * - * Eg - * - "a, b, c" -> ["a", "b", "c"] - * - 'a, "b, b", c' -> ["a", "b, b", "c"] - * @param {string} val - * @returns {string[]} - */ - function splitField(val) { - const m = val.match(/("[^"]+"|[^,"]+)/g); - if (!m) { - throw new Error('Invalid CSV'); - } + function splitField(val) { + const m = val.match(/("[^"]+"|[^,"]+)/g); + if (!m) { + throw new Error('Invalid CSV'); + } - return m - .map(s => s.trim().replace(/^"(.*)"$/, '$1')) - .filter(s => s); - } + return m.map((s) => s.trim().replace(/^"(.*)"$/, '$1')).filter((s) => s); + } - function joinField(vals) { - const escaped = vals.map(val => (val.includes(',')) ? `"${val}"` : val); - return escaped.join(', '); - } + function joinField(vals) { + const escaped = vals.map((val) => (val.includes(',') ? `"${val}"` : val)); + return escaped.join(', '); + } - const default_ac_opts = { - minChars: 2, - max: 25, - matchSubset: false, - autoFill: false, - position: { my: 'right top', at: 'right bottom' }, - termPreprocessor: function(subject_string) { - const terms = splitField(subject_string); - if (terms.length !== dataConfig.data.length) { - return terms.pop(); - } else { - $('ul.ui-autocomplete').hide(); - return ''; - } - }, - select: function(event, ui) { - const terms = splitField(this.value); - terms.splice(terms.length - 1, 1, ui.item.value); - this.value = `${joinField(terms)}, `; - dataConfig.data.push(ui.item.value); - container[0].dataset.config = JSON.stringify(dataConfig); - $(this).trigger('input'); - return false; - }, - response: function(event, ui) { - /* Remove any entries already on the list */ - const terms = splitField(this.value); - ui.content.splice(0, ui.content.length, - ...ui.content.filter(record => !terms.includes(record.value))); - }, + const default_ac_opts = { + minChars: 2, + max: 25, + matchSubset: false, + autoFill: false, + position: { my: 'right top', at: 'right bottom' }, + termPreprocessor: (subject_string) => { + const terms = splitField(subject_string); + if (terms.length !== dataConfig.data.length) { + return terms.pop(); + } else { + $('ul.ui-autocomplete').hide(); + return ''; } - - container.find(autocomplete_selector).each(function() { - const options = $.extend(default_ac_opts, ac_opts); - setup_autocomplete(this, ol_ac_opts, options); - }); + }, + select: function (event, ui) { + const terms = splitField(this.value); + terms.splice(terms.length - 1, 1, ui.item.value); + this.value = `${joinField(terms)}, `; + dataConfig.data.push(ui.item.value); + container[0].dataset.config = JSON.stringify(dataConfig); + $(this).trigger('input'); + return false; + }, + response: function (event, ui) { + /* Remove any entries already on the list */ + const terms = splitField(this.value); + ui.content.splice( + 0, + ui.content.length, + ...ui.content.filter((record) => !terms.includes(record.value)), + ); + }, }; + + container.find(autocomplete_selector).each(function () { + const options = $.extend(default_ac_opts, ac_opts); + setup_autocomplete(this, ol_ac_opts, options); + }); + }; } diff --git a/openlibrary/plugins/openlibrary/js/banner/index.js b/openlibrary/plugins/openlibrary/js/banner/index.js index 1a2d63b213e..3dad5e16486 100644 --- a/openlibrary/plugins/openlibrary/js/banner/index.js +++ b/openlibrary/plugins/openlibrary/js/banner/index.js @@ -8,19 +8,22 @@ * @param {Function} successCallback */ function setBannerCookie(cookieName, cookieDurationDays, successCallback) { - $.ajax({ - type: 'POST', - url: '/hide_banner', - data: JSON.stringify({'cookie-name': cookieName, 'cookie-duration-days': cookieDurationDays}), - contentType: 'application/json', - dataType: 'json', + $.ajax({ + type: 'POST', + url: '/hide_banner', + data: JSON.stringify({ + 'cookie-name': cookieName, + 'cookie-duration-days': cookieDurationDays, + }), + contentType: 'application/json', + dataType: 'json', - beforeSend: function(xhr) { - xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.setRequestHeader('Accept', 'application/json'); - }, - success: successCallback - }); + beforeSend: (xhr) => { + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('Accept', 'application/json'); + }, + success: successCallback, + }); } /** @@ -29,16 +32,18 @@ function setBannerCookie(cookieName, cookieDurationDays, successCallback) { * @param {NodeList<HTMLElement>} banners */ export function initDismissibleBanners(banners) { - for (const banner of banners) { - const cookieName = banner.dataset.cookieName - const cookieDurationDays = banner.dataset.cookieDurationDays + for (const banner of banners) { + const cookieName = banner.dataset.cookieName; + const cookieDurationDays = banner.dataset.cookieDurationDays; - const dismissButton = banner.querySelector('.page-banner--dismissable-close') - dismissButton.addEventListener('click', () => { - const successCallback = () => { - banner.remove() - } - setBannerCookie(cookieName, cookieDurationDays, successCallback) - }) - } + const dismissButton = banner.querySelector( + '.page-banner--dismissable-close', + ); + dismissButton.addEventListener('click', () => { + const successCallback = () => { + banner.remove(); + }; + setBannerCookie(cookieName, cookieDurationDays, successCallback); + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/book-page-lists.js b/openlibrary/plugins/openlibrary/js/book-page-lists.js index eb16d7c0bf8..05eeb459d77 100644 --- a/openlibrary/plugins/openlibrary/js/book-page-lists.js +++ b/openlibrary/plugins/openlibrary/js/book-page-lists.js @@ -1,5 +1,5 @@ -import { buildPartialsUrl } from './utils' -import { initAsyncFollowing } from './following' +import { initAsyncFollowing } from './following'; +import { buildPartialsUrl } from './utils'; /** * Initializes lazy-loading the "Lists" section of Open Library book pages. @@ -7,57 +7,60 @@ import { initAsyncFollowing } from './following' * @param elem {HTMLElement} Container for book page lists section */ export function initListsSection(elem) { - // Show loading indicator - const loadingIndicator = elem.querySelector('.loadingIndicator') - loadingIndicator.classList.remove('hidden') + // Show loading indicator + const loadingIndicator = elem.querySelector('.loadingIndicator'); + loadingIndicator.classList.remove('hidden'); - const ids = JSON.parse(elem.dataset.ids) + const ids = JSON.parse(elem.dataset.ids); - const intersectionObserver = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - // Unregister intersection listener - intersectionObserver.unobserve(entries[0].target) - fetchPartials(ids.work, ids.edition) - .then((resp) => { - // Check response code, continue if not 4XX or 5XX - return resp.json() - }) - .then((data) => { - // Replace loading indicator with partials - const listSection = loadingIndicator.parentElement - const fragment = document.createDocumentFragment() + const intersectionObserver = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + // Unregister intersection listener + intersectionObserver.unobserve(entries[0].target); + fetchPartials(ids.work, ids.edition) + .then((resp) => { + // Check response code, continue if not 4XX or 5XX + return resp.json(); + }) + .then((data) => { + // Replace loading indicator with partials + const listSection = loadingIndicator.parentElement; + const fragment = document.createDocumentFragment(); - for (const htmlString of data.partials) { - const template = document.createElement('template') - template.innerHTML = htmlString - fragment.append(...template.content.childNodes) - } + for (const htmlString of data.partials) { + const template = document.createElement('template'); + template.innerHTML = htmlString; + fragment.append(...template.content.childNodes); + } - listSection.replaceChildren(fragment) + listSection.replaceChildren(fragment); - // Show "See All" link - if (data.hasLists) { - const showAllLink = elem.querySelector('.lists-heading a') - if (showAllLink) { - showAllLink.classList.remove('hidden') - } - } - // Initialize private buttons after content is loaded - initPrivateButtonsAfterLoad(listSection) + // Show "See All" link + if (data.hasLists) { + const showAllLink = elem.querySelector('.lists-heading a'); + if (showAllLink) { + showAllLink.classList.remove('hidden'); + } + } + // Initialize private buttons after content is loaded + initPrivateButtonsAfterLoad(listSection); - const followForms = listSection.querySelectorAll('.follow-form'); - initAsyncFollowing(followForms) - }) - } - }) - }, { - root: null, - rootMargin: '200px', - threshold: 0 - }) + const followForms = listSection.querySelectorAll('.follow-form'); + initAsyncFollowing(followForms); + }); + } + }); + }, + { + root: null, + rootMargin: '200px', + threshold: 0, + }, + ); - intersectionObserver.observe(elem) + intersectionObserver.observe(elem); } /** @@ -65,23 +68,26 @@ export function initListsSection(elem) { * @param {HTMLElement} container - The container that now has the loaded content */ function initPrivateButtonsAfterLoad(container) { - const privateButtons = container.querySelectorAll('.list-follow-card__private-button') - if (privateButtons.length > 0) { - import(/* webpackChunkName: "private-buttons" */ './private-button') - .then(module => { - module.initPrivateButtons(privateButtons) - }) - } + const privateButtons = container.querySelectorAll( + '.list-follow-card__private-button', + ); + if (privateButtons.length > 0) { + import(/* webpackChunkName: "private-buttons" */ './private-button').then( + (module) => { + module.initPrivateButtons(privateButtons); + }, + ); + } } async function fetchPartials(workId, editionId) { - const params = {} - if (workId) { - params.workId = workId - } - if (editionId) { - params.editionId = editionId - } + const params = {}; + if (workId) { + params.workId = workId; + } + if (editionId) { + params.editionId = editionId; + } - return fetch(buildPartialsUrl('BPListsSection', params)); + return fetch(buildPartialsUrl('BPListsSection', params)); } diff --git a/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js b/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js index 35333ed4378..57cdd231ef1 100644 --- a/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js +++ b/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js @@ -4,26 +4,26 @@ * @param {NodeList<HTMLElement>} crumbs - NodeList of breadcrumb select elements. */ export function initBreadcrumbSelect(crumbs) { - const allowedKeys = new Set(['Tab', 'Enter', ' ']); - const preventedKeys = new Set(['ArrowUp', 'ArrowDown']); - // watch crumbs for changes, - // ensures it's a full value change, not a user exploring options via keyboard - function handleNavEvents(nav) { - let ignoreChange = false; + const allowedKeys = new Set(['Tab', 'Enter', ' ']); + const preventedKeys = new Set(['ArrowUp', 'ArrowDown']); + // watch crumbs for changes, + // ensures it's a full value change, not a user exploring options via keyboard + function handleNavEvents(nav) { + let ignoreChange = false; - nav.addEventListener('change', () => { - if (ignoreChange) return; - window.location = nav.value; - }); + nav.addEventListener('change', () => { + if (ignoreChange) return; + window.location = nav.value; + }); - nav.addEventListener('keydown', ({ key }) => { - if (preventedKeys.has(key)) { - ignoreChange = true; - } else if (allowedKeys.has(key)) { - ignoreChange = false; - } - }); - } + nav.addEventListener('keydown', ({ key }) => { + if (preventedKeys.has(key)) { + ignoreChange = true; + } else if (allowedKeys.has(key)) { + ignoreChange = false; + } + }); + } - crumbs.forEach(handleNavEvents); + crumbs.forEach(handleNavEvents); } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger.js index a2f3697a598..f25968137bf 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger.js @@ -2,12 +2,11 @@ * Defines functionality related to the ILE's Bulk Tagger tool. * @module ile/BulkTagger */ -import debounce from 'lodash/debounce' - +import debounce from 'lodash/debounce'; +import { FadingToast } from '../Toast'; import { MenuOption, MenuOptionState } from './BulkTagger/MenuOption'; import { SortedMenuOptionContainer } from './BulkTagger/SortedMenuOptionContainer'; -import { Tag } from './models/Tag' -import { FadingToast } from '../Toast' +import { Tag } from './models/Tag'; /** * Maximum amount of search result to be returned by subject @@ -30,617 +29,705 @@ const COLLECTION_PREFIX = 'collection:'; * @class */ export class BulkTagger { + /** + * Sets references to key Bulk Tagger affordances. + * + * @param {HTMLElement} bulkTagger Reference to root element of the Bulk Tagger + */ + constructor(bulkTagger) { /** - * Sets references to key Bulk Tagger affordances. - * - * @param {HTMLElement} bulkTagger Reference to root element of the Bulk Tagger + * Reference to root Bulk Tagger element. + * @member {HTMLFormElement} */ - constructor(bulkTagger) { - /** - * Reference to root Bulk Tagger element. - * @member {HTMLFormElement} - */ - this.rootElement = bulkTagger - - /** - * Reference to the Bulk Tagger's subject search box. - * @member {HTMLInputElement} - */ - this.searchInput = bulkTagger.querySelector('.subjects-search-input') - - /** - * Menu option container that holds options for staged tags and tags - * that already exist on one or more selected works. - * - * @member {SortedMenuOptionContainer} - */ - this.selectedOptionsContainer - - /** - * Menu option container that holds options representing search results. - * - * @member {SortedMenuOptionContainer} - */ - this.searchResultsOptionsContainer - - /** - * Reference to the element which contains the affordance that creates new subjects. - * @member {HTMLElement} - */ - this.createSubjectElem = bulkTagger.querySelector('.search-subject-row-name') - - /** - * Element which displays the subject name within the "create new tag" affordance. - * @member {HTMLElement} - */ - this.subjectNameElem = this.createSubjectElem.querySelector('.subject-name') - - /** - * Reference to input which holds the subjects to be batch added. - * @member {HTMLInputElement} - */ - this.addSubjectsInput = bulkTagger.querySelector('input[name=tags_to_add]') - - /** - * Input which contains the subjects to be batch removed. - * @member {HTMLInputElement} - */ - this.removeSubjectsInput = bulkTagger.querySelector('input[name=tags_to_remove]') - - /** - * Reference to hidden input which holds a comma-separated list of work OLIDs - * @member {HTMLInputElement} - */ - this.selectedWorksInput = bulkTagger.querySelector('input[name=work_ids]') - - /** - * Reference to the bulk tagger form's submit button. - * - * @member {HTMLButtonElement} - */ - this.submitButton = this.rootElement.querySelector('.bulk-tagging-submit') - - /** - * Stores works' subjects that have been fetched from the server. - * - * Keys to the map are work IDs. - * @member {Map<String, Array<Tag>>} - */ - this.existingSubjects = new Map() - - /** - * Array containing OLIDs of each selected work. - * - * @member {Array<String>} - */ - this.selectedWorks = [] - - /** - * Tags staged for adding to all selected works. - * - * @member {Array<Tag>} - */ - this.tagsToAdd = [] - - /** - * Tags staged for removal from all selected works. - * - * @member {Array<Tag>} - */ - this.tagsToRemove = [] - - /** - * `true` if the bulk tagger appears on a book page. - * - * @type {boolean} - */ - this.isBookPageEdit = false - } + this.rootElement = bulkTagger; /** - * Initialized the menu option containers, and adds event listeners to the Bulk Tagger. + * Reference to the Bulk Tagger's subject search box. + * @member {HTMLInputElement} */ - initialize() { - // Create sorted menu option containers: - this.selectedOptionsContainer = new SortedMenuOptionContainer(this.rootElement.querySelector('.selected-tag-subjects')) - this.searchResultsOptionsContainer = new SortedMenuOptionContainer(this.rootElement.querySelector('.subjects-search-results')) - - // Add "hide menu" functionality: - const closeFormButton = this.rootElement.querySelector('.close-bulk-tagging-form') - closeFormButton.addEventListener('click', () => { - this.hideTaggingMenu() - }) - - // Add input listener to subject search box: - const debouncedInputChangeHandler = debounce(this.onSearchInputChange.bind(this), 500) - this.searchInput.addEventListener('input', () => { - const searchTerm = this.searchInput.value.trim(); - debouncedInputChangeHandler(searchTerm) - }); - - // Prevent redirect on batch subject submission: - this.submitButton.addEventListener('click', (event) => { - event.preventDefault() - this.submitBatch() - }) - - // Add click listeners to "create subject" options: - const createSubjectButtons = this.rootElement.querySelectorAll('.subject-type-option') - for (const elem of createSubjectButtons) { - elem.addEventListener('click', () => this.onCreateTag(new Tag(this.searchInput.value, elem.dataset.tagType))) - } - } + this.searchInput = bulkTagger.querySelector('.subjects-search-input'); /** - * Hides the Bulk Tagger. + * Menu option container that holds options for staged tags and tags + * that already exist on one or more selected works. + * + * @member {SortedMenuOptionContainer} */ - hideTaggingMenu() { - this.rootElement.classList.add('hidden') - this.rootElement.dispatchEvent(new CustomEvent('option-hidden')) - } + this.selectedOptionsContainer; /** - * Displays the Bulk Tagger. + * Menu option container that holds options representing search results. + * + * @member {SortedMenuOptionContainer} */ - showTaggingMenu() { - this.rootElement.classList.remove('hidden') - } + this.searchResultsOptionsContainer; /** - * Updates the BulkTagger when works are selected. - * - * Stores given array in `selectedWorks`, fetches the - * existing tags for each given work, and updates the view with - * the existing tags. - * - * @param {Array<String>} workIds + * Reference to the element which contains the affordance that creates new subjects. + * @member {HTMLElement} */ - async updateWorks(workIds) { - this.showLoadingIndicator() - - this.selectedWorks = workIds - - await this.fetchSubjectsForWorks(workIds) - this.updateMenuOptions() - - this.hideLoadingIndicator() - } + this.createSubjectElem = bulkTagger.querySelector( + '.search-subject-row-name', + ); /** - * Hides all menu options and shows a loading indicator. + * Element which displays the subject name within the "create new tag" affordance. + * @member {HTMLElement} */ - showLoadingIndicator() { - const menuOptionContainer = this.rootElement.querySelector('.selection-container') - menuOptionContainer.classList.add('hidden') - const loadingIndicator = this.rootElement.querySelector('.loading-indicator') - loadingIndicator.classList.remove('hidden') - } + this.subjectNameElem = + this.createSubjectElem.querySelector('.subject-name'); /** - * Hides the loading indicator and shows all menu options. + * Reference to input which holds the subjects to be batch added. + * @member {HTMLInputElement} */ - hideLoadingIndicator() { - const loadingIndicator = this.rootElement.querySelector('.loading-indicator') - loadingIndicator.classList.add('hidden') - const menuOptionContainer = this.rootElement.querySelector('.selection-container') - menuOptionContainer.classList.remove('hidden') - } + this.addSubjectsInput = bulkTagger.querySelector('input[name=tags_to_add]'); /** - * Fetches and stores subject information for the given work OLIDs. - * - * If we already have fetched the data for a work ID, we do not fetch it - * again. - * @param {Array<String>} workIds + * Input which contains the subjects to be batch removed. + * @member {HTMLInputElement} */ - async fetchSubjectsForWorks(workIds) { - const worksWithMissingSubjects = workIds.filter(id => !this.existingSubjects.has(id)) - - await Promise.all(worksWithMissingSubjects.map(async (id) => { - // XXX : Too many network requests --- use bulk search if/when it is available - await this.fetchWork(id) - // XXX : Handle failures - .then(response => response.json()) - .then(data => { - const entry = { - subjects: data.subjects || [], - subject_people: data.subject_people || [], - subject_places: data.subject_places || [], - subject_times: data.subject_times || [] - } - // Move collection labels from `subjects` to `collections` - entry.collections = entry.subjects.filter((label) => label.startsWith(COLLECTION_PREFIX)) - entry.subjects = entry.subjects.filter((label) => !entry.collections.includes(label)) - for (let i = 0; i < entry.collections.length; ++i) { - // Remove collection prefix from label - entry.collections[i] = entry.collections[i].substring(COLLECTION_PREFIX.length) - } - if (!this.existingSubjects.has(id)) { - this.existingSubjects.set(id, []) - } - // `key` is the type, `value` is the array of tag names - for (const [key, value] of Object.entries(entry)) { - for (const tagName of value) { - this.existingSubjects.get(id).push(new Tag(tagName, key)) - } - } - }) - })) - } + this.removeSubjectsInput = bulkTagger.querySelector( + 'input[name=tags_to_remove]', + ); /** - * Creates `MenuOption` affordances for all staged tags, and each existing tag that - * was fetched from the server. + * Reference to hidden input which holds a comma-separated list of work OLIDs + * @member {HTMLInputElement} */ - updateMenuOptions() { - this.selectedOptionsContainer.clear() - - // Add staged tags first, then add all other missing subjects. - // This order prevents unnecessary state mangement steps. - - // Create menu options for each staged tag: - this.tagsToAdd.forEach((tag) => { - const menuOption = new MenuOption(tag, MenuOptionState.ALL_TAGGED, this.selectedWorks.length) - menuOption.initialize() - this.selectedOptionsContainer.add(menuOption) - }) - - this.tagsToRemove.forEach((tag) => { - const menuOption = new MenuOption(tag, MenuOptionState.NONE_TAGGED, 0) - menuOption.initialize() - this.selectedOptionsContainer.add(menuOption) - }) - - // Create menu options for each existing tag: - const stagedMenuOptions = [] - for (const workOlid of this.selectedWorks) { - const existingTagsForWork = this.existingSubjects.get(workOlid) - for (const tag of existingTagsForWork) { - - // Does an option for this tag already exist in the container? - if (!this.selectedOptionsContainer.containsOptionWithTag(tag)) { - - // Have we already created and staged a menu option for this tag? - const stagedOption = stagedMenuOptions.find((option) => option.tag.equals(tag)) - if (stagedOption) { - stagedOption.taggedWorksCount++ - if (stagedOption.taggedWorksCount === this.selectedWorks.length) { - stagedOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED) - } - } else { - const state = this.selectedWorks.length === 1 ? MenuOptionState.ALL_TAGGED : MenuOptionState.SOME_TAGGED - const newOption = new MenuOption(tag, state, 1) - newOption.initialize() - stagedMenuOptions.push(newOption) - } - } - } - } - - stagedMenuOptions.forEach((option) => option.rootElement.addEventListener('click', () => this.onMenuOptionClick(option))) - this.selectedOptionsContainer.add(...stagedMenuOptions) - } + this.selectedWorksInput = bulkTagger.querySelector('input[name=work_ids]'); /** - * Click handler for menu options. - * - * Changes the menu option's state, and stages the option's tag - * for addition or removal. + * Reference to the bulk tagger form's submit button. * - * @param {MenuOption} menuOption The clicked menu option + * @member {HTMLButtonElement} */ - onMenuOptionClick(menuOption) { - let stagedTagIndex - switch (menuOption.optionState) { - case MenuOptionState.NONE_TAGGED: - stagedTagIndex = this.tagsToRemove.findIndex((tag) => (tag.tagName === menuOption.tag.tagName && tag.tagType === menuOption.tag.tagType)) - if (stagedTagIndex > -1) { - this.tagsToRemove.splice(stagedTagIndex, 1) - } - this.tagsToAdd.push(menuOption.tag) - menuOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED) - break - case MenuOptionState.SOME_TAGGED: - this.tagsToAdd.push(menuOption.tag) - menuOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED) - break - case MenuOptionState.ALL_TAGGED: - stagedTagIndex = this.tagsToAdd.findIndex((tag) => (tag.tagName === menuOption.tag.tagName && tag.tagType === menuOption.tag.tagType)) - if (stagedTagIndex > -1) { - this.tagsToAdd.splice(stagedTagIndex, 1) - } - this.tagsToRemove.push(menuOption.tag) - menuOption.updateMenuOptionState(MenuOptionState.NONE_TAGGED) - break - } - - menuOption.stage() - this.updateSubmitButtonState() - } + this.submitButton = this.rootElement.querySelector('.bulk-tagging-submit'); /** - * Disables or enables form submission button. + * Stores works' subjects that have been fetched from the server. * - * Button is enabled if there are any tags staged for submission. - * Otherwise, the button will be disabled. + * Keys to the map are work IDs. + * @member {Map<String, Array<Tag>>} */ - updateSubmitButtonState() { - const stagedTagCount = this.tagsToAdd.length + this.tagsToRemove.length - - if (stagedTagCount > 0) { - this.submitButton.removeAttribute('disabled') - } else { - this.submitButton.setAttribute('disabled', 'true') - } - } + this.existingSubjects = new Map(); /** - * Fetches a work from OL. + * Array containing OLIDs of each selected work. * - * @param {String} workOlid + * @member {Array<String>} */ - async fetchWork(workOlid) { - return fetch(`/works/${workOlid}.json`) - } + this.selectedWorks = []; /** - * Performs a subject search for the given search term, and updates - * the Bulk Tagger with the results. + * Tags staged for adding to all selected works. * - * If the given search term, when trimmed, is an empty string, this - * instead hides the "create subject" affordance. - * - * @param {String} searchTerm + * @member {Array<Tag>} */ - onSearchInputChange(searchTerm) { - // Remove search results that are not selected: - const resultsToRemove = this.searchResultsOptionsContainer.sortedMenuOptions.filter((option) => option.optionState !== MenuOptionState.ALL_TAGGED) - this.searchResultsOptionsContainer.remove(...resultsToRemove) - - // Hide menu options that do not begin with the search term (case-insensitive) - const trimmedSearchTerm = searchTerm.trim() - - const allOptions = this.selectedOptionsContainer.sortedMenuOptions.concat(this.searchResultsOptionsContainer.sortedMenuOptions) - allOptions.forEach((option) => { - if (option.tag.tagName.toLowerCase().startsWith(trimmedSearchTerm.toLowerCase())) { - option.show() - } else { - option.hide() - } - }) - - if (trimmedSearchTerm !== '') { // Perform search: - fetch(`/search/subjects.json?q=${searchTerm}&limit=${maxDisplayResults}`) - .then((response) => response.json()) - .then((data) => { - if (data['docs'].length !== 0) { - for (const obj of data['docs']) { - const tag = new Tag(obj.name, null, obj['subject_type']) - - if (!this.selectedOptionsContainer.containsOptionWithTag(tag) && !this.searchResultsOptionsContainer.containsOptionWithTag(tag)) { - const menuOption = this.createSearchMenuOption(tag) - this.searchResultsOptionsContainer.add(menuOption) - } - } - } - - // Update and show create subject affordance - this.updateAndShowNewSubjectAffordance(trimmedSearchTerm) - }); - } else { - // Hide create subject affordance - this.createSubjectElem.classList.add('hidden') - } - } + this.tagsToAdd = []; /** - * Updates the "create subject" affordance with the given subject name, - * and shows the affordance if it is hidden. + * Tags staged for removal from all selected works. * - * @param {String} subjectName The name of the subject + * @member {Array<Tag>} */ - updateAndShowNewSubjectAffordance(subjectName) { - this.subjectNameElem.innerText = subjectName - this.createSubjectElem.classList.remove('hidden') - } + this.tagsToRemove = []; /** - * Creates, hydrates, and returns a new menu option based on a search result. - * - * In addition to the usual click listener, the newly created element will have an - * `option-hidden` event handler, which will move any selected menu option to the - * selected options container whenever the menu option is hidden. This is done to - * maintain the correct menu option ordering when search results are updated. + * `true` if the bulk tagger appears on a book page. * - * Precondition: Menu option representing the given tag is not attached to the DOM. - * - * @param {Tag} tag - * @returns {MenuOption} A menu option representing the given tag + * @type {boolean} */ - createSearchMenuOption(tag) { - const menuOption = new MenuOption(tag, MenuOptionState.NONE_TAGGED, 0) - menuOption.initialize() - menuOption.rootElement.addEventListener('click', () => this.onMenuOptionClick(menuOption)) - menuOption.rootElement.addEventListener('option-hidden', () => { - // Move to selected menu options container if selected and hidden - if (menuOption.optionState === MenuOptionState.ALL_TAGGED) { - if (menuOption.rootElement.parentElement === this.searchResultsOptionsContainer.rootElement) { - this.searchResultsOptionsContainer.remove(menuOption) - this.selectedOptionsContainer.add(menuOption) - } + this.isBookPageEdit = false; + } + + /** + * Initialized the menu option containers, and adds event listeners to the Bulk Tagger. + */ + initialize() { + // Create sorted menu option containers: + this.selectedOptionsContainer = new SortedMenuOptionContainer( + this.rootElement.querySelector('.selected-tag-subjects'), + ); + this.searchResultsOptionsContainer = new SortedMenuOptionContainer( + this.rootElement.querySelector('.subjects-search-results'), + ); + + // Add "hide menu" functionality: + const closeFormButton = this.rootElement.querySelector( + '.close-bulk-tagging-form', + ); + closeFormButton.addEventListener('click', () => { + this.hideTaggingMenu(); + }); + + // Add input listener to subject search box: + const debouncedInputChangeHandler = debounce( + this.onSearchInputChange.bind(this), + 500, + ); + this.searchInput.addEventListener('input', () => { + const searchTerm = this.searchInput.value.trim(); + debouncedInputChangeHandler(searchTerm); + }); + + // Prevent redirect on batch subject submission: + this.submitButton.addEventListener('click', (event) => { + event.preventDefault(); + this.submitBatch(); + }); + + // Add click listeners to "create subject" options: + const createSubjectButtons = this.rootElement.querySelectorAll( + '.subject-type-option', + ); + for (const elem of createSubjectButtons) { + elem.addEventListener('click', () => + this.onCreateTag(new Tag(this.searchInput.value, elem.dataset.tagType)), + ); + } + } + + /** + * Hides the Bulk Tagger. + */ + hideTaggingMenu() { + this.rootElement.classList.add('hidden'); + this.rootElement.dispatchEvent(new CustomEvent('option-hidden')); + } + + /** + * Displays the Bulk Tagger. + */ + showTaggingMenu() { + this.rootElement.classList.remove('hidden'); + } + + /** + * Updates the BulkTagger when works are selected. + * + * Stores given array in `selectedWorks`, fetches the + * existing tags for each given work, and updates the view with + * the existing tags. + * + * @param {Array<String>} workIds + */ + async updateWorks(workIds) { + this.showLoadingIndicator(); + + this.selectedWorks = workIds; + + await this.fetchSubjectsForWorks(workIds); + this.updateMenuOptions(); + + this.hideLoadingIndicator(); + } + + /** + * Hides all menu options and shows a loading indicator. + */ + showLoadingIndicator() { + const menuOptionContainer = this.rootElement.querySelector( + '.selection-container', + ); + menuOptionContainer.classList.add('hidden'); + const loadingIndicator = + this.rootElement.querySelector('.loading-indicator'); + loadingIndicator.classList.remove('hidden'); + } + + /** + * Hides the loading indicator and shows all menu options. + */ + hideLoadingIndicator() { + const loadingIndicator = + this.rootElement.querySelector('.loading-indicator'); + loadingIndicator.classList.add('hidden'); + const menuOptionContainer = this.rootElement.querySelector( + '.selection-container', + ); + menuOptionContainer.classList.remove('hidden'); + } + + /** + * Fetches and stores subject information for the given work OLIDs. + * + * If we already have fetched the data for a work ID, we do not fetch it + * again. + * @param {Array<String>} workIds + */ + async fetchSubjectsForWorks(workIds) { + const worksWithMissingSubjects = workIds.filter( + (id) => !this.existingSubjects.has(id), + ); + + await Promise.all( + worksWithMissingSubjects.map(async (id) => { + // XXX : Too many network requests --- use bulk search if/when it is available + await this.fetchWork(id) + // XXX : Handle failures + .then((response) => response.json()) + .then((data) => { + const entry = { + subjects: data.subjects || [], + subject_people: data.subject_people || [], + subject_places: data.subject_places || [], + subject_times: data.subject_times || [], + }; + // Move collection labels from `subjects` to `collections` + entry.collections = entry.subjects.filter((label) => + label.startsWith(COLLECTION_PREFIX), + ); + entry.subjects = entry.subjects.filter( + (label) => !entry.collections.includes(label), + ); + for (let i = 0; i < entry.collections.length; ++i) { + // Remove collection prefix from label + entry.collections[i] = entry.collections[i].substring( + COLLECTION_PREFIX.length, + ); } - }) - - return menuOption + if (!this.existingSubjects.has(id)) { + this.existingSubjects.set(id, []); + } + // `key` is the type, `value` is the array of tag names + for (const [key, value] of Object.entries(entry)) { + for (const tagName of value) { + this.existingSubjects.get(id).push(new Tag(tagName, key)); + } + } + }); + }), + ); + } + + /** + * Creates `MenuOption` affordances for all staged tags, and each existing tag that + * was fetched from the server. + */ + updateMenuOptions() { + this.selectedOptionsContainer.clear(); + + // Add staged tags first, then add all other missing subjects. + // This order prevents unnecessary state mangement steps. + + // Create menu options for each staged tag: + this.tagsToAdd.forEach((tag) => { + const menuOption = new MenuOption( + tag, + MenuOptionState.ALL_TAGGED, + this.selectedWorks.length, + ); + menuOption.initialize(); + this.selectedOptionsContainer.add(menuOption); + }); + + this.tagsToRemove.forEach((tag) => { + const menuOption = new MenuOption(tag, MenuOptionState.NONE_TAGGED, 0); + menuOption.initialize(); + this.selectedOptionsContainer.add(menuOption); + }); + + // Create menu options for each existing tag: + const stagedMenuOptions = []; + for (const workOlid of this.selectedWorks) { + const existingTagsForWork = this.existingSubjects.get(workOlid); + for (const tag of existingTagsForWork) { + // Does an option for this tag already exist in the container? + if (!this.selectedOptionsContainer.containsOptionWithTag(tag)) { + // Have we already created and staged a menu option for this tag? + const stagedOption = stagedMenuOptions.find((option) => + option.tag.equals(tag), + ); + if (stagedOption) { + stagedOption.taggedWorksCount++; + if (stagedOption.taggedWorksCount === this.selectedWorks.length) { + stagedOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED); + } + } else { + const state = + this.selectedWorks.length === 1 + ? MenuOptionState.ALL_TAGGED + : MenuOptionState.SOME_TAGGED; + const newOption = new MenuOption(tag, state, 1); + newOption.initialize(); + stagedMenuOptions.push(newOption); + } + } + } } - /** - * Adds a menu option representing the given tag to the selected options container. - * - * If the container already has a menu option for the given tag, this method returns - * without making any changes. - * - * If a corresponding menu option is found in the search results container, that menu - * option is added to the selected options container. Otherwise, a new menu option is - * created, hydrated, and added to the container. - * - * @param {Tag} tag - */ - onCreateTag(tag) { - // Return if menu option already exists in selected options: - if (this.selectedOptionsContainer.containsOptionWithTag(tag)) { - return + stagedMenuOptions.forEach((option) => + option.rootElement.addEventListener('click', () => + this.onMenuOptionClick(option), + ), + ); + this.selectedOptionsContainer.add(...stagedMenuOptions); + } + + /** + * Click handler for menu options. + * + * Changes the menu option's state, and stages the option's tag + * for addition or removal. + * + * @param {MenuOption} menuOption The clicked menu option + */ + onMenuOptionClick(menuOption) { + let stagedTagIndex; + switch (menuOption.optionState) { + case MenuOptionState.NONE_TAGGED: + stagedTagIndex = this.tagsToRemove.findIndex( + (tag) => + tag.tagName === menuOption.tag.tagName && + tag.tagType === menuOption.tag.tagType, + ); + if (stagedTagIndex > -1) { + this.tagsToRemove.splice(stagedTagIndex, 1); } - - // Stage tag for addition: - this.tagsToAdd.push(tag) - - // If tag is represented by a search result object, update existing object - // instead of creating a new one: - const existingOption = this.searchResultsOptionsContainer.findByTag(tag) - if (existingOption) { - this.searchResultsOptionsContainer.remove(existingOption) - this.selectedOptionsContainer.add(existingOption) - existingOption.taggedWorksCount = this.selectedWorks.length - existingOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED) - } else { - const menuOption = new MenuOption(tag, MenuOptionState.ALL_TAGGED, this.selectedWorks.length) - menuOption.initialize() - menuOption.rootElement.addEventListener('click', () => this.onMenuOptionClick(menuOption)) - this.selectedOptionsContainer.add(menuOption) + this.tagsToAdd.push(menuOption.tag); + menuOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED); + break; + case MenuOptionState.SOME_TAGGED: + this.tagsToAdd.push(menuOption.tag); + menuOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED); + break; + case MenuOptionState.ALL_TAGGED: + stagedTagIndex = this.tagsToAdd.findIndex( + (tag) => + tag.tagName === menuOption.tag.tagName && + tag.tagType === menuOption.tag.tagType, + ); + if (stagedTagIndex > -1) { + this.tagsToAdd.splice(stagedTagIndex, 1); } + this.tagsToRemove.push(menuOption.tag); + menuOption.updateMenuOptionState(MenuOptionState.NONE_TAGGED); + break; + } - this.updateSubmitButtonState() + menuOption.stage(); + this.updateSubmitButtonState(); + } + + /** + * Disables or enables form submission button. + * + * Button is enabled if there are any tags staged for submission. + * Otherwise, the button will be disabled. + */ + updateSubmitButtonState() { + const stagedTagCount = this.tagsToAdd.length + this.tagsToRemove.length; + + if (stagedTagCount > 0) { + this.submitButton.removeAttribute('disabled'); + } else { + this.submitButton.setAttribute('disabled', 'true'); } + } + + /** + * Fetches a work from OL. + * + * @param {String} workOlid + */ + async fetchWork(workOlid) { + return fetch(`/works/${workOlid}.json`); + } + + /** + * Performs a subject search for the given search term, and updates + * the Bulk Tagger with the results. + * + * If the given search term, when trimmed, is an empty string, this + * instead hides the "create subject" affordance. + * + * @param {String} searchTerm + */ + onSearchInputChange(searchTerm) { + // Remove search results that are not selected: + const resultsToRemove = + this.searchResultsOptionsContainer.sortedMenuOptions.filter( + (option) => option.optionState !== MenuOptionState.ALL_TAGGED, + ); + this.searchResultsOptionsContainer.remove(...resultsToRemove); + + // Hide menu options that do not begin with the search term (case-insensitive) + const trimmedSearchTerm = searchTerm.trim(); + + const allOptions = this.selectedOptionsContainer.sortedMenuOptions.concat( + this.searchResultsOptionsContainer.sortedMenuOptions, + ); + allOptions.forEach((option) => { + if ( + option.tag.tagName + .toLowerCase() + .startsWith(trimmedSearchTerm.toLowerCase()) + ) { + option.show(); + } else { + option.hide(); + } + }); + + if (trimmedSearchTerm !== '') { + // Perform search: + fetch(`/search/subjects.json?q=${searchTerm}&limit=${maxDisplayResults}`) + .then((response) => response.json()) + .then((data) => { + if (data['docs'].length !== 0) { + for (const obj of data['docs']) { + const tag = new Tag(obj.name, null, obj['subject_type']); + + if ( + !this.selectedOptionsContainer.containsOptionWithTag(tag) && + !this.searchResultsOptionsContainer.containsOptionWithTag(tag) + ) { + const menuOption = this.createSearchMenuOption(tag); + this.searchResultsOptionsContainer.add(menuOption); + } + } + } - /** - * Submits the bulk tagging form and updates the view. - */ - submitBatch() { + // Update and show create subject affordance + this.updateAndShowNewSubjectAffordance(trimmedSearchTerm); + }); + } else { + // Hide create subject affordance + this.createSubjectElem.classList.add('hidden'); + } + } + + /** + * Updates the "create subject" affordance with the given subject name, + * and shows the affordance if it is hidden. + * + * @param {String} subjectName The name of the subject + */ + updateAndShowNewSubjectAffordance(subjectName) { + this.subjectNameElem.innerText = subjectName; + this.createSubjectElem.classList.remove('hidden'); + } + + /** + * Creates, hydrates, and returns a new menu option based on a search result. + * + * In addition to the usual click listener, the newly created element will have an + * `option-hidden` event handler, which will move any selected menu option to the + * selected options container whenever the menu option is hidden. This is done to + * maintain the correct menu option ordering when search results are updated. + * + * Precondition: Menu option representing the given tag is not attached to the DOM. + * + * @param {Tag} tag + * @returns {MenuOption} A menu option representing the given tag + */ + createSearchMenuOption(tag) { + const menuOption = new MenuOption(tag, MenuOptionState.NONE_TAGGED, 0); + menuOption.initialize(); + menuOption.rootElement.addEventListener('click', () => + this.onMenuOptionClick(menuOption), + ); + menuOption.rootElement.addEventListener('option-hidden', () => { + // Move to selected menu options container if selected and hidden + if (menuOption.optionState === MenuOptionState.ALL_TAGGED) { + if ( + menuOption.rootElement.parentElement === + this.searchResultsOptionsContainer.rootElement + ) { + this.searchResultsOptionsContainer.remove(menuOption); + this.selectedOptionsContainer.add(menuOption); + } + } + }); + + return menuOption; + } + + /** + * Adds a menu option representing the given tag to the selected options container. + * + * If the container already has a menu option for the given tag, this method returns + * without making any changes. + * + * If a corresponding menu option is found in the search results container, that menu + * option is added to the selected options container. Otherwise, a new menu option is + * created, hydrated, and added to the container. + * + * @param {Tag} tag + */ + onCreateTag(tag) { + // Return if menu option already exists in selected options: + if (this.selectedOptionsContainer.containsOptionWithTag(tag)) { + return; + } + // Stage tag for addition: + this.tagsToAdd.push(tag); + + // If tag is represented by a search result object, update existing object + // instead of creating a new one: + const existingOption = this.searchResultsOptionsContainer.findByTag(tag); + if (existingOption) { + this.searchResultsOptionsContainer.remove(existingOption); + this.selectedOptionsContainer.add(existingOption); + existingOption.taggedWorksCount = this.selectedWorks.length; + existingOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED); + } else { + const menuOption = new MenuOption( + tag, + MenuOptionState.ALL_TAGGED, + this.selectedWorks.length, + ); + menuOption.initialize(); + menuOption.rootElement.addEventListener('click', () => + this.onMenuOptionClick(menuOption), + ); + this.selectedOptionsContainer.add(menuOption); + } - // Disable button - this.submitButton.disabled = true; + this.updateSubmitButtonState(); + } - this.submitButton.textContent = 'Submitting...'; + /** + * Submits the bulk tagging form and updates the view. + */ + submitBatch() { + // Disable button + this.submitButton.disabled = true; - const url = this.rootElement.action - this.prepareFormForSubmission() - const formData = new FormData(this.rootElement) - if (this.isBookPageEdit) { - formData.append('book_page_edit', true) - } + this.submitButton.textContent = 'Submitting...'; - fetch(url, { - method: 'post', - body: formData - }) - .then(response => { - if (!response.ok) { - this.submitButton.disabled = false; - this.submitButton.textContent = 'Submit'; - new FadingToast('Batch subject update failed. Please try again in a few minutes.').show(); - } else { - this.hideTaggingMenu(); - new FadingToast('Subjects successfully updated.').show() - this.submitButton.textContent = 'Submit'; - this.updateFetchedSubjects(); - this.resetTaggingMenu(); - if (this.isBookPageEdit) { - window.ILE.clearAndReset() - window.location.reload() - } - } - }) + const url = this.rootElement.action; + this.prepareFormForSubmission(); + const formData = new FormData(this.rootElement); + if (this.isBookPageEdit) { + formData.append('book_page_edit', true); } - /** - * Populates the form's hidden inputs. - * - * Expected to be called just before the form is submitted. - */ - prepareFormForSubmission() { - this.selectedWorksInput.value = this.selectedWorks.join(',') - - const addSubjectsValue = { - subjects: this.findMatches(this.tagsToAdd, 'subjects'), - subject_people: this.findMatches(this.tagsToAdd, 'subject_people'), - subject_places: this.findMatches(this.tagsToAdd, 'subject_places'), - subject_times: this.findMatches(this.tagsToAdd, 'subject_times') + fetch(url, { + method: 'post', + body: formData, + }).then((response) => { + if (!response.ok) { + this.submitButton.disabled = false; + this.submitButton.textContent = 'Submit'; + new FadingToast( + 'Batch subject update failed. Please try again in a few minutes.', + ).show(); + } else { + this.hideTaggingMenu(); + new FadingToast('Subjects successfully updated.').show(); + this.submitButton.textContent = 'Submit'; + this.updateFetchedSubjects(); + this.resetTaggingMenu(); + if (this.isBookPageEdit) { + window.ILE.clearAndReset(); + window.location.reload(); } - const collectionsToAdd = this.findMatches(this.tagsToAdd, 'collections') - collectionsToAdd.forEach(label => addSubjectsValue.subjects.push(`${COLLECTION_PREFIX}${label}`)) - this.addSubjectsInput.value = JSON.stringify(addSubjectsValue) - - const removeSubjectsValue = { - subjects: this.findMatches(this.tagsToRemove, 'subjects'), - subject_people: this.findMatches(this.tagsToRemove, 'subject_people'), - subject_places: this.findMatches(this.tagsToRemove, 'subject_places'), - subject_times: this.findMatches(this.tagsToRemove, 'subject_times') + } + }); + } + + /** + * Populates the form's hidden inputs. + * + * Expected to be called just before the form is submitted. + */ + prepareFormForSubmission() { + this.selectedWorksInput.value = this.selectedWorks.join(','); + + const addSubjectsValue = { + subjects: this.findMatches(this.tagsToAdd, 'subjects'), + subject_people: this.findMatches(this.tagsToAdd, 'subject_people'), + subject_places: this.findMatches(this.tagsToAdd, 'subject_places'), + subject_times: this.findMatches(this.tagsToAdd, 'subject_times'), + }; + const collectionsToAdd = this.findMatches(this.tagsToAdd, 'collections'); + collectionsToAdd.forEach((label) => + addSubjectsValue.subjects.push(`${COLLECTION_PREFIX}${label}`), + ); + this.addSubjectsInput.value = JSON.stringify(addSubjectsValue); + + const removeSubjectsValue = { + subjects: this.findMatches(this.tagsToRemove, 'subjects'), + subject_people: this.findMatches(this.tagsToRemove, 'subject_people'), + subject_places: this.findMatches(this.tagsToRemove, 'subject_places'), + subject_times: this.findMatches(this.tagsToRemove, 'subject_times'), + }; + const collectionsToRemove = this.findMatches( + this.tagsToRemove, + 'collections', + ); + collectionsToRemove.forEach((label) => + removeSubjectsValue.subjects.push(`${COLLECTION_PREFIX}${label}`), + ); + this.removeSubjectsInput.value = JSON.stringify(removeSubjectsValue); + } + + /** + * Filters tags that match the given type, and returns the names of + * each filtered tag. + * + * @param {Array<Tag>} tags Tags to be filtered + * @param {String} type Snake-cased tag type + * @returns {Array<String>} The names of the filtered tags + */ + findMatches(tags, type) { + const results = []; + tags.reduce((_acc, tag) => { + if (tag.tagType === type) { + results.push(tag.tagName); + } + }, []); + return results; + } + + /** + * Updates the data structure which contains the fetched works' subjects. + * + * Meant to be called after the form has been submitted, but before the + * `resetTaggingMenu` call is made. + */ + updateFetchedSubjects() { + for (const tag of this.tagsToAdd) { + this.existingSubjects.forEach((tags) => { + const tagExists = + tags.findIndex( + (t) => t.tagName === tag.tagName && t.tagType === tag.tagType, + ) > -1; + if (!tagExists) { + tags.push(tag); } - const collectionsToRemove = this.findMatches(this.tagsToRemove, 'collections') - collectionsToRemove.forEach(label => removeSubjectsValue.subjects.push(`${COLLECTION_PREFIX}${label}`)) - this.removeSubjectsInput.value = JSON.stringify(removeSubjectsValue) + }); } - /** - * Filters tags that match the given type, and returns the names of - * each filtered tag. - * - * @param {Array<Tag>} tags Tags to be filtered - * @param {String} type Snake-cased tag type - * @returns {Array<String>} The names of the filtered tags - */ - findMatches(tags, type) { - const results = [] - tags.reduce((_acc, tag) => { - if (tag.tagType === type) { - results.push(tag.tagName) - } - }, []) - return results - } - - /** - * Updates the data structure which contains the fetched works' subjects. - * - * Meant to be called after the form has been submitted, but before the - * `resetTaggingMenu` call is made. - */ - updateFetchedSubjects() { - for (const tag of this.tagsToAdd) { - this.existingSubjects.forEach((tags) => { - const tagExists = tags.findIndex((t) => t.tagName === tag.tagName && t.tagType === tag.tagType) > -1 - if (!tagExists) { - tags.push(tag) - } - }) + for (const tag of this.tagsToRemove) { + this.existingSubjects.forEach((tags) => { + const tagIndex = tags.findIndex( + (t) => t.tagName === tag.tagName && t.tagType === tag.tagType, + ); + const tagExists = tagIndex > -1; + if (tagExists) { + tags.splice(tagIndex, 1); } - - for (const tag of this.tagsToRemove) { - this.existingSubjects.forEach((tags) => { - const tagIndex = tags.findIndex((t) => t.tagName === tag.tagName && t.tagType === tag.tagType) - const tagExists = tagIndex > -1 - if (tagExists) { - tags.splice(tagIndex, 1) - } - }) - } - } - - /** - * Clears the bulk tagger form. - */ - resetTaggingMenu() { - this.searchInput.value = '' - this.addSubjectsInput.value = '' - this.removeSubjectsInput.value = '' - this.selectedOptionsContainer.clear() - this.searchResultsOptionsContainer.clear() - - this.createSubjectElem.classList.add('hidden') - - this.tagsToAdd = [] - this.tagsToRemove = [] + }); } + } + + /** + * Clears the bulk tagger form. + */ + resetTaggingMenu() { + this.searchInput.value = ''; + this.addSubjectsInput.value = ''; + this.removeSubjectsInput.value = ''; + this.selectedOptionsContainer.clear(); + this.searchResultsOptionsContainer.clear(); + + this.createSubjectElem.classList.add('hidden'); + + this.tagsToAdd = []; + this.tagsToRemove = []; + } } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js index a2280150267..4a90c41c1dd 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js @@ -2,12 +2,12 @@ * Maps tag display types to BEM suffixes. */ const classTypeSuffixes = { - subjects: '--subject', - subject_people: '--person', - subject_places: '--place', - subject_times: '--time', - collections: '--collection' -} + subjects: '--subject', + subject_people: '--person', + subject_places: '--place', + subject_times: '--time', + collections: '--collection', +}; /** * @typedef OptionState @@ -22,165 +22,178 @@ const classTypeSuffixes = { * @enum {OptionState} */ export const MenuOptionState = { - NONE_TAGGED: 0, - SOME_TAGGED: 1, - ALL_TAGGED: 2, -} + NONE_TAGGED: 0, + SOME_TAGGED: 1, + ALL_TAGGED: 2, +}; export class MenuOption { - + /** + * Creates a new MenuOption that represents the given tag. + * + * `rootElement` of this object is not set until `initialize` is called. + * + * @param {Tag} tag + * @param {OptionState} optionState + * @param {Number} taggedWorksCount Number of selected works which have the given tag + */ + constructor(tag, optionState, taggedWorksCount) { /** - * Creates a new MenuOption that represents the given tag. + * Reference to the root element of this MenuOption. * - * `rootElement` of this object is not set until `initialize` is called. - * - * @param {Tag} tag - * @param {OptionState} optionState - * @param {Number} taggedWorksCount Number of selected works which have the given tag - */ - constructor(tag, optionState, taggedWorksCount) { - /** - * Reference to the root element of this MenuOption. - * - * This is not set until `initialize` is called. - * @member {HTMLElement} - * @see {initialize} - */ - this.rootElement - - /** - * Copy of the tag which is represented by this menu option. - * - * @member {Tag} - * @readonly - */ - this.tag = tag - - /** - * Represents the amount of selected works that share this tag. - * - * Not meant to be updated directly. Use `updateMenuOptionState()`, - * which also updates the UI, to set this value. - * - * @member {OptionState} - */ - this.optionState = optionState - - /** - * Tracks number of selected works which have this tag. - * - * @member {Number} - */ - this.taggedWorksCount = taggedWorksCount - } - - /** - * Creates a new menu option. - * - * Must be called before an event handler can be attached to - * this menu option + * This is not set until `initialize` is called. + * @member {HTMLElement} + * @see {initialize} */ - initialize() { - this.createMenuOption() - } + this.rootElement; /** - * Creates a new menu option affordance based on the current menu option state. + * Copy of the tag which is represented by this menu option. * - * Stores newly created element as `rootElement`. The new element is not - * attached to the DOM, and does not yet have any attached event handlers. - */ - createMenuOption() { - const parentElem = document.createElement('div') - parentElem.classList.add('selected-tag') - - let bemSuffix = '' - switch (this.optionState) { - case MenuOptionState.NONE_TAGGED: - bemSuffix = 'none-tagged' - break - case MenuOptionState.SOME_TAGGED: - bemSuffix = 'some-tagged' - break - case MenuOptionState.ALL_TAGGED: - bemSuffix = 'all-tagged' - break - } - - const markup = `<span class="selected-tag__status selected-tag__status--${bemSuffix}"></span> - <span class="selected-tag__name">${this.tag.tagName}</span> - <span class="selected-tag__type-container"> - <span class="selected-tag__type selected-tag__type${classTypeSuffixes[this.tag.tagType]}">${this.tag.displayType}</span> - </span>` - - parentElem.innerHTML = markup - this.rootElement = parentElem - } - - /** - * Removes this MenuOption from the DOM. + * @member {Tag} + * @readonly */ - remove() { - this.rootElement.remove() - } + this.tag = tag; /** - * Sets the value of `optionState` and updates the view. + * Represents the amount of selected works that share this tag. * - * @param {OptionState} menuOptionState + * Not meant to be updated directly. Use `updateMenuOptionState()`, + * which also updates the UI, to set this value. * - * @throws Will throw an error if an unexpected menu option state is passed, or if this - * `MenuOption` was not initialized prior to calling this method. - * @see {@link MenuOptionState} - * @see {initialize} + * @member {OptionState} */ - updateMenuOptionState(menuOptionState) { - if (this.rootElement) { // `rootElement` not set until `initialize` is called - this.optionState = menuOptionState - const statusIndicator = this.rootElement.querySelector('.selected-tag__status') - switch (menuOptionState) { - case MenuOptionState.NONE_TAGGED: - statusIndicator.classList.remove('selected-tag__status--all-tagged', 'selected-tag__status--some-tagged') - statusIndicator.classList.add('selected-tag__status--none-tagged') - break; - case MenuOptionState.SOME_TAGGED: - statusIndicator.classList.remove('selected-tag__status--all-tagged', 'selected-tag__status--none-tagged') - statusIndicator.classList.add('selected-tag__status--some-tagged') - break; - case MenuOptionState.ALL_TAGGED: - statusIndicator.classList.remove('selected-tag__status--none-tagged', 'selected-tag__status--some-tagged') - statusIndicator.classList.add('selected-tag__status--all-tagged') - break; - default: - // XXX : `optionState` is now incorrect - throw new Error('Unexpected value passed for menu option state.') - } - } else { - throw new Error('MenuOption must be initialized before state can be updated.') - } - } + this.optionState = optionState; /** - * Hides this menu option. + * Tracks number of selected works which have this tag. * - * Fires an `option-hidden` event when this is called. + * @member {Number} */ - hide() { - this.rootElement.classList.add('hidden') - this.rootElement.dispatchEvent(new CustomEvent('option-hidden')) + this.taggedWorksCount = taggedWorksCount; + } + + /** + * Creates a new menu option. + * + * Must be called before an event handler can be attached to + * this menu option + */ + initialize() { + this.createMenuOption(); + } + + /** + * Creates a new menu option affordance based on the current menu option state. + * + * Stores newly created element as `rootElement`. The new element is not + * attached to the DOM, and does not yet have any attached event handlers. + */ + createMenuOption() { + const parentElem = document.createElement('div'); + parentElem.classList.add('selected-tag'); + + let bemSuffix = ''; + switch (this.optionState) { + case MenuOptionState.NONE_TAGGED: + bemSuffix = 'none-tagged'; + break; + case MenuOptionState.SOME_TAGGED: + bemSuffix = 'some-tagged'; + break; + case MenuOptionState.ALL_TAGGED: + bemSuffix = 'all-tagged'; + break; } - /** - * Shows this menu option. - */ - show() { - this.rootElement.classList.remove('hidden') - } - - /** - * Stages the selected menu option. - */ - stage() { - this.rootElement.classList.add('selected-tag--staged'); + const markup = `<span class="selected-tag__status selected-tag__status--${bemSuffix}"></span> + <span class="selected-tag__name">${this.tag.tagName}</span> + <span class="selected-tag__type-container"> + <span class="selected-tag__type selected-tag__type${classTypeSuffixes[this.tag.tagType]}">${this.tag.displayType}</span> + </span>`; + + parentElem.innerHTML = markup; + this.rootElement = parentElem; + } + + /** + * Removes this MenuOption from the DOM. + */ + remove() { + this.rootElement.remove(); + } + + /** + * Sets the value of `optionState` and updates the view. + * + * @param {OptionState} menuOptionState + * + * @throws Will throw an error if an unexpected menu option state is passed, or if this + * `MenuOption` was not initialized prior to calling this method. + * @see {@link MenuOptionState} + * @see {initialize} + */ + updateMenuOptionState(menuOptionState) { + if (this.rootElement) { + // `rootElement` not set until `initialize` is called + this.optionState = menuOptionState; + const statusIndicator = this.rootElement.querySelector( + '.selected-tag__status', + ); + switch (menuOptionState) { + case MenuOptionState.NONE_TAGGED: + statusIndicator.classList.remove( + 'selected-tag__status--all-tagged', + 'selected-tag__status--some-tagged', + ); + statusIndicator.classList.add('selected-tag__status--none-tagged'); + break; + case MenuOptionState.SOME_TAGGED: + statusIndicator.classList.remove( + 'selected-tag__status--all-tagged', + 'selected-tag__status--none-tagged', + ); + statusIndicator.classList.add('selected-tag__status--some-tagged'); + break; + case MenuOptionState.ALL_TAGGED: + statusIndicator.classList.remove( + 'selected-tag__status--none-tagged', + 'selected-tag__status--some-tagged', + ); + statusIndicator.classList.add('selected-tag__status--all-tagged'); + break; + default: + // XXX : `optionState` is now incorrect + throw new Error('Unexpected value passed for menu option state.'); + } + } else { + throw new Error( + 'MenuOption must be initialized before state can be updated.', + ); } + } + + /** + * Hides this menu option. + * + * Fires an `option-hidden` event when this is called. + */ + hide() { + this.rootElement.classList.add('hidden'); + this.rootElement.dispatchEvent(new CustomEvent('option-hidden')); + } + + /** + * Shows this menu option. + */ + show() { + this.rootElement.classList.remove('hidden'); + } + + /** + * Stages the selected menu option. + */ + stage() { + this.rootElement.classList.add('selected-tag--staged'); + } } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js index 22ebcb29908..01e76d9f39c 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js @@ -6,130 +6,139 @@ * to the DOM in the correct order, based on tag name and type. */ export class SortedMenuOptionContainer { + /** + * Creates a new sorted menu options container, with the given + * element as the root element. + * + * This container is meant to exclusively hold bulk tagger menu + * options. Adding other elements as direct descendents of this + * container will result bugs during insertion and deletion of + * menu options. + * + * @param {HTMLElement} element The container + */ + constructor(element) { + this.rootElement = element; + this.sortedMenuOptions = []; + } - /** - * Creates a new sorted menu options container, with the given - * element as the root element. - * - * This container is meant to exclusively hold bulk tagger menu - * options. Adding other elements as direct descendents of this - * container will result bugs during insertion and deletion of - * menu options. - * - * @param {HTMLElement} element The container - */ - constructor(element) { - this.rootElement = element - this.sortedMenuOptions = [] + /** + * Attaches the given menu options to this container, in order. + * + * @param {...MenuOption} menuOptions Menu options to be added to the container. + */ + add(...menuOptions) { + for (const option of menuOptions) { + const index = this.findIndex(option); + this.sortedMenuOptions.splice(index, 0, option); + this.updateViewOnAdd(option, index); } + } - /** - * Attaches the given menu options to this container, in order. - * - * @param {...MenuOption} menuOptions Menu options to be added to the container. - */ - add(...menuOptions) { - for (const option of menuOptions) { - const index = this.findIndex(option) - this.sortedMenuOptions.splice(index, 0, option) - this.updateViewOnAdd(option, index) - } - } - - /** - * Adds the given menu option to this container at the given index. - * - * @param {MenuOption} menuOption The option being attached to the DOM. - * @param {Number} index The index where the given option will be inserted. - */ - updateViewOnAdd(menuOption, index) { - if (index === 0) { - this.rootElement.prepend(menuOption.rootElement) - } else { - const sibling = this.rootElement.children[index - 1] - sibling.insertAdjacentElement('afterend', menuOption.rootElement) - } + /** + * Adds the given menu option to this container at the given index. + * + * @param {MenuOption} menuOption The option being attached to the DOM. + * @param {Number} index The index where the given option will be inserted. + */ + updateViewOnAdd(menuOption, index) { + if (index === 0) { + this.rootElement.prepend(menuOption.rootElement); + } else { + const sibling = this.rootElement.children[index - 1]; + sibling.insertAdjacentElement('afterend', menuOption.rootElement); } + } - /** - * Removes the given menu options from this container. - * - * @param {...MenuOption} menuOptions Options that are to be removed from this container - */ - remove(...menuOptions) { - for (const option of menuOptions) { - const index = this.findIndex(option) - const removed = this.sortedMenuOptions.splice(index, 1) - removed.forEach((option) => option.remove()) - } + /** + * Removes the given menu options from this container. + * + * @param {...MenuOption} menuOptions Options that are to be removed from this container + */ + remove(...menuOptions) { + for (const option of menuOptions) { + const index = this.findIndex(option); + const removed = this.sortedMenuOptions.splice(index, 1); + removed.forEach((option) => option.remove()); } + } - /** - * Finds the correct index to insert the given menu option, such that - * the array is alphabetically ordered (case-insensitive). - * - * @param {MenuOption} menuOption - * @returns {Number} Index where the given menu option should be inserted. - */ - findIndex(menuOption) { - let index = 0 + /** + * Finds the correct index to insert the given menu option, such that + * the array is alphabetically ordered (case-insensitive). + * + * @param {MenuOption} menuOption + * @returns {Number} Index where the given menu option should be inserted. + */ + findIndex(menuOption) { + let index = 0; - // XXX : Binary search? - while (index < this.sortedMenuOptions.length) { - const currentMenuOption = this.sortedMenuOptions[index] + // XXX : Binary search? + while (index < this.sortedMenuOptions.length) { + const currentMenuOption = this.sortedMenuOptions[index]; - if (currentMenuOption.tag.tagName.toLowerCase() === menuOption.tag.tagName.toLowerCase()) { - // Compare types - if (currentMenuOption.tag.tagType.toLowerCase() >= menuOption.tag.tagType.toLowerCase()) { - return index - } - } - else if (currentMenuOption.tag.tagName.toLowerCase() > menuOption.tag.tagName.toLowerCase()) { - return index - } - ++index + if ( + currentMenuOption.tag.tagName.toLowerCase() === + menuOption.tag.tagName.toLowerCase() + ) { + // Compare types + if ( + currentMenuOption.tag.tagType.toLowerCase() >= + menuOption.tag.tagType.toLowerCase() + ) { + return index; } - - return index + } else if ( + currentMenuOption.tag.tagName.toLowerCase() > + menuOption.tag.tagName.toLowerCase() + ) { + return index; + } + ++index; } - /** - * Checks if the given menu option is in this container. - * - * @param {MenuOption} menuOption The object that we are searching for - * @returns {boolean} `true` if a matching menu option exists in this container - */ - contains(menuOption) { - return this.sortedMenuOptions.some((option) => menuOption.tag.equals(option.tag)) - } + return index; + } - /** - * Checks if a menu option which represents the given tag is in this container. - * - * @param {Tag} tag - * @returns {boolean} `true` if a menu option which represents the given tag is in this container. - */ - containsOptionWithTag(tag) { - return this.sortedMenuOptions.some((option) => tag.equals(option.tag)) - } + /** + * Checks if the given menu option is in this container. + * + * @param {MenuOption} menuOption The object that we are searching for + * @returns {boolean} `true` if a matching menu option exists in this container + */ + contains(menuOption) { + return this.sortedMenuOptions.some((option) => + menuOption.tag.equals(option.tag), + ); + } - /** - * Returns the first menu option found which represents the given tag, or `undefined` if none were found. - * - * @param {Tag} tag - * @returns {MenuOption|undefined} The first matching menu option, or `undefined` if none were found. - */ - findByTag(tag) { - return this.sortedMenuOptions.find((option) => tag.equals(option.tag)) - } + /** + * Checks if a menu option which represents the given tag is in this container. + * + * @param {Tag} tag + * @returns {boolean} `true` if a menu option which represents the given tag is in this container. + */ + containsOptionWithTag(tag) { + return this.sortedMenuOptions.some((option) => tag.equals(option.tag)); + } - /** - * Removes all menu options from this container. - */ - clear() { - while (this.sortedMenuOptions.length > 0) { - this.sortedMenuOptions.pop() - } - this.rootElement.innerHTML = '' + /** + * Returns the first menu option found which represents the given tag, or `undefined` if none were found. + * + * @param {Tag} tag + * @returns {MenuOption|undefined} The first matching menu option, or `undefined` if none were found. + */ + findByTag(tag) { + return this.sortedMenuOptions.find((option) => tag.equals(option.tag)); + } + + /** + * Removes all menu options from this container. + */ + clear() { + while (this.sortedMenuOptions.length > 0) { + this.sortedMenuOptions.pop(); } + this.rootElement.innerHTML = ''; + } } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js index 6da43278fd3..e036129c848 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js @@ -4,7 +4,7 @@ * @returns HTML for the bulk tagging form */ export function renderBulkTagger() { - return `<form action="/tags/bulk_tag_works" method="post" class="bulk-tagging-form hidden"> + return `<form action="/tags/bulk_tag_works" method="post" class="bulk-tagging-form hidden"> <div class="form-header"> <p>Manage Subjects</p> <div class="close-bulk-tagging-form">x</div> @@ -36,5 +36,5 @@ export function renderBulkTagger() { <div class="submit-tags-section"> <button type="submit" class="bulk-tagging-submit cta-btn cta-btn--primary" disabled>Submit</button> </div> - </form>` + </form>`; } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js index aa3bb1887b3..820527a0f46 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js @@ -3,24 +3,24 @@ * can be displayed in the UI. */ const displayTypeMapping = { - subjects: 'subject', - subject_people: 'person', - subject_places: 'place', - subject_times: 'time', - collections: 'collection', -} + subjects: 'subject', + subject_people: 'person', + subject_places: 'place', + subject_times: 'time', + collections: 'collection', +}; /** * Maps UI-ready subject types to their corresponding * technical types. */ export const subjectTypeMapping = { - subject: 'subjects', - person: 'subject_people', - place: 'subject_places', - time: 'subject_times', - collection: 'collections' -} + subject: 'subjects', + person: 'subject_people', + place: 'subject_places', + time: 'subject_times', + collection: 'collections', +}; /** * Compare function for determining the order of two tags. @@ -34,24 +34,22 @@ export const subjectTypeMapping = { * @see {Array.sort} */ export function compare(tagA, tagB) { - const lowerA = createComparableTag(tagA) - const lowerB = createComparableTag(tagB) + const lowerA = createComparableTag(tagA); + const lowerB = createComparableTag(tagB); - if (lowerA.tagName < lowerB.tagName) { - return -1 - } - else if (lowerA.tagName > lowerB.tagName) { - return 1 - } else { - if (lowerA.tagType < lowerB.tagType) { - return -1 - } - else if (lowerA.tagType > lowerB.tagtype) { - return 1 - } + if (lowerA.tagName < lowerB.tagName) { + return -1; + } else if (lowerA.tagName > lowerB.tagName) { + return 1; + } else { + if (lowerA.tagType < lowerB.tagType) { + return -1; + } else if (lowerA.tagType > lowerB.tagtype) { + return 1; } + } - return 0 + return 0; } /** @@ -65,10 +63,10 @@ export function compare(tagA, tagB) { * @see {compare} */ function createComparableTag(tag) { - return { - tagName: tag.tagName.toLowerCase(), - tagType: tag.tagType.toLowerCase() - } + return { + tagName: tag.tagName.toLowerCase(), + tagType: tag.tagType.toLowerCase(), + }; } /** @@ -78,72 +76,75 @@ function createComparableTag(tag) { * type string that is suitable for displaying in the UI. */ export class Tag { - /** - * Creates a new Tag object. - * - * If only one tag type is passed to the constructor, the missing - * tag type will be inferred and set. - * - * @param {String} tagName The name of the Tag - * @param {String} tagType This tag's technical type - * @param {String} displayType This tag's type, in UI-ready form. - * - * @throws Will throw an error if both `tagType` and `displayType` are falsey - */ - constructor(tagName, tagType = null, displayType = null) { - if (!(tagType || displayType)) { - throw new Error('Tag must have at least one type') - } - this.tagName = tagName - this.tagType = tagType || this.convertToType(displayType) - this.displayType = displayType || this.convertToDisplayType(tagType) + /** + * Creates a new Tag object. + * + * If only one tag type is passed to the constructor, the missing + * tag type will be inferred and set. + * + * @param {String} tagName The name of the Tag + * @param {String} tagType This tag's technical type + * @param {String} displayType This tag's type, in UI-ready form. + * + * @throws Will throw an error if both `tagType` and `displayType` are falsey + */ + constructor(tagName, tagType = null, displayType = null) { + if (!(tagType || displayType)) { + throw new Error('Tag must have at least one type'); } + this.tagName = tagName; + this.tagType = tagType || this.convertToType(displayType); + this.displayType = displayType || this.convertToDisplayType(tagType); + } - /** - * Returns the technical tag type corresponding to the given - * UI-ready type string. - * - * @param {String} displayType A UI-ready type string - * @returns {String} The corresponding technical tag type - * @throws Will throw an error if the given type is unrecognized. - */ - convertToType(displayType) { - const result = subjectTypeMapping[displayType] - if (!result) { - throw new Error('Unrecognized `displayType` value') - } - return result + /** + * Returns the technical tag type corresponding to the given + * UI-ready type string. + * + * @param {String} displayType A UI-ready type string + * @returns {String} The corresponding technical tag type + * @throws Will throw an error if the given type is unrecognized. + */ + convertToType(displayType) { + const result = subjectTypeMapping[displayType]; + if (!result) { + throw new Error('Unrecognized `displayType` value'); } + return result; + } - /** - * Given a technical tag type, returns a type string that can be - * displayed in the UI. - * - * @param {String} tagType The technical tag type - * @returns {String} A type string that can be displayed in the UI - * @throws Will throw an error if the given type is unrecognized - */ - convertToDisplayType(tagType) { - const result = displayTypeMapping[tagType] - if (!result) { - throw new Error('Unrecognized `tagType` value') - } - return result + /** + * Given a technical tag type, returns a type string that can be + * displayed in the UI. + * + * @param {String} tagType The technical tag type + * @returns {String} A type string that can be displayed in the UI + * @throws Will throw an error if the given type is unrecognized + */ + convertToDisplayType(tagType) { + const result = displayTypeMapping[tagType]; + if (!result) { + throw new Error('Unrecognized `tagType` value'); } + return result; + } - /** - * Determins if the given tag is equal to this tag. - * - * Two tags are considered equal if case-insensitive comparisons of - * their names and types are equivalent. - * - * @param {Tag} tag - * @returns `true` if the given tag is considered equivalent to this tag. - */ - equals(tag) { - const lowerSelf = createComparableTag(this) - const lowerTag = createComparableTag(tag) + /** + * Determins if the given tag is equal to this tag. + * + * Two tags are considered equal if case-insensitive comparisons of + * their names and types are equivalent. + * + * @param {Tag} tag + * @returns `true` if the given tag is considered equivalent to this tag. + */ + equals(tag) { + const lowerSelf = createComparableTag(this); + const lowerTag = createComparableTag(tag); - return lowerSelf.tagName === lowerTag.tagName && lowerSelf.tagType === lowerTag.tagType - } + return ( + lowerSelf.tagName === lowerTag.tagName && + lowerSelf.tagType === lowerTag.tagType + ); + } } diff --git a/openlibrary/plugins/openlibrary/js/carousel/Carousel.js b/openlibrary/plugins/openlibrary/js/carousel/Carousel.js index 6c959b80621..bb64bf484f1 100644 --- a/openlibrary/plugins/openlibrary/js/carousel/Carousel.js +++ b/openlibrary/plugins/openlibrary/js/carousel/Carousel.js @@ -1,7 +1,7 @@ // Slick#1.6.0 is not on npm import 'slick-carousel'; import '../../../../../static/css/components/carousel--js.css'; -import { buildPartialsUrl } from '../utils.js'; +import { buildPartialsUrl } from '../utils.js'; /** * @typedef {Object} CarouselConfig @@ -22,164 +22,170 @@ import { buildPartialsUrl } from '../utils.js'; // used in templates/covers/add.html export class Carousel { - /** - * @param {jQuery} $container - */ - constructor($container) { - /** @type {CarouselConfig} */ - this.config = Object.assign( - { - booksPerBreakpoint: [6, 5, 4, 3, 2, 1], - analyticsCategory: 'Carousel', - carouselKey: '', - }, - JSON.parse($container.attr('data-config')) - ); - - /** @type {CarouselConfig['loadMore']} */ - this.loadMore = Object.assign( - { - limit: 18, // 3 pages of 6 books - pageMode: 'page', - locked: false, - allDone: false, - page: 1, - }, - this.config.loadMore || {} - ); - - /** @type {jquery} */ - this.$container = $container; - - //This loads in i18n strings from a hidden input element, generated in the books/custom_carousel.html template. - const i18nInput = document.querySelector('input[name="carousel-i18n-strings"]') - if (i18nInput) { - this.i18n = JSON.parse(i18nInput.value); - } - } - - get slick() { - return this.$container.slick('getSlick'); + /** + * @param {jQuery} $container + */ + constructor($container) { + /** @type {CarouselConfig} */ + this.config = Object.assign( + { + booksPerBreakpoint: [6, 5, 4, 3, 2, 1], + analyticsCategory: 'Carousel', + carouselKey: '', + }, + JSON.parse($container.attr('data-config')), + ); + + /** @type {CarouselConfig['loadMore']} */ + this.loadMore = Object.assign( + { + limit: 18, // 3 pages of 6 books + pageMode: 'page', + locked: false, + allDone: false, + page: 1, + }, + this.config.loadMore || {}, + ); + + /** @type {jquery} */ + this.$container = $container; + + //This loads in i18n strings from a hidden input element, generated in the books/custom_carousel.html template. + const i18nInput = document.querySelector( + 'input[name="carousel-i18n-strings"]', + ); + if (i18nInput) { + this.i18n = JSON.parse(i18nInput.value); } - - init() { - this.$container.slick({ - infinite: false, - speed: 300, - slidesToShow: this.config.booksPerBreakpoint[0], - slidesToScroll: this.config.booksPerBreakpoint[0], - responsive: [1200, 1024, 600, 480, 360] - .map((breakpoint, i) => ({ - breakpoint: breakpoint, - settings: { - slidesToShow: this.config.booksPerBreakpoint[i + 1], - slidesToScroll: this.config.booksPerBreakpoint[i + 1], - infinite: false, - } - })) + } + + get slick() { + return this.$container.slick('getSlick'); + } + + init() { + this.$container.slick({ + infinite: false, + speed: 300, + slidesToShow: this.config.booksPerBreakpoint[0], + slidesToScroll: this.config.booksPerBreakpoint[0], + responsive: [1200, 1024, 600, 480, 360].map((breakpoint, i) => ({ + breakpoint: breakpoint, + settings: { + slidesToShow: this.config.booksPerBreakpoint[i + 1], + slidesToScroll: this.config.booksPerBreakpoint[i + 1], + infinite: false, + }, + })), + }); + + // Slick internally changes the click handlers on the next/prev buttons, + // so we listen via the container instead + this.$container.on('click', '.slick-next', (ev) => { + // Note: This will actually fail on the last 'next', but that's okay + if ($(ev.target).hasClass('slick-disabled')) return; + + window.archive_analytics.ol_send_event_ping({ + category: this.config.analyticsCategory, + action: 'Next', + label: this.config.carouselKey, + }); + }); + + this.$container.on('swipe', (ev, _slick, direction) => { + if (direction === 'left') { + window.archive_analytics.ol_send_event_ping({ + category: this.config.analyticsCategory, + action: 'Next', + label: this.config.carouselKey, }); - - // Slick internally changes the click handlers on the next/prev buttons, - // so we listen via the container instead - this.$container.on('click', '.slick-next', (ev) => { - // Note: This will actually fail on the last 'next', but that's okay - if ($(ev.target).hasClass('slick-disabled')) return; - - window.archive_analytics.ol_send_event_ping({ - category: this.config.analyticsCategory, - action: 'Next', - label: this.config.carouselKey, - }); - }); - - this.$container.on('swipe', (ev, _slick, direction) => { - if (direction === 'left') { - window.archive_analytics.ol_send_event_ping({ - category: this.config.analyticsCategory, - action: 'Next', - label: this.config.carouselKey, - }); - } - }); - - // if a loadMore config is provided and it has a (required) url - const loadMore = this.loadMore; - if (loadMore && loadMore.queryType) { - // Bind an action listener to this carousel on resize or advance - this.$container.on('afterChange', (_ev, _slick, curSlide) => { - const totalSlides = this.slick.$slides.length; - const numActiveSlides = this.slick.$slides.filter('.slick-active').length; - // this allows us to pre-load before hitting last page - const needsMoreCards = totalSlides - curSlide <= (numActiveSlides * 2); - - if (!loadMore.locked && !loadMore.allDone && needsMoreCards) { - loadMore.locked = true; // lock for critical section - - if (loadMore.pageMode === 'page') { - loadMore.page++; - } else { // i.e. offset, start from last slide - loadMore.page = totalSlides; - } - - this.fetchPartials(); - } - }); - - document.addEventListener('filter', (ev) => { - loadMore.extraParams = {published_in: `${ev.detail.yearFrom}-${ev.detail.yearTo}`}; - - // Reset the page count - the result set is now 'new' - if (loadMore.pageMode === 'page') { - loadMore.page = 1; - } else { - loadMore.page = 0; - } - loadMore.allDone = false; - - this.clearCarousel(); - this.fetchPartials(); - }); + } + }); + + // if a loadMore config is provided and it has a (required) url + const loadMore = this.loadMore; + if (loadMore && loadMore.queryType) { + // Bind an action listener to this carousel on resize or advance + this.$container.on('afterChange', (_ev, _slick, curSlide) => { + const totalSlides = this.slick.$slides.length; + const numActiveSlides = + this.slick.$slides.filter('.slick-active').length; + // this allows us to pre-load before hitting last page + const needsMoreCards = totalSlides - curSlide <= numActiveSlides * 2; + + if (!loadMore.locked && !loadMore.allDone && needsMoreCards) { + loadMore.locked = true; // lock for critical section + + if (loadMore.pageMode === 'page') { + loadMore.page++; + } else { + // i.e. offset, start from last slide + loadMore.page = totalSlides; + } + + this.fetchPartials(); } - } - - fetchPartials() { - const loadMore = this.loadMore - const url = buildPartialsUrl('CarouselLoadMore', { - queryType: loadMore.queryType, - q: loadMore.q, - limit: loadMore.limit, - page: loadMore.page, - sorts: loadMore.sorts, - subject: loadMore.subject, - pageMode: loadMore.pageMode, - hasFulltextOnly: loadMore.hasFulltextOnly, - secondaryAction: loadMore.secondaryAction, - key: loadMore.key, - ...loadMore.extraParams - }); - this.appendLoadingSlide(); - $.ajax({url: url, type: 'GET'}) - .then((results) => { - this.removeLoadingSlide(); - const cards = results.partials || [] - cards.forEach(card => this.slick.addSlide(card)) - - if (!cards.length) { - loadMore.allDone = true; - } - loadMore.locked = false; - }) - } - - clearCarousel() { - this.slick.removeSlide(this.slick.$slides.length, true, true); - } - - appendLoadingSlide() { - this.slick.addSlide(`<div class="carousel__item carousel__loading-end">${this.i18n['loading']}</div>`); - } + }); + + document.addEventListener('filter', (ev) => { + loadMore.extraParams = { + published_in: `${ev.detail.yearFrom}-${ev.detail.yearTo}`, + }; + + // Reset the page count - the result set is now 'new' + if (loadMore.pageMode === 'page') { + loadMore.page = 1; + } else { + loadMore.page = 0; + } + loadMore.allDone = false; - removeLoadingSlide() { - this.slick.removeSlide(this.slick.$slides.length - 1); + this.clearCarousel(); + this.fetchPartials(); + }); } + } + + fetchPartials() { + const loadMore = this.loadMore; + const url = buildPartialsUrl('CarouselLoadMore', { + queryType: loadMore.queryType, + q: loadMore.q, + limit: loadMore.limit, + page: loadMore.page, + sorts: loadMore.sorts, + subject: loadMore.subject, + pageMode: loadMore.pageMode, + hasFulltextOnly: loadMore.hasFulltextOnly, + secondaryAction: loadMore.secondaryAction, + key: loadMore.key, + ...loadMore.extraParams, + }); + this.appendLoadingSlide(); + $.ajax({ url: url, type: 'GET' }).then((results) => { + this.removeLoadingSlide(); + const cards = results.partials || []; + cards.forEach((card) => this.slick.addSlide(card)); + + if (!cards.length) { + loadMore.allDone = true; + } + loadMore.locked = false; + }); + } + + clearCarousel() { + this.slick.removeSlide(this.slick.$slides.length, true, true); + } + + appendLoadingSlide() { + this.slick.addSlide( + `<div class="carousel__item carousel__loading-end">${this.i18n['loading']}</div>`, + ); + } + + removeLoadingSlide() { + this.slick.removeSlide(this.slick.$slides.length - 1); + } } diff --git a/openlibrary/plugins/openlibrary/js/carousel/index.js b/openlibrary/plugins/openlibrary/js/carousel/index.js index 76f54623338..7a4d91ab9cc 100644 --- a/openlibrary/plugins/openlibrary/js/carousel/index.js +++ b/openlibrary/plugins/openlibrary/js/carousel/index.js @@ -1,14 +1,14 @@ -import {Carousel} from './Carousel'; +import { Carousel } from './Carousel'; export function initialzeCarousels(elems) { - elems.forEach(elem => { - new Carousel($(elem)).init() - const elemSlides = elem.querySelectorAll('.slick-slide') - elemSlides.forEach(slide => { - const $slide = $(slide) - if ($slide.attr('aria-describedby') !== undefined) { - $slide.attr('id',$(this).attr('aria-describedby')); - } - }) - }) + elems.forEach((elem) => { + new Carousel($(elem)).init(); + const elemSlides = elem.querySelectorAll('.slick-slide'); + elemSlides.forEach((slide) => { + const $slide = $(slide); + if ($slide.attr('aria-describedby') !== undefined) { + $slide.attr('id', $(this).attr('aria-describedby')); + } + }); + }); } diff --git a/openlibrary/plugins/openlibrary/js/clampers.js b/openlibrary/plugins/openlibrary/js/clampers.js index 26e1faadd65..18e4b558942 100644 --- a/openlibrary/plugins/openlibrary/js/clampers.js +++ b/openlibrary/plugins/openlibrary/js/clampers.js @@ -3,29 +3,32 @@ * */ export function initClampers(clampers) { - for (const clamper of clampers) { - if (clamper.clientHeight === clamper.scrollHeight) { - clamper.classList.remove('clamp'); - } else { - - /* + for (const clamper of clampers) { + if (clamper.clientHeight === clamper.scrollHeight) { + clamper.classList.remove('clamp'); + } else { + /* Clamper used to collapse category list by toggling `hidden` style on parent element */ - clamper.addEventListener('click', (event) => { - if (event.target instanceof HTMLAnchorElement) { - return; - } + clamper.addEventListener('click', (event) => { + if (event.target instanceof HTMLAnchorElement) { + return; + } - clamper.style.display = clamper.style.display === '-webkit-box' || clamper.style.display === '' ? 'unset' : '-webkit-box' + clamper.style.display = + clamper.style.display === '-webkit-box' || + clamper.style.display === '' + ? 'unset' + : '-webkit-box'; - if (clamper.getAttribute('data-before') === '\u25BE ') { - clamper.setAttribute('data-before', '\u25B8 ') - } else { - clamper.setAttribute('data-before', '\u25BE ') - } - }) + if (clamper.getAttribute('data-before') === '\u25BE ') { + clamper.setAttribute('data-before', '\u25B8 '); + } else { + clamper.setAttribute('data-before', '\u25BE '); } + }); } + } } diff --git a/openlibrary/plugins/openlibrary/js/compact-title/index.js b/openlibrary/plugins/openlibrary/js/compact-title/index.js index cbf8d1b7e0b..9c90b3a21eb 100644 --- a/openlibrary/plugins/openlibrary/js/compact-title/index.js +++ b/openlibrary/plugins/openlibrary/js/compact-title/index.js @@ -26,13 +26,15 @@ let mainTitleElem; * @param {HTMLElement} title The compact title component */ export function initCompactTitle(navbar, title) { - mainTitleElem = document.querySelector('.work-title-and-author.desktop .work-title') - // Show compact title on page reload: + mainTitleElem = document.querySelector( + '.work-title-and-author.desktop .work-title', + ); + // Show compact title on page reload: + onScroll(navbar, title); + // And update on scroll + window.addEventListener('scroll', () => { onScroll(navbar, title); - // And update on scroll - window.addEventListener('scroll', function() { - onScroll(navbar, title) - }); + }); } /** @@ -45,36 +47,42 @@ export function initCompactTitle(navbar, title) { * @param {HTMLElement} title The compact title component */ function onScroll(navbar, title) { - const compactTitleBounds = title.getBoundingClientRect() - const navbarBounds = navbar.getBoundingClientRect() - const mainTitleBounds = mainTitleElem.getBoundingClientRect() - if (mainTitleBounds.bottom < navbarBounds.bottom) { // The main title is off-screen - if (!navbar.classList.contains('sticky--lowest')) { // Compact title not displayed - // Display compact title - title.classList.remove('hidden') - // Animate navbar - $(navbar).addClass('nav-bar-wrapper--slidedown') - .one('animationend', () => { - $(navbar).addClass('sticky--lowest') - $(navbar).removeClass('nav-bar-wrapper--slidedown') - // Ensure correct nav item is selected after compact title slides in: - updateSelectedNavItem() - }) - } else { - if (navbarBounds.top < compactTitleBounds.bottom) { // We've scrolled to the bottom of the container, and the navbar is unstuck - title.classList.add('hidden') - } else { - title.classList.remove('hidden') - } - } - } else { // At least some of the main title is below the navbar - if (!title.classList.contains('hidden')) { - title.classList.add('hidden') - $(navbar).addClass('nav-bar-wrapper--slideup') - .one('animationend', () => { - $(navbar).removeClass('sticky--lowest') - $(navbar).removeClass('nav-bar-wrapper--slideup') - }) - } + const compactTitleBounds = title.getBoundingClientRect(); + const navbarBounds = navbar.getBoundingClientRect(); + const mainTitleBounds = mainTitleElem.getBoundingClientRect(); + if (mainTitleBounds.bottom < navbarBounds.bottom) { + // The main title is off-screen + if (!navbar.classList.contains('sticky--lowest')) { + // Compact title not displayed + // Display compact title + title.classList.remove('hidden'); + // Animate navbar + $(navbar) + .addClass('nav-bar-wrapper--slidedown') + .one('animationend', () => { + $(navbar).addClass('sticky--lowest'); + $(navbar).removeClass('nav-bar-wrapper--slidedown'); + // Ensure correct nav item is selected after compact title slides in: + updateSelectedNavItem(); + }); + } else { + if (navbarBounds.top < compactTitleBounds.bottom) { + // We've scrolled to the bottom of the container, and the navbar is unstuck + title.classList.add('hidden'); + } else { + title.classList.remove('hidden'); + } } + } else { + // At least some of the main title is below the navbar + if (!title.classList.contains('hidden')) { + title.classList.add('hidden'); + $(navbar) + .addClass('nav-bar-wrapper--slideup') + .one('animationend', () => { + $(navbar).removeClass('sticky--lowest'); + $(navbar).removeClass('nav-bar-wrapper--slideup'); + }); + } + } } diff --git a/openlibrary/plugins/openlibrary/js/covers.js b/openlibrary/plugins/openlibrary/js/covers.js index 8f715657221..0e4e0e39cba 100644 --- a/openlibrary/plugins/openlibrary/js/covers.js +++ b/openlibrary/plugins/openlibrary/js/covers.js @@ -9,169 +9,189 @@ import { closePopup } from './utils'; //cover/change.html export function initCoversChange() { - // Pull data from data-config of class "manageCovers" in covers/manage.html - const data_config_json = $('.manageCovers').data('config'); - const doc_type_key = data_config_json['key']; - const add_url = data_config_json['add_url']; - const manage_url = data_config_json['manage_url']; - - // Add iframes lazily when the popup is loaded. - // This avoids fetching the iframes along with main page. - $('.coverPop') - .on('click', function () { - // clear the content of #imagesAdd and #imagesManage before adding new - $('.imagesAdd').html(''); - $('.imagesManage').html(''); - if (doc_type_key === '/type/work') { - $('.imagesAdd').prepend('<div class="throbber"><h3>$_("Searching for covers")</h3></div>'); - } - setTimeout(function () { - // add iframe to add images - add_iframe('.imagesAdd', add_url); - // add iframe to manage images - add_iframe('.imagesManage', manage_url); - }, 0); - }) - .on('cbox_cleanup', function () { - $('.imagesAdd').html(''); - $('.imagesManage').html(''); - }); + // Pull data from data-config of class "manageCovers" in covers/manage.html + const data_config_json = $('.manageCovers').data('config'); + const doc_type_key = data_config_json['key']; + const add_url = data_config_json['add_url']; + const manage_url = data_config_json['manage_url']; + + // Add iframes lazily when the popup is loaded. + // This avoids fetching the iframes along with main page. + $('.coverPop') + .on('click', () => { + // clear the content of #imagesAdd and #imagesManage before adding new + $('.imagesAdd').html(''); + $('.imagesManage').html(''); + if (doc_type_key === '/type/work') { + $('.imagesAdd').prepend( + '<div class="throbber"><h3>$_("Searching for covers")</h3></div>', + ); + } + setTimeout(() => { + // add iframe to add images + add_iframe('.imagesAdd', add_url); + // add iframe to manage images + add_iframe('.imagesManage', manage_url); + }, 0); + }) + .on('cbox_cleanup', () => { + $('.imagesAdd').html(''); + $('.imagesManage').html(''); + }); } function add_iframe(selector, src) { - $(selector) - .append('<iframe frameborder="0" height="580" width="580" marginheight="0" marginwidth="0" scrolling="auto"></iframe>') - .find('iframe') - .attr('src', src); + $(selector) + .append( + '<iframe frameborder="0" height="580" width="580" marginheight="0" marginwidth="0" scrolling="auto"></iframe>', + ) + .find('iframe') + .attr('src', src); } function showLoadingIndicator() { - const loadingIndicator = document.querySelector('.loadingIndicator'); - const formDivs = document.querySelectorAll('.ol-cover-form, .imageIntro'); + const loadingIndicator = document.querySelector('.loadingIndicator'); + const formDivs = document.querySelectorAll('.ol-cover-form, .imageIntro'); - if (loadingIndicator) { - loadingIndicator.classList.remove('hidden'); - formDivs.forEach(div => div.classList.add('hidden')); - } + if (loadingIndicator) { + loadingIndicator.classList.remove('hidden'); + formDivs.forEach((div) => div.classList.add('hidden')); + } } // covers/manage.html and covers/add.html export function initCoversAddManage() { - $('.ol-cover-form').on('submit', function() { - showLoadingIndicator(); - }); - - $('.column').sortable({ - connectWith: '.trash' - }); - $('.trash').sortable({ - connectWith: '.column' - }); - $('.column').disableSelection(); - $('.trash').disableSelection(); + $('.ol-cover-form').on('submit', () => { + showLoadingIndicator(); + }); + + $('.column').sortable({ + connectWith: '.trash', + }); + $('.trash').sortable({ + connectWith: '.column', + }); + $('.column').disableSelection(); + $('.trash').disableSelection(); } // covers/saved.html // Uses parent.$ in place of $ where elements lie outside of the "saved" window export function initCoversSaved() { - // Save the new image - // Pull data from data-config of class "imageSaved" in covers/saved.html - const data_config_json = parent.$('.manageCovers').data('config'); - const doc_type_key = data_config_json['key']; - const coverstore_url = data_config_json['url']; - const cover_selector = data_config_json['selector']; - const image = $('.imageSaved').data('imageId'); - var cover_url; - - $('.popClose').on('click', closePopup); - - // Update the image for the cover - if (['/type/edition', '/type/work', '/edit'].includes(doc_type_key)) { - if (image) { - cover_url = `${coverstore_url}/b/id/${image}-M.jpg`; - // XXX-Anand: Fix this hack - // set url and show SRPCover and hide SRPCoverBlank - parent.$(cover_selector).attr('src', cover_url) - .parents('div:first').show() - .next().hide(); - parent.$(cover_selector).attr('srcset', cover_url) - .parents('div:first').show() - .next().hide(); - } - else { - // hide SRPCover and show SRPCoverBlank - parent.$(cover_selector) - .parents('div:first').hide() - .next().show(); - } + // Save the new image + // Pull data from data-config of class "imageSaved" in covers/saved.html + const data_config_json = parent.$('.manageCovers').data('config'); + const doc_type_key = data_config_json['key']; + const coverstore_url = data_config_json['url']; + const cover_selector = data_config_json['selector']; + const image = $('.imageSaved').data('imageId'); + var cover_url; + + $('.popClose').on('click', closePopup); + + // Update the image for the cover + if (['/type/edition', '/type/work', '/edit'].includes(doc_type_key)) { + if (image) { + cover_url = `${coverstore_url}/b/id/${image}-M.jpg`; + // XXX-Anand: Fix this hack + // set url and show SRPCover and hide SRPCoverBlank + parent + .$(cover_selector) + .attr('src', cover_url) + .parents('div:first') + .show() + .next() + .hide(); + parent + .$(cover_selector) + .attr('srcset', cover_url) + .parents('div:first') + .show() + .next() + .hide(); + } else { + // hide SRPCover and show SRPCoverBlank + parent.$(cover_selector).parents('div:first').hide().next().show(); } - else { - if (image) { - cover_url = `${coverstore_url}/a/id/${image}-M.jpg`; - } - else { - cover_url = '/images/icons/avatar_author-lg.png'; - } - parent.$(cover_selector).attr('src', cover_url); + } else { + if (image) { + cover_url = `${coverstore_url}/a/id/${image}-M.jpg`; + } else { + cover_url = '/images/icons/avatar_author-lg.png'; } + parent.$(cover_selector).attr('src', cover_url); + } } // This function will be triggered when the user clicks the "Paste" button async function pasteImage() { - let formData = null; - try { - const clipboardItems = await navigator.clipboard.read(); - for (const item of clipboardItems) { - if (!item.types.includes('image/png') && !item.types.includes('image/jpeg') && !item.types.includes('image/jpg')) { - continue; - } - - const mimeType = item.types.includes('image/png') ? 'image/png' : (item.types.includes('image/jpeg') ? 'image/jpeg' : 'image/jpg'); - const fileExtension = mimeType === 'image/png' ? 'png' : (mimeType === 'image/jpeg' ? 'jpeg' : 'jpg'); - const blob = await item.getType(mimeType); - const image = document.createElement('img'); - image.src = URL.createObjectURL(blob); - image.alt = '' - const imageContainer = document.querySelector('.image-container') - imageContainer.replaceChildren(image) - - // Update the global formData with the new image blob - formData = new FormData(); - formData.append('file', blob, `pasted-image.${fileExtension}`); - - // Automatically fill in the hidden file input with the FormData - const fileInput = document.getElementById('hiddenFileInput'); - const file = new File([blob], `pasted-image.${fileExtension}`, { type: mimeType }); - const dataTransfer = new DataTransfer(); - dataTransfer.items.add(file); - fileInput.files = dataTransfer.files; // This sets the file input with the image - - // Show the upload button - const uploadButton = document.getElementById('uploadButtonPaste'); - uploadButton.classList.remove('hidden') - - return formData; - } - alert('No image found in clipboard'); - } catch (error) { - + let formData = null; + try { + const clipboardItems = await navigator.clipboard.read(); + for (const item of clipboardItems) { + if ( + !item.types.includes('image/png') && + !item.types.includes('image/jpeg') && + !item.types.includes('image/jpg') + ) { + continue; + } + + const mimeType = item.types.includes('image/png') + ? 'image/png' + : item.types.includes('image/jpeg') + ? 'image/jpeg' + : 'image/jpg'; + const fileExtension = + mimeType === 'image/png' + ? 'png' + : mimeType === 'image/jpeg' + ? 'jpeg' + : 'jpg'; + const blob = await item.getType(mimeType); + const image = document.createElement('img'); + image.src = URL.createObjectURL(blob); + image.alt = ''; + const imageContainer = document.querySelector('.image-container'); + imageContainer.replaceChildren(image); + + // Update the global formData with the new image blob + formData = new FormData(); + formData.append('file', blob, `pasted-image.${fileExtension}`); + + // Automatically fill in the hidden file input with the FormData + const fileInput = document.getElementById('hiddenFileInput'); + const file = new File([blob], `pasted-image.${fileExtension}`, { + type: mimeType, + }); + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(file); + fileInput.files = dataTransfer.files; // This sets the file input with the image + + // Show the upload button + const uploadButton = document.getElementById('uploadButtonPaste'); + uploadButton.classList.remove('hidden'); + + return formData; } + alert('No image found in clipboard'); + } catch (error) {} } export function initPasteForm(coverForm) { - const pasteButton = coverForm.querySelector('#pasteButton'); - let formData = null; - - pasteButton.addEventListener('click', async () => { - formData = await pasteImage(coverForm); - pasteButton.textContent = 'Change Image' - }); - - coverForm.addEventListener('submit', (event) => { - event.preventDefault(); - if (formData) { - showLoadingIndicator(); - coverForm.submit(); - } - }); + const pasteButton = coverForm.querySelector('#pasteButton'); + let formData = null; + + pasteButton.addEventListener('click', async () => { + formData = await pasteImage(coverForm); + pasteButton.textContent = 'Change Image'; + }); + + coverForm.addEventListener('submit', (event) => { + event.preventDefault(); + if (formData) { + showLoadingIndicator(); + coverForm.submit(); + } + }); } diff --git a/openlibrary/plugins/openlibrary/js/dialog.js b/openlibrary/plugins/openlibrary/js/dialog.js index 1ed311631c8..c46a03aef93 100644 --- a/openlibrary/plugins/openlibrary/js/dialog.js +++ b/openlibrary/plugins/openlibrary/js/dialog.js @@ -8,71 +8,75 @@ import 'jquery-colorbox'; * @return {Function} for creating a confirm dialog */ function initConfirmationDialogs() { - const CONFIRMATION_PROMPT_DEFAULTS = { autoOpen: false, modal: true }; - $('#noMaster').dialog(CONFIRMATION_PROMPT_DEFAULTS); + const CONFIRMATION_PROMPT_DEFAULTS = { autoOpen: false, modal: true }; + $('#noMaster').dialog(CONFIRMATION_PROMPT_DEFAULTS); - const $confirmMerge = $('#confirmMerge') - if ($confirmMerge.length) { - $confirmMerge.dialog( - $.extend({}, CONFIRMATION_PROMPT_DEFAULTS, { - buttons: { - 'Yes, Merge': function() { - const commentInput = document.querySelector('#author-merge-comment') - if (commentInput.value) { - document.querySelector('#hidden-comment-input').value = commentInput.value - } - $('#mergeForm').trigger('submit'); - $(this).parents().find('button').attr('disabled','disabled'); - }, - 'No, Cancel': function() { - $(this).dialog('close'); - } - } - }) - ); - } - $('#leave-waitinglist-dialog').dialog( - $.extend({}, CONFIRMATION_PROMPT_DEFAULTS, { - width: 450, - resizable: false, - buttons: { - 'Yes, I\'m sure': function() { - $(this).dialog('close'); - $(this).data('origin').closest('td').find('form').trigger('submit'); - }, - 'No, cancel': function() { - $(this).dialog('close'); - } + const $confirmMerge = $('#confirmMerge'); + if ($confirmMerge.length) { + $confirmMerge.dialog( + $.extend({}, CONFIRMATION_PROMPT_DEFAULTS, { + buttons: { + 'Yes, Merge': function () { + const commentInput = document.querySelector( + '#author-merge-comment', + ); + if (commentInput.value) { + document.querySelector('#hidden-comment-input').value = + commentInput.value; } - }) + $('#mergeForm').trigger('submit'); + $(this).parents().find('button').attr('disabled', 'disabled'); + }, + 'No, Cancel': function () { + $(this).dialog('close'); + }, + }, + }), ); + } + $('#leave-waitinglist-dialog').dialog( + $.extend({}, CONFIRMATION_PROMPT_DEFAULTS, { + width: 450, + resizable: false, + buttons: { + "Yes, I'm sure": function () { + $(this).dialog('close'); + $(this).data('origin').closest('td').find('form').trigger('submit'); + }, + 'No, cancel': function () { + $(this).dialog('close'); + }, + }, + }), + ); } - export function initPreviewDialogs() { - // Delegated click handler for Book Preview buttons. - // Uses event delegation so dynamically-added buttons (e.g. from - // lazy-loaded carousels) work without re-initialization. - $(document).off('click.bookPreview').on('click.bookPreview', '[data-book-preview]', function (e) { - e.preventDefault(); - const $button = $(this); - $.colorbox({ - width: '100%', - maxWidth: '640px', - inline: true, - opacity: '0.5', - href: '#bookPreview', - onOpen() { - const $iframe = $('#bookPreview iframe'); - $iframe.prop('src', $button.data('iframe-src')); + // Delegated click handler for Book Preview buttons. + // Uses event delegation so dynamically-added buttons (e.g. from + // lazy-loaded carousels) work without re-initialization. + $(document) + .off('click.bookPreview') + .on('click.bookPreview', '[data-book-preview]', function (e) { + e.preventDefault(); + const $button = $(this); + $.colorbox({ + width: '100%', + maxWidth: '640px', + inline: true, + opacity: '0.5', + href: '#bookPreview', + onOpen() { + const $iframe = $('#bookPreview iframe'); + $iframe.prop('src', $button.data('iframe-src')); - const $link = $('#bookPreview .learn-more a'); - $link[0].href = $button.data('iframe-link'); - }, - onCleanup() { - $('#bookPreview iframe').prop('src', ''); - }, - }); + const $link = $('#bookPreview .learn-more a'); + $link[0].href = $button.data('iframe-link'); + }, + onCleanup() { + $('#bookPreview iframe').prop('src', ''); + }, + }); }); } @@ -83,21 +87,28 @@ export function initPreviewDialogs() { * communicates where the HTML of that dialog lives. */ export function initDialogs() { - $('.dialog--open').on('click', function () { - const $link = $(this), - href = `#${$link.attr('aria-controls')}`; + $('.dialog--open').on('click', function () { + const $link = $(this), + href = `#${$link.attr('aria-controls')}`; - $link.colorbox({ inline: true, opacity: '0.5', href, - maxWidth: '640px', width: '100%' }); + $link.colorbox({ + inline: true, + opacity: '0.5', + href, + maxWidth: '640px', + width: '100%', }); + }); - initConfirmationDialogs(); - initPreviewDialogs(); + initConfirmationDialogs(); + initPreviewDialogs(); - // This will close the dialog in the current page. - $('.dialog--close').attr('href', 'javascript:;').on('click', () => $.fn.colorbox.close()); - // This will close the colorbox from the parent. - $('.dialog--close-parent').on('click', () => parent.$.fn.colorbox.close()); + // This will close the dialog in the current page. + $('.dialog--close') + .attr('href', 'javascript:;') + .on('click', () => $.fn.colorbox.close()); + // This will close the colorbox from the parent. + $('.dialog--close-parent').on('click', () => parent.$.fn.colorbox.close()); } /** @@ -106,7 +117,7 @@ export function initDialogs() { * @param {NodeList<Element>} closers */ export function initDialogClosers(closers) { - closers.forEach(closer => { - $(closer).on('click', () => $.fn.colorbox.close()) - }) + closers.forEach((closer) => { + $(closer).on('click', () => $.fn.colorbox.close()); + }); } diff --git a/openlibrary/plugins/openlibrary/js/dropper/Dropper.js b/openlibrary/plugins/openlibrary/js/dropper/Dropper.js index e2caa919f1e..8a6cc37a16a 100644 --- a/openlibrary/plugins/openlibrary/js/dropper/Dropper.js +++ b/openlibrary/plugins/openlibrary/js/dropper/Dropper.js @@ -21,135 +21,139 @@ * @class */ export class Dropper { + /** + * Creates a new dropper. + * + * Sets the initial state of the dropper, and sets references to key + * dropper elements. + * + * @param {HTMLElement} dropper Reference to the dropper's root element + */ + constructor(dropper) { /** - * Creates a new dropper. + * References the root element of the dropper. * - * Sets the initial state of the dropper, and sets references to key - * dropper elements. - * - * @param {HTMLElement} dropper Reference to the dropper's root element - */ - constructor(dropper) { - /** - * References the root element of the dropper. - * - * @member {HTMLElement} - */ - this.dropper = dropper - - /** - * jQuery object containing the root element of the dropper. - * - * **Note:** jQuery is only used here for its slide animations. - * This can be removed when and if these animations are handled - * strictly with CSS. - * - * @member {JQuery<HTMLElement>} - */ - this.$dropper = $(dropper) - - /** - * Reference to the affordance that, when clicked, toggles - * the "Open" state of this dropper. - * - * @member {HTMLElement} - */ - this.dropClick = dropper.querySelector('.generic-dropper__dropclick') - - /** - * Tracks the current "Open" state of this dropper. - * - * @member {boolean} - */ - this.isDropperOpen = dropper.classList.contains('generic-dropper-wrapper--active') - - /** - * Tracks whether this dropper is disabled. - * - * A disabled dropper cannot be toggled. - * - * @member {boolean} - */ - this.isDropperDisabled = dropper.classList.contains('generic-dropper--disabled') - } - - /** - * Adds click listener to dropper's toggle arrow. + * @member {HTMLElement} */ - initialize() { - this.dropClick.addEventListener('click', () => { - this.toggleDropper() - }) - } + this.dropper = dropper; /** - * Function that is called after a dropper has opened. + * jQuery object containing the root element of the dropper. * - * Subclasses of `Dropper` may override this to add - * functionality that should occur on dropper open. + * **Note:** jQuery is only used here for its slide animations. + * This can be removed when and if these animations are handled + * strictly with CSS. + * + * @member {JQuery<HTMLElement>} */ - onOpen() {} + this.$dropper = $(dropper); /** - * Function that is called after a dropper has closed. + * Reference to the affordance that, when clicked, toggles + * the "Open" state of this dropper. * - * Subclasses of `Dropper` may override this to add - * functionality that should occur on dropper close. + * @member {HTMLElement} */ - onClose() {} + this.dropClick = dropper.querySelector('.generic-dropper__dropclick'); /** - * Function that is called when the drop-click affordance of - * a disabled dropper is clicked. + * Tracks the current "Open" state of this dropper. * - * Subclasses of `Dropper` may override this as needed. + * @member {boolean} */ - onDisabledClick() {} + this.isDropperOpen = dropper.classList.contains( + 'generic-dropper-wrapper--active', + ); /** - * Closes dropper if opened; opens dropper if closed. + * Tracks whether this dropper is disabled. * - * Toggles value of `isDropperOpen`. + * A disabled dropper cannot be toggled. * - * Calls `onDisabledClick()` if this dropper is disabled. - * Calls either `onOpen()` or `onClose()` after the dropper - * has been toggled. + * @member {boolean} */ - toggleDropper() { - if (this.isDropperDisabled) { - this.onDisabledClick(); - } else { - this.$dropper.find('.generic-dropper__dropdown').slideToggle(25); - this.$dropper.find('.arrow').toggleClass('up') - this.$dropper.toggleClass('generic-dropper-wrapper--active') - this.isDropperOpen = !this.isDropperOpen + this.isDropperDisabled = dropper.classList.contains( + 'generic-dropper--disabled', + ); + } + + /** + * Adds click listener to dropper's toggle arrow. + */ + initialize() { + this.dropClick.addEventListener('click', () => { + this.toggleDropper(); + }); + } + + /** + * Function that is called after a dropper has opened. + * + * Subclasses of `Dropper` may override this to add + * functionality that should occur on dropper open. + */ + onOpen() {} + + /** + * Function that is called after a dropper has closed. + * + * Subclasses of `Dropper` may override this to add + * functionality that should occur on dropper close. + */ + onClose() {} + + /** + * Function that is called when the drop-click affordance of + * a disabled dropper is clicked. + * + * Subclasses of `Dropper` may override this as needed. + */ + onDisabledClick() {} - if (this.isDropperOpen) { - this.onOpen() - } else { - this.onClose() - } - } + /** + * Closes dropper if opened; opens dropper if closed. + * + * Toggles value of `isDropperOpen`. + * + * Calls `onDisabledClick()` if this dropper is disabled. + * Calls either `onOpen()` or `onClose()` after the dropper + * has been toggled. + */ + toggleDropper() { + if (this.isDropperDisabled) { + this.onDisabledClick(); + } else { + this.$dropper.find('.generic-dropper__dropdown').slideToggle(25); + this.$dropper.find('.arrow').toggleClass('up'); + this.$dropper.toggleClass('generic-dropper-wrapper--active'); + this.isDropperOpen = !this.isDropperOpen; + + if (this.isDropperOpen) { + this.onOpen(); + } else { + this.onClose(); + } } + } - /** - * Closes this dropper. - * - * Sets `isDropperOpen` to `false`. - * - * Calls `onDisabledClick()` if this dropper is disabled. - * Otherwise, closes dropper and calls `onClose()`. - */ - closeDropper() { - if (this.isDropperDisabled) { - this.onDisabledClick(); - } else { - this.$dropper.find('.generic-dropper__dropdown').slideUp(25) - this.$dropper.find('.arrow').removeClass('up'); - this.$dropper.removeClass('generic-dropper-wrapper--active') - this.isDropperOpen = false + /** + * Closes this dropper. + * + * Sets `isDropperOpen` to `false`. + * + * Calls `onDisabledClick()` if this dropper is disabled. + * Otherwise, closes dropper and calls `onClose()`. + */ + closeDropper() { + if (this.isDropperDisabled) { + this.onDisabledClick(); + } else { + this.$dropper.find('.generic-dropper__dropdown').slideUp(25); + this.$dropper.find('.arrow').removeClass('up'); + this.$dropper.removeClass('generic-dropper-wrapper--active'); + this.isDropperOpen = false; - this.onClose() - } + this.onClose(); } + } } diff --git a/openlibrary/plugins/openlibrary/js/dropper/index.js b/openlibrary/plugins/openlibrary/js/dropper/index.js index 62ff4c52c59..b0db00103d5 100644 --- a/openlibrary/plugins/openlibrary/js/dropper/index.js +++ b/openlibrary/plugins/openlibrary/js/dropper/index.js @@ -4,7 +4,7 @@ import { debounce } from '../nonjquery_utils'; * Holds references to each dropper on a page. * @type {Array<HTMLElement>} */ -const droppers = [] +const droppers = []; /** * Adds expand and collapse functionality to our droppers. @@ -12,28 +12,44 @@ const droppers = [] * @param {HTMLCollection<HTMLElement>} dropperElements */ export function initDroppers(dropperElements) { - for (const dropper of dropperElements) { - droppers.push(dropper) + for (const dropper of dropperElements) { + droppers.push(dropper); - $(dropper).on('click', '.dropclick', debounce(function() { - $(this).next('.dropdown').slideToggle(25); - $(this).parent().next('.dropdown').slideToggle(25); - $(this).parent().find('.arrow').toggleClass('up'); - }, 300, false)) + $(dropper).on( + 'click', + '.dropclick', + debounce( + function () { + $(this).next('.dropdown').slideToggle(25); + $(this).parent().next('.dropdown').slideToggle(25); + $(this).parent().find('.arrow').toggleClass('up'); + }, + 300, + false, + ), + ); - $(dropper).on('click', '.dropper__close', debounce(function() { - closeDropper($(dropper)) - }, 300, false)) - } + $(dropper).on( + 'click', + '.dropper__close', + debounce( + () => { + closeDropper($(dropper)); + }, + 300, + false, + ), + ); + } - // Close any open dropdown list if the user clicks outside of component: - $(document).on('click', function(event) { - for (const dropper of droppers) { - if (!dropper.contains(event.target)) { - closeDropper($(dropper)) - } - } - }); + // Close any open dropdown list if the user clicks outside of component: + $(document).on('click', (event) => { + for (const dropper of droppers) { + if (!dropper.contains(event.target)) { + closeDropper($(dropper)); + } + } + }); } /** @@ -41,10 +57,10 @@ export function initDroppers(dropperElements) { * @param {jQuery.Object} $container */ function closeDropper($container) { - $container.find('.dropdown').slideUp(25); // Legacy droppers - $container.find('.generic-dropper__dropdown').slideUp(25) // New generic droppers - $container.find('.arrow').removeClass('up'); - $container.removeClass('generic-dropper-wrapper--active') + $container.find('.dropdown').slideUp(25); // Legacy droppers + $container.find('.generic-dropper__dropdown').slideUp(25); // New generic droppers + $container.find('.arrow').removeClass('up'); + $container.removeClass('generic-dropper-wrapper--active'); } /** @@ -57,14 +73,14 @@ function closeDropper($container) { * @param {NodeList<HTMLElement>} dropperElements */ export function initGenericDroppers(dropperElements) { - const genericDroppers = Array.from(dropperElements) + const genericDroppers = Array.from(dropperElements); - // Close any open dropdown if the user clicks outside of component: - $(document).on('click', function(event) { - for (const dropper of genericDroppers) { - if (!dropper.contains(event.target)) { - closeDropper($(dropper)) - } - } - }); + // Close any open dropdown if the user clicks outside of component: + $(document).on('click', (event) => { + for (const dropper of genericDroppers) { + if (!dropper.contains(event.target)) { + closeDropper($(dropper)); + } + } + }); } diff --git a/openlibrary/plugins/openlibrary/js/edit.js b/openlibrary/plugins/openlibrary/js/edit.js index 17b9066e4c0..5e8854a53bb 100644 --- a/openlibrary/plugins/openlibrary/js/edit.js +++ b/openlibrary/plugins/openlibrary/js/edit.js @@ -1,15 +1,15 @@ -import { isbnOverride } from './isbnOverride'; +import { init as initAutocomplete } from './autocomplete'; import { - parseIsbn, - parseLccn, - isChecksumValidIsbn10, - isChecksumValidIsbn13, - isFormatValidIsbn10, - isFormatValidIsbn13, - isValidLccn, - isIdDupe + isChecksumValidIsbn10, + isChecksumValidIsbn13, + isFormatValidIsbn10, + isFormatValidIsbn13, + isIdDupe, + isValidLccn, + parseIsbn, + parseLccn, } from './idValidation'; -import { init as initAutocomplete } from './autocomplete'; +import { isbnOverride } from './isbnOverride'; import { init as initJqueryRepeat } from './jquery.repeat'; import { trimInputValues } from './utils.js'; @@ -24,20 +24,22 @@ import { trimInputValues } from './utils.js'; /* Globals are provided by the edit about template */ function error(errordiv, input, message) { - $(errordiv).show().html(message); - $(input).trigger('focus'); - return false; + $(errordiv).show().html(message); + $(input).trigger('focus'); + return false; } function update_len() { - var len = $('#excerpts-excerpt').val().length; - var color; - if (len > 2000) { - color = '#e44028'; - } else { - color = 'gray'; - } - $('#excerpts-excerpt-len').html(2000 - len).css('color', color); + var len = $('#excerpts-excerpt').val().length; + var color; + if (len > 2000) { + color = '#e44028'; + } else { + color = 'gray'; + } + $('#excerpts-excerpt-len') + .html(2000 - len) + .css('color', color); } /** @@ -48,14 +50,14 @@ function update_len() { * @return {boolean} is character number below or equal to limit */ function limitChars(textid, limit) { - var text = $(`#${textid}`).val(); - var textlength = text.length; - if (textlength > limit) { - $(`#${textid}`).val(text.substr(0, limit)); - return false; - } else { - return true; - } + var text = $(`#${textid}`).val(); + var textlength = text.length; + if (textlength > limit) { + $(`#${textid}`).val(text.substr(0, limit)); + return false; + } else { + return true; + } } /** @@ -63,32 +65,45 @@ function limitChars(textid, limit) { * @param selector - css selector used by jQuery * @returns {*[]} - array of jQuery elements */ -function getJqueryElements(selector){ - const queryResult = $(selector); - const jQueryElementArray = []; - for (let i = 0; i < queryResult.length; i++){ - jQueryElementArray.push(queryResult.eq(i)) - } - return jQueryElementArray; +function getJqueryElements(selector) { + const queryResult = $(selector); + const jQueryElementArray = []; + for (let i = 0; i < queryResult.length; i++) { + jQueryElementArray.push(queryResult.eq(i)); + } + return jQueryElementArray; } export function initRoleValidation() { - initJqueryRepeat(); - const dataConfig = JSON.parse(document.querySelector('#roles').dataset.config); - $('#roles').repeat({ - vars: {prefix: 'edition--'}, - validate: function (data) { - if (data.role === '' || data.role === '---') { - return error('#role-errors', '#select-role', dataConfig['Please select a role.']); - } - if (data.name === '') { - return error('#role-errors', '#role-name', dataConfig['You need to give this ROLE a name.'].replace(/ROLE/, data.role)); - } - $('#role-errors').hide(); - $('#select-role, #role-name').val(''); - return true; - } - }); + initJqueryRepeat(); + const dataConfig = JSON.parse( + document.querySelector('#roles').dataset.config, + ); + $('#roles').repeat({ + vars: { prefix: 'edition--' }, + validate: (data) => { + if (data.role === '' || data.role === '---') { + return error( + '#role-errors', + '#select-role', + dataConfig['Please select a role.'], + ); + } + if (data.name === '') { + return error( + '#role-errors', + '#role-name', + dataConfig['You need to give this ROLE a name.'].replace( + /ROLE/, + data.role, + ), + ); + } + $('#role-errors').hide(); + $('#select-role, #role-name').val(''); + return true; + }, + }); } /** @@ -98,26 +113,26 @@ export function initRoleValidation() { * @param {String} isbnConfirmString a const with the HTML to create the confirmation message/buttons */ export function isbnConfirmAdd(data) { - const isbnConfirmString = `ISBN ${data.value} may be invalid. Add it anyway? <button class="repeat-add" id="yes-add-isbn" type="button">Yes</button> <button id="do-not-add-isbn" type="button">No</button>`; - // Display the error and option to add the ISBN anyway. - $('#id-errors').show().html(isbnConfirmString); - - const yesButtonSelector = '#yes-add-isbn' - const noButtonSelector = '#do-not-add-isbn' - const onYes = () => { - $('#id-errors').hide(); - }; - const onNo = () => { - $('#id-errors').hide(); - isbnOverride.clear(); - } - $(document).on('click', yesButtonSelector, onYes); - $(document).on('click', noButtonSelector, onNo); + const isbnConfirmString = `ISBN ${data.value} may be invalid. Add it anyway? <button class="repeat-add" id="yes-add-isbn" type="button">Yes</button> <button id="do-not-add-isbn" type="button">No</button>`; + // Display the error and option to add the ISBN anyway. + $('#id-errors').show().html(isbnConfirmString); - // Save the data to isbnOverride so it can be picked up via onAdd in - // js/jquery.repeat.js when the user confirms adding the invalid ISBN. - isbnOverride.set(data) - return false; + const yesButtonSelector = '#yes-add-isbn'; + const noButtonSelector = '#do-not-add-isbn'; + const onYes = () => { + $('#id-errors').hide(); + }; + const onNo = () => { + $('#id-errors').hide(); + isbnOverride.clear(); + }; + $(document).on('click', yesButtonSelector, onYes); + $(document).on('click', noButtonSelector, onNo); + + // Save the data to isbnOverride so it can be picked up via onAdd in + // js/jquery.repeat.js when the user confirms adding the invalid ISBN. + isbnOverride.set(data); + return false; } /** @@ -129,19 +144,29 @@ export function isbnConfirmAdd(data) { * @returns {boolean} true if ISBN passes validation, else returns false and displays appropriate error */ function validateIsbn10(data, dataConfig, label) { - data.value = parseIsbn(data.value); - - if (!isFormatValidIsbn10(data.value)) { - return error('#id-errors', '#id-value', dataConfig['ID must be exactly 10 characters [0-9] or X.'].replace(/ID/, label)); - } - // Here the ISBN has a valid format, but also has an invalid checksum. Give the user a chance to verify - // the ISBN, as books sometimes issue with invalid ISBNs and we want to be able to add them. - // See https://en-academic.com/dic.nsf/enwiki/8948#cite_ref-18 for more. - else if (isFormatValidIsbn10(data.value) === true && isChecksumValidIsbn10(data.value) === false) { - isbnConfirmAdd(data) - return false - } - return true; + data.value = parseIsbn(data.value); + + if (!isFormatValidIsbn10(data.value)) { + return error( + '#id-errors', + '#id-value', + dataConfig['ID must be exactly 10 characters [0-9] or X.'].replace( + /ID/, + label, + ), + ); + } + // Here the ISBN has a valid format, but also has an invalid checksum. Give the user a chance to verify + // the ISBN, as books sometimes issue with invalid ISBNs and we want to be able to add them. + // See https://en-academic.com/dic.nsf/enwiki/8948#cite_ref-18 for more. + else if ( + isFormatValidIsbn10(data.value) === true && + isChecksumValidIsbn10(data.value) === false + ) { + isbnConfirmAdd(data); + return false; + } + return true; } /** @@ -153,19 +178,28 @@ function validateIsbn10(data, dataConfig, label) { * @returns {boolean} true if ISBN passes validation, else returns false and displays appropriate error */ function validateIsbn13(data, dataConfig, label) { - data.value = parseIsbn(data.value); - - if (isFormatValidIsbn13(data.value) === false) { - return error('#id-errors', '#id-value', dataConfig['ID must be exactly 13 digits [0-9]. For example: 978-1-56619-909-4'].replace(/ID/, label)); - } - // Here the ISBN has a valid format, but also has an invalid checksum. Give the user a chance to verify - // the ISBN, as books sometimes issue with invalid ISBNs and we want to be able to add them. - // See https://en-academic.com/dic.nsf/enwiki/8948#cite_ref-18 for more. - else if (isFormatValidIsbn13(data.value) === true && isChecksumValidIsbn13(data.value) === false) { - isbnConfirmAdd(data) - return false - } - return true; + data.value = parseIsbn(data.value); + + if (isFormatValidIsbn13(data.value) === false) { + return error( + '#id-errors', + '#id-value', + dataConfig[ + 'ID must be exactly 13 digits [0-9]. For example: 978-1-56619-909-4' + ].replace(/ID/, label), + ); + } + // Here the ISBN has a valid format, but also has an invalid checksum. Give the user a chance to verify + // the ISBN, as books sometimes issue with invalid ISBNs and we want to be able to add them. + // See https://en-academic.com/dic.nsf/enwiki/8948#cite_ref-18 for more. + else if ( + isFormatValidIsbn13(data.value) === true && + isChecksumValidIsbn13(data.value) === false + ) { + isbnConfirmAdd(data); + return false; + } + return true; } /** @@ -177,13 +211,17 @@ function validateIsbn13(data, dataConfig, label) { * @returns {boolean} true if LCCN passes validation, else returns false and displays appropriate error */ function validateLccn(data, dataConfig, label) { - data.value = parseLccn(data.value); - - if (isValidLccn(data.value) === false) { - $('#id-value').val(data.value); - return error('#id-errors', '#id-value', dataConfig['Invalid ID format'].replace(/ID/, label)); - } - return true; + data.value = parseLccn(data.value); + + if (isValidLccn(data.value) === false) { + $('#id-value').val(data.value); + return error( + '#id-errors', + '#id-value', + dataConfig['Invalid ID format'].replace(/ID/, label), + ); + } + return true; } /** @@ -194,219 +232,281 @@ function validateLccn(data, dataConfig, label) { * @returns {boolean} true if identifier passes validation */ export function validateIdentifiers(data) { - const dataConfig = JSON.parse(document.querySelector('#identifiers').dataset.config); - - if (data.name === '' || data.name === '---') { - $('#id-value').val(data.value); - return error('#id-errors', '#select-id', dataConfig['Please select an identifier.']) - } - const label = $('#select-id').find(`option[value='${data.name}']`).html(); - if (data.value === '') { - return error('#id-errors', '#id-value', dataConfig['You need to give a value to ID.'].replace(/ID/, label)); - } - if (['ocaid'].includes(data.name) && /\s/g.test(data.value)) { - return error('#id-errors', '#id-value', dataConfig['ID ids cannot contain whitespace.'].replace(/ID/, label)); - } - - let validId = true; - if (data.name === 'isbn_10') { - validId = validateIsbn10(data, dataConfig, label); - } - else if (data.name === 'isbn_13') { - validId = validateIsbn13(data, dataConfig, label); - } - else if (data.name === 'lccn') { - validId = validateLccn(data, dataConfig, label); - } - - // checking for duplicate identifier entry on all identifier types - // expects parsed ids so placed after validate - const entries = document.querySelectorAll(`.${data.name}`); - if (isIdDupe(entries, data.value) === true) { - // isbnOverride being set will override the dupe checker, so clear isbnOverride if there's a dupe. - if (isbnOverride.get()) {isbnOverride.clear()} - return error('#id-errors', '#id-value', dataConfig['That ID already exists for this edition.'].replace(/ID/, label)); + const dataConfig = JSON.parse( + document.querySelector('#identifiers').dataset.config, + ); + + if (data.name === '' || data.name === '---') { + $('#id-value').val(data.value); + return error( + '#id-errors', + '#select-id', + dataConfig['Please select an identifier.'], + ); + } + const label = $('#select-id').find(`option[value='${data.name}']`).html(); + if (data.value === '') { + return error( + '#id-errors', + '#id-value', + dataConfig['You need to give a value to ID.'].replace(/ID/, label), + ); + } + if (['ocaid'].includes(data.name) && /\s/g.test(data.value)) { + return error( + '#id-errors', + '#id-value', + dataConfig['ID ids cannot contain whitespace.'].replace(/ID/, label), + ); + } + + let validId = true; + if (data.name === 'isbn_10') { + validId = validateIsbn10(data, dataConfig, label); + } else if (data.name === 'isbn_13') { + validId = validateIsbn13(data, dataConfig, label); + } else if (data.name === 'lccn') { + validId = validateLccn(data, dataConfig, label); + } + + // checking for duplicate identifier entry on all identifier types + // expects parsed ids so placed after validate + const entries = document.querySelectorAll(`.${data.name}`); + if (isIdDupe(entries, data.value) === true) { + // isbnOverride being set will override the dupe checker, so clear isbnOverride if there's a dupe. + if (isbnOverride.get()) { + isbnOverride.clear(); } - - if (validId === false) return false; - $('#id-errors').hide(); - return true; + return error( + '#id-errors', + '#id-value', + dataConfig['That ID already exists for this edition.'].replace( + /ID/, + label, + ), + ); + } + + if (validId === false) return false; + $('#id-errors').hide(); + return true; } export function initClassificationValidation() { - initJqueryRepeat(); - const dataConfig = JSON.parse(document.querySelector('#classifications').dataset.config); - - // Prevent form submission on Enter for classification fields - $('#classification-value').on('keydown', function(e) { - if (e.key === 'Enter') { - e.preventDefault(); - $('#classifications .repeat-add').trigger('click'); - return false; - } - }); - - $('#classifications').repeat({ - vars: {prefix: 'edition--'}, - validate: function (data) { - if (data.name === '' || data.name === '---') { - return error('#classification-errors', '#select-classification', dataConfig['Please select a classification.']); - } - if (data.value === '') { - const label = $('#select-classification').find(`option[value='${data.name}']`).html(); - return error('#classification-errors', '#classification-value', dataConfig['You need to give a value to CLASS.'].replace(/CLASS/, label)); - } - $('#classification-errors').hide(); - $('#select-classification, #classification-value').val(''); - return true; - } - }); + initJqueryRepeat(); + const dataConfig = JSON.parse( + document.querySelector('#classifications').dataset.config, + ); + + // Prevent form submission on Enter for classification fields + $('#classification-value').on('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + $('#classifications .repeat-add').trigger('click'); + return false; + } + }); + + $('#classifications').repeat({ + vars: { prefix: 'edition--' }, + validate: (data) => { + if (data.name === '' || data.name === '---') { + return error( + '#classification-errors', + '#select-classification', + dataConfig['Please select a classification.'], + ); + } + if (data.value === '') { + const label = $('#select-classification') + .find(`option[value='${data.name}']`) + .html(); + return error( + '#classification-errors', + '#classification-value', + dataConfig['You need to give a value to CLASS.'].replace( + /CLASS/, + label, + ), + ); + } + $('#classification-errors').hide(); + $('#select-classification, #classification-value').val(''); + return true; + }, + }); } export function initLanguageMultiInputAutocomplete() { - initAutocomplete(); - $(function() { - getJqueryElements('.multi-input-autocomplete--language').forEach(jqueryElement => { - jqueryElement.setup_multi_input_autocomplete( - render_language_field, - { - endpoint: '/languages/_autocomplete', - sortable: true, - }, - { - max: 6, - formatItem: render_language_autocomplete_item - } - ); - }) - }); + initAutocomplete(); + $(() => { + getJqueryElements('.multi-input-autocomplete--language').forEach( + (jqueryElement) => { + jqueryElement.setup_multi_input_autocomplete( + render_language_field, + { + endpoint: '/languages/_autocomplete', + sortable: true, + }, + { + max: 6, + formatItem: render_language_autocomplete_item, + }, + ); + }, + ); + }); } export function initWorksMultiInputAutocomplete() { - initAutocomplete(); - $(function() { - getJqueryElements('.multi-input-autocomplete--works').forEach(jqueryElement => { - /* Values in the html passed from Python code */ - const dataConfig = JSON.parse(jqueryElement[0].dataset.config || '{}'); - jqueryElement.setup_multi_input_autocomplete( - render_work_field, - { - endpoint: '/works/_autocomplete', - addnew: dataConfig['addnew'] || false, - new_name: dataConfig['new_name'] || '', - allow_empty: dataConfig['allow_empty'] || false, - }, - { - minChars: 2, - max: 11, - matchSubset: false, - autoFill: true, - formatItem: render_work_autocomplete_item, - }); - }); - }); - - // Show the new work options checkboxes only if "New work" selected - $('input[name="works--0"]').on('autocompleteselect', function(_event, ui) { - $('.new-work-options').toggle(ui.item.key === '__new__'); - }); + initAutocomplete(); + $(() => { + getJqueryElements('.multi-input-autocomplete--works').forEach( + (jqueryElement) => { + /* Values in the html passed from Python code */ + const dataConfig = JSON.parse(jqueryElement[0].dataset.config || '{}'); + jqueryElement.setup_multi_input_autocomplete( + render_work_field, + { + endpoint: '/works/_autocomplete', + addnew: dataConfig['addnew'] || false, + new_name: dataConfig['new_name'] || '', + allow_empty: dataConfig['allow_empty'] || false, + }, + { + minChars: 2, + max: 11, + matchSubset: false, + autoFill: true, + formatItem: render_work_autocomplete_item, + }, + ); + }, + ); + }); + + // Show the new work options checkboxes only if "New work" selected + $('input[name="works--0"]').on('autocompleteselect', (_event, ui) => { + $('.new-work-options').toggle(ui.item.key === '__new__'); + }); } export function initSeedsMultiInputAutocomplete() { - initAutocomplete(); - $(function() { - getJqueryElements('.multi-input-autocomplete--seeds').forEach(jqueryElement => { - /* Values in the html passed from Python code */ - jqueryElement.setup_multi_input_autocomplete( - render_seed_field, - { - endpoint: '/works/_autocomplete', - addnew: false, - allow_empty: true, - sortable: true, - }, - { - minChars: 2, - max: 11, - matchSubset: false, - autoFill: true, - formatItem: render_lazy_work_preview, - }); - }); - }); + initAutocomplete(); + $(() => { + getJqueryElements('.multi-input-autocomplete--seeds').forEach( + (jqueryElement) => { + /* Values in the html passed from Python code */ + jqueryElement.setup_multi_input_autocomplete( + render_seed_field, + { + endpoint: '/works/_autocomplete', + addnew: false, + allow_empty: true, + sortable: true, + }, + { + minChars: 2, + max: 11, + matchSubset: false, + autoFill: true, + formatItem: render_lazy_work_preview, + }, + ); + }, + ); + }); } export function initAuthorMultiInputAutocomplete() { - initAutocomplete(); - getJqueryElements('.multi-input-autocomplete--author').forEach(jqueryElement => { - /* Values in the html passed from Python code */ - const dataConfig = JSON.parse(jqueryElement[0].dataset.config); - jqueryElement.setup_multi_input_autocomplete( - render_author.bind(null, dataConfig.name_path, dataConfig.dict_path, false), - { - endpoint: '/authors/_autocomplete', - // Don't render "Create new author" if searching by key - addnew: query => !/OL\d+A/i.test(query), - sortable: true, - }, - { - minChars: 2, - max: 11, - matchSubset: false, - autoFill: true, - formatItem: render_author_autocomplete_item - }); - }); + initAutocomplete(); + getJqueryElements('.multi-input-autocomplete--author').forEach( + (jqueryElement) => { + /* Values in the html passed from Python code */ + const dataConfig = JSON.parse(jqueryElement[0].dataset.config); + jqueryElement.setup_multi_input_autocomplete( + render_author.bind( + null, + dataConfig.name_path, + dataConfig.dict_path, + false, + ), + { + endpoint: '/authors/_autocomplete', + // Don't render "Create new author" if searching by key + addnew: (query) => !/OL\d+A/i.test(query), + sortable: true, + }, + { + minChars: 2, + max: 11, + matchSubset: false, + autoFill: true, + formatItem: render_author_autocomplete_item, + }, + ); + }, + ); } export function initSeriesMultiInputAutocomplete() { - initAutocomplete(); - getJqueryElements('.multi-input-autocomplete--series').forEach(jqueryElement => { - /* Values in the html passed from Python code */ - const dataConfig = JSON.parse(jqueryElement[0].dataset.config); - jqueryElement.setup_multi_input_autocomplete( - render_series.bind(null, dataConfig.name_path, dataConfig.dict_path, false), - { - endpoint: '/series/_autocomplete', - // Don't render "Create new series" if searching by key - addnew: query => !/OL\d+L/i.test(query), - sortable: true, - }, - { - minChars: 2, - max: 11, - matchSubset: false, - autoFill: true, - formatItem: render_series_autocomplete_item - }); - }); + initAutocomplete(); + getJqueryElements('.multi-input-autocomplete--series').forEach( + (jqueryElement) => { + /* Values in the html passed from Python code */ + const dataConfig = JSON.parse(jqueryElement[0].dataset.config); + jqueryElement.setup_multi_input_autocomplete( + render_series.bind( + null, + dataConfig.name_path, + dataConfig.dict_path, + false, + ), + { + endpoint: '/series/_autocomplete', + // Don't render "Create new series" if searching by key + addnew: (query) => !/OL\d+L/i.test(query), + sortable: true, + }, + { + minChars: 2, + max: 11, + matchSubset: false, + autoFill: true, + formatItem: render_series_autocomplete_item, + }, + ); + }, + ); } export function initSubjectsAutocomplete() { - initAutocomplete(); - getJqueryElements('.csv-autocomplete--subjects').forEach(jqueryElement => { - const dataConfig = JSON.parse(jqueryElement[0].dataset.config); - jqueryElement.setup_csv_autocomplete( - 'textarea', - { - endpoint: `/subjects_autocomplete?type=${dataConfig.facet}`, - addnew: false, - }, - { - formatItem: render_subject_autocomplete_item, - } - ); - }); - - /* Resize textarea to fit on input */ - $('.csv-autocomplete--subjects textarea').on('input', function () { - this.style.height = 'auto'; - this.style.height = `${this.scrollHeight + 5}px`; - }); + initAutocomplete(); + getJqueryElements('.csv-autocomplete--subjects').forEach((jqueryElement) => { + const dataConfig = JSON.parse(jqueryElement[0].dataset.config); + jqueryElement.setup_csv_autocomplete( + 'textarea', + { + endpoint: `/subjects_autocomplete?type=${dataConfig.facet}`, + addnew: false, + }, + { + formatItem: render_subject_autocomplete_item, + }, + ); + }); + + /* Resize textarea to fit on input */ + $('.csv-autocomplete--subjects textarea').on('input', function () { + this.style.height = 'auto'; + this.style.height = `${this.scrollHeight + 5}px`; + }); } -export function initEditRow(){ - document.querySelector('#add_row_button').addEventListener('click', ()=>add_row('website')); +export function initEditRow() { + document + .querySelector('#add_row_button') + .addEventListener('click', () => add_row('website')); } /** @@ -414,57 +514,67 @@ export function initEditRow(){ * @param string name - when prefixed with clone_ should match an element identifier in the page. e.g. if name would refer to clone_website */ function add_row(name) { - const inputBoxes = document.querySelectorAll(`#clone_${name} input`); - const inputBox = document.createElement('input'); - inputBox.name = `${name}#${inputBoxes.length}`; - inputBox.type = 'text'; - inputBoxes[inputBoxes.length-1].after(inputBox); + const inputBoxes = document.querySelectorAll(`#clone_${name} input`); + const inputBox = document.createElement('input'); + inputBox.name = `${name}#${inputBoxes.length}`; + inputBox.type = 'text'; + inputBoxes[inputBoxes.length - 1].after(inputBox); } function show_hide_title() { - if ($('#excerpts-display .repeat-item').length > 1) { - $('#excerpts-so-far').show(); - } else { - $('#excerpts-so-far').hide(); - } + if ($('#excerpts-display .repeat-item').length > 1) { + $('#excerpts-so-far').show(); + } else { + $('#excerpts-so-far').hide(); + } } export function initEditExcerpts() { - initJqueryRepeat(); - $('#excerpts').repeat({ - vars: { - prefix: 'work--excerpts', - }, - validate: function(data) { - const i18nStrings = JSON.parse(document.querySelector('#excerpts-errors').dataset.i18n); - - if (!data.excerpt) { - return error('#excerpts-errors', '#excerpts-excerpt', i18nStrings['empty_excerpt']); - } - if (data.excerpt.length > 2000) { - return error('#excerpts-errors', '#excerpts-excerpt', i18nStrings['over_wordcount']); - } - $('#excerpts-errors').hide(); - $('#excerpts-excerpt').val(''); - return true; - } - }); - - // update length on every keystroke - $('#excerpts-excerpt').on('keyup', function() { - limitChars('excerpts-excerpt', 2000); - update_len(); - }); - - // update length on add. - $('#excerpts') - .on('repeat-add', update_len) - .on('repeat-add', show_hide_title) - .on('repeat-remove', show_hide_title); - - // update length on load + initJqueryRepeat(); + $('#excerpts').repeat({ + vars: { + prefix: 'work--excerpts', + }, + validate: (data) => { + const i18nStrings = JSON.parse( + document.querySelector('#excerpts-errors').dataset.i18n, + ); + + if (!data.excerpt) { + return error( + '#excerpts-errors', + '#excerpts-excerpt', + i18nStrings['empty_excerpt'], + ); + } + if (data.excerpt.length > 2000) { + return error( + '#excerpts-errors', + '#excerpts-excerpt', + i18nStrings['over_wordcount'], + ); + } + $('#excerpts-errors').hide(); + $('#excerpts-excerpt').val(''); + return true; + }, + }); + + // update length on every keystroke + $('#excerpts-excerpt').on('keyup', () => { + limitChars('excerpts-excerpt', 2000); update_len(); - show_hide_title(); + }); + + // update length on add. + $('#excerpts') + .on('repeat-add', update_len) + .on('repeat-add', show_hide_title) + .on('repeat-remove', show_hide_title); + + // update length on load + update_len(); + show_hide_title(); } /** @@ -477,38 +587,40 @@ export function initEditExcerpts() { * - '#link-errors' */ export function initEditLinks() { - initJqueryRepeat(); - $('#links').repeat({ - vars: { - prefix: $('#links').data('prefix') - }, - validate: function(data) { - const i18nStrings = JSON.parse(document.querySelector('#link-errors').dataset.i18n); - const url = data.url.trim(); - - if (data.title.trim() === '') { - $('#link-errors').html(i18nStrings['empty_label']); - $('#link-errors').removeClass('hidden'); - $('#link-label').trigger('focus'); - return false; - } - if (url === '') { - $('#link-errors').html(i18nStrings['empty_url']); - $('#link-errors').removeClass('hidden'); - $('#link-url').trigger('focus'); - return false; - } - if (!isValidURL(url)) { - $('#link-errors').html(i18nStrings['invalid_url']); - $('#link-errors').removeClass('hidden'); - $('#link-url').trigger('focus'); - return false; - } - $('#link-errors').addClass('hidden'); - $('#link-label, #link-url').val(''); - return true; - } - }); + initJqueryRepeat(); + $('#links').repeat({ + vars: { + prefix: $('#links').data('prefix'), + }, + validate: (data) => { + const i18nStrings = JSON.parse( + document.querySelector('#link-errors').dataset.i18n, + ); + const url = data.url.trim(); + + if (data.title.trim() === '') { + $('#link-errors').html(i18nStrings['empty_label']); + $('#link-errors').removeClass('hidden'); + $('#link-label').trigger('focus'); + return false; + } + if (url === '') { + $('#link-errors').html(i18nStrings['empty_url']); + $('#link-errors').removeClass('hidden'); + $('#link-url').trigger('focus'); + return false; + } + if (!isValidURL(url)) { + $('#link-errors').html(i18nStrings['invalid_url']); + $('#link-errors').removeClass('hidden'); + $('#link-url').trigger('focus'); + return false; + } + $('#link-errors').addClass('hidden'); + $('#link-label, #link-url').val(''); + return true; + }, + }); } /** @@ -520,24 +632,24 @@ export function initEditLinks() { * - '#contentHead' */ export function initEdit() { - var hash = document.location.hash || '#edition'; - var tab = hash.split('/')[0]; - var link = `#link_${tab.substring(1)}`; - var fieldname = `:input${hash.replace('/', '-')}`; - - trimInputValues('.olform input'); - - $(link).trigger('click'); - - // input field is enabled only after the tab is selected and that takes some time after clicking the link. - // wait for 1 sec after clicking the link and focus the input field - if ($(fieldname).length !== 0) { - setTimeout(function() { - // scroll such that top of the content is visible - $(fieldname).trigger('focus'); - $(window).scrollTop($('#contentHead').offset().top); - }, 1000); - } + var hash = document.location.hash || '#edition'; + var tab = hash.split('/')[0]; + var link = `#link_${tab.substring(1)}`; + var fieldname = `:input${hash.replace('/', '-')}`; + + trimInputValues('.olform input'); + + $(link).trigger('click'); + + // input field is enabled only after the tab is selected and that takes some time after clicking the link. + // wait for 1 sec after clicking the link and focus the input field + if ($(fieldname).length !== 0) { + setTimeout(() => { + // scroll such that top of the content is visible + $(fieldname).trigger('focus'); + $(window).scrollTop($('#contentHead').offset().top); + }, 1000); + } } /** @@ -545,10 +657,10 @@ export function initEdit() { * @param string url */ function isValidURL(url) { - try { - new URL(url); - return true; - } catch (e) { - return false; - } + try { + new URL(url); + return true; + } catch (e) { + return false; + } } diff --git a/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js b/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js index 7bb6228545f..206bec2ef33 100644 --- a/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js +++ b/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js @@ -3,143 +3,151 @@ * @module edition-nav-bar/EditionNavBar */ export default class EdtionNavBar { + /** + * Adds functionality to the given navbar element. + * + * @param {HTMLElement} navbarWrapper + */ + constructor(navbarWrapper) { /** - * Adds functionality to the given navbar element. - * - * @param {HTMLElement} navbarWrapper + * Reference to the parent element of the navbar. + * @type {HTMLElement} */ - constructor(navbarWrapper) { - /** - * Reference to the parent element of the navbar. - * @type {HTMLElement} - */ - this.navbarWrapper = navbarWrapper - /** - * The navbar - * @type {HTMLElement} - */ - this.navbarElem = navbarWrapper.querySelector('.work-menu') - /** - * The mobile-only navigation arrow. Not guaranteed to exist. - * @type {HTMLElement|null} - */ - this.navArrowLeft = navbarWrapper.querySelector('.nav-arrow-left') - /** - * The mobile-only navigation arrow. Not guaranteed to exist. - * @type {HTMLElement|null} - */ - this.navArrowRight = navbarWrapper.querySelector('.nav-arrow-right') - /** - * References each nav item in this navbar. - * @type {Array<HTMLLIElement>} - */ - this.navItems = Array.from(this.navbarElem.querySelectorAll('li')) - /** - * Index of the currently selected nav item. - * @type {number} - */ - this.selectedIndex = 0 - /** - * The nav items' target anchor elements. - * @type {HTMLAnchorElement} - */ - this.targetAnchors = [] - - this.initialize() - } - + this.navbarWrapper = navbarWrapper; + /** + * The navbar + * @type {HTMLElement} + */ + this.navbarElem = navbarWrapper.querySelector('.work-menu'); + /** + * The mobile-only navigation arrow. Not guaranteed to exist. + * @type {HTMLElement|null} + */ + this.navArrowLeft = navbarWrapper.querySelector('.nav-arrow-left'); /** - * Adds the necessary event handlers to the navbar. + * The mobile-only navigation arrow. Not guaranteed to exist. + * @type {HTMLElement|null} */ - initialize() { - // Add click listeners to navbar items: - for (let i = 0; i < this.navItems.length; ++i) { - this.navItems[i].addEventListener('click', () => { - this.selectedIndex = i - this.selectElement(this.navItems[i]) - }) + this.navArrowRight = navbarWrapper.querySelector('.nav-arrow-right'); + /** + * References each nav item in this navbar. + * @type {Array<HTMLLIElement>} + */ + this.navItems = Array.from(this.navbarElem.querySelectorAll('li')); + /** + * Index of the currently selected nav item. + * @type {number} + */ + this.selectedIndex = 0; + /** + * The nav items' target anchor elements. + * @type {HTMLAnchorElement} + */ + this.targetAnchors = []; - // Add this nav item's target anchor to array: - this.targetAnchors.push(document.getElementById(this.navItems[i].children[0].hash.substring(1))) + this.initialize(); + } - // Set selectedIndex to the correct value: - if (this.navItems[i].classList.contains('selected')) { - this.selectedIndex = i - } - } + /** + * Adds the necessary event handlers to the navbar. + */ + initialize() { + // Add click listeners to navbar items: + for (let i = 0; i < this.navItems.length; ++i) { + this.navItems[i].addEventListener('click', () => { + this.selectedIndex = i; + this.selectElement(this.navItems[i]); + }); - // Add click listener to mobile-only navigation arrow: - if (this.navArrowLeft) { - this.navArrowLeft.addEventListener('click', () => { - if (this.selectedIndex > 0) { - --this.selectedIndex - this.navItems[this.selectedIndex].children[0].click() - } - }) - } + // Add this nav item's target anchor to array: + this.targetAnchors.push( + document.getElementById(this.navItems[i].children[0].hash.substring(1)), + ); - if (this.navArrowRight) { - this.navArrowRight.addEventListener('click', () => { - if (this.selectedIndex < this.navItems.length - 1) { - // Simulate click on the next nav item: - ++this.selectedIndex - this.navItems[this.selectedIndex].children[0].click() - } - }) - } + // Set selectedIndex to the correct value: + if (this.navItems[i].classList.contains('selected')) { + this.selectedIndex = i; + } + } - // Add scroll listener for position-aware nav item selection - document.addEventListener('scroll', () => { - this.updateSelected() - }) + // Add click listener to mobile-only navigation arrow: + if (this.navArrowLeft) { + this.navArrowLeft.addEventListener('click', () => { + if (this.selectedIndex > 0) { + --this.selectedIndex; + this.navItems[this.selectedIndex].children[0].click(); + } + }); } - /** - * Determines this navbar's position on the page and updates the selected - * nav item. - */ - updateSelected() { - const navbarHeight = this.navbarWrapper.getBoundingClientRect().height - if (navbarHeight > 0) { - let i = this.navItems.length - // 10 is for a little bit of padding - while (--i > 0 && this.navbarWrapper.offsetTop + navbarHeight < (this.targetAnchors[i].offsetTop - 10)) { } - if (i !== this.selectedIndex) { - this.selectedIndex = i - this.selectElement(this.navItems[i]) - } + if (this.navArrowRight) { + this.navArrowRight.addEventListener('click', () => { + if (this.selectedIndex < this.navItems.length - 1) { + // Simulate click on the next nav item: + ++this.selectedIndex; + this.navItems[this.selectedIndex].children[0].click(); } + }); } - /** - * Centers given nav item in the navbar, if navbar has overflow (mobile views) - * - * @param {HTMLElement} selectedItem Newly selected nav item - */ - scrollNavbar(selectedItem) { - // Note: We don't use the browser native scrollIntoView method because - // that method scrolls _recursively_, so it also tries to scroll the - // body to center the element on the screen, causing weird jitters. + // Add scroll listener for position-aware nav item selection + document.addEventListener('scroll', () => { + this.updateSelected(); + }); + } - this.navbarElem.scrollTo({ - left: selectedItem.offsetLeft - (this.navbarElem.clientWidth - selectedItem.offsetWidth) / 2, - behavior: 'instant' - }) + /** + * Determines this navbar's position on the page and updates the selected + * nav item. + */ + updateSelected() { + const navbarHeight = this.navbarWrapper.getBoundingClientRect().height; + if (navbarHeight > 0) { + let i = this.navItems.length; + // 10 is for a little bit of padding + while ( + --i > 0 && + this.navbarWrapper.offsetTop + navbarHeight < + this.targetAnchors[i].offsetTop - 10 + ) {} + if (i !== this.selectedIndex) { + this.selectedIndex = i; + this.selectElement(this.navItems[i]); + } } + } - /** - * Adds 'selected' class to the given element. - * - * Removes 'selected' class from all navbar links, then adds - * 'selected' to the newly selected link. - * - * @param {HTMLLIElement} selectedElem Element corresponding to the 'selected' navbar item. - */ - selectElement(selectedElem) { - for (const li of this.navItems) { - li.classList.remove('selected') - } - selectedElem.classList.add('selected') - this.scrollNavbar(selectedElem) + /** + * Centers given nav item in the navbar, if navbar has overflow (mobile views) + * + * @param {HTMLElement} selectedItem Newly selected nav item + */ + scrollNavbar(selectedItem) { + // Note: We don't use the browser native scrollIntoView method because + // that method scrolls _recursively_, so it also tries to scroll the + // body to center the element on the screen, causing weird jitters. + + this.navbarElem.scrollTo({ + left: + selectedItem.offsetLeft - + (this.navbarElem.clientWidth - selectedItem.offsetWidth) / 2, + behavior: 'instant', + }); + } + + /** + * Adds 'selected' class to the given element. + * + * Removes 'selected' class from all navbar links, then adds + * 'selected' to the newly selected link. + * + * @param {HTMLLIElement} selectedElem Element corresponding to the 'selected' navbar item. + */ + selectElement(selectedElem) { + for (const li of this.navItems) { + li.classList.remove('selected'); } + selectedElem.classList.add('selected'); + this.scrollNavbar(selectedElem); + } } diff --git a/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js b/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js index dcccf4c477c..df0f1e4530b 100644 --- a/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js +++ b/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js @@ -4,7 +4,7 @@ import EdtionNavBar from './EditionNavBar'; * Holds references to each book page navbar. * @type {Array<EditionNavBar>} */ -const navbars = [] +const navbars = []; /** * Initializes and stores references to each book page navbar. @@ -12,10 +12,10 @@ const navbars = [] * @param {HTMLCollection<HTMLElement>} navbarWrappers Each navbar found on the book page */ export function initNavbars(navbarWrappers) { - for (const wrapper of navbarWrappers) { - const navbar = new EdtionNavBar(wrapper) - navbars.push(navbar) - } + for (const wrapper of navbarWrappers) { + const navbar = new EdtionNavBar(wrapper); + navbars.push(navbar); + } } /** @@ -27,7 +27,7 @@ export function initNavbars(navbarWrappers) { * stickied to a new position). */ export function updateSelectedNavItem() { - for (const navbar of navbars) { - navbar.updateSelected() - } + for (const navbar of navbars) { + navbar.updateSelected(); + } } diff --git a/openlibrary/plugins/openlibrary/js/editions-table/index.js b/openlibrary/plugins/openlibrary/js/editions-table/index.js index 8b297e90999..13cfd006b28 100755 --- a/openlibrary/plugins/openlibrary/js/editions-table/index.js +++ b/openlibrary/plugins/openlibrary/js/editions-table/index.js @@ -5,106 +5,111 @@ const DEFAULT_LENGTH = 3; const LS_RESULTS_LENGTH_KEY = 'editions-table.resultsLength'; export function initEditionsTable() { - var rowCount; - let currentLength; - // Prevent reinitialization of the editions datatable - if ($.fn.DataTable.isDataTable($('#editions'))) { - return; + var rowCount; + let currentLength; + // Prevent reinitialization of the editions datatable + if ($.fn.DataTable.isDataTable($('#editions'))) { + return; + } + $('#editions th.title').on('mouseover', function () { + if ($(this).hasClass('sorting_asc')) { + $(this).attr('title', 'Sort latest to earliest'); + } else if ($(this).hasClass('sorting_desc')) { + $(this).attr('title', 'Sort earliest to latest'); + } else { + $(this).attr('title', 'Sort by publish date'); } - $('#editions th.title').on('mouseover', function(){ - if ($(this).hasClass('sorting_asc')) { - $(this).attr('title','Sort latest to earliest'); - } else if ($(this).hasClass('sorting_desc')) { - $(this).attr('title','Sort earliest to latest'); - } else { - $(this).attr('title','Sort by publish date'); - } - }); - $('#editions th.read').on('mouseover', function(){ - if ($(this).hasClass('sorting_asc')) { - $(this).attr('title','Push readable versions to the bottom'); - } else if ($(this).hasClass('sorting_desc')) { - $(this).attr('title','Sort by editions to read'); - } else { - $(this).attr('title','Available to read'); - } - }); + }); + $('#editions th.read').on('mouseover', function () { + if ($(this).hasClass('sorting_asc')) { + $(this).attr('title', 'Push readable versions to the bottom'); + } else if ($(this).hasClass('sorting_desc')) { + $(this).attr('title', 'Sort by editions to read'); + } else { + $(this).attr('title', 'Available to read'); + } + }); - function toggleSorting(e) { - $('#editions th span').html(''); - $(e).find('span').html(' ↑'); - if ($(e).hasClass('sorting_asc')) { - $(e).find('span').html(' ↓'); - } else if ($(e).hasClass('sorting_desc')) { - $(e).find('span').html(' ↑'); - } + function toggleSorting(e) { + $('#editions th span').html(''); + $(e).find('span').html(' ↑'); + if ($(e).hasClass('sorting_asc')) { + $(e).find('span').html(' ↓'); + } else if ($(e).hasClass('sorting_desc')) { + $(e).find('span').html(' ↑'); } + } - $('#editions th.read span').html(' ↑'); - $('#editions th').on('mouseup', function() { - toggleSorting(this) - }); + $('#editions th.read span').html(' ↑'); + $('#editions th').on('mouseup', function () { + toggleSorting(this); + }); - $('#editions').on('length.dt', function(e, settings, length) { - localStorage.setItem(LS_RESULTS_LENGTH_KEY, length); - }); + $('#editions').on('length.dt', (e, settings, length) => { + localStorage.setItem(LS_RESULTS_LENGTH_KEY, length); + }); - $('#editions th').on('keydown', function(e) { - if (e.key === 'Enter') { - toggleSorting(this); - } - }) + $('#editions th').on('keydown', function (e) { + if (e.key === 'Enter') { + toggleSorting(this); + } + }); - rowCount = $('#editions tbody tr').length; - if (rowCount < 4) { - $('#editions').DataTable({ - aoColumns: [{sType: 'html'},null], - order: [ [1,'asc'] ], - bPaginate: false, - bInfo: false, - bFilter: false, - bStateSave: false, - bAutoWidth: false - }); - } else { - currentLength = Number(localStorage.getItem(LS_RESULTS_LENGTH_KEY)); - $('#editions').DataTable({ - aoColumns: [{sType: 'html'},null], - order: [ [1,'asc'] ], - lengthMenu: [ [3, 10, 25, 50, 100, -1], [3, 10, 25, 50, 100, 'All'] ], - bPaginate: true, - bInfo: true, - sPaginationType: 'full_numbers', - bFilter: true, - bStateSave: false, - bAutoWidth: false, - pageLength: currentLength ? currentLength : DEFAULT_LENGTH, - drawCallback: function() { - if ($('#ile-toolbar')) { - const editionStorage = JSON.parse(sessionStorage.getItem('ile-items'))['edition'] - const matchEdition = (string) => { - return string.match(/OL[0-9]+[a-zA-Z]/) - } - for (const el of $('.ile-selected')) { - const anchor = el.getElementsByTagName('a'); - if (anchor.length) { - const edIdentifier = matchEdition(anchor[0].getAttribute('href')) - if (!editionStorage.includes(edIdentifier[0])) { - el.classList.remove('ile-selected'); - } - } - } - for (const el of $('.ile-selectable')) { - const anchor = el.getElementsByTagName('a'); - if (anchor.length) { - const edIdentifier = matchEdition(anchor[0].getAttribute('href')); - if (editionStorage.includes(edIdentifier[0])) { - el.classList.add('ile-selected'); - } - } - } - } + rowCount = $('#editions tbody tr').length; + if (rowCount < 4) { + $('#editions').DataTable({ + aoColumns: [{ sType: 'html' }, null], + order: [[1, 'asc']], + bPaginate: false, + bInfo: false, + bFilter: false, + bStateSave: false, + bAutoWidth: false, + }); + } else { + currentLength = Number(localStorage.getItem(LS_RESULTS_LENGTH_KEY)); + $('#editions').DataTable({ + aoColumns: [{ sType: 'html' }, null], + order: [[1, 'asc']], + lengthMenu: [ + [3, 10, 25, 50, 100, -1], + [3, 10, 25, 50, 100, 'All'], + ], + bPaginate: true, + bInfo: true, + sPaginationType: 'full_numbers', + bFilter: true, + bStateSave: false, + bAutoWidth: false, + pageLength: currentLength ? currentLength : DEFAULT_LENGTH, + drawCallback: () => { + if ($('#ile-toolbar')) { + const editionStorage = JSON.parse( + sessionStorage.getItem('ile-items'), + )['edition']; + const matchEdition = (string) => { + return string.match(/OL[0-9]+[a-zA-Z]/); + }; + for (const el of $('.ile-selected')) { + const anchor = el.getElementsByTagName('a'); + if (anchor.length) { + const edIdentifier = matchEdition(anchor[0].getAttribute('href')); + if (!editionStorage.includes(edIdentifier[0])) { + el.classList.remove('ile-selected'); + } } - }) - } + } + for (const el of $('.ile-selectable')) { + const anchor = el.getElementsByTagName('a'); + if (anchor.length) { + const edIdentifier = matchEdition(anchor[0].getAttribute('href')); + if (editionStorage.includes(edIdentifier[0])) { + el.classList.add('ile-selected'); + } + } + } + } + }, + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/following.js b/openlibrary/plugins/openlibrary/js/following.js index 0930da0acbf..87d75163a74 100644 --- a/openlibrary/plugins/openlibrary/js/following.js +++ b/openlibrary/plugins/openlibrary/js/following.js @@ -1,40 +1,42 @@ import { PersistentToast } from './Toast'; export async function initAsyncFollowing(followForms) { - followForms.forEach(form => { - form.addEventListener('submit', async (e) => { - e.preventDefault(); - const url = form.action; - const formData = new FormData(form); - const submitButton = form.querySelector('button[type=submit]'); - const stateInput = form.querySelector('input[name=state]'); + followForms.forEach((form) => { + form.addEventListener('submit', async (e) => { + e.preventDefault(); + const url = form.action; + const formData = new FormData(form); + const submitButton = form.querySelector('button[type=submit]'); + const stateInput = form.querySelector('input[name=state]'); - const isFollowRequest = stateInput.value === '0'; - const i18nStrings = JSON.parse(submitButton.dataset.i18n); - submitButton.disabled = true; + const isFollowRequest = stateInput.value === '0'; + const i18nStrings = JSON.parse(submitButton.dataset.i18n); + submitButton.disabled = true; - await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams(formData) - }) - .then(resp => { - if (!resp.ok) { - throw new Error('Network response was not ok'); - } - submitButton.classList.toggle('cta-btn--primary'); - submitButton.classList.toggle('cta-btn--delete'); - submitButton.textContent = isFollowRequest ? i18nStrings.unfollow : i18nStrings.follow; - stateInput.value = isFollowRequest ? '1' : '0'; - }) - .catch(() => { - new PersistentToast(i18nStrings.errorMsg).show(); - }) - .finally(() => { - submitButton.disabled = false; - }); + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams(formData), + }) + .then((resp) => { + if (!resp.ok) { + throw new Error('Network response was not ok'); + } + submitButton.classList.toggle('cta-btn--primary'); + submitButton.classList.toggle('cta-btn--delete'); + submitButton.textContent = isFollowRequest + ? i18nStrings.unfollow + : i18nStrings.follow; + stateInput.value = isFollowRequest ? '1' : '0'; + }) + .catch(() => { + new PersistentToast(i18nStrings.errorMsg).show(); + }) + .finally(() => { + submitButton.disabled = false; }); }); + }); } diff --git a/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js b/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js index 1ac759942b1..47ad82df2cd 100644 --- a/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js +++ b/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js @@ -1,55 +1,66 @@ -import { buildPartialsUrl } from './utils' +import { buildPartialsUrl } from './utils'; export function initFulltextSearchSuggestion(fulltextSearchSuggestion) { - const isLoading = showLoadingIndicators(fulltextSearchSuggestion) - if (isLoading) { - const query = fulltextSearchSuggestion.dataset.query - getPartials(fulltextSearchSuggestion, query) - } + const isLoading = showLoadingIndicators(fulltextSearchSuggestion); + if (isLoading) { + const query = fulltextSearchSuggestion.dataset.query; + getPartials(fulltextSearchSuggestion, query); + } } function showLoadingIndicators(fulltextSearchSuggestion) { - let isLoading = false - const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator') - if (loadingIndicator) { - isLoading = true - loadingIndicator.classList.remove('hidden') - } - return isLoading + let isLoading = false; + const loadingIndicator = + fulltextSearchSuggestion.querySelector('.loadingIndicator'); + if (loadingIndicator) { + isLoading = true; + loadingIndicator.classList.remove('hidden'); + } + return isLoading; } async function getPartials(fulltextSearchSuggestion, query) { - return fetch(buildPartialsUrl('FulltextSearchSuggestion', {data: query})) - .then((resp) => { - if (resp.status !== 200) { - throw new Error(`Failed to fetch partials. Status code: ${resp.status}`) - } - return resp.json() - }) - .then((data) => { - fulltextSearchSuggestion.innerHTML += data['partials'] - const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator'); - if (loadingIndicator) { - loadingIndicator.classList.add('hidden') - } - }) - .catch(() => { - const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator') - if (loadingIndicator) { - loadingIndicator.classList.add('hidden') - } - const existingRetryAffordance = fulltextSearchSuggestion.querySelector('.fulltext-suggestions__retry') - if (existingRetryAffordance) { - existingRetryAffordance.classList.remove('hidden') - } else { - fulltextSearchSuggestion.insertAdjacentHTML('afterbegin', renderRetryLink()) - const retryAffordance = fulltextSearchSuggestion.querySelector('.fulltext-suggestions__retry') - retryAffordance.addEventListener('click', () => { - retryAffordance.classList.add('hidden') - getPartials(fulltextSearchSuggestion, query) - }) - } - - }) + return fetch(buildPartialsUrl('FulltextSearchSuggestion', { data: query })) + .then((resp) => { + if (resp.status !== 200) { + throw new Error( + `Failed to fetch partials. Status code: ${resp.status}`, + ); + } + return resp.json(); + }) + .then((data) => { + fulltextSearchSuggestion.innerHTML += data['partials']; + const loadingIndicator = + fulltextSearchSuggestion.querySelector('.loadingIndicator'); + if (loadingIndicator) { + loadingIndicator.classList.add('hidden'); + } + }) + .catch(() => { + const loadingIndicator = + fulltextSearchSuggestion.querySelector('.loadingIndicator'); + if (loadingIndicator) { + loadingIndicator.classList.add('hidden'); + } + const existingRetryAffordance = fulltextSearchSuggestion.querySelector( + '.fulltext-suggestions__retry', + ); + if (existingRetryAffordance) { + existingRetryAffordance.classList.remove('hidden'); + } else { + fulltextSearchSuggestion.insertAdjacentHTML( + 'afterbegin', + renderRetryLink(), + ); + const retryAffordance = fulltextSearchSuggestion.querySelector( + '.fulltext-suggestions__retry', + ); + retryAffordance.addEventListener('click', () => { + retryAffordance.classList.add('hidden'); + getPartials(fulltextSearchSuggestion, query); + }); + } + }); } /** @@ -58,5 +69,5 @@ async function getPartials(fulltextSearchSuggestion, query) { * @returns {string} HTML for a retry link. */ function renderRetryLink() { - return '<span class="fulltext-suggestions__retry">Failed to fetch fulltext search suggestions. <a href="javascript:;">Retry?</a></span>' + return '<span class="fulltext-suggestions__retry">Failed to fetch fulltext search suggestions. <a href="javascript:;">Retry?</a></span>'; } diff --git a/openlibrary/plugins/openlibrary/js/go-back-links.js b/openlibrary/plugins/openlibrary/js/go-back-links.js index 9a02f46bd3b..47d74ba23bd 100644 --- a/openlibrary/plugins/openlibrary/js/go-back-links.js +++ b/openlibrary/plugins/openlibrary/js/go-back-links.js @@ -3,15 +3,15 @@ * where to redirect the user * * @param {NodeList<HTMLElement>} goBackLinks -*/ + */ export function initGoBackLinks(goBackLinks) { - for (const link of goBackLinks) { - link.addEventListener('click', () => { - if (history.length > 2) { - history.go(-1) - } else { - window.location.href='/' - } - }) - } + for (const link of goBackLinks) { + link.addEventListener('click', () => { + if (history.length > 2) { + history.go(-1); + } else { + window.location.href = '/'; + } + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/goodreads_import.js b/openlibrary/plugins/openlibrary/js/goodreads_import.js index 2a1fdfd46f3..a15c63a015d 100644 --- a/openlibrary/plugins/openlibrary/js/goodreads_import.js +++ b/openlibrary/plugins/openlibrary/js/goodreads_import.js @@ -1,182 +1,191 @@ import Promise from 'promise-polyfill'; export function initGoodreadsImport() { + var count, prevPromise; - var count, prevPromise; - - $(document).on('click', 'th.toggle-all input', function () { - var checked = $(this).prop('checked'); - $('input.add-book').each(function () { - $(this).prop('checked', checked); - if (checked) { - $(this).attr('checked', 'checked'); - } - else { - $(this).removeAttr('checked'); - } - }); - const l = $('.add-book[checked*="checked"]').length; - $('.import-submit').attr('value', `Import ${l} Books`); + $(document).on('click', 'th.toggle-all input', function () { + var checked = $(this).prop('checked'); + $('input.add-book').each(function () { + $(this).prop('checked', checked); + if (checked) { + $(this).attr('checked', 'checked'); + } else { + $(this).removeAttr('checked'); + } }); + const l = $('.add-book[checked*="checked"]').length; + $('.import-submit').attr('value', `Import ${l} Books`); + }); - $(document).on('click', 'input.add-book', function () { - if ($(this).prop('checked')) { - $(this).attr('checked', 'checked'); - } - else { - $(this).removeAttr('checked'); - } - const l = $('.add-book[checked*="checked"]').length; - $('.import-submit').attr('value', `Import ${l} Books`); - }); + $(document).on('click', 'input.add-book', function () { + if ($(this).prop('checked')) { + $(this).attr('checked', 'checked'); + } else { + $(this).removeAttr('checked'); + } + const l = $('.add-book[checked*="checked"]').length; + $('.import-submit').attr('value', `Import ${l} Books`); + }); - //updates the progress bar based on the book count - function func1(value) { - const l = $('.add-book[checked*="checked"]').length; - const elem = document.getElementById('myBar'); - elem.style.width = `${value * (100 / l)}%`; - elem.innerHTML = `${value} Books`; - if (value * (100 / l) >= 100) { - elem.innerHTML = ''; - $('#myBar').append('<a href="/account/books" style="color:white"> Go to your Reading Log </a>'); - $('.cancel-button').addClass('hidden'); - } + //updates the progress bar based on the book count + function func1(value) { + const l = $('.add-book[checked*="checked"]').length; + const elem = document.getElementById('myBar'); + elem.style.width = `${value * (100 / l)}%`; + elem.innerHTML = `${value} Books`; + if (value * (100 / l) >= 100) { + elem.innerHTML = ''; + $('#myBar').append( + '<a href="/account/books" style="color:white"> Go to your Reading Log </a>', + ); + $('.cancel-button').addClass('hidden'); } + } - $('.import-submit').on('click', function () { - $('#myProgress').removeClass('hidden'); - $('.cancel-button').removeClass('hidden'); - $('input.import-submit').addClass('hidden'); - $('th.import-status').removeClass('hidden'); - $('th.status-reason').removeClass('hidden'); - const shelves = { read: 3, 'currently-reading': 2, 'to-read': 1 }; - count = 0; - prevPromise = Promise.resolve(); - $('input.add-book').each(function () { - var input = $(this), - checked = input.prop('checked'); - var value = JSON.parse(input.val().replace(/'/g, '"')); - var shelf = value['Exclusive Shelf']; - var shelf_id = 0; - const hasFailure = function () { - return $(`[isbn=${value['ISBN']}]`).hasClass('import-failure'); - }; - const fail = function (reason) { - if (!hasFailure()) { - const element = $(`[isbn=${value['ISBN']}]`); - element.append(`<td class="error-imported">Error</td><td class="error-imported">${reason}</td>'`) - element.removeClass('selected'); - element.addClass('import-failure'); - } - }; + $('.import-submit').on('click', () => { + $('#myProgress').removeClass('hidden'); + $('.cancel-button').removeClass('hidden'); + $('input.import-submit').addClass('hidden'); + $('th.import-status').removeClass('hidden'); + $('th.status-reason').removeClass('hidden'); + const shelves = { read: 3, 'currently-reading': 2, 'to-read': 1 }; + count = 0; + prevPromise = Promise.resolve(); + $('input.add-book').each(function () { + var input = $(this), + checked = input.prop('checked'); + var value = JSON.parse(input.val().replace(/'/g, '"')); + var shelf = value['Exclusive Shelf']; + var shelf_id = 0; + const hasFailure = () => + $(`[isbn=${value['ISBN']}]`).hasClass('import-failure'); + const fail = (reason) => { + if (!hasFailure()) { + const element = $(`[isbn=${value['ISBN']}]`); + element.append( + `<td class="error-imported">Error</td><td class="error-imported">${reason}</td>'`, + ); + element.removeClass('selected'); + element.addClass('import-failure'); + } + }; - if (!checked) { - func1(++count); - return; - } + if (!checked) { + func1(++count); + return; + } - if (shelves[shelf]) { - shelf_id = shelves[shelf]; - } + if (shelves[shelf]) { + shelf_id = shelves[shelf]; + } - //used 'return' instead of 'return false' because the loop was being exited entirely - if (shelf_id === 0) { - fail('Custom shelves are not supported'); - func1(++count); - return; - } + //used 'return' instead of 'return false' because the loop was being exited entirely + if (shelf_id === 0) { + fail('Custom shelves are not supported'); + func1(++count); + return; + } - prevPromise = prevPromise.then(function () { // prevPromise changes in each iteration - $(`[isbn=${value['ISBN']}]`).addClass('selected'); - return getWork(value['ISBN']); // return a new Promise - }).then(function (data) { - var obj = JSON.parse(data); - $.ajax({ - url: `${obj['works'][0].key}/bookshelves.json`, - type: 'POST', - data: { - dont_remove: true, - edition_id: obj['key'], - bookshelf_id: shelf_id - }, - dataType: 'json' - }).fail(function () { - fail('Failed to add book to reading log'); - }).done(function () { - if (value['My Rating'] !== '0') { - return $.ajax({ - url: `${obj['works'][0].key}/ratings.json`, - type: 'POST', - data: { - rating: parseInt(value['My Rating']), - edition_id: obj['key'], - bookshelf_id: shelf_id - }, - dataType: 'json', - fail: function () { - fail('Failed to add rating'); - } - }); - } - }).then(function () { - if (value['Date Read'] !== '') { - const date_read = value['Date Read'].split('/'); // Format: "YYYY/MM/DD" - return $.ajax({ - url: `${obj['works'][0].key}/check-ins`, - type: 'POST', - data: JSON.stringify({ - edition_key: obj['key'], - event_type: 3, // BookshelfEvent.FINISH - year: parseInt(date_read[0]), - month: parseInt(date_read[1]), - day: parseInt(date_read[2]) - }), - dataType: 'json', - contentType: 'application/json', - beforeSend: function (xhr) { - xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.setRequestHeader('Accept', 'application/json'); - }, - fail: function () { - fail('Failed to set the read date'); - } - }); - } + prevPromise = prevPromise + .then(() => { + // prevPromise changes in each iteration + $(`[isbn=${value['ISBN']}]`).addClass('selected'); + return getWork(value['ISBN']); // return a new Promise + }) + .then((data) => { + var obj = JSON.parse(data); + $.ajax({ + url: `${obj['works'][0].key}/bookshelves.json`, + type: 'POST', + data: { + dont_remove: true, + edition_id: obj['key'], + bookshelf_id: shelf_id, + }, + dataType: 'json', + }) + .fail(() => { + fail('Failed to add book to reading log'); + }) + .done(() => { + if (value['My Rating'] !== '0') { + return $.ajax({ + url: `${obj['works'][0].key}/ratings.json`, + type: 'POST', + data: { + rating: parseInt(value['My Rating']), + edition_id: obj['key'], + bookshelf_id: shelf_id, + }, + dataType: 'json', + fail: () => { + fail('Failed to add rating'); + }, }); - if (!hasFailure()) { - $(`[isbn=${value['ISBN']}]`).append('<td class="success-imported">Imported</td>') - $(`[isbn=${value['ISBN']}]`).removeClass('selected'); - } - func1(++count); - }).catch(function () { - fail('Book not in collection'); - func1(++count); + } + }) + .then(() => { + if (value['Date Read'] !== '') { + const date_read = value['Date Read'].split('/'); // Format: "YYYY/MM/DD" + return $.ajax({ + url: `${obj['works'][0].key}/check-ins`, + type: 'POST', + data: JSON.stringify({ + edition_key: obj['key'], + event_type: 3, // BookshelfEvent.FINISH + year: parseInt(date_read[0]), + month: parseInt(date_read[1]), + day: parseInt(date_read[2]), + }), + dataType: 'json', + contentType: 'application/json', + beforeSend: (xhr) => { + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('Accept', 'application/json'); + }, + fail: () => { + fail('Failed to set the read date'); + }, + }); + } }); + if (!hasFailure()) { + $(`[isbn=${value['ISBN']}]`).append( + '<td class="success-imported">Imported</td>', + ); + $(`[isbn=${value['ISBN']}]`).removeClass('selected'); + } + func1(++count); + }) + .catch(() => { + fail('Book not in collection'); + func1(++count); }); + }); - $('td.books-wo-isbn').each(function () { - $(this).removeClass('hidden'); - }); + $('td.books-wo-isbn').each(function () { + $(this).removeClass('hidden'); }); + }); - function getWork(isbn) { - return new Promise(function (resolve, reject) { - var request = new XMLHttpRequest(); + function getWork(isbn) { + return new Promise((resolve, reject) => { + var request = new XMLHttpRequest(); - request.open('GET', `/isbn/${isbn}.json`); - request.onload = function () { - if (request.status === 200) { - resolve(request.response); // we get the data here, so resolve the Promise - } else { - reject(Error(request.statusText)); // if status is not 200 OK, reject. - } - }; + request.open('GET', `/isbn/${isbn}.json`); + request.onload = () => { + if (request.status === 200) { + resolve(request.response); // we get the data here, so resolve the Promise + } else { + reject(Error(request.statusText)); // if status is not 200 OK, reject. + } + }; - request.onerror = function () { - reject(Error('Error fetching data.')); // error occurred, so reject the Promise - }; + request.onerror = () => { + reject(Error('Error fetching data.')); // error occurred, so reject the Promise + }; - request.send(); // send the request - }); - } + request.send(); // send the request + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/graphs/index.js b/openlibrary/plugins/openlibrary/js/graphs/index.js index 192926e9842..67564fb1d8e 100644 --- a/openlibrary/plugins/openlibrary/js/graphs/index.js +++ b/openlibrary/plugins/openlibrary/js/graphs/index.js @@ -1,34 +1,34 @@ -import { loadGraphIfExists, loadEditionsGraph } from './plot'; import options from './options.js'; +import { loadEditionsGraph, loadGraphIfExists } from './plot'; export function plotAdminGraphs() { - loadGraphIfExists('editgraph', {}, 'edit(s) on'); - loadGraphIfExists('membergraph', {}, 'new members(s) on'); - loadGraphIfExists('works_minigraph', {}, ' works on '); - loadGraphIfExists('editions_minigraph', {}, ' editions on '); - loadGraphIfExists('covers_minigraph', {}, ' covers on '); - loadGraphIfExists('authors_minigraph', {}, ' authors on '); - loadGraphIfExists('lists_minigraph', {}, ' lists on '); - loadGraphIfExists('members_minigraph', {}, ' members on '); - loadGraphIfExists('books-added-per-day', options.booksAdded); + loadGraphIfExists('editgraph', {}, 'edit(s) on'); + loadGraphIfExists('membergraph', {}, 'new members(s) on'); + loadGraphIfExists('works_minigraph', {}, ' works on '); + loadGraphIfExists('editions_minigraph', {}, ' editions on '); + loadGraphIfExists('covers_minigraph', {}, ' covers on '); + loadGraphIfExists('authors_minigraph', {}, ' authors on '); + loadGraphIfExists('lists_minigraph', {}, ' lists on '); + loadGraphIfExists('members_minigraph', {}, ' members on '); + loadGraphIfExists('books-added-per-day', options.booksAdded); } export function initHomepageGraphs() { - loadGraphIfExists('visitors-graph', {}, 'unique visitors on', '#e44028'); - loadGraphIfExists('members-graph', {}, 'new members on', '#748d36'); - loadGraphIfExists('edits-graph', {}, 'catalog edits on', '#00636a'); - loadGraphIfExists('lists-graph', {}, 'lists created on', '#ffa337'); - loadGraphIfExists('ebooks-graph', {}, 'ebooks borrowed on', '#35672e'); + loadGraphIfExists('visitors-graph', {}, 'unique visitors on', '#e44028'); + loadGraphIfExists('members-graph', {}, 'new members on', '#748d36'); + loadGraphIfExists('edits-graph', {}, 'catalog edits on', '#00636a'); + loadGraphIfExists('lists-graph', {}, 'lists created on', '#ffa337'); + loadGraphIfExists('ebooks-graph', {}, 'ebooks borrowed on', '#35672e'); } export function initPublishersGraph() { - if (document.getElementById('chartPubHistory')) { - loadEditionsGraph('chartPubHistory', {}, 'editions in'); - } + if (document.getElementById('chartPubHistory')) { + loadEditionsGraph('chartPubHistory', {}, 'editions in'); + } } export function init() { - plotAdminGraphs(); - initHomepageGraphs(); - initPublishersGraph(); + plotAdminGraphs(); + initHomepageGraphs(); + initPublishersGraph(); } diff --git a/openlibrary/plugins/openlibrary/js/graphs/options.js b/openlibrary/plugins/openlibrary/js/graphs/options.js index 547c7ea5f8c..62612f501bd 100644 --- a/openlibrary/plugins/openlibrary/js/graphs/options.js +++ b/openlibrary/plugins/openlibrary/js/graphs/options.js @@ -1,55 +1,55 @@ const booksAdded = { - series: { - stack: 0, - bars: { - show: true, - align: 'left', - barWidth: 20 * 60 * 60 * 1000, - }, + series: { + stack: 0, + bars: { + show: true, + align: 'left', + barWidth: 20 * 60 * 60 * 1000, }, - grid: { - hoverable: true, - show: true, - borderWidth: 1, - borderColor: '#d9d9d9' - }, - xaxis: { - mode: 'time' - }, - legend: { - show: true, - position: 'nw' - } + }, + grid: { + hoverable: true, + show: true, + borderWidth: 1, + borderColor: '#d9d9d9', + }, + xaxis: { + mode: 'time', + }, + legend: { + show: true, + position: 'nw', + }, }; const loans = { - series: { - stack: 0, - bars: { - show: true, - align: 'left', - barWidth: 20 * 60 * 60 * 1000, - }, - }, - grid: { - hoverable: true, - show: true, - borderWidth: 1, - borderColor: '#d9d9d9' - }, - xaxis: { - mode: 'time' - }, - yaxis: { - position: 'right' + series: { + stack: 0, + bars: { + show: true, + align: 'left', + barWidth: 20 * 60 * 60 * 1000, }, - legend: { - show: true, - position: 'nw' - } + }, + grid: { + hoverable: true, + show: true, + borderWidth: 1, + borderColor: '#d9d9d9', + }, + xaxis: { + mode: 'time', + }, + yaxis: { + position: 'right', + }, + legend: { + show: true, + position: 'nw', + }, }; export default { - booksAdded, - loans + booksAdded, + loans, }; diff --git a/openlibrary/plugins/openlibrary/js/graphs/plot.js b/openlibrary/plugins/openlibrary/js/graphs/plot.js index b4034a2ea39..a51633a7b63 100644 --- a/openlibrary/plugins/openlibrary/js/graphs/plot.js +++ b/openlibrary/plugins/openlibrary/js/graphs/plot.js @@ -16,226 +16,248 @@ import 'flot/jquery.flot.time.js'; * - http://localhost:8080/publishers/Barnes_&_Noble */ export function loadEditionsGraph() { - var data, options, placeholder, - plot, dateFrom, dateTo, previousPoint; - data = [{data: JSON.parse(document.getElementById('graph-json-chartPubHistory').textContent)}]; - options = { - series: { - bars: { - show: true, - fill: 0.6, - color: '#615132', - align: 'center' - }, - points: { - show: true - }, - color: '#615132' - }, - grid: { - hoverable: true, - clickable: true, - autoHighlight: true, - tickColor: '#d9d9d9', - borderWidth: 1, - borderColor: '#d9d9d9', - backgroundColor: '#fff' - }, - xaxis: { tickDecimals: 0 }, - yaxis: { tickDecimals: 0 }, - selection: { mode: 'xy', color: '#00636a' }, - crosshair: { - mode: 'xy', - color: 'rgba(000, 099, 106, 0.4)', - lineWidth: 1 - } - }; + var data, options, placeholder, plot, dateFrom, dateTo, previousPoint; + data = [ + { + data: JSON.parse( + document.getElementById('graph-json-chartPubHistory').textContent, + ), + }, + ]; + options = { + series: { + bars: { + show: true, + fill: 0.6, + color: '#615132', + align: 'center', + }, + points: { + show: true, + }, + color: '#615132', + }, + grid: { + hoverable: true, + clickable: true, + autoHighlight: true, + tickColor: '#d9d9d9', + borderWidth: 1, + borderColor: '#d9d9d9', + backgroundColor: '#fff', + }, + xaxis: { tickDecimals: 0 }, + yaxis: { tickDecimals: 0 }, + selection: { mode: 'xy', color: '#00636a' }, + crosshair: { + mode: 'xy', + color: 'rgba(000, 099, 106, 0.4)', + lineWidth: 1, + }, + }; - placeholder = $('#chartPubHistory'); - function showTooltip(x, y, contents) { - $(`<div id="chartLabel">${contents}</div>`).css({ - position: 'absolute', - display: 'none', - top: y + 12, - left: x + 12, - border: '1px solid #615132', - padding: '2px', - 'background-color': '#fffdcd', - color: '#615132', - 'font-size': '11px', - opacity: 0.90, - 'z-index': 100 - }).appendTo('body').fadeIn(200); - } - previousPoint = null; - placeholder.bind('plothover', function (event, pos, item) { - var x, y; - $('#x').text(pos.x.toFixed(0)); - $('#y').text(pos.y.toFixed(0)); - if (item) { - if (previousPoint !== item.datapoint) { - previousPoint = item.datapoint; - $('#chartLabel').remove(); - x = item.datapoint[0].toFixed(0); - y = item.datapoint[1].toFixed(0); - if (y === 1) { - showTooltip(item.pageX, item.pageY, - `${y} edition in ${x}`); - } else { - showTooltip(item.pageX, item.pageY, - `${y} editions in ${x}`); - } - } - } - else { - $('#chartLabel').remove(); - previousPoint = null; + placeholder = $('#chartPubHistory'); + function showTooltip(x, y, contents) { + $(`<div id="chartLabel">${contents}</div>`) + .css({ + position: 'absolute', + display: 'none', + top: y + 12, + left: x + 12, + border: '1px solid #615132', + padding: '2px', + 'background-color': '#fffdcd', + color: '#615132', + 'font-size': '11px', + opacity: 0.9, + 'z-index': 100, + }) + .appendTo('body') + .fadeIn(200); + } + previousPoint = null; + placeholder.bind('plothover', (event, pos, item) => { + var x, y; + $('#x').text(pos.x.toFixed(0)); + $('#y').text(pos.y.toFixed(0)); + if (item) { + if (previousPoint !== item.datapoint) { + previousPoint = item.datapoint; + $('#chartLabel').remove(); + x = item.datapoint[0].toFixed(0); + y = item.datapoint[1].toFixed(0); + if (y === 1) { + showTooltip(item.pageX, item.pageY, `${y} edition in ${x}`); + } else { + showTooltip(item.pageX, item.pageY, `${y} editions in ${x}`); } - }); + } + } else { + $('#chartLabel').remove(); + previousPoint = null; + } + }); - placeholder.bind('plotclick', function (event, pos, item) { + placeholder.bind('plotclick', (event, pos, item) => { + if (item) { + plot.unhighlight(); + const yearFrom = item.datapoint[0].toFixed(0); + applyDateFilter(yearFrom, yearFrom); - if (item) { - plot.unhighlight(); - const yearFrom = item.datapoint[0].toFixed(0); - applyDateFilter(yearFrom, yearFrom); + plot.highlight(item.series, item.datapoint); + } else { + plot.unhighlight(); + } + }); - plot.highlight(item.series,item.datapoint); - } - else { - plot.unhighlight(); - } - }); + placeholder.bind('plotselected', (event, ranges) => { + plot = $.plot( + placeholder, + data, + $.extend(true, {}, options, { + xaxis: { min: ranges.xaxis.from, max: ranges.xaxis.to }, + yaxis: { min: ranges.yaxis.from, max: ranges.yaxis.to }, + }), + ); - placeholder.bind('plotselected', function (event, ranges) { - plot = $.plot(placeholder, data, - $.extend(true, {}, options, { - xaxis: { min: ranges.xaxis.from, max: ranges.xaxis.to }, - yaxis: { min: ranges.yaxis.from, max: ranges.yaxis.to } - }) - ); + const yearFrom = ranges.xaxis.from.toFixed(0); + const yearTo = ranges.xaxis.to.toFixed(0); + applyDateFilter(yearFrom, yearTo); + }); - const yearFrom = ranges.xaxis.from.toFixed(0); - const yearTo = ranges.xaxis.to.toFixed(0); - applyDateFilter(yearFrom, yearTo); - }); + function applyDateFilter( + yearFrom, + yearTo, + hideSelector = '.chartUnzoom', + showSelector = '.chartZoom', + ) { + document.dispatchEvent( + new CustomEvent('filter', { + detail: { yearFrom: yearFrom, yearTo: yearTo }, + }), + ); + $(hideSelector).hide(); + $(showSelector).removeClass('hidden').show(); + } - function applyDateFilter(yearFrom, yearTo, hideSelector='.chartUnzoom', showSelector='.chartZoom') { - document.dispatchEvent(new CustomEvent('filter', { detail: { yearFrom: yearFrom, yearTo: yearTo } })); - $(hideSelector).hide(); - $(showSelector).removeClass('hidden').show(); - } + plot = $.plot(placeholder, data, options); + dateFrom = plot.getAxes().xaxis.min.toFixed(0); + dateTo = plot.getAxes().xaxis.max.toFixed(0); + $('.resetSelection').on('click', () => { plot = $.plot(placeholder, data, options); - dateFrom = plot.getAxes().xaxis.min.toFixed(0); - dateTo = plot.getAxes().xaxis.max.toFixed(0); - - $('.resetSelection').on('click', function() { - plot = $.plot(placeholder, data, options); - const yearFrom = plot.getAxes().xaxis.min.toFixed(0); - const yearTo = plot.getAxes().xaxis.max.toFixed(0); - applyDateFilter(yearFrom, yearTo, '.chartZoom', '.chartUnzoom'); - }); + const yearFrom = plot.getAxes().xaxis.min.toFixed(0); + const yearTo = plot.getAxes().xaxis.max.toFixed(0); + applyDateFilter(yearFrom, yearTo, '.chartZoom', '.chartUnzoom'); + }); - $('.chartYaxis').css({top: '60px', left: '-60px'}); + $('.chartYaxis').css({ top: '60px', left: '-60px' }); - if (dateFrom === (dateTo - 1)) { - $('.clickdata').text(`Published in ${dateFrom}`); - } else { - $('.clickdata').text(`Published between ${dateFrom} & ${dateTo-1}.`); - } + if (dateFrom === dateTo - 1) { + $('.clickdata').text(`Published in ${dateFrom}`); + } else { + $('.clickdata').text(`Published between ${dateFrom} & ${dateTo - 1}.`); + } } export function plot_minigraph(node, data) { - var options = { - series: { - lines: { - show: true, - fill: 0, - color: '#748d36' - }, - points: { - show: false - }, - color: '#748d36' - }, - grid: { - hoverable: false, - show: false - } - }; - $.plot(node, [data], options); + var options = { + series: { + lines: { + show: true, + fill: 0, + color: '#748d36', + }, + points: { + show: false, + }, + color: '#748d36', + }, + grid: { + hoverable: false, + show: false, + }, + }; + $.plot(node, [data], options); } -export function plot_tooltip_graph(node, data, tooltip_message, color='#748d36') { - var i, options, graph; - // empty set of rows. Escape early. - if (!data.length) { - return; - } - for (i = 0; i < data.length; ++i) { - data[i][0] += 60 * 60 * 1000; - } +export function plot_tooltip_graph( + node, + data, + tooltip_message, + color = '#748d36', +) { + var i, options, graph; + // empty set of rows. Escape early. + if (!data.length) { + return; + } + for (i = 0; i < data.length; ++i) { + data[i][0] += 60 * 60 * 1000; + } - options = { - series: { - bars: { - show: true, - fill: 1, - fillColor: color, - color, - align: 'left', - barWidth: 24 * 60 * 60 * 1000 - }, - points: { - show: false - }, - color - }, - grid: { - hoverable: true, - show: false - }, - xaxis: { - mode: 'time' - } - }; + options = { + series: { + bars: { + show: true, + fill: 1, + fillColor: color, + color, + align: 'left', + barWidth: 24 * 60 * 60 * 1000, + }, + points: { + show: false, + }, + color, + }, + grid: { + hoverable: true, + show: false, + }, + xaxis: { + mode: 'time', + }, + }; - graph = $.plot(node, [data], options); + graph = $.plot(node, [data], options); - function showTooltip(x, y, contents) { - $(`<div id="chartLabelA">${contents}</div>`).css({ - position: 'absolute', - display: 'none', - top: y + 12, - left: x + 12, - border: '1px solid #ccc', - padding: '2px', - backgroundColor: '#efefef', - color: '#454545', - fontSize: '11px', - webkitBoxShadow: '1px 1px 3px #333', - mozBoxShadow: '1px 1px 1px #000', - boxShadow: '1px 1px 1px #000' - }).appendTo('body').fadeIn(200); + function showTooltip(x, y, contents) { + $(`<div id="chartLabelA">${contents}</div>`) + .css({ + position: 'absolute', + display: 'none', + top: y + 12, + left: x + 12, + border: '1px solid #ccc', + padding: '2px', + backgroundColor: '#efefef', + color: '#454545', + fontSize: '11px', + webkitBoxShadow: '1px 1px 3px #333', + mozBoxShadow: '1px 1px 1px #000', + boxShadow: '1px 1px 1px #000', + }) + .appendTo('body') + .fadeIn(200); + } + node.bind('plothover', (event, pos, item) => { + var date, milli, x, y; + $('#x').text(pos.x); + $('#y').text(pos.y.toFixed(0)); + if (item) { + $('#chartLabelA').remove(); + milli = item.datapoint[0]; + date = new Date(milli); + x = date.toDateString(); + y = item.datapoint[1].toFixed(0); + showTooltip(item.pageX, item.pageY, `${y} ${tooltip_message} ${x}`); + } else { + $('#chartLabelA').remove(); } - node.bind('plothover', function (event, pos, item) { - var date, milli, x, y; - $('#x').text(pos.x); - $('#y').text(pos.y.toFixed(0)); - if (item) { - $('#chartLabelA').remove(); - milli = item.datapoint[0]; - date = new Date(milli); - x = date.toDateString(); - y = item.datapoint[1].toFixed(0); - showTooltip(item.pageX, item.pageY, `${y} ${tooltip_message} ${x}`); - } else { - $('#chartLabelA').remove(); - } - }); - return graph; + }); + return graph; } /** @@ -246,32 +268,35 @@ export function plot_tooltip_graph(node, data, tooltip_message, color='#748d36') * @param {string} [color] in hexidecimal to apply to the bars of a tooltip graph. * Ignored if options and no tooltip_message is passed. */ -export function loadGraph(id, options = {}, tooltip_message = '', color = null) { - let data; - const node = document.getElementById(id); - const graphSelector = `graph-json-${id}`; - const dataSource = document.getElementById(graphSelector); - if (!node) { - throw new Error( - `No graph associated with ${id} on the page.` - ); +export function loadGraph( + id, + options = {}, + tooltip_message = '', + color = null, +) { + let data; + const node = document.getElementById(id); + const graphSelector = `graph-json-${id}`; + const dataSource = document.getElementById(graphSelector); + if (!node) { + throw new Error(`No graph associated with ${id} on the page.`); + } + if (!dataSource) { + throw new Error( + `No data associated with ${id} - make sure a script tag with type text/json and id "${graphSelector}" is present on the page.`, + ); + } else { + try { + data = JSON.parse(dataSource.textContent); + } catch (e) { + throw new Error(`Unable to parse JSON in ${graphSelector}`); } - if (!dataSource) { - throw new Error( - `No data associated with ${id} - make sure a script tag with type text/json and id "${graphSelector}" is present on the page.` - ); + if (tooltip_message) { + return plot_tooltip_graph($(node), data, tooltip_message, color); } else { - try { - data = JSON.parse(dataSource.textContent); - } catch (e) { - throw new Error(`Unable to parse JSON in ${graphSelector}`); - } - if (tooltip_message) { - return plot_tooltip_graph($(node), data, tooltip_message, color); - } else { - return $.plot($(node), [{data: data}], options); - } + return $.plot($(node), [{ data: data }], options); } + } } /** @@ -283,7 +308,7 @@ export function loadGraph(id, options = {}, tooltip_message = '', color = null) * Ignored if options and no tooltip_message is passed. */ export function loadGraphIfExists(id, options, tooltip_message, color) { - if ($(`#${id}`).length) { - loadGraph(id, options, tooltip_message, color); - } + if ($(`#${id}`).length) { + loadGraph(id, options, tooltip_message, color); + } } diff --git a/openlibrary/plugins/openlibrary/js/i18n.js b/openlibrary/plugins/openlibrary/js/i18n.js index 2110502ff3a..97f4c4a7e34 100644 --- a/openlibrary/plugins/openlibrary/js/i18n.js +++ b/openlibrary/plugins/openlibrary/js/i18n.js @@ -1,23 +1,21 @@ // used in templates/lists/preview.html export function sprintf(s) { - var args = arguments; - var i = 1; - return s.replace(/%[%s]/g, function(match) { - if (match === '%%') - return '%'; - else - return args[i++]; - }); + var args = arguments; + var i = 1; + return s.replace(/%[%s]/g, (match) => { + if (match === '%%') return '%'; + else return args[i++]; + }); } // dummy i18n functions // used in plugins/upstream/code.py export function ugettext(s) { - return s; + return s; } // used in templates/borrow/read.html export function ungettext(s1, s2, n) { - return n === 1? s1 : s2; + return n === 1 ? s1 : s2; } diff --git a/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js b/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js index ede17d1940c..8a8af1d4825 100644 --- a/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js +++ b/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js @@ -4,29 +4,28 @@ * @param {*} element - The element to be modified by the handleMessageEvent function. */ export function initMessageEventListener(element) { - /** - * Handles messages from archive.org and performs actions based on the message type. - * - * @param {MessageEvent} e - The message event. - */ - function handleMessageEvent(e) { - if (!/[./]archive\.org$$/.test(e.origin)) return; + /** + * Handles messages from archive.org and performs actions based on the message type. + * + * @param {MessageEvent} e - The message event. + */ + function handleMessageEvent(e) { + if (!/[./]archive\.org$$/.test(e.origin)) return; - if (e.data.type === 'resize') { - element.setAttribute('scrolling', 'no'); - if (e.data.height) element.style.height = `${e.data.height}px`; - } - else if (e.data.type === 's3-keys') { - const s3AccessInput = document.querySelector('#access') - const s3SecretInput = document.querySelector('#secret') - s3AccessInput.value = e.data.s3.access - s3SecretInput.value = e.data.s3.secret + if (e.data.type === 'resize') { + element.setAttribute('scrolling', 'no'); + if (e.data.height) element.style.height = `${e.data.height}px`; + } else if (e.data.type === 's3-keys') { + const s3AccessInput = document.querySelector('#access'); + const s3SecretInput = document.querySelector('#secret'); + s3AccessInput.value = e.data.s3.access; + s3SecretInput.value = e.data.s3.secret; - const loginForm = document.querySelector('#register') - loginForm.action = '/account/login' - loginForm.submit() - } + const loginForm = document.querySelector('#register'); + loginForm.action = '/account/login'; + loginForm.submit(); } + } - window.addEventListener('message', handleMessageEvent, false); + window.addEventListener('message', handleMessageEvent, false); } diff --git a/openlibrary/plugins/openlibrary/js/idValidation.js b/openlibrary/plugins/openlibrary/js/idValidation.js index 23b9532fa42..d70adfeadcd 100644 --- a/openlibrary/plugins/openlibrary/js/idValidation.js +++ b/openlibrary/plugins/openlibrary/js/idValidation.js @@ -4,7 +4,7 @@ * @returns {String} parsed isbn string */ export function parseIsbn(isbn) { - return isbn.replace(/[ -]/g, ''); + return isbn.replace(/[ -]/g, ''); } /** @@ -14,8 +14,8 @@ export function parseIsbn(isbn) { * @returns {boolean} true if the isbn has a valid format */ export function isFormatValidIsbn10(isbn) { - const regex = /^[0-9]{9}[0-9X]$/; - return regex.test(isbn); + const regex = /^[0-9]{9}[0-9X]$/; + return regex.test(isbn); } /** @@ -25,15 +25,15 @@ export function isFormatValidIsbn10(isbn) { * @returns {boolean} true if ISBN string is a valid ISBN 10 */ export function isChecksumValidIsbn10(isbn) { - const chars = isbn.replace('X', 'A').split(''); + const chars = isbn.replace('X', 'A').split(''); - chars.reverse(); - const sum = chars - .map((char, idx) => ((idx + 1) * parseInt(char, 16))) - .reduce((acc, sum) => acc + sum, 0); + chars.reverse(); + const sum = chars + .map((char, idx) => (idx + 1) * parseInt(char, 16)) + .reduce((acc, sum) => acc + sum, 0); - // The ISBN 10 is valid if the checksum mod 11 is 0. - return sum % 11 === 0; + // The ISBN 10 is valid if the checksum mod 11 is 0. + return sum % 11 === 0; } /** @@ -43,24 +43,24 @@ export function isChecksumValidIsbn10(isbn) { * @returns {boolean} true if the isbn has a valid format */ export function isFormatValidIsbn13(isbn) { - const regex = /^[0-9]{13}$/; - return regex.test(isbn); + const regex = /^[0-9]{13}$/; + return regex.test(isbn); } /** -* Verify the checksum for ISBN 13. -* Adapted from https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch04s13.html -* @param {String} isbn ISBN string for validating -* @returns {Boolean} true if ISBN string is a valid ISBN 13 -*/ + * Verify the checksum for ISBN 13. + * Adapted from https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch04s13.html + * @param {String} isbn ISBN string for validating + * @returns {Boolean} true if ISBN string is a valid ISBN 13 + */ export function isChecksumValidIsbn13(isbn) { - const chars = isbn.split(''); - const sum = chars - .map((char, idx) => ((idx % 2 * 2 + 1) * parseInt(char, 10))) - .reduce((sum, num) => sum + num, 0); + const chars = isbn.split(''); + const sum = chars + .map((char, idx) => ((idx % 2) * 2 + 1) * parseInt(char, 10)) + .reduce((sum, num) => sum + num, 0); - // The ISBN 13 is valid if the checksum mod 10 is 0. - return sum % 10 === 0; + // The ISBN 13 is valid if the checksum mod 10 is 0. + return sum % 10 === 0; } /** @@ -70,25 +70,26 @@ export function isChecksumValidIsbn13(isbn) { * @returns {String} parsed LCCN string */ export function parseLccn(lccn) { - // cleaning initial lccn entry - const parsed = lccn - // any alpha characters need to be lowercase - .toLowerCase() - // remove any whitespace - .replace(/\s/g, '') - // remove leading and trailing dashes - .replace(/^[-]+/, '').replace(/[-]+$/, '') - // remove any revised text - .replace(/rev.*/g, '') - // remove first forward slash and everything to its right - .replace(/[/]+.*$/, ''); + // cleaning initial lccn entry + const parsed = lccn + // any alpha characters need to be lowercase + .toLowerCase() + // remove any whitespace + .replace(/\s/g, '') + // remove leading and trailing dashes + .replace(/^[-]+/, '') + .replace(/[-]+$/, '') + // remove any revised text + .replace(/rev.*/g, '') + // remove first forward slash and everything to its right + .replace(/[/]+.*$/, ''); - // splitting at hyphen and padding the right hand value with zeros up to 6 characters - const groups = parsed.match(/(.+)-+([0-9]+)/) - if (groups && groups.length === 3) { - return groups[1] + groups[2].padStart(6, '0'); - } - return parsed; + // splitting at hyphen and padding the right hand value with zeros up to 6 characters + const groups = parsed.match(/(.+)-+([0-9]+)/); + if (groups && groups.length === 3) { + return groups[1] + groups[2].padStart(6, '0'); + } + return parsed; } /** @@ -98,10 +99,10 @@ export function parseLccn(lccn) { * @returns {boolean} true if given LCCN is valid syntax, false otherwise */ export function isValidLccn(lccn) { - // matching parsed entry to regex representing valid lccn - // regex taken from /openlibrary/utils/lccn.py - const regex = /^([a-z]|[a-z]?([a-z]{2}|[0-9]{2})|[a-z]{2}[0-9]{2})?[0-9]{8}$/; - return regex.test(lccn); + // matching parsed entry to regex representing valid lccn + // regex taken from /openlibrary/utils/lccn.py + const regex = /^([a-z]|[a-z]?([a-z]{2}|[0-9]{2})|[a-z]{2}[0-9]{2})?[0-9]{8}$/; + return regex.test(lccn); } /** @@ -110,12 +111,14 @@ export function isValidLccn(lccn) { * @returns {String} parsed OCLC string */ export function parseOclc(oclc) { - // cleaning initial oclc entry - return oclc - // remove any whitespace - .replace(/\s/g, '') - // remove leading/padding zeroes - .replace(/^0+/, ''); + // cleaning initial oclc entry + return ( + oclc + // remove any whitespace + .replace(/\s/g, '') + // remove leading/padding zeroes + .replace(/^0+/, '') + ); } /** @@ -128,9 +131,9 @@ export function parseOclc(oclc) { * @returns {boolean} true if given OCLC is valid syntax, false otherwise */ export function isValidOclc(oclc) { - // matching parsed entry to regex representing valid oclc - const regex = /^[1-9][0-9]*$/; - return regex.test(oclc); + // matching parsed entry to regex representing valid oclc + const regex = /^[1-9][0-9]*$/; + return regex.test(oclc); } /** @@ -143,8 +146,6 @@ export function isValidOclc(oclc) { * @returns {boolean} true if the new identifier has already been entered */ export function isIdDupe(idEntries, newId) { - // check each current entry value against new identifier - return Array.from(idEntries).some( - entry => entry['value'] === newId - ); + // check each current entry value against new identifier + return Array.from(idEntries).some((entry) => entry['value'] === newId); } diff --git a/openlibrary/plugins/openlibrary/js/ile/index.js b/openlibrary/plugins/openlibrary/js/ile/index.js index 88478a792c1..a06f36b848b 100644 --- a/openlibrary/plugins/openlibrary/js/ile/index.js +++ b/openlibrary/plugins/openlibrary/js/ile/index.js @@ -1,20 +1,22 @@ // @ts-check -import SelectionManager from './utils/SelectionManager/SelectionManager.js'; -import { renderBulkTagger } from '../bulk-tagger/index.js'; + import { BulkTagger } from '../bulk-tagger/BulkTagger.js'; +import { renderBulkTagger } from '../bulk-tagger/index.js'; +import SelectionManager from './utils/SelectionManager/SelectionManager.js'; export function init() { - const ile = new IntegratedLibrarianEnvironment(); - // @ts-ignore - window.ILE = ile; - ile.init(); + const ile = new IntegratedLibrarianEnvironment(); + // @ts-expect-error + window.ILE = ile; + ile.init(); } export class IntegratedLibrarianEnvironment { - constructor() { - this.selectionManager = new SelectionManager(this); - /** This is the main ILE toolbar. Should be moved to a Vue component. */ - this.$toolbar = $(` + constructor() { + this.selectionManager = new SelectionManager(this); + /** This is the main ILE toolbar. Should be moved to a Vue component. */ + this.$toolbar = $( + ` <div id="ile-toolbar"> <div id="ile-selections"> <div id="ile-drag-status"> @@ -25,78 +27,79 @@ export class IntegratedLibrarianEnvironment { </div> <div id="ile-drag-actions"></div> <div id="ile-hidden-forms"></div> - </div>`.trim()); - this.$selectionActions = this.$toolbar.find('#ile-selection-actions'); - this.$statusText = this.$toolbar.find('.text'); - this.$statusImages = this.$toolbar.find('.images ul'); - this.$actions = this.$toolbar.find('#ile-drag-actions'); - this.$hiddenForms = this.$toolbar.find('#ile-hidden-forms'); - this.bulkTagger = null - } + </div>`.trim(), + ); + this.$selectionActions = this.$toolbar.find('#ile-selection-actions'); + this.$statusText = this.$toolbar.find('.text'); + this.$statusImages = this.$toolbar.find('.images ul'); + this.$actions = this.$toolbar.find('#ile-drag-actions'); + this.$hiddenForms = this.$toolbar.find('#ile-hidden-forms'); + this.bulkTagger = null; + } - init() { - // Add the ILE toolbar to bottom of screen - $(document.body).append(this.$toolbar.hide()); + init() { + // Add the ILE toolbar to bottom of screen + $(document.body).append(this.$toolbar.hide()); - // Ready bulk tagger: - this.createBulkTagger() + // Ready bulk tagger: + this.createBulkTagger(); - this.selectionManager.init(); - } + this.selectionManager.init(); + } - /** @param {string} text */ - setStatusText(text) { - this.$statusText.text(text); - this.$toolbar.toggle(text.length > 0); - } + /** @param {string} text */ + setStatusText(text) { + this.$statusText.text(text); + this.$toolbar.toggle(text.length > 0); + } - /** - * Resets the status bar. - */ - reset() { - for (const elem of $('.ile-selected')) { - elem.classList.remove('ile-selected') - } - this.setStatusText(''); - this.$selectionActions.empty(); - this.$statusImages.empty(); - this.$actions.empty(); + /** + * Resets the status bar. + */ + reset() { + for (const elem of $('.ile-selected')) { + elem.classList.remove('ile-selected'); } + this.setStatusText(''); + this.$selectionActions.empty(); + this.$statusImages.empty(); + this.$actions.empty(); + } - /** - * Clears all items selected in SelectionManager. - * - * This indirectly calls `IntegratedLibrarianEnvironment.reset()`. - */ - clearAndReset() { - this.selectionManager.clearSelectedItems() - } + /** + * Clears all items selected in SelectionManager. + * + * This indirectly calls `IntegratedLibrarianEnvironment.reset()`. + */ + clearAndReset() { + this.selectionManager.clearSelectedItems(); + } - /** - * Creates a new Bulk Tagger component and attaches it to the DOM. - * - * Sets the value of `IntegratedLibrarianEnvironment.bulkTagger` - */ - createBulkTagger() { - const target = this.$hiddenForms[0] - target.innerHTML += renderBulkTagger() - const bulkTaggerElem = document.querySelector('.bulk-tagging-form') - // @ts-ignore - this.bulkTagger = new BulkTagger(bulkTaggerElem) - this.bulkTagger.initialize() - } + /** + * Creates a new Bulk Tagger component and attaches it to the DOM. + * + * Sets the value of `IntegratedLibrarianEnvironment.bulkTagger` + */ + createBulkTagger() { + const target = this.$hiddenForms[0]; + target.innerHTML += renderBulkTagger(); + const bulkTaggerElem = document.querySelector('.bulk-tagging-form'); + // @ts-expect-error + this.bulkTagger = new BulkTagger(bulkTaggerElem); + this.bulkTagger.initialize(); + } - /** - * Updates the Bulk Tagger with the selected works, then displays the tagger. - * - * @param {Array<String>} workIds - * @param {boolean} isBookPageEdit `true` if the bulk tagger is opened on a /books or /works page - */ - updateAndShowBulkTagger(workIds, isBookPageEdit = false) { - if (this.bulkTagger) { - this.bulkTagger.isBookPageEdit = isBookPageEdit - this.bulkTagger.updateWorks(workIds) - this.bulkTagger.showTaggingMenu() - } + /** + * Updates the Bulk Tagger with the selected works, then displays the tagger. + * + * @param {Array<String>} workIds + * @param {boolean} isBookPageEdit `true` if the bulk tagger is opened on a /books or /works page + */ + updateAndShowBulkTagger(workIds, isBookPageEdit = false) { + if (this.bulkTagger) { + this.bulkTagger.isBookPageEdit = isBookPageEdit; + this.bulkTagger.updateWorks(workIds); + this.bulkTagger.showTaggingMenu(); } + } } diff --git a/openlibrary/plugins/openlibrary/js/ile/utils/SelectionManager/SelectionManager.js b/openlibrary/plugins/openlibrary/js/ile/utils/SelectionManager/SelectionManager.js index ba784255fff..01b9bb5e34f 100644 --- a/openlibrary/plugins/openlibrary/js/ile/utils/SelectionManager/SelectionManager.js +++ b/openlibrary/plugins/openlibrary/js/ile/utils/SelectionManager/SelectionManager.js @@ -1,6 +1,6 @@ // @ts-check import $ from 'jquery'; -import { move_to_work, move_to_author } from '../ol.js'; +import { move_to_author, move_to_work } from '../ol.js'; import './SelectionManager.css'; /** @@ -14,318 +14,376 @@ import './SelectionManager.css'; * id of the specific edition surfaced in a search result. */ export default class SelectionManager { - /** - * @param {import('../../index.js').IntegratedLibrarianEnvironment} ile - */ - constructor(ile, curpath=location.pathname) { - this.ile = ile; - this.curpath = curpath; - this.inited = false; - this.selectedItems = {}; - this.lastClicked = null; - - this.processClick = this.processClick.bind(this); - this.toggleSelected = this.toggleSelected.bind(this); - this.clearSelectedItems = this.clearSelectedItems.bind(this); - this.dragStart = this.dragStart.bind(this); - this.dragEnd = this.dragEnd.bind(this); - this.onDrop = this.onDrop.bind(this); - this.allowDrop = this.allowDrop.bind(this); - - // Collator used to naturally order OLIDs before constructing URL - this.collator = new Intl.Collator('en-US', {numeric: true}); - } - - init() { - this.inited = true; - this.getSelectedItems(); - - // Label each selectable element with a class, and bind the click event - const providers = this.getPossibleProviders(); - const providerSelectors = providers.map(p => p.selector); - $(providerSelectors.join(', ')) - .addClass('ile-selectable') - .on('click', this.processClick); - - for (const provider of providers) { - for (const el of $(provider.selector).toArray()) { - // Some providers need a "handle" to allow dragging. - if (provider.addHandle) { - const handle = $('<span class="ile-select-handle">•</span>'); - handle[0].addEventListener('click', ev => ev.preventDefault(), { capture: true }); - $(el).prepend(handle); - } - // Restore any stored selections on this page - for (const type of provider.type) { - if (this.selectedItems[type].length) { - if (this.selectedItems[type].indexOf(provider.data(el)) !== -1) { - this.setElementSelectionAttributes(el, true); - } - } - } - } - } - - // Populate the status bar images for stored selections - for (const type of SelectionManager.TYPES) { - this.selectedItems[type.singular].forEach(olid => { - this.ile.$statusImages.append(`<li><img title="${olid}" src="${type.image(olid)}" /></li>`); - }); + /** + * @param {import('../../index.js').IntegratedLibrarianEnvironment} ile + */ + constructor(ile, curpath = location.pathname) { + this.ile = ile; + this.curpath = curpath; + this.inited = false; + this.selectedItems = {}; + this.lastClicked = null; + + this.processClick = this.processClick.bind(this); + this.toggleSelected = this.toggleSelected.bind(this); + this.clearSelectedItems = this.clearSelectedItems.bind(this); + this.dragStart = this.dragStart.bind(this); + this.dragEnd = this.dragEnd.bind(this); + this.onDrop = this.onDrop.bind(this); + this.allowDrop = this.allowDrop.bind(this); + + // Collator used to naturally order OLIDs before constructing URL + this.collator = new Intl.Collator('en-US', { numeric: true }); + } + + init() { + this.inited = true; + this.getSelectedItems(); + + // Label each selectable element with a class, and bind the click event + const providers = this.getPossibleProviders(); + const providerSelectors = providers.map((p) => p.selector); + $(providerSelectors.join(', ')) + .addClass('ile-selectable') + .on('click', this.processClick); + + for (const provider of providers) { + for (const el of $(provider.selector).toArray()) { + // Some providers need a "handle" to allow dragging. + if (provider.addHandle) { + const handle = $('<span class="ile-select-handle">•</span>'); + handle[0].addEventListener('click', (ev) => ev.preventDefault(), { + capture: true, + }); + $(el).prepend(handle); } - - this.updateToolbar(); - - // Add the drag/drop handlers to the main white part of the page - document.getElementById('test-body-mobile').addEventListener('drop', this.onDrop); - document.getElementById('test-body-mobile').addEventListener('dragover', this.allowDrop); - } - - /** - * @param {MouseEvent & { currentTarget: HTMLElement }} clickEvent - */ - processClick(clickEvent) { - // If there is text selection or the click is on a link that isn't a select handle, don't do anything - if ((!clickEvent.shiftKey && window.getSelection()?.toString() !== '') || - ($(clickEvent.target).closest('a, button, details').length > 0 && - $(clickEvent.target).not('.ile-select-handle').length > 0)) return; - - const el = clickEvent.currentTarget; - if (clickEvent.shiftKey && this.lastClicked) - { - // clear selection ranges created by shift-clicking since they're not suppressed by preventDefault(). - clearTextSelection(); - const siblingSet = this.getSelectableRange(el); - const lastClickedIndex = siblingSet.index(this.lastClicked); - const elIndex = siblingSet.index(el); - if (lastClickedIndex > -1 && Math.abs(elIndex - lastClickedIndex) > 1) { - let affectedElements; - if (elIndex > lastClickedIndex) { - affectedElements = siblingSet.slice(lastClickedIndex + 1, elIndex + 1); - } else { - affectedElements = siblingSet.slice(elIndex, lastClickedIndex); - } - const stateChange = this.lastClicked.classList.contains('ile-selected') ? true : false; - for (const element of affectedElements) this.toggleSelected(element, stateChange); - } else { - this.toggleSelected(el); + // Restore any stored selections on this page + for (const type of provider.type) { + if (this.selectedItems[type].length) { + if (this.selectedItems[type].indexOf(provider.data(el)) !== -1) { + this.setElementSelectionAttributes(el, true); } + } } - else { - this.toggleSelected(el); - } - this.lastClicked = el; - this.updateToolbar(); + } } - /** - * Sets of selectable elements are sometimes HTML siblings and sometimes not. This function - * hides that complexity by finding the common parent between the passed element and the last - * clicked element and generating a set of siblings from that information - * - * @param {HTMLElement} clicked - * @return {JQuery<HTMLElement>} - */ - getSelectableRange(clicked) { - let commonParent = undefined; - const curEls = { clicked, lastClicked: this.lastClicked }; - // Only check up to 3 levels up in the tree - for (let i = 0; i < 3; i++) { - if (!curEls.clicked || !curEls.lastClicked) { - break; - } else if (curEls.clicked === curEls.lastClicked) { - commonParent = curEls.clicked; - break; - } else { - curEls.clicked = curEls.clicked.parentElement; - curEls.lastClicked = curEls.lastClicked.parentElement; - } - } - if (commonParent) { - return $(commonParent).find('.ile-selectable'); - } else { - return $(clicked); - } + // Populate the status bar images for stored selections + for (const type of SelectionManager.TYPES) { + this.selectedItems[type.singular].forEach((olid) => { + this.ile.$statusImages.append( + `<li><img title="${olid}" src="${type.image(olid)}" /></li>`, + ); + }); } - /** - * @param {HTMLElement} el - * @param {boolean} [forceSelected] - * If included, turns the toggle into a one way-only operation. If set to false, elements will only - * be deselected, not selected. If set to true, elements will only be selected, but not deselected. - */ - toggleSelected(el, forceSelected) { - const isCurSelected = el.classList.contains('ile-selected'); - const provider = this.getProvider(el); - const olid = provider.data(el); - const img_src = this.getType(olid)?.image(olid); - - if (isCurSelected === forceSelected) return; - this.setElementSelectionAttributes(el, !isCurSelected); - if (isCurSelected) { - this.removeSelectedItem(olid); - const img_el = $('#ile-drag-status .images img').toArray().find(el => el.src === img_src); - $(img_el).remove(); + this.updateToolbar(); + + // Add the drag/drop handlers to the main white part of the page + document + .getElementById('test-body-mobile') + .addEventListener('drop', this.onDrop); + document + .getElementById('test-body-mobile') + .addEventListener('dragover', this.allowDrop); + } + + /** + * @param {MouseEvent & { currentTarget: HTMLElement }} clickEvent + */ + processClick(clickEvent) { + // If there is text selection or the click is on a link that isn't a select handle, don't do anything + if ( + (!clickEvent.shiftKey && window.getSelection()?.toString() !== '') || + ($(clickEvent.target).closest('a, button, details').length > 0 && + $(clickEvent.target).not('.ile-select-handle').length > 0) + ) + return; + + const el = clickEvent.currentTarget; + if (clickEvent.shiftKey && this.lastClicked) { + // clear selection ranges created by shift-clicking since they're not suppressed by preventDefault(). + clearTextSelection(); + const siblingSet = this.getSelectableRange(el); + const lastClickedIndex = siblingSet.index(this.lastClicked); + const elIndex = siblingSet.index(el); + if (lastClickedIndex > -1 && Math.abs(elIndex - lastClickedIndex) > 1) { + let affectedElements; + if (elIndex > lastClickedIndex) { + affectedElements = siblingSet.slice( + lastClickedIndex + 1, + elIndex + 1, + ); } else { - this.addSelectedItem(olid); - this.ile.$statusImages.append(`<li><img title="${olid}" src="${img_src}"/></li>`); + affectedElements = siblingSet.slice(elIndex, lastClickedIndex); } - + const stateChange = this.lastClicked.classList.contains('ile-selected') + ? true + : false; + for (const element of affectedElements) + this.toggleSelected(element, stateChange); + } else { + this.toggleSelected(el); + } + } else { + this.toggleSelected(el); } - - setElementSelectionAttributes(el, selected) { - el.classList.toggle('ile-selected', selected); - el.draggable = selected; - if (selected) { - el.addEventListener('dragstart', this.dragStart); - el.addEventListener('dragend', this.dragEnd); - } else { - el.removeEventListener('dragstart', this.dragStart); - el.removeEventListener('dragend', this.dragEnd); - } + this.lastClicked = el; + this.updateToolbar(); + } + + /** + * Sets of selectable elements are sometimes HTML siblings and sometimes not. This function + * hides that complexity by finding the common parent between the passed element and the last + * clicked element and generating a set of siblings from that information + * + * @param {HTMLElement} clicked + * @return {JQuery<HTMLElement>} + */ + getSelectableRange(clicked) { + let commonParent; + const curEls = { clicked, lastClicked: this.lastClicked }; + // Only check up to 3 levels up in the tree + for (let i = 0; i < 3; i++) { + if (!curEls.clicked || !curEls.lastClicked) { + break; + } else if (curEls.clicked === curEls.lastClicked) { + commonParent = curEls.clicked; + break; + } else { + curEls.clicked = curEls.clicked.parentElement; + curEls.lastClicked = curEls.lastClicked.parentElement; + } } - - updateToolbar() { - const statusParts = []; - this.ile.$actions.empty(); - this.ile.$selectionActions.empty(); - this.ile.bulkTagger.hideTaggingMenu() - SelectionManager.TYPES.forEach(type => { - const count = this.selectedItems[type.singular].length; - if (count) statusParts.push(`${count} ${count === 1 ? type.singular : type.plural}`); - }); - - if (statusParts.length) { - this.ile.setStatusText(`${statusParts.join(', ')} selected`); - this.ile.$selectionActions.append($('<a>Clear Selections</a>').on('click', this.clearSelectedItems)); - } else { - this.ile.setStatusText(''); - } - - for (const action of SelectionManager.ACTIONS) { - const items = []; - if (action.requires_type.every(type => this.selectedItems[type].length > 0)) { - action.applies_to_type.forEach(type => items.push(...this.selectedItems[type])); - if (action.multiple_only ? items.length > 1 : items.length > 0) - if (action.href) { - this.ile.$actions.append($(`<a target="_blank" href="${action.href(this.getOlidsFromSelectionList(items))}">${action.name}</a>`)); - } else if (action.onclick && action.name === 'Tag Works') { - this.ile.$actions.append($(`<a href="javascript:;">${action.name}</a>`).on('click', () => this.ile.updateAndShowBulkTagger(this.getOlidsFromSelectionList(items)))); - } - } - } + if (commonParent) { + return $(commonParent).find('.ile-selectable'); + } else { + return $(clicked); } - - addSelectedItem(item) { - this.selectedItems[this.getType(item).singular].push(item); - sessionStorage.setItem('ile-items', JSON.stringify(this.selectedItems)); + } + + /** + * @param {HTMLElement} el + * @param {boolean} [forceSelected] + * If included, turns the toggle into a one way-only operation. If set to false, elements will only + * be deselected, not selected. If set to true, elements will only be selected, but not deselected. + */ + toggleSelected(el, forceSelected) { + const isCurSelected = el.classList.contains('ile-selected'); + const provider = this.getProvider(el); + const olid = provider.data(el); + const img_src = this.getType(olid)?.image(olid); + + if (isCurSelected === forceSelected) return; + this.setElementSelectionAttributes(el, !isCurSelected); + if (isCurSelected) { + this.removeSelectedItem(olid); + const img_el = $('#ile-drag-status .images img') + .toArray() + .find((el) => el.src === img_src); + $(img_el).remove(); + } else { + this.addSelectedItem(olid); + this.ile.$statusImages.append( + `<li><img title="${olid}" src="${img_src}"/></li>`, + ); } - - removeSelectedItem(item) { - const type = this.getType(item); - const index = this.selectedItems[type.singular].indexOf(item); - if (index > -1) { - this.selectedItems[type.singular].splice(index, 1); - sessionStorage.setItem('ile-items', JSON.stringify(this.selectedItems)); - } + } + + setElementSelectionAttributes(el, selected) { + el.classList.toggle('ile-selected', selected); + el.draggable = selected; + if (selected) { + el.addEventListener('dragstart', this.dragStart); + el.addEventListener('dragend', this.dragEnd); + } else { + el.removeEventListener('dragstart', this.dragStart); + el.removeEventListener('dragend', this.dragEnd); } - - getSelectedItems() { - if (Object.keys(this.selectedItems).length === 0) { - if (sessionStorage.getItem('ile-items')) { - this.selectedItems = JSON.parse(sessionStorage.getItem('ile-items')); - } else { - SelectionManager.TYPES.forEach(type => {this.selectedItems[type.singular] = []}); - } - } - - const items = []; - for (const type in this.selectedItems) items.push(...this.selectedItems[type]); - return items; + } + + updateToolbar() { + const statusParts = []; + this.ile.$actions.empty(); + this.ile.$selectionActions.empty(); + this.ile.bulkTagger.hideTaggingMenu(); + SelectionManager.TYPES.forEach((type) => { + const count = this.selectedItems[type.singular].length; + if (count) + statusParts.push( + `${count} ${count === 1 ? type.singular : type.plural}`, + ); + }); + + if (statusParts.length) { + this.ile.setStatusText(`${statusParts.join(', ')} selected`); + this.ile.$selectionActions.append( + $('<a>Clear Selections</a>').on('click', this.clearSelectedItems), + ); + } else { + this.ile.setStatusText(''); } - clearSelectedItems() { - for (const type in this.selectedItems) this.selectedItems[type] = []; - sessionStorage.setItem('ile-items', JSON.stringify(this.selectedItems)); - this.ile.reset(); + for (const action of SelectionManager.ACTIONS) { + const items = []; + if ( + action.requires_type.every( + (type) => this.selectedItems[type].length > 0, + ) + ) { + action.applies_to_type.forEach((type) => + items.push(...this.selectedItems[type]), + ); + if (action.multiple_only ? items.length > 1 : items.length > 0) + if (action.href) { + this.ile.$actions.append( + $( + `<a target="_blank" href="${action.href(this.getOlidsFromSelectionList(items))}">${action.name}</a>`, + ), + ); + } else if (action.onclick && action.name === 'Tag Works') { + this.ile.$actions.append( + $(`<a href="javascript:;">${action.name}</a>`).on('click', () => + this.ile.updateAndShowBulkTagger( + this.getOlidsFromSelectionList(items), + ), + ), + ); + } + } } - - /** - * @param {DragEvent} ev - */ - dragStart(ev) { - const items = this.getSelectedItems(); - const from = this.curpath.match(/OL\d+[AWM]/); - if (items.length > 1) { - $('#ile-drag-status .images').addClass('drag-image'); - ev.dataTransfer.setDragImage($('#ile-drag-status')[0], 0, 0); - } - const data = { - from: (from ? from[0] : null), - items: this.getOlidsFromSelectionList(items) - }; - ev.dataTransfer.setData('text/plain', JSON.stringify(data)); - ev.dataTransfer.setData('application/x.ile+json', JSON.stringify(data)); + } + + addSelectedItem(item) { + this.selectedItems[this.getType(item).singular].push(item); + sessionStorage.setItem('ile-items', JSON.stringify(this.selectedItems)); + } + + removeSelectedItem(item) { + const type = this.getType(item); + const index = this.selectedItems[type.singular].indexOf(item); + if (index > -1) { + this.selectedItems[type.singular].splice(index, 1); + sessionStorage.setItem('ile-items', JSON.stringify(this.selectedItems)); } - - dragEnd() { - $('#ile-drag-status .images').removeClass('drag-image'); - } - - /** - * @param {DragEvent} ev - */ - onDrop(ev) { - ev.preventDefault(); - const handler = this.getHandler(); - const data = JSON.parse(ev.dataTransfer.getData('application/x.ile+json')); - handler.ondrop(data); - document.getElementById('test-body-mobile').classList.remove('ile-drag-over'); - } - - /** - * @param {DragEvent} ev - */ - allowDrop(ev) { - if (!ev.dataTransfer?.types.includes('application/x.ile+json') || $('.ile-selected').length) return; - const handler = this.getHandler(); - if (!handler) return; - - ev.preventDefault(); - this.ile.setStatusText(handler.message); - document.getElementById('test-body-mobile').classList.add('ile-drag-over'); - } - - getType(olid) { - return SelectionManager.TYPES.find(t => t.regex.test(olid)); - } - - getHandler() { - return SelectionManager.DROP_HANDLERS.find(h => h.path.test(this.curpath)); - } - - getPossibleProviders() { - return SelectionManager.SELECTION_PROVIDERS.filter(p => p.path.test(this.curpath)); - } - - /** - * @param {HTMLElement} el - */ - getProvider(el) { - return SelectionManager.SELECTION_PROVIDERS - .find(p => p.path.test(this.curpath) && el.matches(p.selector)); + } + + getSelectedItems() { + if (Object.keys(this.selectedItems).length === 0) { + if (sessionStorage.getItem('ile-items')) { + this.selectedItems = JSON.parse(sessionStorage.getItem('ile-items')); + } else { + SelectionManager.TYPES.forEach((type) => { + this.selectedItems[type.singular] = []; + }); + } } - getOlidsFromSelectionList(list) { - return list.map(item => item.split(':')[0]); + const items = []; + for (const type in this.selectedItems) + items.push(...this.selectedItems[type]); + return items; + } + + clearSelectedItems() { + for (const type in this.selectedItems) this.selectedItems[type] = []; + sessionStorage.setItem('ile-items', JSON.stringify(this.selectedItems)); + this.ile.reset(); + } + + /** + * @param {DragEvent} ev + */ + dragStart(ev) { + const items = this.getSelectedItems(); + const from = this.curpath.match(/OL\d+[AWM]/); + if (items.length > 1) { + $('#ile-drag-status .images').addClass('drag-image'); + ev.dataTransfer.setDragImage($('#ile-drag-status')[0], 0, 0); } + const data = { + from: from ? from[0] : null, + items: this.getOlidsFromSelectionList(items), + }; + ev.dataTransfer.setData('text/plain', JSON.stringify(data)); + ev.dataTransfer.setData('application/x.ile+json', JSON.stringify(data)); + } + + dragEnd() { + $('#ile-drag-status .images').removeClass('drag-image'); + } + + /** + * @param {DragEvent} ev + */ + onDrop(ev) { + ev.preventDefault(); + const handler = this.getHandler(); + const data = JSON.parse(ev.dataTransfer.getData('application/x.ile+json')); + handler.ondrop(data); + document + .getElementById('test-body-mobile') + .classList.remove('ile-drag-over'); + } + + /** + * @param {DragEvent} ev + */ + allowDrop(ev) { + if ( + !ev.dataTransfer?.types.includes('application/x.ile+json') || + $('.ile-selected').length + ) + return; + const handler = this.getHandler(); + if (!handler) return; + + ev.preventDefault(); + this.ile.setStatusText(handler.message); + document.getElementById('test-body-mobile').classList.add('ile-drag-over'); + } + + getType(olid) { + return SelectionManager.TYPES.find((t) => t.regex.test(olid)); + } + + getHandler() { + return SelectionManager.DROP_HANDLERS.find((h) => + h.path.test(this.curpath), + ); + } + + getPossibleProviders() { + return SelectionManager.SELECTION_PROVIDERS.filter((p) => + p.path.test(this.curpath), + ); + } + + /** + * @param {HTMLElement} el + */ + getProvider(el) { + return SelectionManager.SELECTION_PROVIDERS.find( + (p) => p.path.test(this.curpath) && el.matches(p.selector), + ); + } + + getOlidsFromSelectionList(list) { + return list.map((item) => item.split(':')[0]); + } } /** * Cross-browser approach to clear any text selections. */ function clearTextSelection() { - const selection = window.getSelection ? window.getSelection() : document.selection ? document.selection : null; - if (!!selection) selection.empty ? selection.empty() : selection.removeAllRanges(); + const selection = window.getSelection + ? window.getSelection() + : document.selection + ? document.selection + : null; + if (selection) + selection.empty ? selection.empty() : selection.removeAllRanges(); } /** @@ -334,173 +392,191 @@ function clearTextSelection() { * where. */ SelectionManager.DROP_HANDLERS = [ - /** Dropping books from one author to another */ - { - path: /\/authors\/OL\d+A.*/, - message: 'Move to this author', - async ondrop(data) { - // eslint-disable-next-line no-console - console.log('move', data); - window.ILE.setStatusText('Working...'); - try { - await move_to_author(data.items, data.from, location.pathname.match(/OL\d+A/)[0]); - window.ILE.setStatusText('Completed!'); - } catch (e) { - window.ILE.setStatusText('Errored!'); - throw e; - } - } + /** Dropping books from one author to another */ + { + path: /\/authors\/OL\d+A.*/, + message: 'Move to this author', + async ondrop(data) { + // eslint-disable-next-line no-console + console.log('move', data); + window.ILE.setStatusText('Working...'); + try { + await move_to_author( + data.items, + data.from, + location.pathname.match(/OL\d+A/)[0], + ); + window.ILE.setStatusText('Completed!'); + } catch (e) { + window.ILE.setStatusText('Errored!'); + throw e; + } }, - /** Dropping editions from one work to another */ - { - path: /(\/works\/OL\d+W.*|\/books\/OL\d+M.*)/, - message: 'Move to this work', - async ondrop(data) { - // eslint-disable-next-line no-console - console.log('move', data); - window.ILE.setStatusText('Working...'); - try { - let workOlid = location.pathname.match(/OL\d+W/)?.[0]; - if (!workOlid) { - const ed = await fetch(`/books/${location.pathname.match(/OL\d+M/)[0]}.json`).then(r => r.json()); - workOlid = ed.works[0].key.match(/OL\d+W/)[0]; - } - await move_to_work(data.items, data.from, workOlid); - window.ILE.setStatusText('Completed!'); - } catch (e) { - window.ILE.setStatusText('Errored!'); - throw e; - } + }, + /** Dropping editions from one work to another */ + { + path: /(\/works\/OL\d+W.*|\/books\/OL\d+M.*)/, + message: 'Move to this work', + async ondrop(data) { + // eslint-disable-next-line no-console + console.log('move', data); + window.ILE.setStatusText('Working...'); + try { + let workOlid = location.pathname.match(/OL\d+W/)?.[0]; + if (!workOlid) { + const ed = await fetch( + `/books/${location.pathname.match(/OL\d+M/)[0]}.json`, + ).then((r) => r.json()); + workOlid = ed.works[0].key.match(/OL\d+W/)[0]; } + await move_to_work(data.items, data.from, workOlid); + window.ILE.setStatusText('Completed!'); + } catch (e) { + window.ILE.setStatusText('Errored!'); + throw e; + } }, + }, ]; SelectionManager.TYPES = [ - { - singular: 'work', - plural: 'works', - regex: /OL\d+W/, - image: olid => { - const imgOlid = olid.split(':').pop(); - if (imgOlid.slice(-1) === 'M') - return `https://covers.openlibrary.org/b/olid/${imgOlid}-M.jpg?default=https://openlibrary.org/images/icons/avatar_book-lg.png` - else - return `https://covers.openlibrary.org/w/olid/${imgOlid}-M.jpg?default=https://openlibrary.org/images/icons/avatar_book-lg.png` - }, + { + singular: 'work', + plural: 'works', + regex: /OL\d+W/, + image: (olid) => { + const imgOlid = olid.split(':').pop(); + if (imgOlid.slice(-1) === 'M') + return `https://covers.openlibrary.org/b/olid/${imgOlid}-M.jpg?default=https://openlibrary.org/images/icons/avatar_book-lg.png`; + else + return `https://covers.openlibrary.org/w/olid/${imgOlid}-M.jpg?default=https://openlibrary.org/images/icons/avatar_book-lg.png`; }, - { - singular: 'edition', - plural: 'editions', - regex: /OL\d+M/, - image: olid => `https://covers.openlibrary.org/b/olid/${olid}-M.jpg?default=https://openlibrary.org/images/icons/avatar_book-lg.png`, - }, - { - singular: 'author', - plural: 'authors', - regex: /OL\d+A/, - image: olid => `https://covers.openlibrary.org/a/olid/${olid}-M.jpg?default=https://openlibrary.org/images/icons/avatar_author-lg.png`, - } -] + }, + { + singular: 'edition', + plural: 'editions', + regex: /OL\d+M/, + image: (olid) => + `https://covers.openlibrary.org/b/olid/${olid}-M.jpg?default=https://openlibrary.org/images/icons/avatar_book-lg.png`, + }, + { + singular: 'author', + plural: 'authors', + regex: /OL\d+A/, + image: (olid) => + `https://covers.openlibrary.org/a/olid/${olid}-M.jpg?default=https://openlibrary.org/images/icons/avatar_author-lg.png`, + }, +]; /** * Selection Providers define what is selectable on a page. E.g. the path regex * determines the url to apply the selection provider to. */ SelectionManager.SELECTION_PROVIDERS = [ + /** + * This selection provider makes books in search results selectable. + */ + { + path: /(\/authors\/OL\d+A.*|\/search)$/, + selector: '.searchResultItem', + type: ['work', 'edition'], /** - * This selection provider makes books in search results selectable. - */ - { - path: /(\/authors\/OL\d+A.*|\/search)$/, - selector: '.searchResultItem', - type: ['work','edition'], - /** - * @param {HTMLElement} el - * @return {import('../ol.js').WorkOLID} - **/ - data: el => { - const parts = $(el).find('.booktitle a')[0].href.match(/OL\d+[WM]/g); - return (parts.length > 1 && parts[0] !== parts[1]) ? parts.join(':') : parts[0]; - }, - }, - /** - * This selection provider makes editions in the editions table selectable. - */ - { - path: /(\/works\/OL\d+W.*|\/books\/OL\d+M.*)/, - selector: '.book', - type: ['edition'], - /** - * @param {HTMLElement} el - * @return {import('../ol.js').EditionOLID} - **/ - data: el => $(el).find('.title a')[0].href.match(/OL\d+M/)[0], + * @param {HTMLElement} el + * @return {import('../ol.js').WorkOLID} + **/ + data: (el) => { + const parts = $(el) + .find('.booktitle a')[0] + .href.match(/OL\d+[WM]/g); + return parts.length > 1 && parts[0] !== parts[1] + ? parts.join(':') + : parts[0]; }, + }, + /** + * This selection provider makes editions in the editions table selectable. + */ + { + path: /(\/works\/OL\d+W.*|\/books\/OL\d+M.*)/, + selector: '.book', + type: ['edition'], /** - * This selection provider makes author names on the books page selectable. - */ - { - path: /(\/works\/OL\d+W.*|\/books\/OL\d+M.*)/, - selector: 'a[href^="/authors/OL"]', - addHandle: true, - type: ['author'], - /** - * @param {HTMLAnchorElement} el - * @return {import('../ol.js').AuthorOLID} - **/ - data: el => el.href.match(/OL\d+A/)[0], - }, + * @param {HTMLElement} el + * @return {import('../ol.js').EditionOLID} + **/ + data: (el) => + $(el) + .find('.title a')[0] + .href.match(/OL\d+M/)[0], + }, + /** + * This selection provider makes author names on the books page selectable. + */ + { + path: /(\/works\/OL\d+W.*|\/books\/OL\d+M.*)/, + selector: 'a[href^="/authors/OL"]', + addHandle: true, + type: ['author'], /** - * This selection provider makes work on the books page selectable. - */ - { - path: /(\/works\/OL\d+W.*|\/books\/OL\d+M.*)/, - selector: '.work-line a[href^="/works/OL"]', - addHandle: true, - type: ['work'], - /** - * @param {HTMLAnchorElement} el - * @return {import('../ol.js').WorkOLID} - **/ - data: el => el.href.match(/OL\d+W/)[0], - }, + * @param {HTMLAnchorElement} el + * @return {import('../ol.js').AuthorOLID} + **/ + data: (el) => el.href.match(/OL\d+A/)[0], + }, + /** + * This selection provider makes work on the books page selectable. + */ + { + path: /(\/works\/OL\d+W.*|\/books\/OL\d+M.*)/, + selector: '.work-line a[href^="/works/OL"]', + addHandle: true, + type: ['work'], /** - * This selection provider makes authors selectable on search result pages - */ - { - path: /^(\/search\/authors)$/, - selector: '.searchResultItem', - type: ['author'], - data: el => $(el).find('a')[0].href.match(/OL\d+A/)[0], - }, + * @param {HTMLAnchorElement} el + * @return {import('../ol.js').WorkOLID} + **/ + data: (el) => el.href.match(/OL\d+W/)[0], + }, + /** + * This selection provider makes authors selectable on search result pages + */ + { + path: /^(\/search\/authors)$/, + selector: '.searchResultItem', + type: ['author'], + data: (el) => + $(el) + .find('a')[0] + .href.match(/OL\d+A/)[0], + }, ]; /** * Actions get enabled when a certain selections are made. */ SelectionManager.ACTIONS = [ - { - applies_to_type: ['work','edition'], - requires_type: ['work'], - multiple_only: false, - name: 'Tag Works', - onclick: true, - }, - { - applies_to_type: ['work', 'edition', 'author'], - requires_type: [], - multiple_only: false, - name: 'Create list...', - href: olids => `/account/lists/add?seeds=${olids.join(',')}`, - }, - { - applies_to_type: ['work','edition'], - requires_type: ['work'], - multiple_only: true, - name: 'Merge Works...', - href: olids => `/works/merge?records=${olids.join(',')}`, - }, - /* Uncomment this when edition merging is available. + { + applies_to_type: ['work', 'edition'], + requires_type: ['work'], + multiple_only: false, + name: 'Tag Works', + onclick: true, + }, + { + applies_to_type: ['work', 'edition', 'author'], + requires_type: [], + multiple_only: false, + name: 'Create list...', + href: (olids) => `/account/lists/add?seeds=${olids.join(',')}`, + }, + { + applies_to_type: ['work', 'edition'], + requires_type: ['work'], + multiple_only: true, + name: 'Merge Works...', + href: (olids) => `/works/merge?records=${olids.join(',')}`, + }, + /* Uncomment this when edition merging is available. { applies_to_type: ['edition'], requires_type: ['edition'], @@ -509,11 +585,11 @@ SelectionManager.ACTIONS = [ href: olids => `/works/merge?records=${olids.join(',')}`, }, */ - { - applies_to_type: ['author'], - requires_type: ['author'], - multiple_only: true, - name: 'Merge Authors...', - href: olids => `/authors/merge?records=${olids.join(',')}`, - }, + { + applies_to_type: ['author'], + requires_type: ['author'], + multiple_only: true, + name: 'Merge Authors...', + href: (olids) => `/authors/merge?records=${olids.join(',')}`, + }, ]; diff --git a/openlibrary/plugins/openlibrary/js/ile/utils/ol.js b/openlibrary/plugins/openlibrary/js/ile/utils/ol.js index 2727cc5faed..db58d00a249 100644 --- a/openlibrary/plugins/openlibrary/js/ile/utils/ol.js +++ b/openlibrary/plugins/openlibrary/js/ile/utils/ol.js @@ -12,16 +12,16 @@ import uniqBy from 'lodash/uniqBy'; * @param {WorkOLID} new_work */ export async function move_to_work(edition_ids, old_work, new_work) { - for (const olid of edition_ids) { - const url = `/books/${olid}.json`; - const record = await fetch(url).then(r => r.json()); + for (const olid of edition_ids) { + const url = `/books/${olid}.json`; + const record = await fetch(url).then((r) => r.json()); - record.works = [{key: `/works/${new_work}`}]; - record._comment = 'move to correct work'; - const r = await fetch(url, { method: 'PUT', body: JSON.stringify(record) }); - // eslint-disable-next-line no-console - console.log(`moved ${olid}; ${r.status}`); - } + record.works = [{ key: `/works/${new_work}` }]; + record._comment = 'move to correct work'; + const r = await fetch(url, { method: 'PUT', body: JSON.stringify(record) }); + // eslint-disable-next-line no-console + console.log(`moved ${olid}; ${r.status}`); + } } /** @@ -31,24 +31,30 @@ export async function move_to_work(edition_ids, old_work, new_work) { * @param {AuthorOLID} new_author */ export async function move_to_author(work_ids, old_author, new_author) { - for (const olid of work_ids) { - const url = `/works/${olid}.json`; - const record = await fetch(url).then(r => r.json()); - if (record.authors.find(a => a.author.key.includes(old_author))) { - record.authors = uniqBy(record.authors.map(a => { - if (!a.author.key.includes(old_author)) return a; + for (const olid of work_ids) { + const url = `/works/${olid}.json`; + const record = await fetch(url).then((r) => r.json()); + if (record.authors.find((a) => a.author.key.includes(old_author))) { + record.authors = uniqBy( + record.authors.map((a) => { + if (!a.author.key.includes(old_author)) return a; - const copy = JSON.parse(JSON.stringify(a)); - copy.author.key = `/authors/${new_author}`; - return copy; - }), a => a.author.key); - record._comment = 'move to correct author'; - const r = await fetch(url, { method: 'PUT', body: JSON.stringify(record) }); - // eslint-disable-next-line no-console - console.log(`moved ${olid}; ${r.status}`) - } else { - // eslint-disable-next-line no-console - console.warn(`${old_author} not in ${url}!`); - } + const copy = JSON.parse(JSON.stringify(a)); + copy.author.key = `/authors/${new_author}`; + return copy; + }), + (a) => a.author.key, + ); + record._comment = 'move to correct author'; + const r = await fetch(url, { + method: 'PUT', + body: JSON.stringify(record), + }); + // eslint-disable-next-line no-console + console.log(`moved ${olid}; ${r.status}`); + } else { + // eslint-disable-next-line no-console + console.warn(`${old_author} not in ${url}!`); } + } } diff --git a/openlibrary/plugins/openlibrary/js/index.js b/openlibrary/plugins/openlibrary/js/index.js index 1a262393ba0..cef525ec4dd 100644 --- a/openlibrary/plugins/openlibrary/js/index.js +++ b/openlibrary/plugins/openlibrary/js/index.js @@ -2,7 +2,7 @@ import 'jquery'; import { exposeGlobally } from './jsdef'; import initAnalytics from './ol.analytics'; import init from './ol.js'; -import initServiceWorker from './service-worker-init.js' +import initServiceWorker from './service-worker-init.js'; import '../../../../static/css/js-all.css'; // polyfill Promise support for IE11 import Promise from 'promise-polyfill'; @@ -24,585 +24,696 @@ initServiceWorker(); initAnalytics(); // Initialise some things -jQuery(function () { - // conditionally load polyfill for <details> tags (IE11) - // See http://diveintohtml5.info/everything.html#details - if (!('open' in document.createElement('details'))) { - import(/* webpackChunkName: "details-polyfill" */ 'details-polyfill'); - } - - // Polyfill for .matches() - if (!Element.prototype.matches) { - Element.prototype.matches = - Element.prototype.msMatchesSelector || - Element.prototype.webkitMatchesSelector; - } - - // Polyfill for .closest() - if (!Element.prototype.closest) { - Element.prototype.closest = function(s) { - let el = this; - do { - if (Element.prototype.matches.call(el, s)) return el; - el = el.parentElement || el.parentNode; - } while (el !== null && el.nodeType === 1); - return null; - }; - } - - const $tabs = $('.ol-tabs'); - if ($tabs.length) { - import(/* webpackChunkName: "tabs" */ './tabs') - .then((module) => module.initTabs($tabs)); - } - - const $autocomplete = $('.multi-input-autocomplete'); - if ($autocomplete.length) { - import(/* webpackChunkName: "autocomplete" */ './autocomplete') - .then((module) => module.init($)); - } - - // hide all images in .no-img - $('.no-img img').hide(); - - // disable save button after click - $('button[name=\'_save\']').on('submit', function() { - $(this).attr('disabled', true); +jQuery(() => { + // conditionally load polyfill for <details> tags (IE11) + // See http://diveintohtml5.info/everything.html#details + if (!('open' in document.createElement('details'))) { + import(/* webpackChunkName: "details-polyfill" */ 'details-polyfill'); + } + + // Polyfill for .matches() + if (!Element.prototype.matches) { + Element.prototype.matches = + Element.prototype.msMatchesSelector || + Element.prototype.webkitMatchesSelector; + } + + // Polyfill for .closest() + if (!Element.prototype.closest) { + Element.prototype.closest = function (s) { + let el = this; + do { + if (Element.prototype.matches.call(el, s)) return el; + el = el.parentElement || el.parentNode; + } while (el !== null && el.nodeType === 1); + return null; + }; + } + + const $tabs = $('.ol-tabs'); + if ($tabs.length) { + import(/* webpackChunkName: "tabs" */ './tabs').then((module) => + module.initTabs($tabs), + ); + } + + const $autocomplete = $('.multi-input-autocomplete'); + if ($autocomplete.length) { + import(/* webpackChunkName: "autocomplete" */ './autocomplete').then( + (module) => module.init($), + ); + } + + // hide all images in .no-img + $('.no-img img').hide(); + + // disable save button after click + $("button[name='_save']").on('submit', function () { + $(this).attr('disabled', true); + }); + + // wmd editor + const $markdownTextAreas = $('textarea.markdown'); + if ($markdownTextAreas.length) { + import(/* webpackChunkName: "markdown-editor" */ './markdown-editor').then( + (module) => module.initMarkdownEditor($markdownTextAreas), + ); + } + + init($); + + const edition = document.getElementById('addWork'); + const autocompleteAuthor = document.querySelector( + '.multi-input-autocomplete--author', + ); + const autocompleteSeries = document.querySelector( + '.multi-input-autocomplete--series', + ); + const autocompleteLanguage = document.querySelector( + '.multi-input-autocomplete--language', + ); + const autocompleteWorks = document.querySelector( + '.multi-input-autocomplete--works', + ); + const autocompleteSeeds = document.querySelector( + '.multi-input-autocomplete--seeds', + ); + const autocompleteSubjects = document.querySelector( + '.csv-autocomplete--subjects', + ); + const addRowButton = document.getElementById('add_row_button'); + const roles = document.querySelector('#roles'); + const classifications = document.querySelector('#classifications'); + const excerpts = document.getElementById('excerpts'); + const links = document.getElementById('links'); + + // conditionally load for user edit page + if ( + edition || + autocompleteAuthor || + autocompleteSeries || + autocompleteLanguage || + autocompleteWorks || + autocompleteSeeds || + autocompleteSubjects || + addRowButton || + roles || + classifications || + excerpts || + links + ) { + import(/* webpackChunkName: "user-website" */ './edit').then((module) => { + if (edition) { + module.initEdit(); + } + if (addRowButton) { + module.initEditRow(); + } + if (excerpts) { + module.initEditExcerpts(); + } + if (links) { + module.initEditLinks(); + } + if (autocompleteAuthor) { + module.initAuthorMultiInputAutocomplete(); + } + if (autocompleteSeries) { + module.initSeriesMultiInputAutocomplete(); + } + if (roles) { + module.initRoleValidation(); + } + if (classifications) { + module.initClassificationValidation(); + } + if (autocompleteLanguage) { + module.initLanguageMultiInputAutocomplete(); + } + if (autocompleteWorks) { + module.initWorksMultiInputAutocomplete(); + } + if (autocompleteSubjects) { + module.initSubjectsAutocomplete(); + } + if (autocompleteSeeds) { + module.initSeedsMultiInputAutocomplete(); + } }); - - // wmd editor - const $markdownTextAreas = $('textarea.markdown'); - if ($markdownTextAreas.length) { - import(/* webpackChunkName: "markdown-editor" */ './markdown-editor') - .then((module) => module.initMarkdownEditor($markdownTextAreas)); - } - - init($); - - const edition = document.getElementById('addWork'); - const autocompleteAuthor = document.querySelector('.multi-input-autocomplete--author'); - const autocompleteSeries = document.querySelector('.multi-input-autocomplete--series'); - const autocompleteLanguage = document.querySelector('.multi-input-autocomplete--language'); - const autocompleteWorks = document.querySelector('.multi-input-autocomplete--works'); - const autocompleteSeeds = document.querySelector('.multi-input-autocomplete--seeds'); - const autocompleteSubjects = document.querySelector('.csv-autocomplete--subjects'); - const addRowButton = document.getElementById('add_row_button'); - const roles = document.querySelector('#roles'); - const classifications = document.querySelector('#classifications'); - const excerpts = document.getElementById('excerpts'); - const links = document.getElementById('links'); - - // conditionally load for user edit page - if ( - edition || - autocompleteAuthor || autocompleteSeries || autocompleteLanguage || autocompleteWorks || - autocompleteSeeds || autocompleteSubjects || - addRowButton || roles || classifications || - excerpts || links - ) { - import(/* webpackChunkName: "user-website" */ './edit') - .then(module => { - if (edition) { - module.initEdit(); - } - if (addRowButton) { - module.initEditRow(); - } - if (excerpts) { - module.initEditExcerpts(); - } - if (links) { - module.initEditLinks(); - } - if (autocompleteAuthor) { - module.initAuthorMultiInputAutocomplete(); - } - if (autocompleteSeries) { - module.initSeriesMultiInputAutocomplete(); - } - if (roles) { - module.initRoleValidation(); - } - if (classifications) { - module.initClassificationValidation(); - } - if (autocompleteLanguage) { - module.initLanguageMultiInputAutocomplete(); - } - if (autocompleteWorks) { - module.initWorksMultiInputAutocomplete(); - } - if (autocompleteSubjects) { - module.initSubjectsAutocomplete(); - } - if (autocompleteSeeds) { - module.initSeedsMultiInputAutocomplete(); - } - }); - } - - // conditionally load for author merge page - const mergePageElement = document.querySelector('#author-merge-page'); - const preMergePageElement = document.getElementById('preMerge'); - if (mergePageElement || preMergePageElement) { - import(/* webpackChunkName: "merge" */ './merge') - .then(module => { - if (mergePageElement) { - module.initAuthorMergePage(); - } - if (preMergePageElement) { - module.initAuthorView(); - } - }); - } - - // conditionally load for type changing input - const typeChanger = document.getElementById('type.key') - if (typeChanger) { - import(/* webpackChunkName: "type-changer" */ './type_changer.js') - .then(module => module.initTypeChanger(typeChanger)); - } - - // conditionally load validation and submission js for registration form - if (document.querySelector('form[name=signup]')) { - import(/* webpackChunkName: "signup" */'./signup.js') - .then(module => module.initSignupForm()); - } - - // conditionally load submission js for login form - if (document.querySelector('form[name=login]')) { - import(/* webpackChunkName: "signup" */'./signup.js') - .then(module => module.initLoginForm()); - } - - // conditionally load clamping components - const clampers = document.querySelectorAll('.clamp'); - if (clampers.length) { - import(/* webpackChunkName: "clampers" */ './clampers.js') - .then(module => { - if (clampers.length) { - module.initClampers(clampers); - } - }); - } - - // conditionally loads Goodreads import based on class in the page - if (document.getElementsByClassName('import-table').length) { - import(/* webpackChunkName: "goodreads-import" */'./goodreads_import.js') - .then(module => module.initGoodreadsImport()); - } - // conditionally load list seed item deletion dialog functionality based on id on lists pages - if (document.getElementById('listResults')) { - import(/* webpackChunkName: "ListViewBody" */'./lists/ListViewBody.js'); - } - - // Enable any carousels in the page - const carouselElements = document.querySelectorAll('.carousel--progressively-enhanced') - if (carouselElements.length) { - import(/* webpackChunkName: "carousel" */ './carousel') - .then((module) => { - module.initialzeCarousels(carouselElements) - }) - } - if ($('script[type="text/json+graph"]').length > 0) { - import(/* webpackChunkName: "graphs" */ './graphs') - .then((module) => module.init()); - } - - const readingLogCharts = document.querySelector('.readinglog-charts') - if (readingLogCharts) { - const readingLogConfig = JSON.parse(readingLogCharts.dataset.config) - import(/* webpackChunkName: "readinglog-stats" */ './readinglog_stats') - .then(module => module.init(readingLogConfig)); - } - - if (document.getElementsByClassName('toast').length) { - import(/* webpackChunkName: "Toast" */ './Toast') - .then((module) => { - Array.from(document.getElementsByClassName('toast')) - .forEach(el => new module.Toast($(el))); - }); - } - - if ($('.lazy-thing-preview').length) { - import(/* webpackChunkName: "lazy-thing-preview" */ './lazy-thing-preview') - .then((module) => new module.LazyThingPreview().init()); - } - - // Disable data export buttons on form submit - const patronImportForms = document.querySelectorAll('.patron-export-form') - if (patronImportForms.length) { - import(/* webpackChunkName: "patron-exports" */ './patron_exports') - .then(module => module.initPatronExportForms(patronImportForms)); - } - - const $observationModalLinks = $('.observations-modal-link'); - const $notesModalLinks = $('.notes-modal-link'); - const $notesPageButtons = $('.note-page-buttons'); - const $shareModalLinks = $('.share-modal-link'); - if ($observationModalLinks.length || $notesModalLinks.length || $notesPageButtons.length || $shareModalLinks.length) { - import(/* webpackChunkName: "modal-links" */ './modals') - .then(module => { - if ($observationModalLinks.length) { - module.initObservationsModal($observationModalLinks); - } - if ($notesModalLinks.length) { - module.initNotesModal($notesModalLinks); - } - if ($notesPageButtons.length) { - module.addNotesPageButtonListeners(); - } - if ($shareModalLinks.length) { - module.initShareModal($shareModalLinks) - } - }); - } - - - const manageCoversElement = document.getElementsByClassName('manageCovers').length; - const addCoversElement = document.getElementsByClassName('imageIntro').length; - const saveCoversElement = document.getElementsByClassName('imageSaved').length; - const coverForm = document.querySelector('.ol-cover-form--clipboard'); - - if (addCoversElement || manageCoversElement || saveCoversElement || coverForm) { - import(/* webpackChunkName: "covers" */ './covers') - .then((module) => { - if (manageCoversElement) { - module.initCoversChange(); - } - if (addCoversElement) { - module.initCoversAddManage(); - } - if (saveCoversElement) { - module.initCoversSaved(); - } - if (coverForm) { - module.initPasteForm(coverForm); - } - }); - } - - if (document.getElementById('addbook')) { - import(/* webpackChunkName: "add-book" */ './add-book') - .then(module => module.initAddBookImport()); - } - - if (document.getElementById('autofill-dev-credentials')) { - document.getElementById('username').value = 'openlibrary@example.com' - document.getElementById('password').value = 'admin123' - document.getElementById('remember').checked = true - } - const anonymizationButton = document.querySelector('.account-anonymization-button') - const adminLinks = document.getElementById('adminLinks') - const confirmButtons = document.querySelectorAll('.do-confirm') - if (adminLinks || anonymizationButton || confirmButtons.length) { - import(/* webpackChunkName: "admin" */ './admin') - .then(module => { - if (adminLinks) { - module.initAdmin(); - } - if (anonymizationButton) { - module.initAnonymizationButton(anonymizationButton); - } - if (confirmButtons.length) { - module.initConfirmationButtons(confirmButtons); - } - }); - } - - if (window.matchMedia('(display-mode: standalone)').matches) { - import(/* webpackChunkName: "offline-banner" */ './offline-banner') - .then((module) => module.initOfflineBanner()); - } - - const searchFacets = document.getElementById('searchFacets') - if (searchFacets) { - import(/* webpackChunkName: "search" */ './search') - .then((module) => module.initSearchFacets(searchFacets)); - } - - // Conditionally load Integrated Librarian Environment - if (document.getElementsByClassName('show-librarian-tools').length) { - import(/* webpackChunkName: "ile" */ './ile') - .then((module) => module.init()) - .then(() => { - // book page subject editing - // Handle pencil clicks - document.querySelectorAll('.edit-subject-btn').forEach(btn => { - btn.addEventListener('click', (e) => { - e.preventDefault() - const workOlid = btn.dataset.workOlid - if (!window.ILE.selectionManager.selectedItems.work.includes(workOlid)) { - window.ILE.selectionManager.addSelectedItem(workOlid) - window.ILE.selectionManager.updateToolbar() - } - window.ILE.updateAndShowBulkTagger([workOlid], true) - }) - }) - }) - // Import ile then the datatable to apply clickable classes to all listed editions - if (document.getElementsByClassName('editions-table--progressively-enhanced').length) { - import(/* webpackChunkName: "editions-table" */ './editions-table') - .then(module => module.initEditionsTable()) + } + + // conditionally load for author merge page + const mergePageElement = document.querySelector('#author-merge-page'); + const preMergePageElement = document.getElementById('preMerge'); + if (mergePageElement || preMergePageElement) { + import(/* webpackChunkName: "merge" */ './merge').then((module) => { + if (mergePageElement) { + module.initAuthorMergePage(); + } + if (preMergePageElement) { + module.initAuthorView(); + } + }); + } + + // conditionally load for type changing input + const typeChanger = document.getElementById('type.key'); + if (typeChanger) { + import(/* webpackChunkName: "type-changer" */ './type_changer.js').then( + (module) => module.initTypeChanger(typeChanger), + ); + } + + // conditionally load validation and submission js for registration form + if (document.querySelector('form[name=signup]')) { + import(/* webpackChunkName: "signup" */ './signup.js').then((module) => + module.initSignupForm(), + ); + } + + // conditionally load submission js for login form + if (document.querySelector('form[name=login]')) { + import(/* webpackChunkName: "signup" */ './signup.js').then((module) => + module.initLoginForm(), + ); + } + + // conditionally load clamping components + const clampers = document.querySelectorAll('.clamp'); + if (clampers.length) { + import(/* webpackChunkName: "clampers" */ './clampers.js').then( + (module) => { + if (clampers.length) { + module.initClampers(clampers); } - } - // conditionally load functionality based on what's in the page - if (document.getElementsByClassName('editions-table--progressively-enhanced').length) { - import(/* webpackChunkName: "editions-table" */ './editions-table') - .then(module => module.initEditionsTable()); - } - if ($('#cboxPrevious').length) { - $('#cboxPrevious').attr({'aria-label': 'Previous button', 'aria-hidden': 'true'}); - } - if ($('#cboxNext').length) { - $('#cboxNext').attr({'aria-label': 'Next button', 'aria-hidden': 'true'}); - } - if ($('#cboxSlideshow').length) { - $('#cboxSlideshow').attr({'aria-label': 'Slideshow button', 'aria-hidden': 'true'}); - } - - const droppers = document.querySelectorAll('.dropper') - const genericDroppers = document.querySelectorAll('.generic-dropper-wrapper') - if (droppers.length || genericDroppers.length) { - import(/* webpackChunkName: "droppers" */ './dropper') - .then((module) => { - module.initDroppers(droppers) - module.initGenericDroppers(genericDroppers) - }) - } - - // My Books Droppers (includes New List Form and Reading Check-Ins): - const myBooksDroppers = document.querySelectorAll('.my-books-dropper') - if (myBooksDroppers.length) { - const actionableListShowcases = document.querySelectorAll('.actionable-item') - - import(/* webpackChunkName: "my-books" */ './my-books') - .then((module) => { - module.initMyBooksAffordances(myBooksDroppers, actionableListShowcases) - }) - } - - // TODO: Make these selectors a consistent interface - const $dialogs = $('.dialog--open,.dialog--close,#noMaster,#confirmMerge,#leave-waitinglist-dialog,#bookPreview'); - if ($dialogs.length) { - import(/* webpackChunkName: "dialog" */ './dialog') - .then(module => module.initDialogs()) - } - - const nativeDialogs = document.querySelectorAll('.native-dialog'); - if (nativeDialogs.length) { - import(/* webpackChunkName: "native-dialog" */ './native-dialog') - .then(module => module.initDialogs(nativeDialogs)) - } - - // Yearly reading goal functionality - const setGoalLinks = document.querySelectorAll('.set-reading-goal-link') - const goalEditLinks = document.querySelectorAll('.edit-reading-goal-link') - const goalSubmitButtons = document.querySelectorAll('.reading-goal-submit-button') - const yearElements = document.querySelectorAll('.use-local-year') - if (setGoalLinks.length || goalEditLinks.length || goalSubmitButtons.length || yearElements.length) { - import(/* webpackChunkName: "reading-goals" */ './reading-goals') - .then((module) => { - if (setGoalLinks.length) { - module.initYearlyGoalPrompt(setGoalLinks) - } - if (goalEditLinks.length) { - module.initGoalEditLinks(goalEditLinks) - } - if (goalSubmitButtons.length) { - module.initGoalSubmitButtons(goalSubmitButtons) - } - if (yearElements.length) { - module.displayLocalYear(yearElements) - } - }) - } - - $(document).on('click', '.slide-toggle', function () { - $(`#${$(this).attr('aria-controls')}`).slideToggle(); + }, + ); + } + + // conditionally loads Goodreads import based on class in the page + if (document.getElementsByClassName('import-table').length) { + import( + /* webpackChunkName: "goodreads-import" */ './goodreads_import.js' + ).then((module) => module.initGoodreadsImport()); + } + // conditionally load list seed item deletion dialog functionality based on id on lists pages + if (document.getElementById('listResults')) { + import(/* webpackChunkName: "ListViewBody" */ './lists/ListViewBody.js'); + } + + // Enable any carousels in the page + const carouselElements = document.querySelectorAll( + '.carousel--progressively-enhanced', + ); + if (carouselElements.length) { + import(/* webpackChunkName: "carousel" */ './carousel').then((module) => { + module.initialzeCarousels(carouselElements); }); - - $('#wikiselect').on('focus', function(){$(this).trigger('select');}) - - $('.hamburger-component .mask-menu').on('click', function () { - $('details[open]').not(this).removeAttr('open'); + } + if ($('script[type="text/json+graph"]').length > 0) { + import(/* webpackChunkName: "graphs" */ './graphs').then((module) => + module.init(), + ); + } + + const readingLogCharts = document.querySelector('.readinglog-charts'); + if (readingLogCharts) { + const readingLogConfig = JSON.parse(readingLogCharts.dataset.config); + import( + /* webpackChunkName: "readinglog-stats" */ './readinglog_stats' + ).then((module) => module.init(readingLogConfig)); + } + + if (document.getElementsByClassName('toast').length) { + import(/* webpackChunkName: "Toast" */ './Toast').then((module) => { + Array.from(document.getElementsByClassName('toast')).forEach( + (el) => new module.Toast($(el)), + ); }); - - $('.header-dropdown').on('keydown', function (event) { - if (event.key === 'Escape') { - $('.header-dropdown > details[open]').removeAttr('open'); - } + } + + if ($('.lazy-thing-preview').length) { + import( + /* webpackChunkName: "lazy-thing-preview" */ './lazy-thing-preview' + ).then((module) => new module.LazyThingPreview().init()); + } + + // Disable data export buttons on form submit + const patronImportForms = document.querySelectorAll('.patron-export-form'); + if (patronImportForms.length) { + import(/* webpackChunkName: "patron-exports" */ './patron_exports').then( + (module) => module.initPatronExportForms(patronImportForms), + ); + } + + const $observationModalLinks = $('.observations-modal-link'); + const $notesModalLinks = $('.notes-modal-link'); + const $notesPageButtons = $('.note-page-buttons'); + const $shareModalLinks = $('.share-modal-link'); + if ( + $observationModalLinks.length || + $notesModalLinks.length || + $notesPageButtons.length || + $shareModalLinks.length + ) { + import(/* webpackChunkName: "modal-links" */ './modals').then((module) => { + if ($observationModalLinks.length) { + module.initObservationsModal($observationModalLinks); + } + if ($notesModalLinks.length) { + module.initNotesModal($notesModalLinks); + } + if ($notesPageButtons.length) { + module.addNotesPageButtonListeners(); + } + if ($shareModalLinks.length) { + module.initShareModal($shareModalLinks); + } }); - - $('.dropdown-menu').each(function() { - $(this).find('a').last().on('focusout', function() { - $('.header-dropdown > details[open]').removeAttr('open'); + } + + const manageCoversElement = + document.getElementsByClassName('manageCovers').length; + const addCoversElement = document.getElementsByClassName('imageIntro').length; + const saveCoversElement = + document.getElementsByClassName('imageSaved').length; + const coverForm = document.querySelector('.ol-cover-form--clipboard'); + + if ( + addCoversElement || + manageCoversElement || + saveCoversElement || + coverForm + ) { + import(/* webpackChunkName: "covers" */ './covers').then((module) => { + if (manageCoversElement) { + module.initCoversChange(); + } + if (addCoversElement) { + module.initCoversAddManage(); + } + if (saveCoversElement) { + module.initCoversSaved(); + } + if (coverForm) { + module.initPasteForm(coverForm); + } + }); + } + + if (document.getElementById('addbook')) { + import(/* webpackChunkName: "add-book" */ './add-book').then((module) => + module.initAddBookImport(), + ); + } + + if (document.getElementById('autofill-dev-credentials')) { + document.getElementById('username').value = 'openlibrary@example.com'; + document.getElementById('password').value = 'admin123'; + document.getElementById('remember').checked = true; + } + const anonymizationButton = document.querySelector( + '.account-anonymization-button', + ); + const adminLinks = document.getElementById('adminLinks'); + const confirmButtons = document.querySelectorAll('.do-confirm'); + if (adminLinks || anonymizationButton || confirmButtons.length) { + import(/* webpackChunkName: "admin" */ './admin').then((module) => { + if (adminLinks) { + module.initAdmin(); + } + if (anonymizationButton) { + module.initAnonymizationButton(anonymizationButton); + } + if (confirmButtons.length) { + module.initConfirmationButtons(confirmButtons); + } + }); + } + + if (window.matchMedia('(display-mode: standalone)').matches) { + import(/* webpackChunkName: "offline-banner" */ './offline-banner').then( + (module) => module.initOfflineBanner(), + ); + } + + const searchFacets = document.getElementById('searchFacets'); + if (searchFacets) { + import(/* webpackChunkName: "search" */ './search').then((module) => + module.initSearchFacets(searchFacets), + ); + } + + // Conditionally load Integrated Librarian Environment + if (document.getElementsByClassName('show-librarian-tools').length) { + import(/* webpackChunkName: "ile" */ './ile') + .then((module) => module.init()) + .then(() => { + // book page subject editing + // Handle pencil clicks + document.querySelectorAll('.edit-subject-btn').forEach((btn) => { + btn.addEventListener('click', (e) => { + e.preventDefault(); + const workOlid = btn.dataset.workOlid; + if ( + !window.ILE.selectionManager.selectedItems.work.includes(workOlid) + ) { + window.ILE.selectionManager.addSelectedItem(workOlid); + window.ILE.selectionManager.updateToolbar(); + } + window.ILE.updateAndShowBulkTagger([workOlid], true); + }); }); + }); + // Import ile then the datatable to apply clickable classes to all listed editions + if ( + document.getElementsByClassName('editions-table--progressively-enhanced') + .length + ) { + import(/* webpackChunkName: "editions-table" */ './editions-table').then( + (module) => module.initEditionsTable(), + ); + } + } + // conditionally load functionality based on what's in the page + if ( + document.getElementsByClassName('editions-table--progressively-enhanced') + .length + ) { + import(/* webpackChunkName: "editions-table" */ './editions-table').then( + (module) => module.initEditionsTable(), + ); + } + if ($('#cboxPrevious').length) { + $('#cboxPrevious').attr({ + 'aria-label': 'Previous button', + 'aria-hidden': 'true', }); - - // Open one dropdown at a time. - $(document).on('click', function (event) { - const $openMenus = $('.header-dropdown details[open]').parents('.header-dropdown'); - $openMenus - .filter((_, menu) => !$(event.target).closest(menu).length) - .find('details') - .removeAttr('open'); + } + if ($('#cboxNext').length) { + $('#cboxNext').attr({ 'aria-label': 'Next button', 'aria-hidden': 'true' }); + } + if ($('#cboxSlideshow').length) { + $('#cboxSlideshow').attr({ + 'aria-label': 'Slideshow button', + 'aria-hidden': 'true', }); + } + + const droppers = document.querySelectorAll('.dropper'); + const genericDroppers = document.querySelectorAll('.generic-dropper-wrapper'); + if (droppers.length || genericDroppers.length) { + import(/* webpackChunkName: "droppers" */ './dropper').then((module) => { + module.initDroppers(droppers); + module.initGenericDroppers(genericDroppers); + }); + } - // Prevent default star rating behavior: - const ratingForms = document.querySelectorAll('.star-rating-form') - if (ratingForms.length) { - import(/* webpackChunkName: "star-ratings" */'./star-ratings') - .then((module) => module.initRatingHandlers(ratingForms)); - } - - // Book page navbar initialization: - const navbarWrappers = document.querySelectorAll('.nav-bar-wrapper') - if (navbarWrappers.length) { - // Add JS for book page navbar: - import(/* webpackChunkName: "nav-bar" */ './edition-nav-bar') - .then((module) => { - module.initNavbars(navbarWrappers) - }); - // Add sticky title component animations to desktop views: - import(/* webpackChunkName: "compact-title" */ './compact-title') - .then((module) => { - const compactTitle = document.querySelector('.compact-title') - const desktopNavbar = [...navbarWrappers].find(elem => elem.classList.contains('desktop-only')) - module.initCompactTitle(desktopNavbar, compactTitle) - }) - } - - // Add functionality for librarian merge request table: - const librarianQueue = document.querySelector('.librarian-queue-wrapper') - - if (librarianQueue) { - import(/* webpackChunkName: "merge-request-table" */'./merge-request-table') - .then(module => { - module.initLibrarianQueue(librarianQueue) - }) - } - - // Add functionality to the team page for filtering members: - const teamCards = document.querySelector('.teamCards_container') - if (teamCards) { - import(/* webpackChunkName "team" */ './team') - .then(module => { - module.initTeamFilter(); - }) - } - - // Add new providers in edit edition view: - const addProviderRowLink = document.querySelector('#add-new-provider-row') - if (addProviderRowLink) { - import(/* webpackChunkName "add-provider-link" */ './add_provider') - .then(module => module.initAddProviderRowLink(addProviderRowLink)) - } - - - // Allow banner announcements to be dismissed by logged-in users: - const banners = document.querySelectorAll('.page-banner--dismissable') - if (banners.length) { - import(/* webpackChunkName: "dismissible-banner" */ './banner') - .then(module => module.initDismissibleBanners(banners)) - } - - const returnForms = document.querySelectorAll('.return-form') - if (returnForms.length) { - import(/* webpackChunkName: "return-form" */ './return-form') - .then(module => module.initReturnForms(returnForms)) - } - - const crumbs = document.querySelectorAll('.crumb select'); - if (crumbs.length) { - import(/* webpackChunkName: "breadcrumb-select" */ './breadcrumb_select') - .then(module => module.initBreadcrumbSelect(crumbs)); - } - - const interstitial = document.querySelector('.interstitial'); - if (interstitial) { - import (/* webpackChunkName: "interstitial" */ './interstitial') - .then(module => module.initInterstitial(interstitial)); - } - - const leaveWaitlistLinks = document.querySelectorAll('a.leave'); - if (leaveWaitlistLinks.length && document.getElementById('leave-waitinglist-dialog')) { - import(/* webpackChunkName: "waitlist" */ './waitlist') - .then(module => module.initLeaveWaitlist(leaveWaitlistLinks)); - } - - const thirdPartyLoginsIframe = document.getElementById('ia-third-party-logins'); - if (thirdPartyLoginsIframe) { - import(/* webpackChunkName: "ia_thirdparty_logins" */ './ia_thirdparty_logins') - .then((module) => module.initMessageEventListener(thirdPartyLoginsIframe)); - } - - // Password visibility toggle: - const passwordVisibilityToggle = document.querySelector('.password-visibility-toggle') - if (passwordVisibilityToggle) { - import(/* webpackChunkName: "password-visibility-toggle" */ './password-toggle') - .then(module => module.initPasswordToggling(passwordVisibilityToggle)) - } - - // Affiliate links: - const affiliateLinksSection = document.querySelectorAll('.affiliate-links-section') - if (affiliateLinksSection.length) { - import(/* webpackChunkName: "affiliate-links" */ './affiliate-links') - .then(module => module.initAffiliateLinks(affiliateLinksSection)) - } - - // Fulltext search box: - const fulltextSearchSuggestion = document.querySelector('#fulltext-search-suggestion') - if (fulltextSearchSuggestion) { - import(/* webpackChunkName: "fulltext-search-suggestion" */ './fulltext-search-suggestion') - .then(module => module.initFulltextSearchSuggestion(fulltextSearchSuggestion)) - } - - // Go back redirect: - const backLinks = document.querySelectorAll('.go-back-link') - if (backLinks.length) { - import (/* webpackChunkName: "go-back-links" */ './go-back-links') - .then(module => module.initGoBackLinks(backLinks)) - } - - // Lazy-load book page lists section - const listSection = document.querySelector('.lists-section') - if (listSection) { - import(/* webpackChunkName: "book-page-lists" */ './book-page-lists') - .then(module => module.initListsSection(listSection)) - } - - // Initialize follow forms lazily - const followForms = document.querySelectorAll('.follow-form'); - if (followForms.length) { - import(/* webpackChunkName: "following" */ './following') - .then(module => module.initAsyncFollowing(followForms)) - } - - // Generalized carousel lazy-loading - const lazyCarousels = document.querySelectorAll('.lazy-carousel') - if (lazyCarousels.length) { - import(/* webpackChunkName: "lazy-carousels" */ './lazy-carousel') - .then(module => module.initLazyCarousel(lazyCarousels)) - } - - // Librarian Dashboard - const librarianDashboard = document.querySelector('.librarian-dashboard') - if (librarianDashboard) { - import(/* webpackChunkName: "librarian-dashboard" */ './librarian-dashboard') - .then(module => module.initLibrarianDashboard(librarianDashboard)) - } + // My Books Droppers (includes New List Form and Reading Check-Ins): + const myBooksDroppers = document.querySelectorAll('.my-books-dropper'); + if (myBooksDroppers.length) { + const actionableListShowcases = + document.querySelectorAll('.actionable-item'); - // List books - if (document.querySelector('.list-books')) { - import(/* webpackChunkName: "list-books" */ './list_books') - .then(module => module.ListBooks.init()); - } + import(/* webpackChunkName: "my-books" */ './my-books').then((module) => { + module.initMyBooksAffordances(myBooksDroppers, actionableListShowcases); + }); + } + + // TODO: Make these selectors a consistent interface + const $dialogs = $( + '.dialog--open,.dialog--close,#noMaster,#confirmMerge,#leave-waitinglist-dialog,#bookPreview', + ); + if ($dialogs.length) { + import(/* webpackChunkName: "dialog" */ './dialog').then((module) => + module.initDialogs(), + ); + } + + const nativeDialogs = document.querySelectorAll('.native-dialog'); + if (nativeDialogs.length) { + import(/* webpackChunkName: "native-dialog" */ './native-dialog').then( + (module) => module.initDialogs(nativeDialogs), + ); + } + + // Yearly reading goal functionality + const setGoalLinks = document.querySelectorAll('.set-reading-goal-link'); + const goalEditLinks = document.querySelectorAll('.edit-reading-goal-link'); + const goalSubmitButtons = document.querySelectorAll( + '.reading-goal-submit-button', + ); + const yearElements = document.querySelectorAll('.use-local-year'); + if ( + setGoalLinks.length || + goalEditLinks.length || + goalSubmitButtons.length || + yearElements.length + ) { + import(/* webpackChunkName: "reading-goals" */ './reading-goals').then( + (module) => { + if (setGoalLinks.length) { + module.initYearlyGoalPrompt(setGoalLinks); + } + if (goalEditLinks.length) { + module.initGoalEditLinks(goalEditLinks); + } + if (goalSubmitButtons.length) { + module.initGoalSubmitButtons(goalSubmitButtons); + } + if (yearElements.length) { + module.displayLocalYear(yearElements); + } + }, + ); + } + + $(document).on('click', '.slide-toggle', function () { + $(`#${$(this).attr('aria-controls')}`).slideToggle(); + }); + + $('#wikiselect').on('focus', function () { + $(this).trigger('select'); + }); + + $('.hamburger-component .mask-menu').on('click', function () { + $('details[open]').not(this).removeAttr('open'); + }); + + $('.header-dropdown').on('keydown', (event) => { + if (event.key === 'Escape') { + $('.header-dropdown > details[open]').removeAttr('open'); + } + }); + + $('.dropdown-menu').each(function () { + $(this) + .find('a') + .last() + .on('focusout', () => { + $('.header-dropdown > details[open]').removeAttr('open'); + }); + }); + + // Open one dropdown at a time. + $(document).on('click', (event) => { + const $openMenus = $('.header-dropdown details[open]').parents( + '.header-dropdown', + ); + $openMenus + .filter((_, menu) => !$(event.target).closest(menu).length) + .find('details') + .removeAttr('open'); + }); + + // Prevent default star rating behavior: + const ratingForms = document.querySelectorAll('.star-rating-form'); + if (ratingForms.length) { + import(/* webpackChunkName: "star-ratings" */ './star-ratings').then( + (module) => module.initRatingHandlers(ratingForms), + ); + } + + // Book page navbar initialization: + const navbarWrappers = document.querySelectorAll('.nav-bar-wrapper'); + if (navbarWrappers.length) { + // Add JS for book page navbar: + import(/* webpackChunkName: "nav-bar" */ './edition-nav-bar').then( + (module) => { + module.initNavbars(navbarWrappers); + }, + ); + // Add sticky title component animations to desktop views: + import(/* webpackChunkName: "compact-title" */ './compact-title').then( + (module) => { + const compactTitle = document.querySelector('.compact-title'); + const desktopNavbar = [...navbarWrappers].find((elem) => + elem.classList.contains('desktop-only'), + ); + module.initCompactTitle(desktopNavbar, compactTitle); + }, + ); + } + + // Add functionality for librarian merge request table: + const librarianQueue = document.querySelector('.librarian-queue-wrapper'); + + if (librarianQueue) { + import( + /* webpackChunkName: "merge-request-table" */ './merge-request-table' + ).then((module) => { + module.initLibrarianQueue(librarianQueue); + }); + } - // Stats page login counts - const monthlyLoginStats = document.querySelector('.monthly-login-counts') - if (monthlyLoginStats) { - import(/* webpackChunkName: "stats" */ './stats') - .then(module => module.initUniqueLoginCounts(monthlyLoginStats)) - } + // Add functionality to the team page for filtering members: + const teamCards = document.querySelector('.teamCards_container'); + if (teamCards) { + import(/* webpackChunkName "team" */ './team').then((module) => { + module.initTeamFilter(); + }); + } + + // Add new providers in edit edition view: + const addProviderRowLink = document.querySelector('#add-new-provider-row'); + if (addProviderRowLink) { + import(/* webpackChunkName "add-provider-link" */ './add_provider').then( + (module) => module.initAddProviderRowLink(addProviderRowLink), + ); + } + + // Allow banner announcements to be dismissed by logged-in users: + const banners = document.querySelectorAll('.page-banner--dismissable'); + if (banners.length) { + import(/* webpackChunkName: "dismissible-banner" */ './banner').then( + (module) => module.initDismissibleBanners(banners), + ); + } + + const returnForms = document.querySelectorAll('.return-form'); + if (returnForms.length) { + import(/* webpackChunkName: "return-form" */ './return-form').then( + (module) => module.initReturnForms(returnForms), + ); + } + + const crumbs = document.querySelectorAll('.crumb select'); + if (crumbs.length) { + import( + /* webpackChunkName: "breadcrumb-select" */ './breadcrumb_select' + ).then((module) => module.initBreadcrumbSelect(crumbs)); + } + + const interstitial = document.querySelector('.interstitial'); + if (interstitial) { + import(/* webpackChunkName: "interstitial" */ './interstitial').then( + (module) => module.initInterstitial(interstitial), + ); + } + + const leaveWaitlistLinks = document.querySelectorAll('a.leave'); + if ( + leaveWaitlistLinks.length && + document.getElementById('leave-waitinglist-dialog') + ) { + import(/* webpackChunkName: "waitlist" */ './waitlist').then((module) => + module.initLeaveWaitlist(leaveWaitlistLinks), + ); + } + + const thirdPartyLoginsIframe = document.getElementById( + 'ia-third-party-logins', + ); + if (thirdPartyLoginsIframe) { + import( + /* webpackChunkName: "ia_thirdparty_logins" */ './ia_thirdparty_logins' + ).then((module) => module.initMessageEventListener(thirdPartyLoginsIframe)); + } + + // Password visibility toggle: + const passwordVisibilityToggle = document.querySelector( + '.password-visibility-toggle', + ); + if (passwordVisibilityToggle) { + import( + /* webpackChunkName: "password-visibility-toggle" */ './password-toggle' + ).then((module) => module.initPasswordToggling(passwordVisibilityToggle)); + } + + // Affiliate links: + const affiliateLinksSection = document.querySelectorAll( + '.affiliate-links-section', + ); + if (affiliateLinksSection.length) { + import(/* webpackChunkName: "affiliate-links" */ './affiliate-links').then( + (module) => module.initAffiliateLinks(affiliateLinksSection), + ); + } + + // Fulltext search box: + const fulltextSearchSuggestion = document.querySelector( + '#fulltext-search-suggestion', + ); + if (fulltextSearchSuggestion) { + import( + /* webpackChunkName: "fulltext-search-suggestion" */ './fulltext-search-suggestion' + ).then((module) => + module.initFulltextSearchSuggestion(fulltextSearchSuggestion), + ); + } + + // Go back redirect: + const backLinks = document.querySelectorAll('.go-back-link'); + if (backLinks.length) { + import(/* webpackChunkName: "go-back-links" */ './go-back-links').then( + (module) => module.initGoBackLinks(backLinks), + ); + } + + // Lazy-load book page lists section + const listSection = document.querySelector('.lists-section'); + if (listSection) { + import(/* webpackChunkName: "book-page-lists" */ './book-page-lists').then( + (module) => module.initListsSection(listSection), + ); + } + + // Initialize follow forms lazily + const followForms = document.querySelectorAll('.follow-form'); + if (followForms.length) { + import(/* webpackChunkName: "following" */ './following').then((module) => + module.initAsyncFollowing(followForms), + ); + } + + // Generalized carousel lazy-loading + const lazyCarousels = document.querySelectorAll('.lazy-carousel'); + if (lazyCarousels.length) { + import(/* webpackChunkName: "lazy-carousels" */ './lazy-carousel').then( + (module) => module.initLazyCarousel(lazyCarousels), + ); + } + + // Librarian Dashboard + const librarianDashboard = document.querySelector('.librarian-dashboard'); + if (librarianDashboard) { + import( + /* webpackChunkName: "librarian-dashboard" */ './librarian-dashboard' + ).then((module) => module.initLibrarianDashboard(librarianDashboard)); + } + + // List books + if (document.querySelector('.list-books')) { + import(/* webpackChunkName: "list-books" */ './list_books').then((module) => + module.ListBooks.init(), + ); + } + + // Stats page login counts + const monthlyLoginStats = document.querySelector('.monthly-login-counts'); + if (monthlyLoginStats) { + import(/* webpackChunkName: "stats" */ './stats').then((module) => + module.initUniqueLoginCounts(monthlyLoginStats), + ); + } }); diff --git a/openlibrary/plugins/openlibrary/js/interstitial.js b/openlibrary/plugins/openlibrary/js/interstitial.js index 43542b622da..29c212c0c1f 100644 --- a/openlibrary/plugins/openlibrary/js/interstitial.js +++ b/openlibrary/plugins/openlibrary/js/interstitial.js @@ -1,21 +1,21 @@ export function initInterstitial(elem) { - let seconds = elem.dataset.wait - const url = elem.dataset.url - const timerElement = elem.querySelector('#timer') - const countdown = setInterval(() => { - seconds-- - timerElement.textContent = seconds - if (seconds === 0) { - clearInterval(countdown) - window.location.href = url - } - }, 1000) // 1 second interval - - // Add cancel button handler - const cancelButton = elem.querySelector('.close-window'); - if (cancelButton) { - cancelButton.addEventListener('click', () => { - window.close() - }); + let seconds = elem.dataset.wait; + const url = elem.dataset.url; + const timerElement = elem.querySelector('#timer'); + const countdown = setInterval(() => { + seconds--; + timerElement.textContent = seconds; + if (seconds === 0) { + clearInterval(countdown); + window.location.href = url; } + }, 1000); // 1 second interval + + // Add cancel button handler + const cancelButton = elem.querySelector('.close-window'); + if (cancelButton) { + cancelButton.addEventListener('click', () => { + window.close(); + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/isbnOverride.js b/openlibrary/plugins/openlibrary/js/isbnOverride.js index 23b4bf80b56..7e2976c96b3 100644 --- a/openlibrary/plugins/openlibrary/js/isbnOverride.js +++ b/openlibrary/plugins/openlibrary/js/isbnOverride.js @@ -8,8 +8,14 @@ * @property {Function} clear - clears the ISBN object */ export const isbnOverride = { - data: null, - set(isbnData) { this.data = isbnData }, - get() { return this.data }, - clear() { this.data = null }, -} + data: null, + set(isbnData) { + this.data = isbnData; + }, + get() { + return this.data; + }, + clear() { + this.data = null; + }, +}; diff --git a/openlibrary/plugins/openlibrary/js/jquery.repeat.js b/openlibrary/plugins/openlibrary/js/jquery.repeat.js index 5e83377fd68..76e81fc46bd 100644 --- a/openlibrary/plugins/openlibrary/js/jquery.repeat.js +++ b/openlibrary/plugins/openlibrary/js/jquery.repeat.js @@ -1,5 +1,5 @@ -import Template from './template' -import { isbnOverride } from '../../openlibrary/js/isbnOverride' +import { isbnOverride } from '../../openlibrary/js/isbnOverride'; +import Template from './template'; /** * jquery repeat: jquery plugin to handle repetitive inputs in a form. @@ -7,108 +7,108 @@ import { isbnOverride } from '../../openlibrary/js/isbnOverride' * Used in addbook process. */ export function init() { - // used in books/edit/exercpt, books/edit/web and books/edit/edition - $.fn.repeat = function(options) { - var addSelector, removeSelector, id, elems, t, code, - nextRowId; - options = options || {}; + // used in books/edit/exercpt, books/edit/web and books/edit/edition + $.fn.repeat = function (options) { + var addSelector, removeSelector, id, elems, t, code, nextRowId; + options = options || {}; - id = `#${this.attr('id')}`; - elems = { - _this: this, - add: $(`${id}-add`), - form: $(`${id}-form`), - display: $(`${id}-display`), - template: $(`${id}-template`) - } + id = `#${this.attr('id')}`; + elems = { + _this: this, + add: $(`${id}-add`), + form: $(`${id}-form`), + display: $(`${id}-display`), + template: $(`${id}-template`), + }; - function createTemplate(selector) { - code = $(selector).html() - .replace(/%7B%7B/gi, '<%=') - .replace(/%7D%7D/gi, '%>') - .replace(/{{/g, '<%=') - .replace(/}}/g, '%>'); - return Template(code); - } + function createTemplate(selector) { + code = $(selector) + .html() + .replace(/%7B%7B/gi, '<%=') + .replace(/%7D%7D/gi, '%>') + .replace(/{{/g, '<%=') + .replace(/}}/g, '%>'); + return Template(code); + } - t = createTemplate(`${id}-template`); + t = createTemplate(`${id}-template`); - /** - * Search elems.form for input fields and create an - * object representing. - * @return {object} data mapping names to values - */ - function formdata() { - var data = {}; - $(':input', elems.form).each(function() { - var $e = $(this), - name = $e.attr('name'), - type = $e.attr('type'), - _id = $e.attr('id'); + /** + * Search elems.form for input fields and create an + * object representing. + * @return {object} data mapping names to values + */ + function formdata() { + var data = {}; + $(':input', elems.form).each(function () { + var $e = $(this), + name = $e.attr('name'), + type = $e.attr('type'), + _id = $e.attr('id'); - data[name] = $e.val().trim(); + data[name] = $e.val().trim(); - if (type === 'text' && _id === 'id-value') { - $e.val(''); - } - }); - return data; + if (type === 'text' && _id === 'id-value') { + $e.val(''); } + }); + return data; + } - /** - * triggered when "add link" button is clicked on author edit field. - * Creates a removable `repeat-item`. - * @param {jQuery.Event} event - */ - function onAdd(event) { - var data, newid; - const isbnOverrideData = isbnOverride.get(); - event.preventDefault(); + /** + * triggered when "add link" button is clicked on author edit field. + * Creates a removable `repeat-item`. + * @param {jQuery.Event} event + */ + function onAdd(event) { + var data, newid; + const isbnOverrideData = isbnOverride.get(); + event.preventDefault(); - // if no index, set it to the number of children - if (!nextRowId) { - nextRowId = elems.display.children().length; - } + // if no index, set it to the number of children + if (!nextRowId) { + nextRowId = elems.display.children().length; + } - // If a user confirms adding an ISBN with a failed checksum in - // js/edit.js, the {data} object is filled from the - // isbnOverrideData object rather than the input form. - if (isbnOverrideData) { - data = isbnOverrideData; - isbnOverride.clear(); - } else { - data = formdata(); - data.index = nextRowId; + // If a user confirms adding an ISBN with a failed checksum in + // js/edit.js, the {data} object is filled from the + // isbnOverrideData object rather than the input form. + if (isbnOverrideData) { + data = isbnOverrideData; + isbnOverride.clear(); + } else { + data = formdata(); + data.index = nextRowId; - if (options.validate && options.validate(data) === false) { - return; - } - } + if (options.validate && options.validate(data) === false) { + return; + } + } - $.extend(data, options.vars || {}); + $.extend(data, options.vars || {}); - newid = `${elems._this.attr('id')}--${nextRowId}`; - // increment the index to avoid situations where more than one element have same - nextRowId++; - // Create the HTML of a hidden input - elems.template - .clone() - .attr('id', newid) - .html(t(data)) - .show() - .appendTo(elems.display); + newid = `${elems._this.attr('id')}--${nextRowId}`; + // increment the index to avoid situations where more than one element have same + nextRowId++; + // Create the HTML of a hidden input + elems.template + .clone() + .attr('id', newid) + .html(t(data)) + .show() + .appendTo(elems.display); - elems._this.trigger('repeat-add'); - } - function onRemove(event) { - event.preventDefault(); - $(this).parents('.repeat-item').eq(0).remove(); - elems._this.trigger('repeat-remove'); - } - addSelector = `${id} .repeat-add`; - removeSelector = `${id} .repeat-remove`; - // Click handlers should apply to newly created add/remove selectors - $(document).on('click', addSelector, onAdd); - $(document).on('click', removeSelector, onRemove); + elems._this.trigger('repeat-add'); + } + function onRemove(event) { + event.preventDefault(); + $(this).parents('.repeat-item').eq(0).remove(); + elems._this.trigger('repeat-remove'); } + addSelector = `${id} .repeat-add`; + removeSelector = `${id} .repeat-remove`; + // Click handlers should apply to newly created add/remove selectors + $(document).on('click', addSelector, onAdd); + $(document).on('click', removeSelector, onRemove); + }; } diff --git a/openlibrary/plugins/openlibrary/js/jsdef.js b/openlibrary/plugins/openlibrary/js/jsdef.js index df0f7fda1bd..65ca9d44711 100644 --- a/openlibrary/plugins/openlibrary/js/jsdef.js +++ b/openlibrary/plugins/openlibrary/js/jsdef.js @@ -4,10 +4,10 @@ * For more details, see: * http://github.com/anandology/notebook/tree/master/2010/03/jsdef/ */ -import { ungettext, ugettext, sprintf } from './i18n'; +import { sprintf, ugettext, ungettext } from './i18n'; // TODO: Can likely move some of these methods into this file -import { commify, urlencode, slice } from './python'; -import { truncate, cond } from './utils'; +import { commify, slice, urlencode } from './python'; +import { cond, truncate } from './utils'; /** * Python range function. @@ -22,18 +22,18 @@ import { truncate, cond } from './utils'; //used in templates/lib/pagination.html export function range(begin, end, step) { - var r, i; - step = step || 1; - if (end === undefined) { - end = begin; - begin = 0; - } - - r = []; - for (i=begin; i<end; i += step) { - r[r.length] = i; - } - return r; + var r, i; + step = step || 1; + if (end === undefined) { + end = begin; + begin = 0; + } + + r = []; + for (i = begin; i < end; i += step) { + r[r.length] = i; + } + return r; } /** @@ -43,7 +43,7 @@ export function range(begin, end, step) { * a - b - c */ export function join(items) { - return items.join(this); + return items.join(this); } /** @@ -52,82 +52,79 @@ export function join(items) { // used in templates/admin/loans.html export function len(array) { - return array.length; + return array.length; } // used in templates/type/permission/edit.html export function enumerate(a) { - var b = new Array(a.length); - var i; - for (i in a) { - b[i] = [i, a[i]]; - } - return b; + var b = new Array(a.length); + var i; + for (i in a) { + b[i] = [i, a[i]]; + } + return b; } export function ForLoop(parent, seq) { - this.parent = parent; - this.seq = seq; + this.parent = parent; + this.seq = seq; - this.length = seq.length; - this.index0 = -1; + this.length = seq.length; + this.index0 = -1; } -ForLoop.prototype.next = function() { - var i = this.index0+1; +ForLoop.prototype.next = function () { + var i = this.index0 + 1; - this.index0 = i; - this.index = i+1; + this.index0 = i; + this.index = i + 1; - this.first = (i === 0); - this.last = (i === this.length-1); + this.first = i === 0; + this.last = i === this.length - 1; - this.odd = (this.index % 2 === 1); - this.even = (this.index % 2 === 0); - this.parity = ['even', 'odd'][this.index % 2]; + this.odd = this.index % 2 === 1; + this.even = this.index % 2 === 0; + this.parity = ['even', 'odd'][this.index % 2]; - this.revindex0 = this.length - i; - this.revindex = this.length - i + 1; -} + this.revindex0 = this.length - i; + this.revindex = this.length - i + 1; +}; // used in plugins/upstream/jsdef.py export function foreach(seq, parent_loop, callback) { - var loop = new ForLoop(parent_loop, seq); - var i, args, j; - - for (i=0; i<seq.length; i++) { - loop.next(); - - args = [loop]; - - // case of "for a, b in ..." - if (callback.length > 2) { - for (j in seq[i]) { - args.push(seq[i][j]); - } - } - else { - args[1] = seq[i]; - } - callback.apply(this, args); + var loop = new ForLoop(parent_loop, seq); + var i, args, j; + + for (i = 0; i < seq.length; i++) { + loop.next(); + + args = [loop]; + + // case of "for a, b in ..." + if (callback.length > 2) { + for (j in seq[i]) { + args.push(seq[i][j]); + } + } else { + args[1] = seq[i]; } + callback.apply(this, args); + } } // used in templates/lists/widget.html export function websafe(value) { - // Safari 6 is failing with weird javascript error in this function. - // Added try-catch to avoid it. - try { - if (value === null || value === undefined) { - return ''; - } - else { - return htmlquote(value.toString()); - } - } - catch (e) { - return ''; + // Safari 6 is failing with weird javascript error in this function. + // Added try-catch to avoid it. + try { + if (value === null || value === undefined) { + return ''; + } else { + return htmlquote(value.toString()); } + } catch (e) { + return ''; + } } /** @@ -136,21 +133,20 @@ export function websafe(value) { * @param {string|number} text to quote */ export function htmlquote(text) { - // This code exists for compatibility with template.js - text = String(text); - text = text.replace(/&/g, '&'); // Must be done first! - text = text.replace(/</g, '<'); - text = text.replace(/>/g, '>'); - text = text.replace(/'/g, '''); - text = text.replace(/"/g, '"'); - return text; + // This code exists for compatibility with template.js + text = String(text); + text = text.replace(/&/g, '&'); // Must be done first! + text = text.replace(/</g, '<'); + text = text.replace(/>/g, '>'); + text = text.replace(/'/g, '''); + text = text.replace(/"/g, '"'); + return text; } export function is_jsdef() { - return true; + return true; } - /** * foo.get(KEY, default) isn't defined in js, so we can't use that construct * in our jsdef methods. This helper function provides a workaround, and works @@ -160,28 +156,28 @@ export function is_jsdef() { * @param {string} key - the key to get from the object * @param {any} def - the default value to return if the key isn't found */ -export function jsdef_get(obj, key, def=null) { - return (key in obj) ? obj[key] : def; +export function jsdef_get(obj, key, def = null) { + return key in obj ? obj[key] : def; } export function exposeGlobally() { - // Extend existing prototypes - String.prototype.join = join; - - window.commify = commify; - window.cond = cond; - window.enumerate = enumerate; - window.foreach = foreach; - window.htmlquote = htmlquote; - window.jsdef_get = jsdef_get; - window.len = len; - window.range = range; - window.slice = slice; - window.sprintf = sprintf; - window.truncate = truncate; - window.urlencode = urlencode; - window.websafe = websafe; - window._ = ugettext; - window.ungettext = ungettext; - window.uggettext = ugettext; + // Extend existing prototypes + String.prototype.join = join; + + window.commify = commify; + window.cond = cond; + window.enumerate = enumerate; + window.foreach = foreach; + window.htmlquote = htmlquote; + window.jsdef_get = jsdef_get; + window.len = len; + window.range = range; + window.slice = slice; + window.sprintf = sprintf; + window.truncate = truncate; + window.urlencode = urlencode; + window.websafe = websafe; + window._ = ugettext; + window.ungettext = ungettext; + window.uggettext = ugettext; } diff --git a/openlibrary/plugins/openlibrary/js/lazy-carousel.js b/openlibrary/plugins/openlibrary/js/lazy-carousel.js index 603b10715e8..66b9ad9d7cf 100644 --- a/openlibrary/plugins/openlibrary/js/lazy-carousel.js +++ b/openlibrary/plugins/openlibrary/js/lazy-carousel.js @@ -1,4 +1,4 @@ -import {initialzeCarousels} from './carousel'; +import { initialzeCarousels } from './carousel'; import { buildPartialsUrl } from './utils'; /** @@ -8,24 +8,24 @@ import { buildPartialsUrl } from './utils'; * @param elems {NodeList<HTMLElement>} Collection of placeholder carousel elements */ export function initLazyCarousel(elems) { - // Create intersection observer - const intersectionObserver = new IntersectionObserver(intersectionCallback, { - root: null, - rootMargin: '200px', - threshold: 0 - }) + // Create intersection observer + const intersectionObserver = new IntersectionObserver(intersectionCallback, { + root: null, + rootMargin: '200px', + threshold: 0, + }); - elems.forEach(elem => { - // Observe element for intersections - intersectionObserver.observe(elem) + elems.forEach((elem) => { + // Observe element for intersections + intersectionObserver.observe(elem); - // Add retry listener - const retryElem = elem.querySelector('.retry-btn') - retryElem.addEventListener('click', (e) => { - e.preventDefault() - handleRetry(elem); - }) - }) + // Add retry listener + const retryElem = elem.querySelector('.retry-btn'); + retryElem.addEventListener('click', (e) => { + e.preventDefault(); + handleRetry(elem); + }); + }); } /** @@ -35,7 +35,7 @@ export function initLazyCarousel(elems) { * @returns {Promise<Response>} */ async function fetchPartials(data) { - return fetch(buildPartialsUrl('LazyCarousel', {...data})) + return fetch(buildPartialsUrl('LazyCarousel', { ...data })); } /** @@ -50,43 +50,47 @@ async function fetchPartials(data) { * @param target {HTMLElement} A placeholder element for a carousel */ function doFetchAndUpdate(target) { - const config = JSON.parse(target.dataset.config) - const loadingIndicator = target.querySelector('.loadingIndicator') + const config = JSON.parse(target.dataset.config); + const loadingIndicator = target.querySelector('.loadingIndicator'); - fetchPartials(config) - .then(resp => { - if (!resp.ok) { - throw new Error('Failed to fetch partials from server') - } - return resp.json() - }) - .then(data => { - const newElem = document.createElement('div') - newElem.innerHTML = data.partials.trim() - const carouselElements = newElem.querySelectorAll('.carousel--progressively-enhanced') - loadingIndicator.classList.add('hidden') + fetchPartials(config) + .then((resp) => { + if (!resp.ok) { + throw new Error('Failed to fetch partials from server'); + } + return resp.json(); + }) + .then((data) => { + const newElem = document.createElement('div'); + newElem.innerHTML = data.partials.trim(); + const carouselElements = newElem.querySelectorAll( + '.carousel--progressively-enhanced', + ); + loadingIndicator.classList.add('hidden'); - if (carouselElements.length === 0 && config.fallback) { - // No results, disable filters - if (typeof config.fallback === 'string') { - config.query = config.fallback; - } - config.has_fulltext_only = false; - config.fallback = false; // Prevents infinite retries - target.dataset.config = JSON.stringify(config); + if (carouselElements.length === 0 && config.fallback) { + // No results, disable filters + if (typeof config.fallback === 'string') { + config.query = config.fallback; + } + config.has_fulltext_only = false; + config.fallback = false; // Prevents infinite retries + target.dataset.config = JSON.stringify(config); - target.querySelector('.lazy-carousel-fallback').classList.remove('hidden'); - } else { - target.parentNode.insertBefore(newElem, target) - target.remove() - initialzeCarousels(carouselElements) - } - }) - .catch(() => { - loadingIndicator.classList.add('hidden'); - const retryElem = target.querySelector('.lazy-carousel-retry') - retryElem.classList.remove('hidden') - }) + target + .querySelector('.lazy-carousel-fallback') + .classList.remove('hidden'); + } else { + target.parentNode.insertBefore(newElem, target); + target.remove(); + initialzeCarousels(carouselElements); + } + }) + .catch(() => { + loadingIndicator.classList.add('hidden'); + const retryElem = target.querySelector('.lazy-carousel-retry'); + retryElem.classList.remove('hidden'); + }); } /** @@ -96,13 +100,13 @@ function doFetchAndUpdate(target) { * @param target {Element} */ function handleRetry(target) { - target.querySelector('.loadingIndicator').classList.remove('hidden') - target.querySelector('.lazy-carousel-retry').classList.add('hidden') - const carouselFallbackElem = target.querySelector('.lazy-carousel-fallback') - if (carouselFallbackElem) { - carouselFallbackElem.classList.add('hidden') - } - doFetchAndUpdate(target) + target.querySelector('.loadingIndicator').classList.remove('hidden'); + target.querySelector('.lazy-carousel-retry').classList.add('hidden'); + const carouselFallbackElem = target.querySelector('.lazy-carousel-fallback'); + if (carouselFallbackElem) { + carouselFallbackElem.classList.add('hidden'); + } + doFetchAndUpdate(target); } /** @@ -114,11 +118,11 @@ function handleRetry(target) { * @param observer {IntersectionObserver} */ function intersectionCallback(entries, observer) { - entries.forEach(entry => { - if (entry.isIntersecting) { - const target = entry.target - observer.unobserve(target) - doFetchAndUpdate(target) - } - }) + entries.forEach((entry) => { + if (entry.isIntersecting) { + const target = entry.target; + observer.unobserve(target); + doFetchAndUpdate(target); + } + }); } diff --git a/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js b/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js index aca22bf8a28..eebde3583c5 100644 --- a/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js +++ b/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js @@ -1,6 +1,7 @@ // @ts-check -import debounce from 'lodash/debounce'; + import chunk from 'lodash/chunk'; +import debounce from 'lodash/debounce'; /** * Responds to HTML like: @@ -18,121 +19,132 @@ import chunk from 'lodash/chunk'; * Currently only works with works, editions, and authors. */ export class LazyThingPreview { - constructor() { - /** @type {Array<{key: string, render_fn: Function}>} */ - this.queue = []; - /** @type {Object<string, object>} */ - this.cache = {}; + constructor() { + /** @type {Array<{key: string, render_fn: Function}>} */ + this.queue = []; + /** @type {Object<string, object>} */ + this.cache = {}; - this.renderDebounced = debounce(this.render.bind(this), 100); - } + this.renderDebounced = debounce(this.render.bind(this), 100); + } - init() { - $('.lazy-thing-preview').each((i, el) => { - this.push({ - key: el.dataset.key, - render_fn_name: el.dataset.renderFn, - }); - }); - } + init() { + $('.lazy-thing-preview').each((i, el) => { + this.push({ + key: el.dataset.key, + render_fn_name: el.dataset.renderFn, + }); + }); + } - /** - * @param {{key: string, render_fn_name: string}} arg0 - */ - push({key, render_fn_name}) { - const render_fn = window[render_fn_name]; - if (this.cache[key]) { - this.renderKey(key, render_fn, this.cache[key]); - } else { - this.queue.push({key, render_fn}); - this.renderDebounced(); - } + /** + * @param {{key: string, render_fn_name: string}} arg0 + */ + push({ key, render_fn_name }) { + const render_fn = window[render_fn_name]; + if (this.cache[key]) { + this.renderKey(key, render_fn, this.cache[key]); + } else { + this.queue.push({ key, render_fn }); + this.renderDebounced(); } + } - /** - * @param {string} key - * @param {Function} render_fn - * @param {object} book - */ - renderKey(key, render_fn, book) { - const $el = $(`.lazy-thing-preview[data-key="${key}"]`); - $el.html(render_fn(book)); - } + /** + * @param {string} key + * @param {Function} render_fn + * @param {object} book + */ + renderKey(key, render_fn, book) { + const $el = $(`.lazy-thing-preview[data-key="${key}"]`); + $el.html(render_fn(book)); + } - /** - * @param {string[]} keys - * @returns {AsyncGenerator<object[]>} - */ - async* getThings(keys) { - const workKeys = keys.filter(key => key.startsWith('/works/')); - const editionKeys = keys.filter(key => key.startsWith('/books/')); - const authorKeys = keys.filter(key => key.startsWith('/authors/')); - const fields = 'key,type,cover_i,first_publish_year,author_name,title,subtitle,edition_count,editions'; - for (const keys of chunk(workKeys, 100)) { - const resp = await fetch(`/search.json?${new URLSearchParams({ - q: `key:(${keys.join(' OR ')})`, - fields, - limit: '100', - })}`).then(r => r.json()); - yield resp.docs; - } - for (const keys of chunk(editionKeys, 100)) { - const resp = await fetch(`/search.json?${new URLSearchParams({ - q: `edition_key:(${keys - .map(key => key.split('/').pop()) - .join(' OR ')})`, - fields, - limit: '100', - })}`).then(r => r.json()); - yield resp.docs; - } - for (const keys of chunk(authorKeys, 100)) { - const resp = await fetch(`/search/authors.json?${new URLSearchParams({ - q: `key:(${keys.join(' OR ')})`, - fields: 'key,type,name,top_work,top_subjects,birth_date,death_date', - limit: '100', - })}`).then(r => r.json()); - for (const doc of resp.docs) { - // This API returns keys without the /authors/ prefix 😭 - doc.key = `/authors/${doc.key}`; - } - yield resp.docs; - } + /** + * @param {string[]} keys + * @returns {AsyncGenerator<object[]>} + */ + async *getThings(keys) { + const workKeys = keys.filter((key) => key.startsWith('/works/')); + const editionKeys = keys.filter((key) => key.startsWith('/books/')); + const authorKeys = keys.filter((key) => key.startsWith('/authors/')); + const fields = + 'key,type,cover_i,first_publish_year,author_name,title,subtitle,edition_count,editions'; + for (const keys of chunk(workKeys, 100)) { + const resp = await fetch( + `/search.json?${new URLSearchParams({ + q: `key:(${keys.join(' OR ')})`, + fields, + limit: '100', + })}`, + ).then((r) => r.json()); + yield resp.docs; } + for (const keys of chunk(editionKeys, 100)) { + const resp = await fetch( + `/search.json?${new URLSearchParams({ + q: `edition_key:(${keys + .map((key) => key.split('/').pop()) + .join(' OR ')})`, + fields, + limit: '100', + })}`, + ).then((r) => r.json()); + yield resp.docs; + } + for (const keys of chunk(authorKeys, 100)) { + const resp = await fetch( + `/search/authors.json?${new URLSearchParams({ + q: `key:(${keys.join(' OR ')})`, + fields: 'key,type,name,top_work,top_subjects,birth_date,death_date', + limit: '100', + })}`, + ).then((r) => r.json()); + for (const doc of resp.docs) { + // This API returns keys without the /authors/ prefix 😭 + doc.key = `/authors/${doc.key}`; + } + yield resp.docs; + } + } - async render() { - const keys = this.queue.map(({key}) => key); - const render_fn_map = Object.fromEntries( - this.queue.map(({key, render_fn}) => [key, render_fn]) - ); - for await (const thingBatch of this.getThings(keys)) { - for (const thing of thingBatch) { - this.cache[thing.key] = thing; - if (thing.type === 'work') { - const book = thing; - book.full_title = book.subtitle ? `${book.title}: ${book.subtitle}` : book.title; - if (book.editions.docs.length) { - const ed = book.editions.docs[0]; - ed.full_title = ed.subtitle ? `${ed.title}: ${ed.subtitle}` : ed.title; - ed.author_name = book.author_name; - ed.edition_count = book.edition_count; - this.cache[ed.key] = ed; - - if (ed.key in render_fn_map) { - this.renderKey(ed.key, render_fn_map[ed.key], ed); - } - } - } + async render() { + const keys = this.queue.map(({ key }) => key); + const render_fn_map = Object.fromEntries( + this.queue.map(({ key, render_fn }) => [key, render_fn]), + ); + for await (const thingBatch of this.getThings(keys)) { + for (const thing of thingBatch) { + this.cache[thing.key] = thing; + if (thing.type === 'work') { + const book = thing; + book.full_title = book.subtitle + ? `${book.title}: ${book.subtitle}` + : book.title; + if (book.editions.docs.length) { + const ed = book.editions.docs[0]; + ed.full_title = ed.subtitle + ? `${ed.title}: ${ed.subtitle}` + : ed.title; + ed.author_name = book.author_name; + ed.edition_count = book.edition_count; + this.cache[ed.key] = ed; - if (thing.key in render_fn_map) { - this.renderKey(thing.key, render_fn_map[thing.key], thing); - } + if (ed.key in render_fn_map) { + this.renderKey(ed.key, render_fn_map[ed.key], ed); } + } } - const missingKeys = keys.filter(key => !this.cache[key]); - // eslint-disable-next-line no-console - console.warn('Books missing from cache', missingKeys); - this.queue = []; + if (thing.key in render_fn_map) { + this.renderKey(thing.key, render_fn_map[thing.key], thing); + } + } } + + const missingKeys = keys.filter((key) => !this.cache[key]); + // eslint-disable-next-line no-console + console.warn('Books missing from cache', missingKeys); + this.queue = []; + } } diff --git a/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js b/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js index 73bb92ba585..9d7a967720a 100644 --- a/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js +++ b/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js @@ -9,11 +9,15 @@ let i18nStrings; * @param {HTMLDetailsElement} rootElement */ export function initLibrarianDashboard(rootElement) { - i18nStrings = JSON.parse(rootElement.dataset.i18n) - const table = rootElement.querySelector('.dq-table') - rootElement.addEventListener('click', () => { - populateTable(table) - }, {once: true}) + i18nStrings = JSON.parse(rootElement.dataset.i18n); + const table = rootElement.querySelector('.dq-table'); + rootElement.addEventListener( + 'click', + () => { + populateTable(table); + }, + { once: true }, + ); } /** @@ -23,10 +27,10 @@ export function initLibrarianDashboard(rootElement) { * @returns {Promise<void>} */ async function populateTable(table) { - const bookCount = Number(table.dataset.totalBooks) - const rows = table.querySelectorAll('.dq-table__row') + const bookCount = Number(table.dataset.totalBooks); + const rows = table.querySelectorAll('.dq-table__row'); - await Promise.all([...rows].map(row => updateRow(row, bookCount))) + await Promise.all([...rows].map((row) => updateRow(row, bookCount))); } /** @@ -37,44 +41,44 @@ async function populateTable(table) { * @returns {Promise<void>} */ async function updateRow(row, totalCount) { - const queryFragment = row.dataset.queryFragment - const apiUrl = buildUrl(queryFragment, false) - const searchPageUrl = buildUrl(queryFragment) - - // Make query - const data = await fetch(apiUrl) - .then((resp) => { - if (!resp.ok) { - throw new Error(`Data quality response status : ${resp.status}`) - } - return resp.json() - }) - .catch(() => { - return null; - }) - - // Render status cell markup - let newCellMarkup - if (data === null) { - newCellMarkup = renderErrorCell(searchPageUrl) - } else { - newCellMarkup = renderResultsCells(data, totalCount, searchPageUrl) - } - - // Include retry affordance, regardless of result - newCellMarkup += renderRetryCell() - - replaceStatusCells(row, newCellMarkup) - - // Add listener to retry affordance - const retryAffordance = row.querySelector('.dqs-run-again') - retryAffordance.addEventListener('click', () => { - // Update view to "pending" - replaceStatusCells(row, renderPendingCell()) - - // Retry query - updateRow(row, totalCount) + const queryFragment = row.dataset.queryFragment; + const apiUrl = buildUrl(queryFragment, false); + const searchPageUrl = buildUrl(queryFragment); + + // Make query + const data = await fetch(apiUrl) + .then((resp) => { + if (!resp.ok) { + throw new Error(`Data quality response status : ${resp.status}`); + } + return resp.json(); }) + .catch(() => { + return null; + }); + + // Render status cell markup + let newCellMarkup; + if (data === null) { + newCellMarkup = renderErrorCell(searchPageUrl); + } else { + newCellMarkup = renderResultsCells(data, totalCount, searchPageUrl); + } + + // Include retry affordance, regardless of result + newCellMarkup += renderRetryCell(); + + replaceStatusCells(row, newCellMarkup); + + // Add listener to retry affordance + const retryAffordance = row.querySelector('.dqs-run-again'); + retryAffordance.addEventListener('click', () => { + // Update view to "pending" + replaceStatusCells(row, renderPendingCell()); + + // Retry query + updateRow(row, totalCount); + }); } /** @@ -84,12 +88,14 @@ async function updateRow(row, totalCount) { * @param {boolean} forUi */ function buildUrl(queryFragment, forUi = true) { - const match = window.location.pathname.match(/authors\/(OL\d+A)/) - const queryParamString = match ? `?q=author_key:${match[1]}` : window.location.search - - const params = new URLSearchParams(queryParamString) - params.set('q', `${params.get('q')} ${queryFragment}`) - return `/search${forUi ? '' : '.json'}?${params.toString()}` + const match = window.location.pathname.match(/authors\/(OL\d+A)/); + const queryParamString = match + ? `?q=author_key:${match[1]}` + : window.location.search; + + const params = new URLSearchParams(queryParamString); + params.set('q', `${params.get('q')} ${queryFragment}`); + return `/search${forUi ? '' : '.json'}?${params.toString()}`; } /** @@ -99,14 +105,14 @@ function buildUrl(queryFragment, forUi = true) { * @param {string} newCellMarkup Markup for the new status cells */ function replaceStatusCells(row, newCellMarkup) { - const statusCells = row.querySelectorAll('td:not(.dq-table__criterion-cell)') - for (const cell of statusCells) { - cell.remove() - } - - const template = document.createElement('template') - template.innerHTML = newCellMarkup - row.append(...template.content.children) + const statusCells = row.querySelectorAll('td:not(.dq-table__criterion-cell)'); + for (const cell of statusCells) { + cell.remove(); + } + + const template = document.createElement('template'); + template.innerHTML = newCellMarkup; + row.append(...template.content.children); } /** @@ -119,16 +125,16 @@ function replaceStatusCells(row, newCellMarkup) { * @returns {string} HTML string */ function renderResultsCells(results, totalCount, failingHref) { - const numFound = results.numFound - const percentage = Math.floor(((totalCount - numFound) / totalCount) * 100) + const numFound = results.numFound; + const percentage = Math.floor(((totalCount - numFound) / totalCount) * 100); - return `<td class="dq-table__results-cell"> + return `<td class="dq-table__results-cell"> <meter title="${numFound} of ${totalCount}" min="0" max="100" value="${percentage}"></meter> <span>${percentage}%</span> </td> <td style="text-align:right"> <a href="${failingHref}">${numFound} ${i18nStrings['failing']}</a> - </td>` + </td>`; } /** @@ -137,11 +143,11 @@ function renderResultsCells(results, totalCount, failingHref) { * @returns {string} HTML string */ function renderRetryCell() { - return `<td> + return `<td> <button class="dqs-run-again"> ${i18nStrings['reload']} </button> - </td>` + </td>`; } /** @@ -151,9 +157,9 @@ function renderRetryCell() { * @returns {string} */ function renderErrorCell(href) { - return `<td colspan="2"> + return `<td colspan="2"> <a href="${href}">${i18nStrings['error']}</a> - </td>` + </td>`; } /** @@ -162,5 +168,5 @@ function renderErrorCell(href) { * @returns {string} */ function renderPendingCell() { - return `<td colspan="3">${i18nStrings['loading']}</td>` + return `<td colspan="3">${i18nStrings['loading']}</td>`; } diff --git a/openlibrary/plugins/openlibrary/js/list_books.js b/openlibrary/plugins/openlibrary/js/list_books.js index ab4c6b6ef4f..438b29f82f1 100644 --- a/openlibrary/plugins/openlibrary/js/list_books.js +++ b/openlibrary/plugins/openlibrary/js/list_books.js @@ -1,37 +1,37 @@ export class ListBooks { - /** - * @param {HTMLElement} listBooks - * @param {HTMLElement} layoutToolbar - **/ - constructor(listBooks, layoutToolbar) { - this.listBooks = listBooks; - this.layoutToolbar = layoutToolbar; + /** + * @param {HTMLElement} listBooks + * @param {HTMLElement} layoutToolbar + **/ + constructor(listBooks, layoutToolbar) { + this.listBooks = listBooks; + this.layoutToolbar = layoutToolbar; - this.activeLayout = this.layoutToolbar.querySelector('a.active'); - } + this.activeLayout = this.layoutToolbar.querySelector('a.active'); + } - attach() { - $(this.layoutToolbar).on('click', 'a', this.updateLayout.bind(this)); - } + attach() { + $(this.layoutToolbar).on('click', 'a', this.updateLayout.bind(this)); + } - /** - * @param {MouseEvent} event - */ - updateLayout(event) { - event.preventDefault(); - const layoutAnchor = event.target; - this.layoutToolbar.querySelector('a.active').classList.remove('active'); - layoutAnchor.classList.add('active'); - const layout = layoutAnchor.dataset.value; - this.listBooks.classList.toggle('list-books--grid', layout === 'grid'); - document.cookie = `LBL=${layout}; path=/; max-age=31536000`; - } + /** + * @param {MouseEvent} event + */ + updateLayout(event) { + event.preventDefault(); + const layoutAnchor = event.target; + this.layoutToolbar.querySelector('a.active').classList.remove('active'); + layoutAnchor.classList.add('active'); + const layout = layoutAnchor.dataset.value; + this.listBooks.classList.toggle('list-books--grid', layout === 'grid'); + document.cookie = `LBL=${layout}; path=/; max-age=31536000`; + } - static init() { - // Assume only one list-books/layout per page - new ListBooks( - document.querySelector('.list-books'), - document.querySelector('.tools--layout'), - ).attach(); - } + static init() { + // Assume only one list-books/layout per page + new ListBooks( + document.querySelector('.list-books'), + document.querySelector('.tools--layout'), + ).attach(); + } } diff --git a/openlibrary/plugins/openlibrary/js/lists/ListService.js b/openlibrary/plugins/openlibrary/js/lists/ListService.js index 17b4ab9f586..434cf4446ad 100644 --- a/openlibrary/plugins/openlibrary/js/lists/ListService.js +++ b/openlibrary/plugins/openlibrary/js/lists/ListService.js @@ -13,14 +13,14 @@ import { buildPartialsUrl } from '../utils'; * @returns {Promise<Response>} The results of the POST request */ export async function createList(userKey, data) { - return await fetch(`${userKey}/lists.json`, { - method: 'post', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: JSON.stringify(data), - }); + return await fetch(`${userKey}/lists.json`, { + method: 'post', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify(data), + }); } /** @@ -31,15 +31,15 @@ export async function createList(userKey, data) { * @returns {Promise<Response>} The result of the POST request */ export async function addItem(listKey, seed) { - const body = { add: [seed] }; - return await fetch(`${listKey}/seeds.json`, { - method: 'post', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: JSON.stringify(body), - }); + const body = { add: [seed] }; + return await fetch(`${listKey}/seeds.json`, { + method: 'post', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify(body), + }); } /** @@ -50,24 +50,23 @@ export async function addItem(listKey, seed) { * @returns {Promise<Response>} The POST response */ export async function removeItem(listKey, seed) { - const body = { remove: [seed] }; - return await fetch(`${listKey}/seeds.json`, { - method: 'post', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: JSON.stringify(body), - }); + const body = { remove: [seed] }; + return await fetch(`${listKey}/seeds.json`, { + method: 'post', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify(body), + }); } // XXX : jsdoc export async function getListPartials() { - return await fetch(buildPartialsUrl('MyBooksDropperLists'), { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - }); + return await fetch(buildPartialsUrl('MyBooksDropperLists'), { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }); } - diff --git a/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js b/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js index 44e77eb245a..398d3a2dbb2 100644 --- a/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js +++ b/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js @@ -9,13 +9,13 @@ import 'jquery-ui/ui/widgets/dialog'; */ const itemsWithDeleteList = $('.deleteList .resultTitle'); if (itemsWithDeleteList.length) { - const deleteListLink = $('.listDelete--myLists'); - itemsWithDeleteList.each(function() { - $(deleteListLink).clone().prependTo(this).removeClass('hidden'); - }); + const deleteListLink = $('.listDelete--myLists'); + itemsWithDeleteList.each(function () { + $(deleteListLink).clone().prependTo(this).removeClass('hidden'); + }); - // Clean up and remove placeholder element - $('.listDelete--myLists.hidden').remove(); + // Clean up and remove placeholder element + $('.listDelete--myLists.hidden').remove(); } /** @@ -23,13 +23,13 @@ if (itemsWithDeleteList.length) { */ const itemsWithDeleteSeed = $('.deleteSeed .resultTitle'); if (itemsWithDeleteSeed.length) { - const deleteSeedLink = $('.seedDelete--myLists'); - itemsWithDeleteSeed.each(function() { - $(deleteSeedLink).clone().prependTo(this).removeClass('hidden'); - }); + const deleteSeedLink = $('.seedDelete--myLists'); + itemsWithDeleteSeed.each(function () { + $(deleteSeedLink).clone().prependTo(this).removeClass('hidden'); + }); - // Clean up and remove placeholder element - $('.seedDelete--myLists.hidden').remove(); + // Clean up and remove placeholder element + $('.seedDelete--myLists.hidden').remove(); } /** @@ -39,32 +39,32 @@ if (itemsWithDeleteSeed.length) { * @param {function} success - click function */ function remove_seed(list_key, seed, success) { - if (seed[0] === '/') { - seed = {key: seed} - } - - $.ajax({ - type: 'POST', - url: `${list_key}/seeds.json`, - contentType: 'application/json', - data: JSON.stringify({ - remove: [seed] - }), - dataType: 'json', - - beforeSend: function(xhr) { - xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.setRequestHeader('Accept', 'application/json'); - }, - success: success - }); + if (seed[0] === '/') { + seed = { key: seed }; + } + + $.ajax({ + type: 'POST', + url: `${list_key}/seeds.json`, + contentType: 'application/json', + data: JSON.stringify({ + remove: [seed], + }), + dataType: 'json', + + beforeSend: (xhr) => { + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('Accept', 'application/json'); + }, + success: success, + }); } /** * @returns {number} count of number of seed books in a list */ function get_seed_count() { - return $('ul#listResults').children().length; + return $('ul#listResults').children().length; } /** @@ -72,7 +72,7 @@ function get_seed_count() { * @returns {string} i18n cancel text */ const getCancelButtonLabelText = () => { - return $('.listDelete a').data('cancel-text'); + return $('.listDelete a').data('cancel-text'); }; /** @@ -80,92 +80,91 @@ const getCancelButtonLabelText = () => { * @returns {string} i18n confirmation text */ const getConfirmButtonLabelText = () => { - return $('.listDelete a').data('confirm-text'); + return $('.listDelete a').data('confirm-text'); }; // Add listeners to each .listDelete link element // Sometimes .listDelete is dynamically added to the DOM, so we'll add the listener to a parent element -$('#listResults').on('click', '.listDelete a', function() { - if (get_seed_count() > 1 && !$(this).parent().hasClass('listDelete--myLists')) { - $('#remove-seed-dialog') - .data('seed-key', $(this).closest('[data-seed-key]').data('seed-key')) - .data('list-key', $(this).closest('[data-list-key]').data('list-key')) - .dialog('open'); - $('#remove-seed-dialog').removeClass('hidden'); - } - else { - $('#delete-list-dialog') - .data('list-key', $(this).closest('[data-list-key]').data('list-key')) - .dialog('open'); - $('#delete-list-dialog').removeClass('hidden'); - } +$('#listResults').on('click', '.listDelete a', function () { + if ( + get_seed_count() > 1 && + !$(this).parent().hasClass('listDelete--myLists') + ) { + $('#remove-seed-dialog') + .data('seed-key', $(this).closest('[data-seed-key]').data('seed-key')) + .data('list-key', $(this).closest('[data-list-key]').data('list-key')) + .dialog('open'); + $('#remove-seed-dialog').removeClass('hidden'); + } else { + $('#delete-list-dialog') + .data('list-key', $(this).closest('[data-list-key]').data('list-key')) + .dialog('open'); + $('#delete-list-dialog').removeClass('hidden'); + } }); // Set up 'Remove Seed' dialog; force user to confirm the destructive action of removing a seed $('#remove-seed-dialog').dialog({ - autoOpen: false, - width: 400, - modal: true, - resizable: false, - buttons: { - ConfirmRemoveSeed: { - text: getConfirmButtonLabelText(), - id: 'remove-seed-dialog--confirm', - click: function() { - var list_key = $(this).data('list-key'); - var seed_key = $(this).data('seed-key'); - - var _this = this; - - remove_seed(list_key, seed_key, function() { - $(`[data-seed-key='${seed_key}']`).remove(); - // update seed count - $('#list-items-count').load(`${location.href} #list-items-count`); - - // TODO: update edition count - - $(_this).dialog('close'); - $('#remove-seed-dialog').addClass('hidden'); - }); - } - }, - CancelRemoveSeed: { - text: getCancelButtonLabelText(), - id: 'remove-seed-dialog--cancel', - click: function() { - $(this).dialog('close'); - $('#remove-seed-dialog').addClass('hidden'); - } - } - } + autoOpen: false, + width: 400, + modal: true, + resizable: false, + buttons: { + ConfirmRemoveSeed: { + text: getConfirmButtonLabelText(), + id: 'remove-seed-dialog--confirm', + click: function () { + var list_key = $(this).data('list-key'); + var seed_key = $(this).data('seed-key'); + + remove_seed(list_key, seed_key, () => { + $(`[data-seed-key='${seed_key}']`).remove(); + // update seed count + $('#list-items-count').load(`${location.href} #list-items-count`); + + // TODO: update edition count + + $(this).dialog('close'); + $('#remove-seed-dialog').addClass('hidden'); + }); + }, + }, + CancelRemoveSeed: { + text: getCancelButtonLabelText(), + id: 'remove-seed-dialog--cancel', + click: function () { + $(this).dialog('close'); + $('#remove-seed-dialog').addClass('hidden'); + }, + }, + }, }); // Set up 'Delete List' dialog; force user to confirm the destructive action of deleting a list $('#delete-list-dialog').dialog({ - autoOpen: false, - width: 400, - modal: true, - resizable: false, - buttons: { - ConfirmDeleteList: { - text: getConfirmButtonLabelText(), - id: 'delete-list-dialog--confirm', - click: function() { - var list_key = $(this).data('list-key'); - var _this = this; - - $.post(`${list_key}/delete.json`, function() { - $(_this).dialog('close'); - window.location.reload(); - }); - } - }, - CancelDeleteList: { - text: getCancelButtonLabelText(), - id: 'delete-list-dialog--cancel', - click: function() { - $(this).dialog('close'); - } - } - } + autoOpen: false, + width: 400, + modal: true, + resizable: false, + buttons: { + ConfirmDeleteList: { + text: getConfirmButtonLabelText(), + id: 'delete-list-dialog--confirm', + click: function () { + var list_key = $(this).data('list-key'); + + $.post(`${list_key}/delete.json`, () => { + $(this).dialog('close'); + window.location.reload(); + }); + }, + }, + CancelDeleteList: { + text: getCancelButtonLabelText(), + id: 'delete-list-dialog--cancel', + click: function () { + $(this).dialog('close'); + }, + }, + }, }); diff --git a/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js b/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js index f20a633ec3c..79ca3c587f8 100644 --- a/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js +++ b/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js @@ -1,8 +1,9 @@ /** * @module lists/ShowcaseItem.js */ -import { removeItem } from './ListService' -import myBooksStore from '../my-books/store' + +import myBooksStore from '../my-books/store'; +import { removeItem } from './ListService'; /** * Represents an actionable list showcase item. @@ -25,169 +26,175 @@ import myBooksStore from '../my-books/store' * @class */ export class ShowcaseItem { + /** + * Creates a new `ShowcaseItem` obect. + * + * Sets references needed for this ShowcaseItem's functionality. + * + * @param {HTMLElement} showcaseElem + */ + constructor(showcaseElem) { /** - * Creates a new `ShowcaseItem` obect. - * - * Sets references needed for this ShowcaseItem's functionality. - * - * @param {HTMLElement} showcaseElem + * Reference to the root element of this component. + * @member {HTMLElement} */ - constructor(showcaseElem) { - /** - * Reference to the root element of this component. - * @member {HTMLElement} - */ - this.showcaseElem = showcaseElem - - /** - * `true` if this object represents the active lists showcase. - * @member {boolean} - */ - this.isActiveShowcase = showcaseElem.parentElement.classList.contains('already-lists') - - /** - * Reference to the affordance which removes an item from this list. - * @member {HTMLElement} - */ - this.removeFromListAffordance = showcaseElem.querySelector('.remove-from-list') + this.showcaseElem = showcaseElem; - /** - * Unique identifier for the showcased list. - * @member {string} - */ - this.listKey = this.removeFromListAffordance.dataset.listKey + /** + * `true` if this object represents the active lists showcase. + * @member {boolean} + */ + this.isActiveShowcase = + showcaseElem.parentElement.classList.contains('already-lists'); - /** - * Unique identifier for the showcased list member. - * @member {string} - */ - this.seedKey = showcaseElem.querySelector('input[name=seed-key]').value + /** + * Reference to the affordance which removes an item from this list. + * @member {HTMLElement} + */ + this.removeFromListAffordance = + showcaseElem.querySelector('.remove-from-list'); - /** - * The list item's type. - * @member {'subject'|'edition'|'work'|'author'} - */ - this.type = showcaseElem.querySelector('input[name=seed-type]').value + /** + * Unique identifier for the showcased list. + * @member {string} + */ + this.listKey = this.removeFromListAffordance.dataset.listKey; - /** - * `true` if this list item is a subject. - * @member {boolean} - */ - this.isSubject = this.type === 'subject' + /** + * Unique identifier for the showcased list member. + * @member {string} + */ + this.seedKey = showcaseElem.querySelector('input[name=seed-key]').value; - /** - * `true` if this list item is a work - * @member {boolean} - */ - this.isWork = !this.isSubject && this.seedKey.slice(-1) === 'W' + /** + * The list item's type. + * @member {'subject'|'edition'|'work'|'author'} + */ + this.type = showcaseElem.querySelector('input[name=seed-type]').value; - /** - * `POST` request-ready representation of the list's seed key. - * @member {string|object} - */ - this.seed - if (this.isSubject) { - this.seed = this.seedKey - } else { - this.seed = { key: this.seedKey } - } - } + /** + * `true` if this list item is a subject. + * @member {boolean} + */ + this.isSubject = this.type === 'subject'; /** - * Attaches click listeners to the showcase item's "Remove from list" - * affordance. + * `true` if this list item is a work + * @member {boolean} */ - initialize() { - this.removeFromListAffordance.addEventListener('click', (event) => { - event.preventDefault() - this.removeShowcaseItem() - }) - } + this.isWork = !this.isSubject && this.seedKey.slice(-1) === 'W'; /** - * Sends request to remove an item from a list, then updates the view. - * - * Removes any affiliated showcase items from the DOM, and updates all - * dropper list affordances. + * `POST` request-ready representation of the list's seed key. + * @member {string|object} */ - async removeShowcaseItem() { - await removeItem(this.listKey, this.seed) - .then(response => response.json()) - .then(() => { - const showcases = myBooksStore.getShowcases() + this.seed; + if (this.isSubject) { + this.seed = this.seedKey; + } else { + this.seed = { key: this.seedKey }; + } + } - // Remove self: - this.removeSelf() + /** + * Attaches click listeners to the showcase item's "Remove from list" + * affordance. + */ + initialize() { + this.removeFromListAffordance.addEventListener('click', (event) => { + event.preventDefault(); + this.removeShowcaseItem(); + }); + } - // Remove other showcase items that are associated with the list and seed key: - for (const showcase of showcases) { - if (showcase.isShowcaseForListAndSeed(this.listKey, this.seedKey)) { - showcase.removeSelf() - } - } + /** + * Sends request to remove an item from a list, then updates the view. + * + * Removes any affiliated showcase items from the DOM, and updates all + * dropper list affordances. + */ + async removeShowcaseItem() { + await removeItem(this.listKey, this.seed) + .then((response) => response.json()) + .then(() => { + const showcases = myBooksStore.getShowcases(); - // Update droppers: - const droppers = myBooksStore.getDroppers() - for (const dropper of droppers) { - dropper.readingLists.updateViewAfterModifyingList(this.listKey, this.isWork, false) - } - }) - } + // Remove self: + this.removeSelf(); - /** - * Removes associated showcase item from the DOM. - * - * Removes self from the myBooksStore's showcase array - * upon success. - */ - removeSelf() { - const showcases = myBooksStore.getShowcases() - const thisIndex = showcases.indexOf(this) - if (thisIndex >= 0) { - this.showcaseElem.remove() - showcases.splice(thisIndex, 1) + // Remove other showcase items that are associated with the list and seed key: + for (const showcase of showcases) { + if (showcase.isShowcaseForListAndSeed(this.listKey, this.seedKey)) { + showcase.removeSelf(); + } } - } - /** - * Toggles the visiblity of active showcase items depending on their seed type. - * - * If `showWorks` is `true`, the only active showcase items that will be visible will - * be those with a work seed type. Otherwise, these active work showcase items are - * hidden and all others are displayed. - * - * This function has no effect on non-active showcase items. - * - * @param {boolean} showWorks `true` if only active showcase items related to works should be displayed - */ - toggleVisibility(showWorks) { - if (this.isActiveShowcase) { - if (showWorks) { - if (this.isWork) { - this.showcaseElem.classList.remove('hidden') - } else { - this.showcaseElem.classList.add('hidden') - } - } else { - if (this.isWork) { - this.showcaseElem.classList.add('hidden') - } else { - this.showcaseElem.classList.remove('hidden') - } - } + // Update droppers: + const droppers = myBooksStore.getDroppers(); + for (const dropper of droppers) { + dropper.readingLists.updateViewAfterModifyingList( + this.listKey, + this.isWork, + false, + ); } + }); + } + + /** + * Removes associated showcase item from the DOM. + * + * Removes self from the myBooksStore's showcase array + * upon success. + */ + removeSelf() { + const showcases = myBooksStore.getShowcases(); + const thisIndex = showcases.indexOf(this); + if (thisIndex >= 0) { + this.showcaseElem.remove(); + showcases.splice(thisIndex, 1); } + } - /** - * Determines if this showcase item is linked to the given keys. - * - * @param {string} listKey - * @param {string} seedKey - * @return {boolean} `true` if the given keys match this item's keys - */ - isShowcaseForListAndSeed(listKey, seedKey) { - return (this.listKey === listKey) && (this.seedKey === seedKey) + /** + * Toggles the visiblity of active showcase items depending on their seed type. + * + * If `showWorks` is `true`, the only active showcase items that will be visible will + * be those with a work seed type. Otherwise, these active work showcase items are + * hidden and all others are displayed. + * + * This function has no effect on non-active showcase items. + * + * @param {boolean} showWorks `true` if only active showcase items related to works should be displayed + */ + toggleVisibility(showWorks) { + if (this.isActiveShowcase) { + if (showWorks) { + if (this.isWork) { + this.showcaseElem.classList.remove('hidden'); + } else { + this.showcaseElem.classList.add('hidden'); + } + } else { + if (this.isWork) { + this.showcaseElem.classList.add('hidden'); + } else { + this.showcaseElem.classList.remove('hidden'); + } + } } + } + + /** + * Determines if this showcase item is linked to the given keys. + * + * @param {string} listKey + * @param {string} seedKey + * @return {boolean} `true` if the given keys match this item's keys + */ + isShowcaseForListAndSeed(listKey, seedKey) { + return this.listKey === listKey && this.seedKey === seedKey; + } } /** @@ -195,9 +202,9 @@ export class ShowcaseItem { * showcase items. * @type {Record<string, string>} */ -let i18nStrings +let i18nStrings; -const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png' +const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; /** * Returns the inferred type of the given seed key. @@ -206,19 +213,19 @@ const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png' * @returns {string} Type of the given seed key. */ function getSeedType(seed) { - // XXX : validate input? - if (seed[0] !== '/') { - return 'subject' - } - if (seed.endsWith('M')) { - return 'edition' - } - if (seed.endsWith('W')) { - return 'work' - } - if (seed.endsWith('A')) { - return 'author' - } + // XXX : validate input? + if (seed[0] !== '/') { + return 'subject'; + } + if (seed.endsWith('M')) { + return 'edition'; + } + if (seed.endsWith('W')) { + return 'work'; + } + if (seed.endsWith('A')) { + return 'author'; + } } // XXX : remove this? @@ -233,17 +240,22 @@ function getSeedType(seed) { * @param {string} [coverUrl] * @returns {HTMLLIElement} */ -export function createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl = DEFAULT_COVER_URL) { - if (!i18nStrings) { - const i18nInput = document.querySelector('input[name=list-i18n-strings]') - i18nStrings = JSON.parse(i18nInput.value) - } +export function createActiveShowcaseItem( + listKey, + seedKey, + listTitle, + coverUrl = DEFAULT_COVER_URL, +) { + if (!i18nStrings) { + const i18nInput = document.querySelector('input[name=list-i18n-strings]'); + i18nStrings = JSON.parse(i18nInput.value); + } - const splitKey = listKey.split('/') - const userKey = `/${splitKey[1]}/${splitKey[2]}` - const seedType = getSeedType(seedKey) + const splitKey = listKey.split('/'); + const userKey = `/${splitKey[1]}/${splitKey[2]}`; + const seedType = getSeedType(seedKey); - const itemMarkUp = `<span class="image"> + const itemMarkUp = `<span class="image"> <a href="${listKey}"><img src="${coverUrl}" alt="${i18nStrings['cover_of']}${listTitle}" title="${i18nStrings['cover_of']}${listTitle}"/></a> </span> <span class="data"> @@ -255,14 +267,14 @@ export function createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl = <a href="${listKey}" class="remove-from-list red smaller arial plain" data-list-key="${listKey}" title="${i18nStrings['remove_from_list']}">[X]</a> </span> <span class="owner">${i18nStrings['from']} <a href="${userKey}">${i18nStrings['you']}</a></span> - </span>` + </span>`; - const li = document.createElement('li') - li.classList.add('actionable-item') - li.dataset.listKey = listKey - li.innerHTML = itemMarkUp + const li = document.createElement('li'); + li.classList.add('actionable-item'); + li.dataset.listKey = listKey; + li.innerHTML = itemMarkUp; - return li + return li; } /** @@ -276,9 +288,9 @@ export function createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl = * @param {boolean} showWorksOnly */ export function toggleActiveShowcaseItems(showWorksOnly) { - for (const item of myBooksStore.getShowcases()) { - item.toggleVisibility(showWorksOnly) - } + for (const item of myBooksStore.getShowcases()) { + item.toggleVisibility(showWorksOnly); + } } /** @@ -296,16 +308,21 @@ export function toggleActiveShowcaseItems(showWorksOnly) { * @param {string} listTitle * @param {string} [coverUrl] */ -export function attachNewActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl = DEFAULT_COVER_URL) { - const activeListsShowcase = document.querySelector('.already-lists') +export function attachNewActiveShowcaseItem( + listKey, + seedKey, + listTitle, + coverUrl = DEFAULT_COVER_URL, +) { + const activeListsShowcase = document.querySelector('.already-lists'); - if (activeListsShowcase) { - const li = createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl) - activeListsShowcase.appendChild(li) + if (activeListsShowcase) { + const li = createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl); + activeListsShowcase.appendChild(li); - const showcase = new ShowcaseItem(li) - showcase.initialize() + const showcase = new ShowcaseItem(li); + showcase.initialize(); - myBooksStore.getShowcases().push(showcase) - } + myBooksStore.getShowcases().push(showcase); + } } diff --git a/openlibrary/plugins/openlibrary/js/markdown-editor/index.js b/openlibrary/plugins/openlibrary/js/markdown-editor/index.js index 67af03eb33f..75aac27a135 100644 --- a/openlibrary/plugins/openlibrary/js/markdown-editor/index.js +++ b/openlibrary/plugins/openlibrary/js/markdown-editor/index.js @@ -1,20 +1,22 @@ // unversioned. -import '../../../../../vendor/js/wmd/jquery.wmd.js' +import '../../../../../vendor/js/wmd/jquery.wmd.js'; /** * Sets up Wikitext markdown editor interface inside $textarea * @param {jQuery.Object} $textareas */ export function initMarkdownEditor($textareas) { - $textareas.on('focus', function (){ - // reveal the previous when the user focuses on the textarea for the first time - $('.wmd-preview').show(); - if ($('#prevHead').length === 0) { - $('.wmd-preview').before('<h3 id="prevHead">Preview</h3>'); - } - }).wmd({ - helpLink: '/help/markdown', - helpHoverTitle: 'Formatting Help', - helpTarget: '_new' + $textareas + .on('focus', () => { + // reveal the previous when the user focuses on the textarea for the first time + $('.wmd-preview').show(); + if ($('#prevHead').length === 0) { + $('.wmd-preview').before('<h3 id="prevHead">Preview</h3>'); + } + }) + .wmd({ + helpLink: '/help/markdown', + helpHoverTitle: 'Formatting Help', + helpTarget: '_new', }); } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestService.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestService.js index 41d212542c9..61c1157b4b2 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestService.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestService.js @@ -1,31 +1,36 @@ - export const REQUEST_TYPES = { - WORK_MERGE: 1, - AUTHOR_MERGE: 2 -} + WORK_MERGE: 1, + AUTHOR_MERGE: 2, +}; -export async function createRequest(olids, action, type, comment = null, primary = null) { - const data = { - rtype: 'create-request', - action: action, - mr_type: type, - olids: olids - } - if (comment) { - data['comment'] = comment - } - if (primary) { - data['primary'] = primary - } +export async function createRequest( + olids, + action, + type, + comment = null, + primary = null, +) { + const data = { + rtype: 'create-request', + action: action, + mr_type: type, + olids: olids, + }; + if (comment) { + data['comment'] = comment; + } + if (primary) { + data['primary'] = primary; + } - return fetch('/merges', { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }) + return fetch('/merges', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); } /** @@ -37,23 +42,23 @@ export async function createRequest(olids, action, type, comment = null, primary * @returns {Promise<Response>} */ async function updateRequest(action, mrid, comment = null) { - const data = { - rtype: 'update-request', - action: action, - mrid: mrid - } - if (comment) { - data['comment'] = comment - } + const data = { + rtype: 'update-request', + action: action, + mrid: mrid, + }; + if (comment) { + data['comment'] = comment; + } - return fetch('/merges', { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }) + return fetch('/merges', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); } /** @@ -64,7 +69,7 @@ async function updateRequest(action, mrid, comment = null) { * @returns {Promise<Response>} The results of the update POST request */ export async function commentOnRequest(mrid, comment) { - return updateRequest('comment', mrid, comment) + return updateRequest('comment', mrid, comment); } /** @@ -73,17 +78,17 @@ export async function commentOnRequest(mrid, comment) { * @param {Number} mrid Unique identifier for the request being claimed */ export async function claimRequest(mrid) { - return updateRequest('claim', mrid) + return updateRequest('claim', mrid); } export async function unassignRequest(mrid) { - return updateRequest('unassign', mrid) + return updateRequest('unassign', mrid); } export async function declineRequest(mrid, comment) { - return updateRequest('decline', mrid, comment) + return updateRequest('decline', mrid, comment); } export async function approveRequest(mrid, comment) { - return updateRequest('approve', mrid, comment) + return updateRequest('approve', mrid, comment); } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js index 7e850020557..9e15ad21068 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js @@ -5,8 +5,8 @@ * @module merge-request-table/MergeRequestTable */ -import TableHeader from './MergeRequestTable/TableHeader' -import { setI18nStrings, TableRow } from './MergeRequestTable/TableRow' +import TableHeader from './MergeRequestTable/TableHeader'; +import { setI18nStrings, TableRow } from './MergeRequestTable/TableRow'; /** * Class representing the librarian request table. @@ -14,48 +14,51 @@ import { setI18nStrings, TableRow } from './MergeRequestTable/TableRow' * @class */ export default class MergeRequestTable { - + /** + * Creates references to the table and its header and hydrates each. + * + * @param {HTMLElement} mergeRequestTable + */ + constructor(mergeRequestTable) { /** - * Creates references to the table and its header and hydrates each. + * The `username` of the authenticated patron, or '' if logged out. * - * @param {HTMLElement} mergeRequestTable + * @param {string} */ - constructor(mergeRequestTable) { - /** - * The `username` of the authenticated patron, or '' if logged out. - * - * @param {string} - */ - this.username = mergeRequestTable.dataset.username - - const localizedStrings = JSON.parse(mergeRequestTable.dataset.i18n) - setI18nStrings(localizedStrings) + this.username = mergeRequestTable.dataset.username; - /** - * Reference to this table's header. - * - * @param {HTMLElement} - */ - this.tableHeader = new TableHeader(mergeRequestTable.querySelector('.table-header')) + const localizedStrings = JSON.parse(mergeRequestTable.dataset.i18n); + setI18nStrings(localizedStrings); - /** - * References to each row in the table. - * - * @param {Array<TableRow>} - */ - this.tableRows = [] - const rowElements = mergeRequestTable.querySelectorAll('.mr-table-row') - for (const elem of rowElements) { - this.tableRows.push(new TableRow(elem, this.username)) - } - } + /** + * Reference to this table's header. + * + * @param {HTMLElement} + */ + this.tableHeader = new TableHeader( + mergeRequestTable.querySelector('.table-header'), + ); /** - * Hydrates the librarian request table. + * References to each row in the table. + * + * @param {Array<TableRow>} */ - initialize() { - this.tableHeader.initialize() - document.addEventListener('click', (event) => this.tableHeader.closeMenusIfClickOutside(event)) - this.tableRows.forEach(elem => elem.initialize()) + this.tableRows = []; + const rowElements = mergeRequestTable.querySelectorAll('.mr-table-row'); + for (const elem of rowElements) { + this.tableRows.push(new TableRow(elem, this.username)); } + } + + /** + * Hydrates the librarian request table. + */ + initialize() { + this.tableHeader.initialize(); + document.addEventListener('click', (event) => + this.tableHeader.closeMenusIfClickOutside(event), + ); + this.tableRows.forEach((elem) => elem.initialize()); + } } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js index 39be68c2b3d..2dd83ecc543 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js @@ -14,126 +14,131 @@ * @class */ export default class TableHeader { + /** + * Sets references to many table header affordances. + * + * @param {HTMLElement} tableHeader + */ + constructor(tableHeader) { /** - * Sets references to many table header affordances. + * References to each select menu. These are always visible + * in the header bar, and, when clicked, display a drop-down + * menu with filtering options. * - * @param {HTMLElement} tableHeader + * @param {NodeList<HTMLElement>} */ - constructor(tableHeader) { - /** - * References to each select menu. These are always visible - * in the header bar, and, when clicked, display a drop-down - * menu with filtering options. - * - * @param {NodeList<HTMLElement>} - */ - this.dropMenuButtons = tableHeader.querySelectorAll('.mr-dropdown') - /** - * References each drop-down filter option menu. - * - * @param {NodeList<HTMLElement>} - */ - this.dropMenus = tableHeader.querySelectorAll('.mr-dropdown-menu') - /** - * References each drop-down menu "X" affordance, which closes - * the appropriate drop-down menu. - * - * @param{NodeList<HTMLElement>} - */ - this.closeButtons = tableHeader.querySelectorAll('.dropdown-close') - /** - * References each text input filter. - * - * @param{NodeList<HTMLElement>} - */ - this.searchInputs = tableHeader.querySelectorAll('.filter') - } - - /** - * Hydrates the table header affordances. - */ - initialize() { - this.initFilters() - } - + this.dropMenuButtons = tableHeader.querySelectorAll('.mr-dropdown'); /** - * Toggle a dropdown menu while closing other dropdown menus. + * References each drop-down filter option menu. * - * @param {Event} event - * @param {string} menuButtonId + * @param {NodeList<HTMLElement>} */ - toggleAMenuWhileClosingOthers(event, menuButtonId) { - // prevent closing of menu on bubbling unless click menuButton itself - if (event.target.id === menuButtonId) { - // close other open menus, then toggle selected menu - this.closeOtherMenus(menuButtonId) - event.target.firstElementChild.classList.toggle('hidden') - } - } - + this.dropMenus = tableHeader.querySelectorAll('.mr-dropdown-menu'); /** - * Close dropdown menus whose menu button doesn't match a given id. + * References each drop-down menu "X" affordance, which closes + * the appropriate drop-down menu. * - * @param {string} menuButtonId + * @param{NodeList<HTMLElement>} */ - closeOtherMenus(menuButtonId) { - this.dropMenuButtons.forEach((menuButton) => { - if (menuButton.id !== menuButtonId) { - menuButton.firstElementChild.classList.add('hidden') - } - }) - } - + this.closeButtons = tableHeader.querySelectorAll('.dropdown-close'); /** - * Filters of dropdown menu items using case-insensitive string matching. + * References each text input filter. * - * @param {Event} event + * @param{NodeList<HTMLElement>} */ - filterMenuItems(event) { - const input = document.getElementById(event.target.id) - const filter = input.value.toUpperCase() - const menu = input.closest('.mr-dropdown-menu') - const items = menu.getElementsByClassName('dropdown-item') - // skip first item in menu - for (let i=1; i < items.length; i++) { - const text = items[i].textContent - items[i].classList.toggle('hidden', text.toUpperCase().indexOf(filter) === -1); - } + this.searchInputs = tableHeader.querySelectorAll('.filter'); + } + + /** + * Hydrates the table header affordances. + */ + initialize() { + this.initFilters(); + } + + /** + * Toggle a dropdown menu while closing other dropdown menus. + * + * @param {Event} event + * @param {string} menuButtonId + */ + toggleAMenuWhileClosingOthers(event, menuButtonId) { + // prevent closing of menu on bubbling unless click menuButton itself + if (event.target.id === menuButtonId) { + // close other open menus, then toggle selected menu + this.closeOtherMenus(menuButtonId); + event.target.firstElementChild.classList.toggle('hidden'); } + } - /** - * Close all dropdown menus when click anywhere on screen that's not part of - * the dropdown menu; otherwise, keep dropdown menu open. - * - * @param {Event} event - */ - closeMenusIfClickOutside(event) { - const menusClicked = Array.from(this.dropMenuButtons).filter((menuButton) => { - return menuButton.contains(event.target) - }) - // want to preserve clicking in a menu, i.e. when filtering for users - if (!menusClicked.length) { - this.dropMenus.forEach((menu) => menu.classList.add('hidden')) - } + /** + * Close dropdown menus whose menu button doesn't match a given id. + * + * @param {string} menuButtonId + */ + closeOtherMenus(menuButtonId) { + this.dropMenuButtons.forEach((menuButton) => { + if (menuButton.id !== menuButtonId) { + menuButton.firstElementChild.classList.add('hidden'); + } + }); + } + + /** + * Filters of dropdown menu items using case-insensitive string matching. + * + * @param {Event} event + */ + filterMenuItems(event) { + const input = document.getElementById(event.target.id); + const filter = input.value.toUpperCase(); + const menu = input.closest('.mr-dropdown-menu'); + const items = menu.getElementsByClassName('dropdown-item'); + // skip first item in menu + for (let i = 1; i < items.length; i++) { + const text = items[i].textContent; + items[i].classList.toggle( + 'hidden', + text.toUpperCase().indexOf(filter) === -1, + ); } + } - /** - * Initialize events for merge queue filter dropdown menu functionality. - * - */ - initFilters() { - this.dropMenuButtons.forEach((menuButton) => { - menuButton.addEventListener('click', (event) => { - this.toggleAMenuWhileClosingOthers(event, menuButton.id) - }) - }) - this.closeButtons.forEach((button) => { - button.addEventListener('click', (event) => { - event.target.closest('.mr-dropdown-menu').classList.toggle('hidden') - }) - }) - this.searchInputs.forEach((input) => { - input.addEventListener('keyup', (event) => this.filterMenuItems(event)) - }) + /** + * Close all dropdown menus when click anywhere on screen that's not part of + * the dropdown menu; otherwise, keep dropdown menu open. + * + * @param {Event} event + */ + closeMenusIfClickOutside(event) { + const menusClicked = Array.from(this.dropMenuButtons).filter( + (menuButton) => { + return menuButton.contains(event.target); + }, + ); + // want to preserve clicking in a menu, i.e. when filtering for users + if (!menusClicked.length) { + this.dropMenus.forEach((menu) => menu.classList.add('hidden')); } + } + + /** + * Initialize events for merge queue filter dropdown menu functionality. + * + */ + initFilters() { + this.dropMenuButtons.forEach((menuButton) => { + menuButton.addEventListener('click', (event) => { + this.toggleAMenuWhileClosingOthers(event, menuButton.id); + }); + }); + this.closeButtons.forEach((button) => { + button.addEventListener('click', (event) => { + event.target.closest('.mr-dropdown-menu').classList.toggle('hidden'); + }); + }); + this.searchInputs.forEach((input) => { + input.addEventListener('keyup', (event) => this.filterMenuItems(event)); + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js index b78e5988c56..360fc4a22ba 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js @@ -4,13 +4,18 @@ * @module merge-request-table/MergeRequestTable/TableRow */ -import { claimRequest, commentOnRequest, declineRequest, unassignRequest } from '../MergeRequestService' -import { FadingToast } from '../../Toast' +import { FadingToast } from '../../Toast'; +import { + claimRequest, + commentOnRequest, + declineRequest, + unassignRequest, +} from '../MergeRequestService'; let i18nStrings; export function setI18nStrings(localizedStrings) { - i18nStrings = localizedStrings; + i18nStrings = localizedStrings; } /** @@ -26,245 +31,274 @@ export function setI18nStrings(localizedStrings) { * @class */ export class TableRow { + /** + * Creates a new librarian request table row. + * + * Stores reference to each interactive element in a row. + * + * @param {HTMLElement} row Root element of a table row + * @param {string} username `username` of logged-in patron. Empty if unauthenticated. + */ + constructor(row, username) { /** - * Creates a new librarian request table row. + * Reference to this row. * - * Stores reference to each interactive element in a row. + * @param {HTMLElement} + */ + this.row = row; + /** + * `username` of authenticated patron, or '' if unauthenticated. * - * @param {HTMLElement} row Root element of a table row - * @param {string} username `username` of logged-in patron. Empty if unauthenticated. + * @param {HTMLElement} */ - constructor(row, username) { - /** - * Reference to this row. - * - * @param {HTMLElement} - */ - this.row = row - /** - * `username` of authenticated patron, or '' if unauthenticated. - * - * @param {HTMLElement} - */ - this.username = username - /** - * Unique identifier for this row. - * - * @param {Number} - */ - this.mrid = row.dataset.mrid - /** - * Button used to toggle the full comments display's visibility. - * - * @param {HTMLElement} - */ - this.toggleCommentButton = row.querySelector('.mr-comment-toggle__comment-expand') - /** - * Element which displays this row's comment count. - * - * @param {HTMLElement} - */ - this.commentCountDisplay = row.querySelector('.mr-comment-toggle__comment-count') - /** - * Element displaying the most recent comment on this request. - * - * @param {HTMLElement} - */ - this.commentPreview = row.querySelector('.mr-details__comment-preview') - /** - * Hidden comments display. Also contains reply inputs, if rendered. - * - * @param {HTMLElement} - */ - this.fullCommentsPanel = row.querySelector('.comment-panel') - /** - * Element that displays all of the comments for this request. - * - * @param {HTMLElement} - */ - this.commentsDisplay = this.fullCommentsPanel.querySelector('.comment-panel__comment-display') - /** - * The comment text input. - * - * @param {HTMLElement|null} - */ - this.commentReplyInput = this.fullCommentsPanel.querySelector('.comment-panel__reply-input') - /** - * The comment reply button. - * - * @param {HTMLElement|null} - */ - this.replyButton = this.fullCommentsPanel.querySelector('.comment-panel__reply-btn') - /** - * Affordance which allows one to close their own request. - * - * Only available on a patron's own open requests. - * - * @param {HTMLElement|null} - */ - this.closeRequestButton = this.row.querySelector('.mr-close-link') - /** - * Button used by super-librarians to claim a request. - * - * @param {HTMLElement} - */ - this.reviewButton = this.row.querySelector('.mr-review-actions__review-btn') - /** - * Reference to root element of the assignee display. - * - * @param {HTMLElement} - */ - this.assigneeElement = this.row.querySelector('.mr-review-actions__assignee') - /** - * Assignee display element which displays the assignee's name. - * - * @param {HTMLElement} - */ - this.assigneeLabel = this.row.querySelector('.mr-review-actions__assignee-name') - /** - * Element that unassignees the current reviewer when clicked. - * - * @param {HTMLElement} - */ - this.unassignReviewerButton = this.row.querySelector('.mr-review-actions__unassign') - } - + this.username = username; /** - * Hydrates interactive elements in this row. + * Unique identifier for this row. + * + * @param {Number} */ - initialize() { - this.toggleCommentButton.addEventListener('click', () => this.toggleComments()) - if (this.closeRequestButton) { - this.closeRequestButton.addEventListener('click', () => this.closeRequest()) - } - if (this.replyButton && this.commentReplyInput) { - this.replyButton.addEventListener('click', () => this.addComment()) - } - this.reviewButton.addEventListener('click', () => this.claimRequest()) - if (this.unassignReviewerButton) { - this.unassignReviewerButton.addEventListener('click', () => this.unassignReviewer()) - } - } - + this.mrid = row.dataset.mrid; /** - * Toggles which comment display is currently visible. + * Button used to toggle the full comments display's visibility. * - * On page load the comment preview display is visible, while - * the full comments panel is hidden. This function toggles - * each element's visibility. + * @param {HTMLElement} */ - toggleComments() { - this.commentPreview.classList.toggle('hidden') - this.fullCommentsPanel.classList.toggle('hidden') - - // Add depressed effect to toggle button: - this.toggleCommentButton.classList.toggle('mr-comment-toggle__comment-expand--active'); - } - + this.toggleCommentButton = row.querySelector( + '.mr-comment-toggle__comment-expand', + ); /** - * Closes the request linked to this row, and removes this - * row from the DOM. + * Element which displays this row's comment count. + * + * @param {HTMLElement} */ - async closeRequest() { - const comment = prompt(i18nStrings['close_request_comment_prompt']) - if (comment !== null) { // Comment will be `null` if "Cancel" button pressed - await declineRequest(this.mrid, comment) - .then(result => result.json()) - .then(data => { - if (data.status === 'ok') { - this.row.parentElement.removeChild(this.row) - } - }) - .catch(e => { - // XXX : toast? - throw e - }) - } - } - + this.commentCountDisplay = row.querySelector( + '.mr-comment-toggle__comment-count', + ); /** - * `POST`s a new comment to the server. + * Element displaying the most recent comment on this request. * - * Updates the view on success. + * @param {HTMLElement} */ - async addComment() { - const comment = this.commentReplyInput.value.trim() - if (comment) { - await commentOnRequest(this.mrid, comment) - .then(result => result.json()) - .then(data => { - if (data.status === 'ok') { - this.updateCommentViews(comment) - this.commentReplyInput.value = '' - } else { - new FadingToast(i18nStrings['comment_submission_failure_message']).show() - } - }) - .catch(e => { - throw e - }) - } - } - + this.commentPreview = row.querySelector('.mr-details__comment-preview'); + /** + * Hidden comments display. Also contains reply inputs, if rendered. + * + * @param {HTMLElement} + */ + this.fullCommentsPanel = row.querySelector('.comment-panel'); + /** + * Element that displays all of the comments for this request. + * + * @param {HTMLElement} + */ + this.commentsDisplay = this.fullCommentsPanel.querySelector( + '.comment-panel__comment-display', + ); + /** + * The comment text input. + * + * @param {HTMLElement|null} + */ + this.commentReplyInput = this.fullCommentsPanel.querySelector( + '.comment-panel__reply-input', + ); + /** + * The comment reply button. + * + * @param {HTMLElement|null} + */ + this.replyButton = this.fullCommentsPanel.querySelector( + '.comment-panel__reply-btn', + ); + /** + * Affordance which allows one to close their own request. + * + * Only available on a patron's own open requests. + * + * @param {HTMLElement|null} + */ + this.closeRequestButton = this.row.querySelector('.mr-close-link'); + /** + * Button used by super-librarians to claim a request. + * + * @param {HTMLElement} + */ + this.reviewButton = this.row.querySelector( + '.mr-review-actions__review-btn', + ); /** - * Updates row, setting given comment as most recent. + * Reference to root element of the assignee display. * - * First, escapes given comment. Replaces text of comment - * preview with escaped comment. Add new comment element to - * full comments display. Increments the row's comment count. + * @param {HTMLElement} + */ + this.assigneeElement = this.row.querySelector( + '.mr-review-actions__assignee', + ); + /** + * Assignee display element which displays the assignee's name. + * + * @param {HTMLElement} + */ + this.assigneeLabel = this.row.querySelector( + '.mr-review-actions__assignee-name', + ); + /** + * Element that unassignees the current reviewer when clicked. * - * @param {string} comment The newly added comment. + * @param {HTMLElement} */ - updateCommentViews(comment) { - const escapedComment = document.createTextNode(comment) + this.unassignReviewerButton = this.row.querySelector( + '.mr-review-actions__unassign', + ); + } - // Update preview: - this.commentPreview.innerText = escapedComment.textContent + /** + * Hydrates interactive elements in this row. + */ + initialize() { + this.toggleCommentButton.addEventListener('click', () => + this.toggleComments(), + ); + if (this.closeRequestButton) { + this.closeRequestButton.addEventListener('click', () => + this.closeRequest(), + ); + } + if (this.replyButton && this.commentReplyInput) { + this.replyButton.addEventListener('click', () => this.addComment()); + } + this.reviewButton.addEventListener('click', () => this.claimRequest()); + if (this.unassignReviewerButton) { + this.unassignReviewerButton.addEventListener('click', () => + this.unassignReviewer(), + ); + } + } - // Update full display: - const newComment = document.createElement('div') - newComment.classList.add('comment-panel__comment') - newComment.innerHTML = `<span class="commenter">@${this.username}</span> ` - newComment.appendChild(escapedComment) + /** + * Toggles which comment display is currently visible. + * + * On page load the comment preview display is visible, while + * the full comments panel is hidden. This function toggles + * each element's visibility. + */ + toggleComments() { + this.commentPreview.classList.toggle('hidden'); + this.fullCommentsPanel.classList.toggle('hidden'); - this.commentsDisplay.appendChild(newComment) - this.commentsDisplay.scrollTop = this.commentsDisplay.scrollHeight + // Add depressed effect to toggle button: + this.toggleCommentButton.classList.toggle( + 'mr-comment-toggle__comment-expand--active', + ); + } - // Update comment count: - const count = Number(this.commentCountDisplay.innerText) + 1 - this.commentCountDisplay.innerText = count + /** + * Closes the request linked to this row, and removes this + * row from the DOM. + */ + async closeRequest() { + const comment = prompt(i18nStrings['close_request_comment_prompt']); + if (comment !== null) { + // Comment will be `null` if "Cancel" button pressed + await declineRequest(this.mrid, comment) + .then((result) => result.json()) + .then((data) => { + if (data.status === 'ok') { + this.row.parentElement.removeChild(this.row); + } + }) + .catch((e) => { + // XXX : toast? + throw e; + }); } + } - /** - * `POST`s claim to review this request, then updates the view. - * - * Hides the review button, and shows the assignee display. - */ - async claimRequest() { - await claimRequest(this.mrid) - .then(result => result.json()) - .then(data => { - if (data.status === 'ok') { - this.assigneeLabel.innerText = `@${this.username}` - this.assigneeElement.classList.remove('hidden') - this.reviewButton.classList.add('hidden') - } - }) + /** + * `POST`s a new comment to the server. + * + * Updates the view on success. + */ + async addComment() { + const comment = this.commentReplyInput.value.trim(); + if (comment) { + await commentOnRequest(this.mrid, comment) + .then((result) => result.json()) + .then((data) => { + if (data.status === 'ok') { + this.updateCommentViews(comment); + this.commentReplyInput.value = ''; + } else { + new FadingToast( + i18nStrings['comment_submission_failure_message'], + ).show(); + } + }) + .catch((e) => { + throw e; + }); } + } - /** - * `POST`s request to remove current assignee, then updates the view. - * - * Hides the assignee display and shows the review button on success. - */ - async unassignReviewer() { - await unassignRequest(this.mrid) - .then(result => result.json()) - .then(data => { - if (data.status === 'ok') { - this.assigneeElement.classList.add('hidden') - this.reviewButton.classList.remove('hidden') - } - }) - } + /** + * Updates row, setting given comment as most recent. + * + * First, escapes given comment. Replaces text of comment + * preview with escaped comment. Add new comment element to + * full comments display. Increments the row's comment count. + * + * @param {string} comment The newly added comment. + */ + updateCommentViews(comment) { + const escapedComment = document.createTextNode(comment); + + // Update preview: + this.commentPreview.innerText = escapedComment.textContent; + + // Update full display: + const newComment = document.createElement('div'); + newComment.classList.add('comment-panel__comment'); + newComment.innerHTML = `<span class="commenter">@${this.username}</span> `; + newComment.appendChild(escapedComment); + + this.commentsDisplay.appendChild(newComment); + this.commentsDisplay.scrollTop = this.commentsDisplay.scrollHeight; + + // Update comment count: + const count = Number(this.commentCountDisplay.innerText) + 1; + this.commentCountDisplay.innerText = count; + } + + /** + * `POST`s claim to review this request, then updates the view. + * + * Hides the review button, and shows the assignee display. + */ + async claimRequest() { + await claimRequest(this.mrid) + .then((result) => result.json()) + .then((data) => { + if (data.status === 'ok') { + this.assigneeLabel.innerText = `@${this.username}`; + this.assigneeElement.classList.remove('hidden'); + this.reviewButton.classList.add('hidden'); + } + }); + } + + /** + * `POST`s request to remove current assignee, then updates the view. + * + * Hides the assignee display and shows the review button on success. + */ + async unassignReviewer() { + await unassignRequest(this.mrid) + .then((result) => result.json()) + .then((data) => { + if (data.status === 'ok') { + this.assigneeElement.classList.add('hidden'); + this.reviewButton.classList.remove('hidden'); + } + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/index.js b/openlibrary/plugins/openlibrary/js/merge-request-table/index.js index a98cff2c265..70a0a5b9b4c 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/index.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/index.js @@ -6,6 +6,6 @@ import MergeRequestTable from './MergeRequestTable'; * @param {HTMLElement} elem Reference to the queue's root element. */ export function initLibrarianQueue(elem) { - const librarianQueue = new MergeRequestTable(elem) - librarianQueue.initialize() + const librarianQueue = new MergeRequestTable(elem); + librarianQueue.initialize(); } diff --git a/openlibrary/plugins/openlibrary/js/merge.js b/openlibrary/plugins/openlibrary/js/merge.js index 4a61df9080d..4f5ccc22e35 100644 --- a/openlibrary/plugins/openlibrary/js/merge.js +++ b/openlibrary/plugins/openlibrary/js/merge.js @@ -2,62 +2,66 @@ import 'jquery-ui/ui/widgets/dialog'; import { declineRequest } from './merge-request-table/MergeRequestService'; export function initAuthorMergePage() { - $('#save').on('click', function () { - const n = $('#mergeForm input[type=radio]:checked').length; - const confirmMergeButton = document.querySelector('#confirmMerge') - if (n === 0) { - $('#noMaster').dialog('open'); - } else if (confirmMergeButton) { - $('#confirmMerge').dialog('open'); - } else { - $('#mergeForm').trigger('submit') - } - return false; - }); - $('div.radio').first().find('input[type=radio]').prop('checked', true); - $('div.checkbox').first().find('input[type=checkbox]').prop('checked', true); - $('div.author').first().addClass('master'); - $('#include input[type=radio]').on('mouseover', function () { - $(this).parent().parent().addClass('mouseoverHighlight', 300); - }); - $('#include input[type=radio]').on('mouseout', function () { - $(this).parent().parent().removeClass('mouseoverHighlight', 100); - }); - $('#include input[type=radio]').on('click', function () { - const previousMaster = $('.merge').find('div.master'); - previousMaster.removeClass('master mergeSelection'); - previousMaster.find('input[type=checkbox]').prop('checked', false); - $(this).parent().parent().addClass('master'); - $(this).parent().parent().find('input[type=checkbox]').prop('checked', true); - }); - $('#include input[type=checkbox]').on('change', function () { - if (!$(this).parent().parent().hasClass('master')) { - if ($(this).is(':checked')) { - $(this).parent().parent().addClass('mergeSelection'); - } else { - $(this).parent().parent().removeClass('mergeSelection'); - } - } - }); - initRejectButton() + $('#save').on('click', () => { + const n = $('#mergeForm input[type=radio]:checked').length; + const confirmMergeButton = document.querySelector('#confirmMerge'); + if (n === 0) { + $('#noMaster').dialog('open'); + } else if (confirmMergeButton) { + $('#confirmMerge').dialog('open'); + } else { + $('#mergeForm').trigger('submit'); + } + return false; + }); + $('div.radio').first().find('input[type=radio]').prop('checked', true); + $('div.checkbox').first().find('input[type=checkbox]').prop('checked', true); + $('div.author').first().addClass('master'); + $('#include input[type=radio]').on('mouseover', function () { + $(this).parent().parent().addClass('mouseoverHighlight', 300); + }); + $('#include input[type=radio]').on('mouseout', function () { + $(this).parent().parent().removeClass('mouseoverHighlight', 100); + }); + $('#include input[type=radio]').on('click', function () { + const previousMaster = $('.merge').find('div.master'); + previousMaster.removeClass('master mergeSelection'); + previousMaster.find('input[type=checkbox]').prop('checked', false); + $(this).parent().parent().addClass('master'); + $(this) + .parent() + .parent() + .find('input[type=checkbox]') + .prop('checked', true); + }); + $('#include input[type=checkbox]').on('change', function () { + if (!$(this).parent().parent().hasClass('master')) { + if ($(this).is(':checked')) { + $(this).parent().parent().addClass('mergeSelection'); + } else { + $(this).parent().parent().removeClass('mergeSelection'); + } + } + }); + initRejectButton(); } function initRejectButton() { - const rejectButton = document.querySelector('#reject-author-merge-btn') - if (rejectButton) { - rejectButton.addEventListener('click', function() { - rejectMerge() - rejectButton.disabled = true - const approveButton = document.querySelector('#save') - approveButton.disabled = true - }) - } + const rejectButton = document.querySelector('#reject-author-merge-btn'); + if (rejectButton) { + rejectButton.addEventListener('click', () => { + rejectMerge(); + rejectButton.disabled = true; + const approveButton = document.querySelector('#save'); + approveButton.disabled = true; + }); + } } function rejectMerge() { - const commentInput = document.querySelector('#author-merge-comment') - const mridInput = document.querySelector('#mrid-input') - declineRequest(Number(mridInput.value), commentInput.value) + const commentInput = document.querySelector('#author-merge-comment'); + const mridInput = document.querySelector('#mrid-input'); + declineRequest(Number(mridInput.value), commentInput.value); } /** @@ -67,38 +71,38 @@ function rejectMerge() { * Assumes presence of element with '#preMerge' id and 'data-keys' attribute. */ export function initAuthorView() { - const dataKeysJSON = $('#preMerge').data('keys'); + const dataKeysJSON = $('#preMerge').data('keys'); - $('#preMerge').show(); - $('#preMerge').parent().show(); + $('#preMerge').show(); + $('#preMerge').parent().show(); - const data = { - master: dataKeysJSON['master'], - duplicates: dataKeysJSON['duplicates'], - olids: dataKeysJSON['olids'] - }; + const data = { + master: dataKeysJSON['master'], + duplicates: dataKeysJSON['duplicates'], + olids: dataKeysJSON['olids'], + }; - const mrid = dataKeysJSON['mrid'] - const comment = dataKeysJSON['comment'] + const mrid = dataKeysJSON['mrid']; + const comment = dataKeysJSON['comment']; - if (mrid) { - data['mrid'] = mrid - } - if (comment) { - data['comment'] = comment - } + if (mrid) { + data['mrid'] = mrid; + } + if (comment) { + data['comment'] = comment; + } - $.ajax({ - url: '/authors/merge.json', - type: 'POST', - data: JSON.stringify(data), - error: function() { - $('#preMerge').fadeOut(); - $('#errorMerge').fadeIn(); - }, - success: function() { - $('#preMerge').fadeOut(); - $('#postMerge').fadeIn(); - } - }); + $.ajax({ + url: '/authors/merge.json', + type: 'POST', + data: JSON.stringify(data), + error: () => { + $('#preMerge').fadeOut(); + $('#errorMerge').fadeIn(); + }, + success: () => { + $('#preMerge').fadeOut(); + $('#postMerge').fadeIn(); + }, + }); } diff --git a/openlibrary/plugins/openlibrary/js/modals/index.js b/openlibrary/plugins/openlibrary/js/modals/index.js index cb3f921c2b1..d52ecfbb08d 100644 --- a/openlibrary/plugins/openlibrary/js/modals/index.js +++ b/openlibrary/plugins/openlibrary/js/modals/index.js @@ -2,25 +2,23 @@ import 'jquery-colorbox'; import { FadingToast } from '../Toast.js'; import '../../../../../static/css/components/metadata-form.css'; - - /** * Initializes share modal. */ export function initShareModal($modalLinks) { - addClickListeners($modalLinks, '400px'); - addShareModalButtonListeners(); + addClickListeners($modalLinks, '400px'); + addShareModalButtonListeners(); } /** * Adds click listeners to buttons in all notes modals on a page. */ -function addShareModalButtonListeners (){ - $('#social-modal-content .copy-url-btn').on('click', function(event){ - event.preventDefault(); - navigator.clipboard.writeText(window.location.href); - showToast('URL copied to clipboard') - $.colorbox.close() - }) +function addShareModalButtonListeners() { + $('#social-modal-content .copy-url-btn').on('click', (event) => { + event.preventDefault(); + navigator.clipboard.writeText(window.location.href); + showToast('URL copied to clipboard'); + $.colorbox.close(); + }); } /** @@ -29,136 +27,136 @@ function addShareModalButtonListeners (){ * @param {JQuery} $modalLinks A collection of notes modal links. */ export function initNotesModal($modalLinks) { - addClickListeners($modalLinks, '640px'); - addNotesModalButtonListeners(); - addNotesReloadListeners($('.notes-textarea')); + addClickListeners($modalLinks, '640px'); + addNotesModalButtonListeners(); + addNotesReloadListeners($('.notes-textarea')); } /** * Adds click listeners to buttons in all notes modals on a page. */ function addNotesModalButtonListeners() { - $('.update-note-button').on('click', function(event){ - event.preventDefault(); - // Get form data - const formData = new FormData($(this).closest('form')[0]); - if (formData.get('notes')) { - const $deleteButton = $($(this).siblings()[0]); - - // Post data - const workOlid = formData.get('work_id'); - formData.delete('work_id'); - - $.ajax({ - url: `/works/${workOlid}/notes.json`, - data: formData, - type: 'POST', - contentType: false, - processData: false, - success: function() { - showToast('Update successful!') - $.colorbox.close(); - $deleteButton.removeClass('hidden'); - } - }); - } - }); - - $('.delete-note-button').on('click', function() { - if (confirm('Really delete this book note?')) { - const $button = $(this); - - // Get form data - const formData = new FormData($button.prop('form')); - - // Post data - const workOlid = formData.get('work_id'); - formData.delete('work_id'); - formData.delete('notes'); - - $.ajax({ - url: `/works/${workOlid}/notes.json`, - data: formData, - type: 'POST', - contentType: false, - processData: false, - success: function() { - showToast('Note deleted.'); - $.colorbox.close(); - $button.toggleClass('hidden'); - $button.closest('form').find('textarea').val(''); - } - }); - } - }); + $('.update-note-button').on('click', function (event) { + event.preventDefault(); + // Get form data + const formData = new FormData($(this).closest('form')[0]); + if (formData.get('notes')) { + const $deleteButton = $($(this).siblings()[0]); + + // Post data + const workOlid = formData.get('work_id'); + formData.delete('work_id'); + + $.ajax({ + url: `/works/${workOlid}/notes.json`, + data: formData, + type: 'POST', + contentType: false, + processData: false, + success: () => { + showToast('Update successful!'); + $.colorbox.close(); + $deleteButton.removeClass('hidden'); + }, + }); + } + }); + + $('.delete-note-button').on('click', function () { + if (confirm('Really delete this book note?')) { + const $button = $(this); + + // Get form data + const formData = new FormData($button.prop('form')); + + // Post data + const workOlid = formData.get('work_id'); + formData.delete('work_id'); + formData.delete('notes'); + + $.ajax({ + url: `/works/${workOlid}/notes.json`, + data: formData, + type: 'POST', + contentType: false, + processData: false, + success: () => { + showToast('Note deleted.'); + $.colorbox.close(); + $button.toggleClass('hidden'); + $button.closest('form').find('textarea').val(''); + }, + }); + } + }); } /** -* Add listeners to update and delete buttons on the notes page. -* -* On successful delete, list elements related to the note are removedd -* from the view. -*/ + * Add listeners to update and delete buttons on the notes page. + * + * On successful delete, list elements related to the note are removedd + * from the view. + */ export function addNotesPageButtonListeners() { - $('.update-note-link-button').on('click', function(event) { - event.preventDefault(); - const workId = $(this).parent().siblings('input')[0].value; - const editionId = $(this).parent().attr('id').split('-')[0]; - const note = $(this).parent().siblings('textarea')[0].value; - - const formData = new FormData(); - formData.append('notes', note); - formData.append('edition_id', `OL${editionId}M`); - - $.ajax({ - url: `/works/OL${workId}W/notes.json`, - data: formData, - type: 'POST', - contentType: false, - processData: false, - success: function() { - showToast('Update successful!') - } - }); - }); + $('.update-note-link-button').on('click', function (event) { + event.preventDefault(); + const workId = $(this).parent().siblings('input')[0].value; + const editionId = $(this).parent().attr('id').split('-')[0]; + const note = $(this).parent().siblings('textarea')[0].value; - $('.delete-note-button').on('click', function() { - if (confirm('Really delete this book note?')) { - const $parent = $(this).parent(); - - const workId = $(this).parent().siblings('input')[0].value; - const editionId = $(this).parent().attr('id').split('-')[0]; - - const formData = new FormData(); - formData.append('edition_id', `OL${editionId}M`); - - $.ajax({ - url: `/works/OL${workId}W/notes.json`, - data: formData, - type: 'POST', - contentType: false, - processData: false, - success: function() { - showToast('Note deleted.'); - - // Remove list element from UI: - if ($parent.closest('.notes-list').children().length === 1) { - // This is the last edition for a set of notes on a work. - // Remove the work element: - $parent.closest('.main-list-item').remove(); - - if (!$('.main-list-item').length) { - $('.list-container')[0].innerText = 'No notes found.'; - } - } else { - // Notes for other editions of the work exist - // Remove the edition's notes list item: - $parent.closest('.notes-list-item').remove(); - } - } - }); - } + const formData = new FormData(); + formData.append('notes', note); + formData.append('edition_id', `OL${editionId}M`); + + $.ajax({ + url: `/works/OL${workId}W/notes.json`, + data: formData, + type: 'POST', + contentType: false, + processData: false, + success: () => { + showToast('Update successful!'); + }, }); + }); + + $('.delete-note-button').on('click', function () { + if (confirm('Really delete this book note?')) { + const $parent = $(this).parent(); + + const workId = $(this).parent().siblings('input')[0].value; + const editionId = $(this).parent().attr('id').split('-')[0]; + + const formData = new FormData(); + formData.append('edition_id', `OL${editionId}M`); + + $.ajax({ + url: `/works/OL${workId}W/notes.json`, + data: formData, + type: 'POST', + contentType: false, + processData: false, + success: () => { + showToast('Note deleted.'); + + // Remove list element from UI: + if ($parent.closest('.notes-list').children().length === 1) { + // This is the last edition for a set of notes on a work. + // Remove the work element: + $parent.closest('.main-list-item').remove(); + + if (!$('.main-list-item').length) { + $('.list-container')[0].innerText = 'No notes found.'; + } + } else { + // Notes for other editions of the work exist + // Remove the edition's notes list item: + $parent.closest('.notes-list-item').remove(); + } + }, + }); + } + }); } /** @@ -170,14 +168,16 @@ export function addNotesPageButtonListeners() { * @param {JQuery} $notesTextareas All notes text areas on a page. */ function addNotesReloadListeners($notesTextareas) { - $notesTextareas.each(function(_i, textarea) { - const $textarea = $(textarea); - - $textarea.on('contentReload', function() { - const newValue = $textarea.parent().find('.notes-modal-textarea')[0].value; - $textarea.val(newValue); - }); + $notesTextareas.each((_i, textarea) => { + const $textarea = $(textarea); + + $textarea.on('contentReload', () => { + const newValue = $textarea + .parent() + .find('.notes-modal-textarea')[0].value; + $textarea.val(newValue); }); + }); } /** @@ -187,7 +187,7 @@ function addNotesReloadListeners($notesTextareas) { * @param {JQuery} $parent Mount point for toast component */ function showToast(message, $parent) { - new FadingToast(message, $parent).show(); + new FadingToast(message, $parent).show(); } /** @@ -199,16 +199,16 @@ function showToast(message, $parent) { * @param {JQuery} $modalLinks A collection of observations modal links. */ export function initObservationsModal($modalLinks) { - addClickListeners($modalLinks, '800px'); - addObservationReloadListeners($('.observations-list')) - addDeleteObservationsListeners($('.delete-observations-button')); + addClickListeners($modalLinks, '800px'); + addObservationReloadListeners($('.observations-list')); + addDeleteObservationsListeners($('.delete-observations-button')); - $modalLinks.each(function(_i, modalLinkElement) { - const $element = $(modalLinkElement); - const context = JSON.parse(getModalContent($element).dataset['context']) + $modalLinks.each((_i, modalLinkElement) => { + const $element = $(modalLinkElement); + const context = JSON.parse(getModalContent($element).dataset['context']); - addObservationChangeListeners($element.next(), context); - }) + addObservationChangeListeners($element.next(), context); + }); } /** @@ -220,13 +220,13 @@ export function initObservationsModal($modalLinks) { * @param {JQuery} $modalLinks A collection of modal links. */ function addClickListeners($modalLinks, maxWidth) { - $modalLinks.each(function(_i, modalLinkElement) { - $(modalLinkElement).on('click', function() { - // Get context, which is attached to the modal content - const content = getModalContent($(this)) - displayModal(content, maxWidth); - }) - }) + $modalLinks.each((_i, modalLinkElement) => { + $(modalLinkElement).on('click', function () { + // Get context, which is attached to the modal content + const content = getModalContent($(this)); + displayModal(content, maxWidth); + }); + }); } /** @@ -237,7 +237,7 @@ function addClickListeners($modalLinks, maxWidth) { * @returns {HTMLElement} Reference to a modal's content */ function getModalContent($modalLink) { - return $modalLink.siblings()[0].children[0] + return $modalLink.siblings()[0].children[0]; } /** @@ -251,61 +251,61 @@ function getModalContent($modalLink) { * @param {JQuery} $observationLists All of the observations lists on a page */ function addObservationReloadListeners($observationLists) { - $observationLists.each(function(_i, list) { - $(list).on('contentReload', function() { - const $list = $(this); - const $buttonsDiv = $list.siblings('div').first(); - const id = $list.attr('id'); - const workOlid = `OL${id.split('-')[0]}W`; - - $list.empty(); - $list.append(` + $observationLists.each((_i, list) => { + $(list).on('contentReload', function () { + const $list = $(this); + const $buttonsDiv = $list.siblings('div').first(); + const id = $list.attr('id'); + const workOlid = `OL${id.split('-')[0]}W`; + + $list.empty(); + $list.append(` <li class="throbber-li"> <div class="throbber"><h3>Updating observations</h3></div> </li> - `) - - $.ajax({ - type: 'GET', - url: `/works/${workOlid}/observations`, - dataType: 'json' - }) - .done(function(data) { - let listItems = ''; - for (const [category, values] of Object.entries(data)) { - let observations = values.join(', '); - observations = observations.charAt(0).toUpperCase() + observations.slice(1); - - listItems += ` + `); + + $.ajax({ + type: 'GET', + url: `/works/${workOlid}/observations`, + dataType: 'json', + }).done((data) => { + let listItems = ''; + for (const [category, values] of Object.entries(data)) { + let observations = values.join(', '); + observations = + observations.charAt(0).toUpperCase() + observations.slice(1); + + listItems += ` <li> <span class="observation-category">${category.charAt(0).toUpperCase() + category.slice(1)}:</span> ${observations} </li> `; - } + } - $list.empty(); + $list.empty(); - if (listItems.length === 0) { - listItems = ` + if (listItems.length === 0) { + listItems = ` <li> No observations for this work. </li> `; - $list.addClass('no-content'); - $buttonsDiv.removeClass('observation-buttons'); - $buttonsDiv.addClass('no-content'); - $buttonsDiv.children().first().addClass('hidden'); - } else { - $list.removeClass('no-content'); - $buttonsDiv.removeClass('no-content'); - $buttonsDiv.addClass('observation-buttons'); - $buttonsDiv.children().first().removeClass('hidden'); - } - - $list.append(listItems); - }) - }) - }) + $list.addClass('no-content'); + $buttonsDiv.removeClass('observation-buttons'); + $buttonsDiv.addClass('no-content'); + $buttonsDiv.children().first().addClass('hidden'); + } else { + $list.removeClass('no-content'); + $buttonsDiv.removeClass('no-content'); + $buttonsDiv.addClass('observation-buttons'); + $buttonsDiv.children().first().removeClass('hidden'); + } + + $list.append(listItems); + }); + }); + }); } /** @@ -319,39 +319,39 @@ function addObservationReloadListeners($observationLists) { * @param {JQuery} $deleteButtons All observation delete buttons found on a page. */ function addDeleteObservationsListeners($deleteButtons) { - $deleteButtons.each(function(_i, deleteButton) { - const $button = $(deleteButton); - - $button.on('click', function() { - const workOlid = `OL${$button.prop('id').split('-')[0]}W`; - - $.ajax({ - url: `/works/${workOlid}/observations`, - type: 'DELETE', - contentType: 'application/json', - success: function() { - // Remove observations in view - const $observationsView = $button.closest('.observation-view'); - const $list = $observationsView.find('ul'); - - $list.empty(); - $list.append(` + $deleteButtons.each((_i, deleteButton) => { + const $button = $(deleteButton); + + $button.on('click', () => { + const workOlid = `OL${$button.prop('id').split('-')[0]}W`; + + $.ajax({ + url: `/works/${workOlid}/observations`, + type: 'DELETE', + contentType: 'application/json', + success: () => { + // Remove observations in view + const $observationsView = $button.closest('.observation-view'); + const $list = $observationsView.find('ul'); + + $list.empty(); + $list.append(` <li> No observations for this work. </li> - `) - $list.addClass('no-content'); - - $button.parent().removeClass('observation-buttons'); - $button.parent().addClass('no-content'); - $button.addClass('hidden'); - - // find and clear modal selections - clearForm($button.siblings().find('form')); - } - }); - }) + `); + $list.addClass('no-content'); + + $button.parent().removeClass('observation-buttons'); + $button.parent().addClass('no-content'); + $button.addClass('hidden'); + + // find and clear modal selections + clearForm($button.siblings().find('form')); + }, + }); }); + }); } /** @@ -360,11 +360,11 @@ function addDeleteObservationsListeners($deleteButtons) { * @param {JQuery} $form An observations modal form */ function clearForm($form) { - $form.find('input').each(function(_i, input) { - if (input.checked) { - input.checked = false; - } - }); + $form.find('input').each((_i, input) => { + if (input.checked) { + input.checked = false; + } + }); } /** @@ -376,22 +376,24 @@ function clearForm($form) { * @param {String} maxWidth The max width of the modal */ function displayModal(content, maxWidth) { - const modalId = `#${content.id}` - const context = content.dataset['context'] ? JSON.parse(content.dataset['context']) : null; - const reloadId = context ? context.reloadId : null; - - $.colorbox({ - inline: true, - opacity: '0.5', - href: modalId, - width: '100%', - maxWidth: maxWidth, - onClosed: function() { - if (reloadId) { - $(`#${reloadId}`).trigger('contentReload'); - } - } - }); + const modalId = `#${content.id}`; + const context = content.dataset['context'] + ? JSON.parse(content.dataset['context']) + : null; + const reloadId = context ? context.reloadId : null; + + $.colorbox({ + inline: true, + opacity: '0.5', + href: modalId, + width: '100%', + maxWidth: maxWidth, + onClosed: () => { + if (reloadId) { + $(`#${reloadId}`).trigger('contentReload'); + } + }, + }); } /** @@ -406,30 +408,30 @@ function displayModal(content, maxWidth) { * @param {Object} context An object containing the patron's username and the work's OLID. */ function addObservationChangeListeners($parent, context) { - const $questionSections = $parent.find('.aspect-section'); - const username = context.username; - const workOlid = context.work.split('/')[2]; - - $questionSections.each(function() { - const $inputs = $(this).find('input') - - $inputs.each(function() { - $(this).on('change', function() { - const type = $(this).attr('name'); - const value = $(this).attr('value'); - const observation = {}; - observation[type] = value; - - const data = { - username: username, - action: `${$(this).prop('checked') ? 'add': 'delete'}`, - observation: observation - } - - submitObservation($(this), workOlid, data, type); - }); - }) + const $questionSections = $parent.find('.aspect-section'); + const username = context.username; + const workOlid = context.work.split('/')[2]; + + $questionSections.each(function () { + const $inputs = $(this).find('input'); + + $inputs.each(function () { + $(this).on('change', function () { + const type = $(this).attr('name'); + const value = $(this).attr('value'); + const observation = {}; + observation[type] = value; + + const data = { + username: username, + action: `${$(this).prop('checked') ? 'add' : 'delete'}`, + observation: observation, + }; + + submitObservation($(this), workOlid, data, type); + }); }); + }); } /** @@ -440,23 +442,24 @@ function addObservationChangeListeners($parent, context) { * @param {String} sectionType Name of the input's section. */ function submitObservation($input, workOlid, data, sectionType) { - let toastMessage; - const capitalizedType = sectionType[0].toUpperCase() + sectionType.substring(1); - - // Make AJAX call - $.ajax({ - type: 'POST', - url: `/works/${workOlid}/observations`, - contentType: 'application/json', - data: JSON.stringify(data) + let toastMessage; + const capitalizedType = + sectionType[0].toUpperCase() + sectionType.substring(1); + + // Make AJAX call + $.ajax({ + type: 'POST', + url: `/works/${workOlid}/observations`, + contentType: 'application/json', + data: JSON.stringify(data), + }) + .done(() => { + toastMessage = `${capitalizedType} saved!`; + }) + .fail(() => { + toastMessage = `${capitalizedType} save failed...`; }) - .done(function() { - toastMessage = `${capitalizedType} saved!`; - }) - .fail(function() { - toastMessage = `${capitalizedType} save failed...`; - }) - .always(function() { - showToast(toastMessage, $input.closest('.metadata-form')); - }); + .always(() => { + showToast(toastMessage, $input.closest('.metadata-form')); + }); } diff --git a/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js b/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js index 89660d454c7..79d35dce3bc 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js +++ b/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js @@ -4,10 +4,10 @@ * @module my-books/CreateListForm.js */ import 'jquery-colorbox'; -import myBooksStore from './store' -import { websafe } from '../jsdef' -import { createList } from '../lists/ListService' -import { attachNewActiveShowcaseItem } from '../lists/ShowcaseItem' +import { websafe } from '../jsdef'; +import { createList } from '../lists/ListService'; +import { attachNewActiveShowcaseItem } from '../lists/ShowcaseItem'; +import myBooksStore from './store'; /** * Represents the list creation form displayed when a patron @@ -20,119 +20,123 @@ import { attachNewActiveShowcaseItem } from '../lists/ShowcaseItem' * @class */ export class CreateListForm { - + /** + * Creates a new `CreateListForm` object. + * + * Sets references to form inputs and "Create List" button. + * + * @param {HTMLElement} form + */ + constructor(form) { /** - * Creates a new `CreateListForm` object. - * - * Sets references to form inputs and "Create List" button. + * References this form's "Create List" button. * - * @param {HTMLElement} form + * @member {HTMLElement} */ - constructor(form) { - /** - * References this form's "Create List" button. - * - * @member {HTMLElement} - */ - this.createListButton = form.querySelector('#create-list-button') - - /** - * References the form's list title input field. - * - * @member {HTMLElement} - */ - this.listTitleInput = form.querySelector('#list_label') - - /** - * References the form's list description input field. - * - * @member {HTMLElement} - */ - this.listDescriptionInput = form.querySelector('#list_desc') - - // Clear form on page refresh: - this.resetForm() - } + this.createListButton = form.querySelector('#create-list-button'); /** - * Attaches click listener to the "Create List" button. + * References the form's list title input field. + * + * @member {HTMLElement} */ - initialize() { - this.createListButton.addEventListener('click', (event) =>{ - event.preventDefault() - this.createNewList() - }) - } + this.listTitleInput = form.querySelector('#list_label'); /** - * Creates a new patron list. - * - * When a new list is created, the list's title and description - * are taken from the form. The patron's user key and the seed - * identifier of the first list item are provided by the open dropper - * referenced in the shared My Books store. - * - * On success, updates all My Books droppers on the page, - * resets the list creation form fields, and closes the - * modal containing the form. A new showcase item is added - * to the active lists showcase, if the showcase exists. + * References the form's list description input field. * - * @async + * @member {HTMLElement} */ - async createNewList() { - // Construct seed object for first list item: - const listTitle = websafe(this.listTitleInput.value) - const listDescription = websafe(this.listDescriptionInput.value) + this.listDescriptionInput = form.querySelector('#list_desc'); - const openDropper = myBooksStore.getOpenDropper() - const seed = openDropper.readingLists.getSeed() + // Clear form on page refresh: + this.resetForm(); + } - const postData = { - name: listTitle, - description: listDescription, - seeds: [seed] - } + /** + * Attaches click listener to the "Create List" button. + */ + initialize() { + this.createListButton.addEventListener('click', (event) => { + event.preventDefault(); + this.createNewList(); + }); + } - // Call list creation service with seed object: - await createList(myBooksStore.getUserKey(), postData) - .then(response => response.json()) - .then((data) => { - // Update active lists showcase: - attachNewActiveShowcaseItem(data['key'], seed, listTitle, data['key']) + /** + * Creates a new patron list. + * + * When a new list is created, the list's title and description + * are taken from the form. The patron's user key and the seed + * identifier of the first list item are provided by the open dropper + * referenced in the shared My Books store. + * + * On success, updates all My Books droppers on the page, + * resets the list creation form fields, and closes the + * modal containing the form. A new showcase item is added + * to the active lists showcase, if the showcase exists. + * + * @async + */ + async createNewList() { + // Construct seed object for first list item: + const listTitle = websafe(this.listTitleInput.value); + const listDescription = websafe(this.listDescriptionInput.value); - // Update all droppers with new list data - this.updateDroppersOnListCreation(data['key'], listTitle, data['key']) + const openDropper = myBooksStore.getOpenDropper(); + const seed = openDropper.readingLists.getSeed(); - // Clear list creation form fields, nullify seed - this.resetForm() - }) - .finally(() => { - // Close the modal - $.colorbox.close() - }) - } + const postData = { + name: listTitle, + description: listDescription, + seeds: [seed], + }; - /** - * Updates lists section of each dropper with a new list. - * - * @param {string} listKey Key of the newly created list - * @param {string} listTitle Title of the new list - */ - updateDroppersOnListCreation(listKey, listTitle, coverUrl) { - const droppers = myBooksStore.getDroppers() - const openDropper = myBooksStore.getOpenDropper() + // Call list creation service with seed object: + await createList(myBooksStore.getUserKey(), postData) + .then((response) => response.json()) + .then((data) => { + // Update active lists showcase: + attachNewActiveShowcaseItem(data['key'], seed, listTitle, data['key']); - for (const dropper of droppers) { - const isActive = dropper === openDropper - dropper.readingLists.onListCreationSuccess(listKey, listTitle, isActive, coverUrl) - } - } + // Update all droppers with new list data + this.updateDroppersOnListCreation(data['key'], listTitle, data['key']); - /** - * Clears the list title and desciption fields in the form. - */ - resetForm() { - this.listTitleInput.value = '' - this.listDescriptionInput.value = '' + // Clear list creation form fields, nullify seed + this.resetForm(); + }) + .finally(() => { + // Close the modal + $.colorbox.close(); + }); + } + + /** + * Updates lists section of each dropper with a new list. + * + * @param {string} listKey Key of the newly created list + * @param {string} listTitle Title of the new list + */ + updateDroppersOnListCreation(listKey, listTitle, coverUrl) { + const droppers = myBooksStore.getDroppers(); + const openDropper = myBooksStore.getOpenDropper(); + + for (const dropper of droppers) { + const isActive = dropper === openDropper; + dropper.readingLists.onListCreationSuccess( + listKey, + listTitle, + isActive, + coverUrl, + ); } + } + + /** + * Clears the list title and desciption fields in the form. + */ + resetForm() { + this.listTitleInput.value = ''; + this.listDescriptionInput.value = ''; + } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js index d01ec785bbe..6ec2cb3cbbb 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js @@ -2,12 +2,16 @@ * Defines functionality related to Open Library's My Books dropper components. * @module my-books/MyBooksDropper */ -import myBooksStore from './store' -import { CheckInComponents } from './MyBooksDropper/CheckInComponents' -import { ReadingLists } from './MyBooksDropper/ReadingLists' -import {ReadingLogForms, ReadingLogShelves} from './MyBooksDropper/ReadingLogForms' -import { Dropper } from '../dropper/Dropper' -import { removeChildren } from '../utils' + +import { Dropper } from '../dropper/Dropper'; +import { removeChildren } from '../utils'; +import { CheckInComponents } from './MyBooksDropper/CheckInComponents'; +import { ReadingLists } from './MyBooksDropper/ReadingLists'; +import { + ReadingLogForms, + ReadingLogShelves, +} from './MyBooksDropper/ReadingLogForms'; +import myBooksStore from './store'; /** * Represents a single My Books Dropper. @@ -26,191 +30,208 @@ import { removeChildren } from '../utils' * @augments Dropper */ export class MyBooksDropper extends Dropper { - /** - * Creates references to the given dropper's reading log forms, read date affordances, and - * list affordances. - * - * @param {HTMLElement} dropper - */ - constructor(dropper) { - super(dropper) - - const dropperActionCallbacks = { - closeDropper: this.closeDropper.bind(this), - toggleDropper: this.toggleDropper.bind(this) - } - - /** - * Reference to this dropper's list content. - * @member {ReadingLists} - */ - this.readingLists = new ReadingLists(dropper, dropperActionCallbacks) - - /** - * Reference to the dropper's list loading indicator. - * - * This is only rendered when the patron is logged in. - * @member {HTMLElement|null} - */ - this.loadingIndicator = dropper.querySelector('.list-loading-indicator') - - /** - * Reference to the interval ID of the animation `setInterval` call. - * @member {NodeJS.Timer|undefined} - */ - this.loadingAnimationId - - /** - * The work key associated with this dropper, if any. - * - * @member {string|undefined} - */ - this.workKey = this.dropper.dataset.workKey - - const splitKey = this.workKey ? this.workKey.split('/') : [''] - const workOlid = splitKey[splitKey.length - 1] - - /** - * @type {CheckInComponents|null} - */ - this.checkInComponents = workOlid ? new CheckInComponents(document.querySelector(`#check-in-container-${workOlid}`)) : null - - /** - * References this dropper's reading log buttons. - * @member {ReadingLogForms} - */ - this.readingLogForms = new ReadingLogForms(dropper, this.checkInComponents, dropperActionCallbacks) - } - - /** - * Hydrates dropper contents and loads patron's lists. - */ - initialize() { - super.initialize() - - this.readingLogForms.initialize() - this.readingLists.initialize() - if (this.checkInComponents) { - this.checkInComponents.initialize() - } - - this.loadingAnimationId = this.initLoadingAnimation(this.dropper.querySelector('.loading-ellipsis')) - } + /** + * Creates references to the given dropper's reading log forms, read date affordances, and + * list affordances. + * + * @param {HTMLElement} dropper + */ + constructor(dropper) { + super(dropper); + + const dropperActionCallbacks = { + closeDropper: this.closeDropper.bind(this), + toggleDropper: this.toggleDropper.bind(this), + }; /** - * Creates loading animation for list loading indicator. - * - * @param {HTMLElement} loadingIndicator - * @returns {NodeJS.Timer} + * Reference to this dropper's list content. + * @member {ReadingLists} */ - initLoadingAnimation(loadingIndicator) { - let count = 0 - const intervalId = setInterval(function() { - let ellipsis = '' - for (let i = 0; i < count % 4; ++i) { - ellipsis += '.' - } - loadingIndicator.innerText = ellipsis - ++count - }, 1500) - - return intervalId - } + this.readingLists = new ReadingLists(dropper, dropperActionCallbacks); /** - * Replaces dropper loading indicator with the given - * partially rendered list affordances. + * Reference to the dropper's list loading indicator. * - * @param {string} partialHtml + * This is only rendered when the patron is logged in. + * @member {HTMLElement|null} */ - updateReadingLists(partialHtml) { - clearInterval(this.loadingAnimationId) - this.replaceLoadingIndicators(this.loadingIndicator, partialHtml) - } + this.loadingIndicator = dropper.querySelector('.list-loading-indicator'); /** - * Returns an array of seed keys associated with this dropper. - * - * If the seed identifies a book, there should be both an - * edition and work key in the results. Otherwise, the results - * array should only contain the primary seed key. - * - * @returns {Array<string>} + * Reference to the interval ID of the animation `setInterval` call. + * @member {NodeJS.Timer|undefined} */ - getSeedKeys() { - const results = [this.readingLists.seedKey] - if (this.readingLists.workKey) { - results.push(this.readingLists.workKey) - } - return results - } + this.loadingAnimationId; /** - * Object returned by the list partials endpoint. - * - * @typedef {Object} ListPartials - * @property {string} dropper HTML string for dropdown list content - * @property {string} active HTML string for patron's active lists - */ - /** - * Replaces list loading indicators with the given partially rendered HTML. + * The work key associated with this dropper, if any. * - * @param {HTMLElement} dropperListsPlaceholder Loading indicator found inside of the dropdown content - * @param {ListPartials} partials + * @member {string|undefined} */ - replaceLoadingIndicators(dropperListsPlaceholder, partialHTML) { - const dropperParent = dropperListsPlaceholder ? dropperListsPlaceholder.parentElement : null + this.workKey = this.dropper.dataset.workKey; - if (dropperParent) { - removeChildren(dropperParent) - dropperParent.insertAdjacentHTML('afterbegin', partialHTML) - - const anchors = this.dropper.querySelectorAll('.modify-list') - this.readingLists.initModifyListAffordances(anchors) - } - } + const splitKey = this.workKey ? this.workKey.split('/') : ['']; + const workOlid = splitKey[splitKey.length - 1]; /** - * Updates this dropper's primary button's state and display to show that a book is active on the - * given shelf. - * - * When we update to the "Already Read" shelf, the appropriate last read date affordance is - * displayed. - * - * @param shelf {ReadingLogShelf} + * @type {CheckInComponents|null} */ - updateShelfDisplay(shelf) { - this.readingLogForms.updateActivatedStatus(true) - this.readingLogForms.updatePrimaryBookshelfId(Number(shelf)) - this.readingLogForms.updatePrimaryButtonText(this.readingLogForms.getDisplayString(shelf)) - - if (this.checkInComponents) { - if (!this.checkInComponents.hasReadDate() && shelf === ReadingLogShelves.ALREADY_READ) { - this.checkInComponents.showCheckInDisplay() - } else { - this.checkInComponents.hideCheckInPrompt() - } - } - } + this.checkInComponents = workOlid + ? new CheckInComponents( + document.querySelector(`#check-in-container-${workOlid}`), + ) + : null; - // Dropper overrides: /** - * Updates store with reference to the opened dropper. - * - * @override + * References this dropper's reading log buttons. + * @member {ReadingLogForms} */ - onOpen() { - myBooksStore.setOpenDropper(this) + this.readingLogForms = new ReadingLogForms( + dropper, + this.checkInComponents, + dropperActionCallbacks, + ); + } + + /** + * Hydrates dropper contents and loads patron's lists. + */ + initialize() { + super.initialize(); + + this.readingLogForms.initialize(); + this.readingLists.initialize(); + if (this.checkInComponents) { + this.checkInComponents.initialize(); } - /** - * Redirects to login page when disabled dropper is clicked. - * - * My Books droppers are disabled for unauthenticated patrons. - * - * @override - */ - onDisabledClick() { - window.location = `/account/login?redirect=${location.pathname}` + this.loadingAnimationId = this.initLoadingAnimation( + this.dropper.querySelector('.loading-ellipsis'), + ); + } + + /** + * Creates loading animation for list loading indicator. + * + * @param {HTMLElement} loadingIndicator + * @returns {NodeJS.Timer} + */ + initLoadingAnimation(loadingIndicator) { + let count = 0; + const intervalId = setInterval(() => { + let ellipsis = ''; + for (let i = 0; i < count % 4; ++i) { + ellipsis += '.'; + } + loadingIndicator.innerText = ellipsis; + ++count; + }, 1500); + + return intervalId; + } + + /** + * Replaces dropper loading indicator with the given + * partially rendered list affordances. + * + * @param {string} partialHtml + */ + updateReadingLists(partialHtml) { + clearInterval(this.loadingAnimationId); + this.replaceLoadingIndicators(this.loadingIndicator, partialHtml); + } + + /** + * Returns an array of seed keys associated with this dropper. + * + * If the seed identifies a book, there should be both an + * edition and work key in the results. Otherwise, the results + * array should only contain the primary seed key. + * + * @returns {Array<string>} + */ + getSeedKeys() { + const results = [this.readingLists.seedKey]; + if (this.readingLists.workKey) { + results.push(this.readingLists.workKey); + } + return results; + } + + /** + * Object returned by the list partials endpoint. + * + * @typedef {Object} ListPartials + * @property {string} dropper HTML string for dropdown list content + * @property {string} active HTML string for patron's active lists + */ + /** + * Replaces list loading indicators with the given partially rendered HTML. + * + * @param {HTMLElement} dropperListsPlaceholder Loading indicator found inside of the dropdown content + * @param {ListPartials} partials + */ + replaceLoadingIndicators(dropperListsPlaceholder, partialHTML) { + const dropperParent = dropperListsPlaceholder + ? dropperListsPlaceholder.parentElement + : null; + + if (dropperParent) { + removeChildren(dropperParent); + dropperParent.insertAdjacentHTML('afterbegin', partialHTML); + + const anchors = this.dropper.querySelectorAll('.modify-list'); + this.readingLists.initModifyListAffordances(anchors); + } + } + + /** + * Updates this dropper's primary button's state and display to show that a book is active on the + * given shelf. + * + * When we update to the "Already Read" shelf, the appropriate last read date affordance is + * displayed. + * + * @param shelf {ReadingLogShelf} + */ + updateShelfDisplay(shelf) { + this.readingLogForms.updateActivatedStatus(true); + this.readingLogForms.updatePrimaryBookshelfId(Number(shelf)); + this.readingLogForms.updatePrimaryButtonText( + this.readingLogForms.getDisplayString(shelf), + ); + + if (this.checkInComponents) { + if ( + !this.checkInComponents.hasReadDate() && + shelf === ReadingLogShelves.ALREADY_READ + ) { + this.checkInComponents.showCheckInDisplay(); + } else { + this.checkInComponents.hideCheckInPrompt(); + } } + } + + // Dropper overrides: + /** + * Updates store with reference to the opened dropper. + * + * @override + */ + onOpen() { + myBooksStore.setOpenDropper(this); + } + + /** + * Redirects to login page when disabled dropper is clicked. + * + * My Books droppers are disabled for unauthenticated patrons. + * + * @override + */ + onDisabledClick() { + window.location = `/account/login?redirect=${location.pathname}`; + } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js index c7318194397..399522928aa 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js @@ -3,7 +3,7 @@ * @module my-books/MyBooksDropper/CheckInComponents */ import { initDialogClosers } from '../../dialog'; -import { PersistentToast } from '../../Toast' +import { PersistentToast } from '../../Toast'; /** * Array of days for each month, listed in order starting with January. @@ -12,7 +12,7 @@ import { PersistentToast } from '../../Toast' * @readonly * @type {array<number>} */ -const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] +const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; /** * Determines if the given year is a leap year. @@ -21,7 +21,7 @@ const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] * @returns `true` if the given year is a leap year. */ function isLeapYear(year) { - return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); } /** @@ -37,299 +37,325 @@ function isLeapYear(year) { * @class */ export class CheckInComponents { - /** - * @param checkInContainer - */ - constructor(checkInContainer) { - // HTML for the check-in components is not rendered if - // the patron is unauthenticated, or if the dropper - // is for an orphaned edition. - if (!checkInContainer) { - return - } - - /** - * @typedef {object} ReadDateConfig - * @property {string} workOlid - * @property {string} [editionKey] - * @property {string} [lastReadDate] - * @property {number} [eventId] - */ - /** - * @type {ReadDateConfig} - */ - this.config = JSON.parse(checkInContainer.dataset.config) - - const checkInPromptElem = checkInContainer.querySelector('.check-in-prompt') - /** - * @type {CheckInPrompt} - */ - this.checkInPrompt = new CheckInPrompt(checkInPromptElem) - - const checkInDisplayElem = checkInContainer.querySelector('.last-read-date') - /** - * @type {CheckInDisplay} - */ - this.checkInDisplay = new CheckInDisplay(checkInDisplayElem) - - /** - * References element that will be displayed in last read date form modal. - * Set during form initialization. - * - * @type {HTMLElement|undefined} - */ - this.modalContent = undefined - - /** - * @type {CheckInForm|undefined} - */ - this.checkInForm = undefined - } - - initialize() { - this.checkInPrompt.initialize() - this.checkInPrompt.getRootElement().addEventListener('submit-check-in', (event) => { - const year = event.detail.year - const month = event.detail.month - const day = event.detail.day - - const eventData = this.prepareEventRequest(year, month, day) - this.postCheckIn(eventData, this.checkInForm.getFormAction()) - .then((resp) => { - if (!resp.ok) { - throw Error(`Check-in request failed. Status: ${resp.status}`) - } - this.updateDateAndShowDisplay(year, month, day) - }) - .catch(() => { - new PersistentToast('Failed to submit check-in. Please try again in a few moments.').show() - }) - }) - - let hiddenModalContentContainer = document.querySelector('#hidden-modal-content-container') - if (!hiddenModalContentContainer) { - hiddenModalContentContainer = document.createElement('div') - hiddenModalContentContainer.classList.add('hidden') - hiddenModalContentContainer.id = 'hidden-modal-content-container' - document.body.appendChild(hiddenModalContentContainer) - } - - const modalContent = this.createModalContentFromTemplate() - hiddenModalContentContainer.appendChild(modalContent) - - this.modalContent = hiddenModalContentContainer.querySelector(`#modal-content-${this.config.workOlid}`) - - const formElem = this.modalContent.querySelector('form') - this.checkInForm = new CheckInForm(formElem, this.config.workOlid, this.config.editionKey || '', this.config.lastReadDate || '', this.config.eventId) - this.checkInForm.initialize() - this.checkInForm.getRootElement().addEventListener('delete-check-in', () => { - this.deleteCheckIn(this.checkInForm.getEventId()) - .then(resp => { - if (!resp.ok) { - throw Error(`Check-in delete request failed. Status: ${resp.status}`) - } - - this.checkInForm.resetForm() - this.checkInDisplay.hide() - this.checkInPrompt.show() - }) - .catch(() => { - // TODO : Use localized strings - new PersistentToast('Failed to delete check-in. Please try again in a few moments.').show() - }) - .finally(() => { - this.closeModal() - }) - }) - this.checkInForm.getRootElement().addEventListener('submit-check-in', (event) => { - const year = event.detail.year - const month = event.detail.month - const day = event.detail.day - - const eventData = this.prepareEventRequest(year, month, day) - this.postCheckIn(eventData, this.checkInForm.getFormAction()) - .then((resp) => { - if (!resp.ok) { - throw Error(`Check-in request failed. Status: ${resp.status}`) - } - this.updateDateAndShowDisplay(year, month, day) - }) - .catch(() => { - // TODO : Use localized strings - new PersistentToast('Failed to submit check-in. Please try again in a few moments.').show() - }) - .finally(() => { - this.closeModal() - }) - }) - - const closeModalElements = this.modalContent.querySelectorAll('.dialog--close') - initDialogClosers(closeModalElements) + /** + * @param checkInContainer + */ + constructor(checkInContainer) { + // HTML for the check-in components is not rendered if + // the patron is unauthenticated, or if the dropper + // is for an orphaned edition. + if (!checkInContainer) { + return; } /** - * Creates a new element containing the check-in form and `colorbox` modal content. - * - * @returns {HTMLElement} + * @typedef {object} ReadDateConfig + * @property {string} workOlid + * @property {string} [editionKey] + * @property {string} [lastReadDate] + * @property {number} [eventId] */ - createModalContentFromTemplate() { - const templateElem = document.createElement('template') - const modalContentTemplate = document.querySelector('#check-in-form-modal') - templateElem.innerHTML = modalContentTemplate.outerHTML - const modalContent = templateElem.content.firstElementChild - modalContent.id = `modal-content-${this.config.workOlid}` - - return modalContent - } - /** - * Updates the date display and form with the given date, and shows the display. - * - * @param {number} year - * @param {number|null} month - * @param {number|null} day + * @type {ReadDateConfig} */ - updateDateAndShowDisplay(year, month = null, day = null) { - // Update last read date display - let dateString = String(year) - if (month) { - dateString += `-${String(month).padStart(2, '0')}` - if (day) { - dateString += `-${String(day).padStart(2, '0')}` - } - } - this.checkInDisplay.updateDateDisplay(dateString) - - // Update component visibility - this.checkInPrompt.hide() - this.checkInDisplay.show() - - // Update submission form - this.checkInForm.updateSelectedDate(year, month, day) - this.checkInForm.showDeleteButton() - - } + this.config = JSON.parse(checkInContainer.dataset.config); + const checkInPromptElem = + checkInContainer.querySelector('.check-in-prompt'); /** - * @typedef {object} CheckInEventPostRequestData - * @property {number} event_type - * @property {number} year - * @property {number|null} month - * @property {number|null} day - * @property {number|null} event_id - * @property {string} [edition_key] + * @type {CheckInPrompt} */ - /** - * Posts the given data to the backend check-in handler. - * - * @param {CheckInEventPostRequestData} eventData - * @param {string} url - * @returns {Promise<Response>} - */ - postCheckIn(eventData, url) { - return fetch(url, { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded', - accept: 'application/json' - }, - body: JSON.stringify(eventData) - }) - } + this.checkInPrompt = new CheckInPrompt(checkInPromptElem); + const checkInDisplayElem = + checkInContainer.querySelector('.last-read-date'); /** - * Posts request to delete the read date record with the given ID. - * - * @param {string} eventId - * @returns {Promise<Response>} + * @type {CheckInDisplay} */ - async deleteCheckIn(eventId) { - return fetch(`/check-ins/${eventId}`, { - method: 'DELETE' - }) - } + this.checkInDisplay = new CheckInDisplay(checkInDisplayElem); /** - * Prepares data for a `postEvent` call. + * References element that will be displayed in last read date form modal. + * Set during form initialization. * - * @param {number} year - * @param {number|null} month - * @param {number|null} day - * @returns {CheckInEventPostRequestData} + * @type {HTMLElement|undefined} */ - prepareEventRequest(year, month = null, day = null) { - // Get event id - const eventId = this.checkInForm.getEventId() - - // Get event type - const eventType = this.checkInForm.getEventType() - - const eventRequest = { - event_id: eventId ? Number(eventId) : null, - event_type: Number(eventType), - year: year, - month: month, - day: day - } - - const editionKey = this.checkInForm.getEditionKey() || null - if (editionKey) { - eventRequest.edition_key = editionKey - } - - return eventRequest - } + this.modalContent = undefined; /** - * Returns `true` if the check-in display is visible on the screen. - * - * @returns {boolean} + * @type {CheckInForm|undefined} */ - hasReadDate() { - return !this.checkInDisplay.getRootElement().classList.contains('hidden') - } + this.checkInForm = undefined; + } - /** - * Resets the check-in form. - */ - resetForm() { - this.checkInForm.resetForm() - } - - /** - * Show the check-in display. - */ - showCheckInDisplay() { - this.checkInDisplay.show() - } - - /** - * Hide the check-in display. - */ - hideCheckInDisplay() { - this.checkInDisplay.hide() - } - - /** - * Show the check-in prompt. - */ - showCheckInPrompt() { - this.checkInPrompt.show() - } + initialize() { + this.checkInPrompt.initialize(); + this.checkInPrompt + .getRootElement() + .addEventListener('submit-check-in', (event) => { + const year = event.detail.year; + const month = event.detail.month; + const day = event.detail.day; - /** - * Hide the check-in prompt. - */ - hideCheckInPrompt() { - this.checkInPrompt.hide() - } + const eventData = this.prepareEventRequest(year, month, day); + this.postCheckIn(eventData, this.checkInForm.getFormAction()) + .then((resp) => { + if (!resp.ok) { + throw Error(`Check-in request failed. Status: ${resp.status}`); + } + this.updateDateAndShowDisplay(year, month, day); + }) + .catch(() => { + new PersistentToast( + 'Failed to submit check-in. Please try again in a few moments.', + ).show(); + }); + }); + + let hiddenModalContentContainer = document.querySelector( + '#hidden-modal-content-container', + ); + if (!hiddenModalContentContainer) { + hiddenModalContentContainer = document.createElement('div'); + hiddenModalContentContainer.classList.add('hidden'); + hiddenModalContentContainer.id = 'hidden-modal-content-container'; + document.body.appendChild(hiddenModalContentContainer); + } + + const modalContent = this.createModalContentFromTemplate(); + hiddenModalContentContainer.appendChild(modalContent); + + this.modalContent = hiddenModalContentContainer.querySelector( + `#modal-content-${this.config.workOlid}`, + ); + + const formElem = this.modalContent.querySelector('form'); + this.checkInForm = new CheckInForm( + formElem, + this.config.workOlid, + this.config.editionKey || '', + this.config.lastReadDate || '', + this.config.eventId, + ); + this.checkInForm.initialize(); + this.checkInForm + .getRootElement() + .addEventListener('delete-check-in', () => { + this.deleteCheckIn(this.checkInForm.getEventId()) + .then((resp) => { + if (!resp.ok) { + throw Error( + `Check-in delete request failed. Status: ${resp.status}`, + ); + } - /** - * Closes the opened `colorbox` modal. - */ - closeModal() { - $.colorbox.close() - } + this.checkInForm.resetForm(); + this.checkInDisplay.hide(); + this.checkInPrompt.show(); + }) + .catch(() => { + // TODO : Use localized strings + new PersistentToast( + 'Failed to delete check-in. Please try again in a few moments.', + ).show(); + }) + .finally(() => { + this.closeModal(); + }); + }); + this.checkInForm + .getRootElement() + .addEventListener('submit-check-in', (event) => { + const year = event.detail.year; + const month = event.detail.month; + const day = event.detail.day; + + const eventData = this.prepareEventRequest(year, month, day); + this.postCheckIn(eventData, this.checkInForm.getFormAction()) + .then((resp) => { + if (!resp.ok) { + throw Error(`Check-in request failed. Status: ${resp.status}`); + } + this.updateDateAndShowDisplay(year, month, day); + }) + .catch(() => { + // TODO : Use localized strings + new PersistentToast( + 'Failed to submit check-in. Please try again in a few moments.', + ).show(); + }) + .finally(() => { + this.closeModal(); + }); + }); + + const closeModalElements = + this.modalContent.querySelectorAll('.dialog--close'); + initDialogClosers(closeModalElements); + } + + /** + * Creates a new element containing the check-in form and `colorbox` modal content. + * + * @returns {HTMLElement} + */ + createModalContentFromTemplate() { + const templateElem = document.createElement('template'); + const modalContentTemplate = document.querySelector('#check-in-form-modal'); + templateElem.innerHTML = modalContentTemplate.outerHTML; + const modalContent = templateElem.content.firstElementChild; + modalContent.id = `modal-content-${this.config.workOlid}`; + + return modalContent; + } + + /** + * Updates the date display and form with the given date, and shows the display. + * + * @param {number} year + * @param {number|null} month + * @param {number|null} day + */ + updateDateAndShowDisplay(year, month = null, day = null) { + // Update last read date display + let dateString = String(year); + if (month) { + dateString += `-${String(month).padStart(2, '0')}`; + if (day) { + dateString += `-${String(day).padStart(2, '0')}`; + } + } + this.checkInDisplay.updateDateDisplay(dateString); + + // Update component visibility + this.checkInPrompt.hide(); + this.checkInDisplay.show(); + + // Update submission form + this.checkInForm.updateSelectedDate(year, month, day); + this.checkInForm.showDeleteButton(); + } + + /** + * @typedef {object} CheckInEventPostRequestData + * @property {number} event_type + * @property {number} year + * @property {number|null} month + * @property {number|null} day + * @property {number|null} event_id + * @property {string} [edition_key] + */ + /** + * Posts the given data to the backend check-in handler. + * + * @param {CheckInEventPostRequestData} eventData + * @param {string} url + * @returns {Promise<Response>} + */ + postCheckIn(eventData, url) { + return fetch(url, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + accept: 'application/json', + }, + body: JSON.stringify(eventData), + }); + } + + /** + * Posts request to delete the read date record with the given ID. + * + * @param {string} eventId + * @returns {Promise<Response>} + */ + async deleteCheckIn(eventId) { + return fetch(`/check-ins/${eventId}`, { + method: 'DELETE', + }); + } + + /** + * Prepares data for a `postEvent` call. + * + * @param {number} year + * @param {number|null} month + * @param {number|null} day + * @returns {CheckInEventPostRequestData} + */ + prepareEventRequest(year, month = null, day = null) { + // Get event id + const eventId = this.checkInForm.getEventId(); + + // Get event type + const eventType = this.checkInForm.getEventType(); + + const eventRequest = { + event_id: eventId ? Number(eventId) : null, + event_type: Number(eventType), + year: year, + month: month, + day: day, + }; + + const editionKey = this.checkInForm.getEditionKey() || null; + if (editionKey) { + eventRequest.edition_key = editionKey; + } + + return eventRequest; + } + + /** + * Returns `true` if the check-in display is visible on the screen. + * + * @returns {boolean} + */ + hasReadDate() { + return !this.checkInDisplay.getRootElement().classList.contains('hidden'); + } + + /** + * Resets the check-in form. + */ + resetForm() { + this.checkInForm.resetForm(); + } + + /** + * Show the check-in display. + */ + showCheckInDisplay() { + this.checkInDisplay.show(); + } + + /** + * Hide the check-in display. + */ + hideCheckInDisplay() { + this.checkInDisplay.hide(); + } + + /** + * Show the check-in prompt. + */ + showCheckInPrompt() { + this.checkInPrompt.show(); + } + + /** + * Hide the check-in prompt. + */ + hideCheckInPrompt() { + this.checkInPrompt.hide(); + } + + /** + * Closes the opened `colorbox` modal. + */ + closeModal() { + $.colorbox.close(); + } } /** @@ -339,73 +365,73 @@ export class CheckInComponents { * @class */ class CheckInPrompt { - /** - * @param {HTMLElement} checkInPrompt - */ - constructor(checkInPrompt) { - this.rootElem = checkInPrompt - } - - initialize() { - const yearLink = this.rootElem.querySelector('.prompt-current-year') - yearLink.addEventListener('click', () => { - // Get the current year - const year = new Date().getFullYear() - - this.dispatchCheckInSubmission(year) - }) - - const todayLink = this.rootElem.querySelector('.prompt-today') - todayLink.addEventListener('click', () => { - // Get today's date - const now = new Date() - const year = now.getFullYear() - const month = now.getMonth() + 1 - const day = now.getDate() - - this.dispatchCheckInSubmission(year, month, day) - }) - } - - /** - * Dispatches a custom `submit-check-in` event with the given date. - * - * @param {number} year - * @param {number|null} month - * @param {number|null} day - */ - dispatchCheckInSubmission(year, month = null, day = null) { - const submitEvent = new CustomEvent('submit-check-in', { - detail: { - year: year, - month: month, - day: day - } - }) - this.rootElem.dispatchEvent(submitEvent) - } - - /** - * Hides this check-in prompt. - */ - hide() { - this.rootElem.classList.add('hidden') - } - - /** - * Shows this check-in prompt. - */ - show() { - this.rootElem.classList.remove('hidden') - } - - /** - * Returns reference to the root element of this check-in prompt. - * @returns {HTMLElement} - */ - getRootElement() { - return this.rootElem - } + /** + * @param {HTMLElement} checkInPrompt + */ + constructor(checkInPrompt) { + this.rootElem = checkInPrompt; + } + + initialize() { + const yearLink = this.rootElem.querySelector('.prompt-current-year'); + yearLink.addEventListener('click', () => { + // Get the current year + const year = new Date().getFullYear(); + + this.dispatchCheckInSubmission(year); + }); + + const todayLink = this.rootElem.querySelector('.prompt-today'); + todayLink.addEventListener('click', () => { + // Get today's date + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth() + 1; + const day = now.getDate(); + + this.dispatchCheckInSubmission(year, month, day); + }); + } + + /** + * Dispatches a custom `submit-check-in` event with the given date. + * + * @param {number} year + * @param {number|null} month + * @param {number|null} day + */ + dispatchCheckInSubmission(year, month = null, day = null) { + const submitEvent = new CustomEvent('submit-check-in', { + detail: { + year: year, + month: month, + day: day, + }, + }); + this.rootElem.dispatchEvent(submitEvent); + } + + /** + * Hides this check-in prompt. + */ + hide() { + this.rootElem.classList.add('hidden'); + } + + /** + * Shows this check-in prompt. + */ + show() { + this.rootElem.classList.remove('hidden'); + } + + /** + * Returns reference to the root element of this check-in prompt. + * @returns {HTMLElement} + */ + getRootElement() { + return this.rootElem; + } } /** @@ -414,43 +440,43 @@ class CheckInPrompt { * @class */ class CheckInDisplay { - /** - * @param {HTMLElement} checkInDisplay - */ - constructor(checkInDisplay) { - this.rootElem = checkInDisplay - this.dateDisplayElem = this.rootElem.querySelector('.check-in-date') - } - - /** - * Updates the date displayed to the given string. - * - * @param {string} date - */ - updateDateDisplay(date) { - this.dateDisplayElem.textContent = date - } - - /** - * Hides this date display. - */ - hide() { - this.rootElem.classList.add('hidden') - } - - /** - * Shows this date display. - */ - show() { - this.rootElem.classList.remove('hidden') - } - - /** - * @returns {HTMLElement} - */ - getRootElement() { - return this.rootElem - } + /** + * @param {HTMLElement} checkInDisplay + */ + constructor(checkInDisplay) { + this.rootElem = checkInDisplay; + this.dateDisplayElem = this.rootElem.querySelector('.check-in-date'); + } + + /** + * Updates the date displayed to the given string. + * + * @param {string} date + */ + updateDateDisplay(date) { + this.dateDisplayElem.textContent = date; + } + + /** + * Hides this date display. + */ + hide() { + this.rootElem.classList.add('hidden'); + } + + /** + * Shows this date display. + */ + show() { + this.rootElem.classList.remove('hidden'); + } + + /** + * @returns {HTMLElement} + */ + getRootElement() { + return this.rootElem; + } } /** @@ -464,327 +490,347 @@ class CheckInDisplay { * @class */ export class CheckInForm { - /** - * @param {HTMLFormElement} formElem - * @param {string} workOlid - * @param {string|null} editionKey - * @param {string|null} lastReadDate - * @param {number|null} eventId - */ - constructor(formElem, workOlid, editionKey = null, lastReadDate = null, eventId = null) { - this.rootElem = formElem - this.workOlid = workOlid - this.editionKey = editionKey - this.lastReadDate = lastReadDate - this.eventId = eventId - - /** - * Reference to hidden `event_type` form input. - * - * @type {HTMLInputElement|undefined} - */ - this.eventTypeInput = this.rootElem.querySelector('input[name=event_type]') - - /** - * Reference to hidden `event_id` form input. - * - * @type {HTMLInputElement|undefined} - */ - this.eventIdInput = this.rootElem.querySelector('input[name=event_id]') - - /** - * Reference to hidden `edition_key` form input. - * - * @type {HTMLInputElement} - */ - this.editionKeyInput = this.rootElem.querySelector('input[name=edition_key]') - - /** - * Reference to the form's year `select` element. - * - * @type {HTMLSelectElement} - */ - this.yearSelect = this.rootElem.querySelector('select[name=year]') - - /** - * Reference to the form's month `select` element. - * - * @type {HTMLSelectElement} - */ - this.monthSelect = this.rootElem.querySelector('select[name=month]') - - /** - * Reference to the form's day `select` element. - * - * @type {HTMLSelectElement} - */ - this.daySelect = this.rootElem.querySelector('select[name=day]') - - /** - * Reference to the form's submit button. - * @type {HTMLButtonElement} - */ - this.submitButton = this.rootElem.querySelector('.check-in__submit-btn') - - /** - * Reference to the form's delete button. - * - * @type {HTMLButtonElement} - */ - this.deleteButton = this.rootElem.querySelector('.check-in__delete-btn') - } - - initialize() { - // Set form's action - this.rootElem.action = `/works/${this.workOlid}/check-ins.json` - // Set form's event ID - if (this.eventId) { - this.setEventId(this.eventId) - this.showDeleteButton() - } - // Set form's edition_key - if (this.editionKey) { - this.editionKeyInput.value = this.editionKey - } - // Set date select elements to the last read date - const [yearString, monthString, dayString] = this.lastReadDate ? this.lastReadDate.split('-') : [null, null, null] - this.updateSelectedDate(Number(yearString), Number(monthString), Number(dayString)) - - // Update form for new years day - const currentYear = new Date().getFullYear(); - const hiddenYear = this.yearSelect.querySelector('.show-if-local-year') - // The year select element has a hidden option for next year. This - // option is shown on 1 January if the client's local year is different - // from the server's local year. - if (Number(hiddenYear.value) === currentYear) { - hiddenYear.classList.remove('hidden') - } - - // Associate labels with select elements - const yearLabel = this.rootElem.querySelector('.check-in__year-label') - const yearSelectId = `year-select-${this.workOlid}` - this.yearSelect.id = yearSelectId - yearLabel.htmlFor = yearSelectId - - const monthLabel = this.rootElem.querySelector('.check-in__month-label') - const monthSelectId = `month-select-${this.workOlid}` - this.monthSelect.id = monthSelectId - monthLabel.htmlFor = monthSelectId - - const dayLabel = this.rootElem.querySelector('.check-in__day-label') - const daySelectId = `day-select-${this.workOlid}` - this.daySelect.id = daySelectId - dayLabel.htmlFor = daySelectId - - // Add listeners to form elements: - this.yearSelect.addEventListener('change', () => { - this.onDateSelectionChange() - }) - this.monthSelect.addEventListener('change', () => { - this.onDateSelectionChange() - }) - this.deleteButton.addEventListener('click', (event) => { - event.preventDefault() - const deleteEvent = new CustomEvent('delete-check-in') - this.rootElem.dispatchEvent(deleteEvent) - }) - this.submitButton.addEventListener('click', (event) => { - event.preventDefault() - const submitEvent = new CustomEvent('submit-check-in', { - detail: { - year: this.getSelectedYear(), - month: this.getSelectedMonth(), - day: this.getSelectedDay() - } - }) - this.rootElem.dispatchEvent(submitEvent) - }) - const todayLink = this.rootElem.querySelector('.check-in__today') - todayLink.addEventListener('click', () => { - // Get today's date - const now = new Date() - const year = now.getFullYear() - const month = now.getMonth() + 1 - const day = now.getDate() - - this.updateSelectedDate(year, month, day) - }) - } - - /** - * Gets currently selected date, then updates the form. - */ - onDateSelectionChange() { - const year = this.yearSelect.selectedIndex ? Number(this.yearSelect.value) : null - this.updateSelectedDate(year, this.monthSelect.selectedIndex, this.daySelect.selectedIndex) - } - - /** - * Updates date select elements based on the given year, month, and day. - * - * @param {number|null} year - * @param {number|null} month - * @param {number|null} day - */ - updateSelectedDate(year = null, month = null, day = null) { - if (!month) { - day = null - } - if (!year) { - month = null - day = null - } - - if (year) { - this.yearSelect.value = year || '' - this.monthSelect.disabled = false - this.submitButton.disabled = false - } else { - this.yearSelect.selectedIndex = 0 - this.monthSelect.disabled = true - this.submitButton.disabled = true - } - if (month) { - this.monthSelect.value = month || '' - this.daySelect.disabled = false - - // Update daySelect options for month/leap year - let daysInMonth = DAYS_IN_MONTH[month - 1] - if (month === 2 && isLeapYear(year)) { - ++daysInMonth - } - this.updateDayOptions(daysInMonth) - } else { - this.monthSelect.selectedIndex = 0 - this.daySelect.disabled = true - } - if (day) { - const daysInMonth = DAYS_IN_MONTH[this.monthSelect.selectedIndex - 1] - this.daySelect.selectedIndex = day > daysInMonth ? 0 : day - } else { - this.daySelect.selectedIndex = 0 - } - } - - /** - * Updates day select options, hiding days greater than the given amount. + /** + * @param {HTMLFormElement} formElem + * @param {string} workOlid + * @param {string|null} editionKey + * @param {string|null} lastReadDate + * @param {number|null} eventId + */ + constructor( + formElem, + workOlid, + editionKey = null, + lastReadDate = null, + eventId = null, + ) { + this.rootElem = formElem; + this.workOlid = workOlid; + this.editionKey = editionKey; + this.lastReadDate = lastReadDate; + this.eventId = eventId; + + /** + * Reference to hidden `event_type` form input. * - * @param {number} daysInMonth + * @type {HTMLInputElement|undefined} */ - updateDayOptions(daysInMonth) { - for (let i = 0; i < this.daySelect.options.length; ++i) { - if (i <= daysInMonth) { - this.daySelect.options[i].classList.remove('hidden') - } else { - this.daySelect.options[i].classList.add('hidden') - } - } - } + this.eventTypeInput = this.rootElem.querySelector('input[name=event_type]'); /** - * Resets the form. + * Reference to hidden `event_id` form input. * - * Unsets the `event_id` input value, hides the delete button, and - * resets the date select elements to their default values. - */ - resetForm() { - this.setEventId('') - this.updateSelectedDate() - this.hideDeleteButton() - } - - /** - * Shows this form's delete button. - */ - showDeleteButton() { - this.deleteButton.classList.remove('invisible') - } - - /** - * Hides this form's delete button. + * @type {HTMLInputElement|undefined} */ - hideDeleteButton() { - this.deleteButton.classList.add('invisible') - } + this.eventIdInput = this.rootElem.querySelector('input[name=event_id]'); /** - * Returns the numeric value of the selected year. + * Reference to hidden `edition_key` form input. * - * @returns {number|null} The selected year, or `null` if none selected + * @type {HTMLInputElement} */ - getSelectedYear() { - return this.yearSelect.selectedIndex ? Number(this.yearSelect.value) : null - } + this.editionKeyInput = this.rootElem.querySelector( + 'input[name=edition_key]', + ); /** - * Returns the numeric value of the selected month. + * Reference to the form's year `select` element. * - * @returns {number|null} The selected month, or `null` if none selected + * @type {HTMLSelectElement} */ - getSelectedMonth() { - return this.monthSelect.selectedIndex || null - } + this.yearSelect = this.rootElem.querySelector('select[name=year]'); /** - * Returns the numeric value of the selected day. + * Reference to the form's month `select` element. * - * @returns {number|null} The selected day, or `null` if none selected + * @type {HTMLSelectElement} */ - getSelectedDay() { - return this.daySelect.selectedIndex || null - } + this.monthSelect = this.rootElem.querySelector('select[name=month]'); /** - * Returns the value of this form's `event_id` input. + * Reference to the form's day `select` element. * - * @returns {string} + * @type {HTMLSelectElement} */ - getEventId() { - return this.eventIdInput.value - } + this.daySelect = this.rootElem.querySelector('select[name=day]'); /** - * Updates the value of the form's `event_id` input. - * - * @param value + * Reference to the form's submit button. + * @type {HTMLButtonElement} */ - setEventId(value) { - this.eventIdInput.value = value - } + this.submitButton = this.rootElem.querySelector('.check-in__submit-btn'); /** - * Returns the value of this form's `event_type` input. + * Reference to the form's delete button. * - * @returns {string} - */ - getEventType() { - return this.eventTypeInput.value - } - - /** - * Returns the value of the form's edition key input. - * - * @returns {string} - */ - getEditionKey() { - return this.editionKeyInput.value - } - - /** - * Returns this form's `action` - * - * @returns {string} - */ - getFormAction() { - return this.rootElem.action - } - - /** - * Returns a reference to this check-in form. - * - * @returns {HTMLFormElement} - */ - getRootElement() { - return this.rootElem - } + * @type {HTMLButtonElement} + */ + this.deleteButton = this.rootElem.querySelector('.check-in__delete-btn'); + } + + initialize() { + // Set form's action + this.rootElem.action = `/works/${this.workOlid}/check-ins.json`; + // Set form's event ID + if (this.eventId) { + this.setEventId(this.eventId); + this.showDeleteButton(); + } + // Set form's edition_key + if (this.editionKey) { + this.editionKeyInput.value = this.editionKey; + } + // Set date select elements to the last read date + const [yearString, monthString, dayString] = this.lastReadDate + ? this.lastReadDate.split('-') + : [null, null, null]; + this.updateSelectedDate( + Number(yearString), + Number(monthString), + Number(dayString), + ); + + // Update form for new years day + const currentYear = new Date().getFullYear(); + const hiddenYear = this.yearSelect.querySelector('.show-if-local-year'); + // The year select element has a hidden option for next year. This + // option is shown on 1 January if the client's local year is different + // from the server's local year. + if (Number(hiddenYear.value) === currentYear) { + hiddenYear.classList.remove('hidden'); + } + + // Associate labels with select elements + const yearLabel = this.rootElem.querySelector('.check-in__year-label'); + const yearSelectId = `year-select-${this.workOlid}`; + this.yearSelect.id = yearSelectId; + yearLabel.htmlFor = yearSelectId; + + const monthLabel = this.rootElem.querySelector('.check-in__month-label'); + const monthSelectId = `month-select-${this.workOlid}`; + this.monthSelect.id = monthSelectId; + monthLabel.htmlFor = monthSelectId; + + const dayLabel = this.rootElem.querySelector('.check-in__day-label'); + const daySelectId = `day-select-${this.workOlid}`; + this.daySelect.id = daySelectId; + dayLabel.htmlFor = daySelectId; + + // Add listeners to form elements: + this.yearSelect.addEventListener('change', () => { + this.onDateSelectionChange(); + }); + this.monthSelect.addEventListener('change', () => { + this.onDateSelectionChange(); + }); + this.deleteButton.addEventListener('click', (event) => { + event.preventDefault(); + const deleteEvent = new CustomEvent('delete-check-in'); + this.rootElem.dispatchEvent(deleteEvent); + }); + this.submitButton.addEventListener('click', (event) => { + event.preventDefault(); + const submitEvent = new CustomEvent('submit-check-in', { + detail: { + year: this.getSelectedYear(), + month: this.getSelectedMonth(), + day: this.getSelectedDay(), + }, + }); + this.rootElem.dispatchEvent(submitEvent); + }); + const todayLink = this.rootElem.querySelector('.check-in__today'); + todayLink.addEventListener('click', () => { + // Get today's date + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth() + 1; + const day = now.getDate(); + + this.updateSelectedDate(year, month, day); + }); + } + + /** + * Gets currently selected date, then updates the form. + */ + onDateSelectionChange() { + const year = this.yearSelect.selectedIndex + ? Number(this.yearSelect.value) + : null; + this.updateSelectedDate( + year, + this.monthSelect.selectedIndex, + this.daySelect.selectedIndex, + ); + } + + /** + * Updates date select elements based on the given year, month, and day. + * + * @param {number|null} year + * @param {number|null} month + * @param {number|null} day + */ + updateSelectedDate(year = null, month = null, day = null) { + if (!month) { + day = null; + } + if (!year) { + month = null; + day = null; + } + + if (year) { + this.yearSelect.value = year || ''; + this.monthSelect.disabled = false; + this.submitButton.disabled = false; + } else { + this.yearSelect.selectedIndex = 0; + this.monthSelect.disabled = true; + this.submitButton.disabled = true; + } + if (month) { + this.monthSelect.value = month || ''; + this.daySelect.disabled = false; + + // Update daySelect options for month/leap year + let daysInMonth = DAYS_IN_MONTH[month - 1]; + if (month === 2 && isLeapYear(year)) { + ++daysInMonth; + } + this.updateDayOptions(daysInMonth); + } else { + this.monthSelect.selectedIndex = 0; + this.daySelect.disabled = true; + } + if (day) { + const daysInMonth = DAYS_IN_MONTH[this.monthSelect.selectedIndex - 1]; + this.daySelect.selectedIndex = day > daysInMonth ? 0 : day; + } else { + this.daySelect.selectedIndex = 0; + } + } + + /** + * Updates day select options, hiding days greater than the given amount. + * + * @param {number} daysInMonth + */ + updateDayOptions(daysInMonth) { + for (let i = 0; i < this.daySelect.options.length; ++i) { + if (i <= daysInMonth) { + this.daySelect.options[i].classList.remove('hidden'); + } else { + this.daySelect.options[i].classList.add('hidden'); + } + } + } + + /** + * Resets the form. + * + * Unsets the `event_id` input value, hides the delete button, and + * resets the date select elements to their default values. + */ + resetForm() { + this.setEventId(''); + this.updateSelectedDate(); + this.hideDeleteButton(); + } + + /** + * Shows this form's delete button. + */ + showDeleteButton() { + this.deleteButton.classList.remove('invisible'); + } + + /** + * Hides this form's delete button. + */ + hideDeleteButton() { + this.deleteButton.classList.add('invisible'); + } + + /** + * Returns the numeric value of the selected year. + * + * @returns {number|null} The selected year, or `null` if none selected + */ + getSelectedYear() { + return this.yearSelect.selectedIndex ? Number(this.yearSelect.value) : null; + } + + /** + * Returns the numeric value of the selected month. + * + * @returns {number|null} The selected month, or `null` if none selected + */ + getSelectedMonth() { + return this.monthSelect.selectedIndex || null; + } + + /** + * Returns the numeric value of the selected day. + * + * @returns {number|null} The selected day, or `null` if none selected + */ + getSelectedDay() { + return this.daySelect.selectedIndex || null; + } + + /** + * Returns the value of this form's `event_id` input. + * + * @returns {string} + */ + getEventId() { + return this.eventIdInput.value; + } + + /** + * Updates the value of the form's `event_id` input. + * + * @param value + */ + setEventId(value) { + this.eventIdInput.value = value; + } + + /** + * Returns the value of this form's `event_type` input. + * + * @returns {string} + */ + getEventType() { + return this.eventTypeInput.value; + } + + /** + * Returns the value of the form's edition key input. + * + * @returns {string} + */ + getEditionKey() { + return this.editionKeyInput.value; + } + + /** + * Returns this form's `action` + * + * @returns {string} + */ + getFormAction() { + return this.rootElem.action; + } + + /** + * Returns a reference to this check-in form. + * + * @returns {HTMLFormElement} + */ + getRootElement() { + return this.rootElem; + } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js index c4ebbf1b2ed..dbc68b5ce87 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js @@ -3,13 +3,16 @@ * @module my-books/MyBooksDropper/ReadingLists */ import 'jquery-colorbox'; -import myBooksStore from '../store' -import { addItem, removeItem } from '../../lists/ListService' -import { attachNewActiveShowcaseItem, toggleActiveShowcaseItems } from '../../lists/ShowcaseItem' -import { FadingToast } from '../../Toast' +import { addItem, removeItem } from '../../lists/ListService'; +import { + attachNewActiveShowcaseItem, + toggleActiveShowcaseItems, +} from '../../lists/ShowcaseItem'; +import { FadingToast } from '../../Toast'; +import myBooksStore from '../store'; -const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png' +const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; /** * Represents a single My Books dropper's list affordances, and defines their @@ -18,354 +21,385 @@ const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png' * @class */ export class ReadingLists { + /** + * Adds functionality to the given dropper's list affordances. + * @param {HTMLElement} dropper + */ + constructor(dropper) { /** - * Adds functionality to the given dropper's list affordances. - * @param {HTMLElement} dropper - */ - constructor(dropper) { - /** - * References the given My Books Dropper root element. - * - * @member {HTMLElement} - */ - this.dropper = dropper - - /** - * Reference to the "Use work" checkbox. - * - * @member {HTMLElement|null} - */ - this.workCheckBox = dropper.querySelector('.work-checkbox') - if (this.workCheckBox) { - // Uncheck "Use work" checkbox on page refresh - this.workCheckBox.checked = false - } - - /** - * Reference to the "My Reading Lists" section of the dropdown content. - * - * @member {HTMLElement} - */ - this.dropperListsElement = dropper.querySelector('.my-lists') - - /** - * Key of the document that will be added to or removed from a list. - * - * @member {string} - */ - this.seedKey = this.dropperListsElement.dataset.seedKey - - /** - * Key of the work associated with this dropper. Will be an empty - * string if no work is associated. - * - * @member {string} - */ - this.workKey = this.dropperListsElement.dataset.workKey - - /** - * The patron's user key. - * - * @member {string} - */ - this.userKey = this.dropperListsElement.dataset.userKey - - /** - * Stores information about a single list. - * - * @typedef ActiveListData - * @type {object} - * @property {string} title The title of the list - * @property {string} coverUrl URL for the seed's image - * @property {boolean} itemOnList True if the list contains the default seed key - * @property {boolean} workOnList True if the list contains a reference to a work - * @property {HTMLElement} dropperListAffordance Reference to the "Add to list" dropdown affordance - */ - /** - * Maps list keys to objects containing more data about the list. - * - * @member {Record<string, ActiveListData>} - */ - this.patronLists = {} - } - - /** - * Adds functionality to all of the dropper's list affordances. + * References the given My Books Dropper root element. + * + * @member {HTMLElement} */ - initialize() { - this.initModifyListAffordances(this.dropper.querySelectorAll('.modify-list')) - - const openListModalButton = this.dropper.querySelector('.create-new-list') - - if (openListModalButton) { - this.addOpenListModalClickListener(openListModalButton) - } - - if (this.workCheckBox) { - this.workCheckBox.addEventListener('click', () => { - this.updateListDisplays() - toggleActiveShowcaseItems(this.workCheckBox.checked) - }) - } - } + this.dropper = dropper; /** - * Updates dropdown list affordances when an update occurs. + * Reference to the "Use work" checkbox. + * + * @member {HTMLElement|null} */ - updateListDisplays() { - const isWorkSelected = this.workCheckBox && this.workCheckBox.checked - for (const key of Object.keys(this.patronLists)) { - const listData = this.patronLists[key] - - if (isWorkSelected) { - this.toggleDisplayedType(listData.workOnList, key) - } else { - this.toggleDisplayedType(listData.itemOnList, key) - } - } + this.workCheckBox = dropper.querySelector('.work-checkbox'); + if (this.workCheckBox) { + // Uncheck "Use work" checkbox on page refresh + this.workCheckBox.checked = false; } /** - * Changes list affordance visibility in the dropper and "Already list" - * list based on an item's membership to the given list. - * - * If the item is on the list, the "Already list" list affordance is displayed - * and the dropdown affordance will display a checkmark. + * Reference to the "My Reading Lists" section of the dropdown content. * - * @param {boolean} isListMember True if the item is on the list - * @param {string} listKey Unique identifier for a list + * @member {HTMLElement} */ - toggleDisplayedType(isListMember, listKey) { - const listData = this.patronLists[listKey] - - if (isListMember) { - listData.dropperListAffordance.classList.add('list--active') - } else { - listData.dropperListAffordance.classList.remove('list--active') - } - } + this.dropperListsElement = dropper.querySelector('.my-lists'); /** - * Hydrates the given dropdown list affordance elements and stores list data. + * Key of the document that will be added to or removed from a list. * - * Each given element is decorated with additional information about the list. - * This method also populates the patronLists record. - * - * @param {NodeList<HTMLElement>} modifyListElements + * @member {string} */ - initModifyListAffordances(modifyListElements) { - for (const elem of modifyListElements) { - const listItemKeys = elem.dataset.listItems - const listKey = elem.dataset.listKey - const itemOnList = listItemKeys.includes(this.seedKey) - const elemParent = elem.parentElement - - this.patronLists[listKey] = { - title: elem.innerText, - coverUrl: elem.dataset.listCoverUrl, - itemOnList: itemOnList, - dropperListAffordance: elemParent, // The .list element - } - if (!this.patronLists[listKey].coverUrl) { - this.patronLists[listKey].coverUrl = DEFAULT_COVER_URL - } - if (this.workCheckBox) { - // Check for work key membership: - const workOnList = listItemKeys.includes(this.workKey) - this.patronLists[listKey].workOnList = workOnList - - if (this.workCheckBox.checked) { - if (workOnList) { - elemParent.classList.add('list--active') - } - } else { - if (itemOnList) { - elemParent.classList.add('list--active') - } - } - } else { - if (itemOnList) { - elemParent.classList.add('list--active') - } - } - - elem.addEventListener('click', (event) => { - event.preventDefault() - const isAddingItem = !this.patronLists[listKey].dropperListAffordance.classList.contains('list--active') - this.modifyList(listKey, isAddingItem) - }) - } - } + this.seedKey = this.dropperListsElement.dataset.seedKey; /** - * Adds or removes a document to or from the list identified by the given key. + * Key of the work associated with this dropper. Will be an empty + * string if no work is associated. * - * @async - * @param {string} listKey Unique key for list - * @param {boolean} isAddingItem `true` if an item is being added to a list + * @member {string} */ - async modifyList(listKey, isAddingItem) { - let seed - const isWork = this.workCheckBox && this.workCheckBox.checked - - // Seed will be a string if its type is 'subject' - const isSubjectSeed = this.seedKey[0] !== '/' - - if (isWork) { - seed = { key: this.workKey } - } else if (isSubjectSeed) { - seed = this.seedKey - } else { - seed = { key: this.seedKey } - } - - const makeChange = isAddingItem ? addItem : removeItem - this.patronLists[listKey].dropperListAffordance.classList.remove('list--active') - this.patronLists[listKey].dropperListAffordance.classList.add('list--pending') - - await makeChange(listKey, seed) - .then((response) => { - if (response.status >= 400) { - throw new Error('List update failed') - } - response.json() - }) - .then(() => { - this.updateViewAfterModifyingList(listKey, isWork, isAddingItem) - - const seedKey = isWork ? this.workKey : this.seedKey - if (isAddingItem) { - // make new active showcase item - const listTitle = this.patronLists[listKey].title - attachNewActiveShowcaseItem(listKey, seedKey, listTitle, this.patronLists[listKey].coverUrl) - } else { - // remove existing showcase items - const showcases = myBooksStore.getShowcases() - const matchingShowcases = showcases.filter((item) => item.listKey === listKey && item.seedKey === seedKey) - for (const item of matchingShowcases) { - item.removeSelf() - } - } - }) - .catch(() => { - if (!isAddingItem) { - // Replace check mark if patron was removing an item from a list - this.patronLists[listKey].dropperListAffordance.classList.add('list--active') - } - new FadingToast('Could not update list. Please try again later.').show() - }) - .finally(() => this.patronLists[listKey].dropperListAffordance.classList.remove('list--pending')) - } + this.workKey = this.dropperListsElement.dataset.workKey; /** - * Updates `patronLists` with the new list membership information, - * then updates the view. + * The patron's user key. * - * @param {string} listKey Unique identifier for the modified list - * @param {boolean} isWork `true` if a work was added or removed - * @param {boolean} wasItemAdded `true` if item was added to list + * @member {string} */ - updateViewAfterModifyingList(listKey, isWork, wasItemAdded) { - if (isWork) { - this.patronLists[listKey].workOnList = wasItemAdded - } else { - this.patronLists[listKey].itemOnList = wasItemAdded - } - - this.updateListDisplays() - } + this.userKey = this.dropperListsElement.dataset.userKey; /** - * Adds click listener to the given "Create a new list" button. - * - * When the button is clicked, a modal containing the list creation form - * is displayed. When the modal is closed, the form's inputs are cleared. + * Stores information about a single list. * - * @param {HTMLElement} openListModalButton + * @typedef ActiveListData + * @type {object} + * @property {string} title The title of the list + * @property {string} coverUrl URL for the seed's image + * @property {boolean} itemOnList True if the list contains the default seed key + * @property {boolean} workOnList True if the list contains a reference to a work + * @property {HTMLElement} dropperListAffordance Reference to the "Add to list" dropdown affordance */ - addOpenListModalClickListener(openListModalButton) { - openListModalButton.addEventListener('click', (event) => { - event.preventDefault() - - $.colorbox({ - inline: true, - opacity: '0.5', - href: '#addList' - }) - }) - } - /** - * Adds new entry to `patronLists` record and updates list dropdown. + * Maps list keys to objects containing more data about the list. * - * Creates and hydrates an "Add to list" dropdown affordance. - * - * @param {string} listKey Unique identifier for the new list - * @param {string} listTitle Title of the list - * @param {boolean} isActive `True` if this dropper's seed is on the list - * @param {string} coverUrl URL for the list's cover image + * @member {Record<string, ActiveListData>} */ - onListCreationSuccess(listKey, listTitle, isActive, coverUrl) { - const dropperListAffordance = this.createDropdownListAffordance(listKey, listTitle, isActive) + this.patronLists = {}; + } - this.patronLists[listKey] = { - title: listTitle, - coverUrl: coverUrl, - dropperListAffordance: dropperListAffordance - } + /** + * Adds functionality to all of the dropper's list affordances. + */ + initialize() { + this.initModifyListAffordances( + this.dropper.querySelectorAll('.modify-list'), + ); - if (isActive) { - if (this.workCheckBox && this.workCheckBox.checked) { - this.patronLists[listKey].itemOnList = false - this.patronLists[listKey].workOnList = true - } else { - this.patronLists[listKey].itemOnList = true - this.patronLists[listKey].workOnList = false - } - } + const openListModalButton = this.dropper.querySelector('.create-new-list'); + + if (openListModalButton) { + this.addOpenListModalClickListener(openListModalButton); } - /** - * Creates and hydrates a new "Add to list" dropdown affordance. - * - * @param {string} listKey Unique identifier for a list - * @param {string} listTitle The list's title - * @param {boolean} isActive `true` if the seed is on this list - * @returns {HTMLElement} Reference to the newly created element - */ - createDropdownListAffordance(listKey, listTitle, isActive) { - const itemMarkUp = `<span class="list__status-indicator"></span> - <a href="${listKey}" class="modify-list dropper__close" data-list-cover-url="${listKey}" data-list-key="${listKey}">${listTitle}</a> - ` - const p = document.createElement('p') - p.classList.add('list') - if (isActive) { - p.classList.add('list--active') + if (this.workCheckBox) { + this.workCheckBox.addEventListener('click', () => { + this.updateListDisplays(); + toggleActiveShowcaseItems(this.workCheckBox.checked); + }); + } + } + + /** + * Updates dropdown list affordances when an update occurs. + */ + updateListDisplays() { + const isWorkSelected = this.workCheckBox && this.workCheckBox.checked; + for (const key of Object.keys(this.patronLists)) { + const listData = this.patronLists[key]; + + if (isWorkSelected) { + this.toggleDisplayedType(listData.workOnList, key); + } else { + this.toggleDisplayedType(listData.itemOnList, key); + } + } + } + + /** + * Changes list affordance visibility in the dropper and "Already list" + * list based on an item's membership to the given list. + * + * If the item is on the list, the "Already list" list affordance is displayed + * and the dropdown affordance will display a checkmark. + * + * @param {boolean} isListMember True if the item is on the list + * @param {string} listKey Unique identifier for a list + */ + toggleDisplayedType(isListMember, listKey) { + const listData = this.patronLists[listKey]; + + if (isListMember) { + listData.dropperListAffordance.classList.add('list--active'); + } else { + listData.dropperListAffordance.classList.remove('list--active'); + } + } + + /** + * Hydrates the given dropdown list affordance elements and stores list data. + * + * Each given element is decorated with additional information about the list. + * This method also populates the patronLists record. + * + * @param {NodeList<HTMLElement>} modifyListElements + */ + initModifyListAffordances(modifyListElements) { + for (const elem of modifyListElements) { + const listItemKeys = elem.dataset.listItems; + const listKey = elem.dataset.listKey; + const itemOnList = listItemKeys.includes(this.seedKey); + const elemParent = elem.parentElement; + + this.patronLists[listKey] = { + title: elem.innerText, + coverUrl: elem.dataset.listCoverUrl, + itemOnList: itemOnList, + dropperListAffordance: elemParent, // The .list element + }; + if (!this.patronLists[listKey].coverUrl) { + this.patronLists[listKey].coverUrl = DEFAULT_COVER_URL; + } + if (this.workCheckBox) { + // Check for work key membership: + const workOnList = listItemKeys.includes(this.workKey); + this.patronLists[listKey].workOnList = workOnList; + + if (this.workCheckBox.checked) { + if (workOnList) { + elemParent.classList.add('list--active'); + } + } else { + if (itemOnList) { + elemParent.classList.add('list--active'); + } } - p.innerHTML = itemMarkUp - this.dropperListsElement.appendChild(p) - const listAffordance = p.querySelector('.modify-list') - - listAffordance.addEventListener('click', (event) => { - event.preventDefault() - const isAddingItem = !this.patronLists[listKey].dropperListAffordance.classList.contains('list--active') - this.modifyList(listKey, isAddingItem) - }) - - return p + } else { + if (itemOnList) { + elemParent.classList.add('list--active'); + } + } + + elem.addEventListener('click', (event) => { + event.preventDefault(); + const isAddingItem = + !this.patronLists[listKey].dropperListAffordance.classList.contains( + 'list--active', + ); + this.modifyList(listKey, isAddingItem); + }); + } + } + + /** + * Adds or removes a document to or from the list identified by the given key. + * + * @async + * @param {string} listKey Unique key for list + * @param {boolean} isAddingItem `true` if an item is being added to a list + */ + async modifyList(listKey, isAddingItem) { + let seed; + const isWork = this.workCheckBox && this.workCheckBox.checked; + + // Seed will be a string if its type is 'subject' + const isSubjectSeed = this.seedKey[0] !== '/'; + + if (isWork) { + seed = { key: this.workKey }; + } else if (isSubjectSeed) { + seed = this.seedKey; + } else { + seed = { key: this.seedKey }; } - /** - * Returns the seed of the object that can be added to this list. - * - * @returns {string} The seed key - */ - getSeed() { - if (this.workCheckBox && this.workCheckBox.checked) { - // seed is the work key: - return this.workKey + const makeChange = isAddingItem ? addItem : removeItem; + this.patronLists[listKey].dropperListAffordance.classList.remove( + 'list--active', + ); + this.patronLists[listKey].dropperListAffordance.classList.add( + 'list--pending', + ); + + await makeChange(listKey, seed) + .then((response) => { + if (response.status >= 400) { + throw new Error('List update failed'); } + response.json(); + }) + .then(() => { + this.updateViewAfterModifyingList(listKey, isWork, isAddingItem); + + const seedKey = isWork ? this.workKey : this.seedKey; + if (isAddingItem) { + // make new active showcase item + const listTitle = this.patronLists[listKey].title; + attachNewActiveShowcaseItem( + listKey, + seedKey, + listTitle, + this.patronLists[listKey].coverUrl, + ); + } else { + // remove existing showcase items + const showcases = myBooksStore.getShowcases(); + const matchingShowcases = showcases.filter( + (item) => item.listKey === listKey && item.seedKey === seedKey, + ); + for (const item of matchingShowcases) { + item.removeSelf(); + } + } + }) + .catch(() => { + if (!isAddingItem) { + // Replace check mark if patron was removing an item from a list + this.patronLists[listKey].dropperListAffordance.classList.add( + 'list--active', + ); + } + new FadingToast( + 'Could not update list. Please try again later.', + ).show(); + }) + .finally(() => + this.patronLists[listKey].dropperListAffordance.classList.remove( + 'list--pending', + ), + ); + } + + /** + * Updates `patronLists` with the new list membership information, + * then updates the view. + * + * @param {string} listKey Unique identifier for the modified list + * @param {boolean} isWork `true` if a work was added or removed + * @param {boolean} wasItemAdded `true` if item was added to list + */ + updateViewAfterModifyingList(listKey, isWork, wasItemAdded) { + if (isWork) { + this.patronLists[listKey].workOnList = wasItemAdded; + } else { + this.patronLists[listKey].itemOnList = wasItemAdded; + } - return this.seedKey + this.updateListDisplays(); + } + + /** + * Adds click listener to the given "Create a new list" button. + * + * When the button is clicked, a modal containing the list creation form + * is displayed. When the modal is closed, the form's inputs are cleared. + * + * @param {HTMLElement} openListModalButton + */ + addOpenListModalClickListener(openListModalButton) { + openListModalButton.addEventListener('click', (event) => { + event.preventDefault(); + + $.colorbox({ + inline: true, + opacity: '0.5', + href: '#addList', + }); + }); + } + + /** + * Adds new entry to `patronLists` record and updates list dropdown. + * + * Creates and hydrates an "Add to list" dropdown affordance. + * + * @param {string} listKey Unique identifier for the new list + * @param {string} listTitle Title of the list + * @param {boolean} isActive `True` if this dropper's seed is on the list + * @param {string} coverUrl URL for the list's cover image + */ + onListCreationSuccess(listKey, listTitle, isActive, coverUrl) { + const dropperListAffordance = this.createDropdownListAffordance( + listKey, + listTitle, + isActive, + ); + + this.patronLists[listKey] = { + title: listTitle, + coverUrl: coverUrl, + dropperListAffordance: dropperListAffordance, + }; + + if (isActive) { + if (this.workCheckBox && this.workCheckBox.checked) { + this.patronLists[listKey].itemOnList = false; + this.patronLists[listKey].workOnList = true; + } else { + this.patronLists[listKey].itemOnList = true; + this.patronLists[listKey].workOnList = false; + } } + } + + /** + * Creates and hydrates a new "Add to list" dropdown affordance. + * + * @param {string} listKey Unique identifier for a list + * @param {string} listTitle The list's title + * @param {boolean} isActive `true` if the seed is on this list + * @returns {HTMLElement} Reference to the newly created element + */ + createDropdownListAffordance(listKey, listTitle, isActive) { + const itemMarkUp = `<span class="list__status-indicator"></span> + <a href="${listKey}" class="modify-list dropper__close" data-list-cover-url="${listKey}" data-list-key="${listKey}">${listTitle}</a> + `; + const p = document.createElement('p'); + p.classList.add('list'); + if (isActive) { + p.classList.add('list--active'); + } + p.innerHTML = itemMarkUp; + this.dropperListsElement.appendChild(p); + const listAffordance = p.querySelector('.modify-list'); + + listAffordance.addEventListener('click', (event) => { + event.preventDefault(); + const isAddingItem = + !this.patronLists[listKey].dropperListAffordance.classList.contains( + 'list--active', + ); + this.modifyList(listKey, isAddingItem); + }); + + return p; + } + + /** + * Returns the seed of the object that can be added to this list. + * + * @returns {string} The seed key + */ + getSeed() { + if (this.workCheckBox && this.workCheckBox.checked) { + // seed is the work key: + return this.workKey; + } + + return this.seedKey; + } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLogForms.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLogForms.js index 685127fecc7..af9028388bb 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLogForms.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLogForms.js @@ -3,7 +3,6 @@ * @module my-books/MyBooksDropper/ReadingLogForms */ - /** * @typedef {string} ReadingLogShelf */ @@ -13,10 +12,10 @@ * @enum {ReadingLogShelf} */ export const ReadingLogShelves = { - WANT_TO_READ: '1', - CURRENTLY_READING: '2', - ALREADY_READ: '3' -} + WANT_TO_READ: '1', + CURRENTLY_READING: '2', + ALREADY_READ: '3', +}; /** * Class representing a dropper's reading log forms. @@ -37,249 +36,269 @@ export const ReadingLogShelves = { * @class */ export class ReadingLogForms { + /** + * Adds functionality to a single dropper's reading log forms. + * + * @param {HTMLElement} dropper + * @param {import('./CheckInComponents')} checkInComponents + * @param {Record<string, CallableFunction>} dropperActionCallbacks + */ + constructor(dropper, checkInComponents, dropperActionCallbacks) { /** - * Adds functionality to a single dropper's reading log forms. + * Contains references to the parent dropper's close and + * toggle functions. These functions are bound to the + * parent dropper element. * - * @param {HTMLElement} dropper - * @param {import('./CheckInComponents')} checkInComponents - * @param {Record<string, CallableFunction>} dropperActionCallbacks + * @member {Record<string, CallableFunction>} */ - constructor(dropper, checkInComponents, dropperActionCallbacks) { - /** - * Contains references to the parent dropper's close and - * toggle functions. These functions are bound to the - * parent dropper element. - * - * @member {Record<string, CallableFunction>} - */ - this.dropperActions = dropperActionCallbacks - - /** - * Reference to each reading log submit button. This includes the - * primary dropper button and the buttons in the dropdown. - * - * @member {NodeList<HTMLElement>} - */ - this.submitButtons = dropper.querySelectorAll('.reading-log button') - - /** - * Reference to this dropper's primary form. - * - * @member {HTMLFormElement} - */ - this.primaryForm = null; - - /** - * Reference to this dropper's primary button. - * - * @member {HTMLButtonElement} - */ - this.primaryButton = null; - - /** - * Reference to this dropper's "Remove from shelf" button. - * - * @member {HTMLButtonElement} - */ - this.removeButton = null; - - for (const button of this.submitButtons) { - if (button.classList.contains('primary-action')) { - this.primaryButton = button - this.primaryForm = button.closest('form') - } - else if (button.classList.contains('remove-from-list')) { // XXX : Rename class `remove-from-shelf`? - this.removeButton = button - } - } - - if (!this.primaryButton) { // This dropper only contains list affordances - this.primaryButton = dropper.querySelector('.primary-action') - } - - /** - * @member {import('./CheckInComponents') | null} - */ - this.checkInComponents = checkInComponents - - this.readingLogForms = dropper.querySelectorAll('form.reading-log') - this.isDropperDisabled = dropper.classList.contains('generic-dropper--disabled') - } + this.dropperActions = dropperActionCallbacks; /** - * Adds click listeners to each of the form's submit buttons. + * Reference to each reading log submit button. This includes the + * primary dropper button and the buttons in the dropdown. * - * If dropper is disabled, no event listeners will be added. + * @member {NodeList<HTMLElement>} */ - initialize() { - if (!this.isDropperDisabled) { - if (this.readingLogForms.length) { - for (const form of this.readingLogForms) { - const submitButton = form.querySelector('button[type=submit]') - submitButton.addEventListener('click', (event) => { - event.preventDefault() - this.updateReadingLog(form) - - // Close the dropper - this.dropperActions.closeDropper() - }) - } - } else { - // Toggle the dropper when there is no "Reading Log" primary action: - this.primaryButton.addEventListener('click', () => { - this.dropperActions.toggleDropper() - }) - } - } - } + this.submitButtons = dropper.querySelectorAll('.reading-log button'); /** - * POSTs the given form and updates the dropper accordingly. + * Reference to this dropper's primary form. * - * @param {HTMLFormElement} form + * @member {HTMLFormElement} */ - updateReadingLog(form) { - let newPrimaryButtonText = this.primaryButton.querySelector('.btn-text').innerText - // XXX: Use i18n strings - this.updatePrimaryButtonText('saving...') - - const formData = new FormData(form) - const url = form.getAttribute('action') - - const hasAddedBook = formData.get('action') === 'add' - - let canUpdateShelf = true - - if (!hasAddedBook && this.checkInComponents && this.checkInComponents.hasReadDate()) { - // XXX: Use i18n strings - canUpdateShelf = confirm('Removing this book from your shelves will delete your check-ins for this work. Continue?') - } - - if (canUpdateShelf) { - fetch(url, { - method: 'post', - body: formData - }) - .then(response => response.json()) - .then((data) => { - if (!('error' in data)) { // XXX: Serve correct HTTP codes to avoid this - this.updateActivatedStatus(hasAddedBook) - - if (hasAddedBook) { - const primaryButtonClicked = form.classList.contains('primary-action') - const newBookshelfId = form.querySelector('input[name=bookshelf_id]').value - - if (!primaryButtonClicked) { - // A book has been added to a shelf chosen from the dropdown. - // The primary form and dropdown selections must now be updated. - const clickedButton = form.querySelector('button[type=submit]') - newPrimaryButtonText = clickedButton.innerText - - this.updatePrimaryBookshelfId(newBookshelfId) - - this.updateDropdownButtonVisibility(clickedButton) - } - - // Update check-ins: - if (this.checkInComponents) { - if (!this.checkInComponents.hasReadDate() && newBookshelfId === ReadingLogShelves.ALREADY_READ) { - this.checkInComponents.showCheckInPrompt() - } else { - this.checkInComponents.hideCheckInPrompt() - } - } - - } else if (this.checkInComponents) { - // Update check-ins: - this.checkInComponents.hideCheckInPrompt() - this.checkInComponents.hideCheckInDisplay() - this.checkInComponents.resetForm() - } - } - - // Remove "saving..." message from button: - this.updatePrimaryButtonText(newPrimaryButtonText) - }) - } else { - // Remove "saving..." message from button if shelf cannot be updated: - this.updatePrimaryButtonText(newPrimaryButtonText) - } - } + this.primaryForm = null; /** - * Updates "active" status of the primary form. - * - * An "active" dropper will display a checkmark in the primary button, and a remove - * button in the dropdown. - * - * The primary form's `action` input is "remove" when the dropper is active, and - * "add" otherwise. + * Reference to this dropper's primary button. * - * @param {boolean} isActivated `true` if the dropper is changing to an "active" status + * @member {HTMLButtonElement} */ - updateActivatedStatus(isActivated) { - if (isActivated) { - this.primaryButton.querySelector('.activated-check').classList.remove('hidden') - this.removeButton.classList.remove('hidden') - this.primaryForm.querySelector('input[name=action]').value = 'remove' - } else { - this.primaryButton.querySelector('.activated-check').classList.add('hidden') - this.removeButton.classList.add('hidden') - this.primaryForm.querySelector('input[name=action]').value = 'add' - } - - this.primaryButton.classList.toggle('activated') - this.primaryButton.classList.toggle('unactivated') - } + this.primaryButton = null; /** - * Sets that primary button's text to the given string. + * Reference to this dropper's "Remove from shelf" button. * - * @param {string} newText + * @member {HTMLButtonElement} */ - updatePrimaryButtonText(newText) { - this.primaryButton.querySelector('.btn-text').innerText = newText + this.removeButton = null; + + for (const button of this.submitButtons) { + if (button.classList.contains('primary-action')) { + this.primaryButton = button; + this.primaryForm = button.closest('form'); + } else if (button.classList.contains('remove-from-list')) { + // XXX : Rename class `remove-from-shelf`? + this.removeButton = button; + } } - /** - * Changes value of primary form's `bookshelf_id` input to the given number. - * - * @param {number} newId - */ - updatePrimaryBookshelfId(newId) { - this.primaryForm.querySelector('input[name=bookshelf_id]').value = newId + if (!this.primaryButton) { + // This dropper only contains list affordances + this.primaryButton = dropper.querySelector('.primary-action'); } /** - * Updates the visibility of dropdown buttons, hiding the given button. - * - * All other dropdown buttons will be visible after this method exits. - * - * @param {HTMLButtonElement} transitioningButton + * @member {import('./CheckInComponents') | null} */ - updateDropdownButtonVisibility(transitioningButton) { - for (const button of this.submitButtons) { - button.classList.remove('hidden') + this.checkInComponents = checkInComponents; + + this.readingLogForms = dropper.querySelectorAll('form.reading-log'); + this.isDropperDisabled = dropper.classList.contains( + 'generic-dropper--disabled', + ); + } + + /** + * Adds click listeners to each of the form's submit buttons. + * + * If dropper is disabled, no event listeners will be added. + */ + initialize() { + if (!this.isDropperDisabled) { + if (this.readingLogForms.length) { + for (const form of this.readingLogForms) { + const submitButton = form.querySelector('button[type=submit]'); + submitButton.addEventListener('click', (event) => { + event.preventDefault(); + this.updateReadingLog(form); + + // Close the dropper + this.dropperActions.closeDropper(); + }); } - - transitioningButton.classList.add('hidden') + } else { + // Toggle the dropper when there is no "Reading Log" primary action: + this.primaryButton.addEventListener('click', () => { + this.dropperActions.toggleDropper(); + }); + } + } + } + + /** + * POSTs the given form and updates the dropper accordingly. + * + * @param {HTMLFormElement} form + */ + updateReadingLog(form) { + let newPrimaryButtonText = + this.primaryButton.querySelector('.btn-text').innerText; + // XXX: Use i18n strings + this.updatePrimaryButtonText('saving...'); + + const formData = new FormData(form); + const url = form.getAttribute('action'); + + const hasAddedBook = formData.get('action') === 'add'; + + let canUpdateShelf = true; + + if ( + !hasAddedBook && + this.checkInComponents && + this.checkInComponents.hasReadDate() + ) { + // XXX: Use i18n strings + canUpdateShelf = confirm( + 'Removing this book from your shelves will delete your check-ins for this work. Continue?', + ); } - /** - * Returns the display string used to denote the given reading log shelf ID. - * - * @param shelfId {ReadingLogShelf} - */ - getDisplayString(shelfId) { - const matchingFormElem = Array.from(this.readingLogForms).find(elem => { - if (elem === this.primaryForm) { - return false + if (canUpdateShelf) { + fetch(url, { + method: 'post', + body: formData, + }) + .then((response) => response.json()) + .then((data) => { + if (!('error' in data)) { + // XXX: Serve correct HTTP codes to avoid this + this.updateActivatedStatus(hasAddedBook); + + if (hasAddedBook) { + const primaryButtonClicked = + form.classList.contains('primary-action'); + const newBookshelfId = form.querySelector( + 'input[name=bookshelf_id]', + ).value; + + if (!primaryButtonClicked) { + // A book has been added to a shelf chosen from the dropdown. + // The primary form and dropdown selections must now be updated. + const clickedButton = form.querySelector('button[type=submit]'); + newPrimaryButtonText = clickedButton.innerText; + + this.updatePrimaryBookshelfId(newBookshelfId); + + this.updateDropdownButtonVisibility(clickedButton); + } + + // Update check-ins: + if (this.checkInComponents) { + if ( + !this.checkInComponents.hasReadDate() && + newBookshelfId === ReadingLogShelves.ALREADY_READ + ) { + this.checkInComponents.showCheckInPrompt(); + } else { + this.checkInComponents.hideCheckInPrompt(); + } + } + } else if (this.checkInComponents) { + // Update check-ins: + this.checkInComponents.hideCheckInPrompt(); + this.checkInComponents.hideCheckInDisplay(); + this.checkInComponents.resetForm(); } - const bookshelfInput = elem.querySelector('input[name=bookshelf_id]') - return shelfId === bookshelfInput.value - }) + } + + // Remove "saving..." message from button: + this.updatePrimaryButtonText(newPrimaryButtonText); + }); + } else { + // Remove "saving..." message from button if shelf cannot be updated: + this.updatePrimaryButtonText(newPrimaryButtonText); + } + } + + /** + * Updates "active" status of the primary form. + * + * An "active" dropper will display a checkmark in the primary button, and a remove + * button in the dropdown. + * + * The primary form's `action` input is "remove" when the dropper is active, and + * "add" otherwise. + * + * @param {boolean} isActivated `true` if the dropper is changing to an "active" status + */ + updateActivatedStatus(isActivated) { + if (isActivated) { + this.primaryButton + .querySelector('.activated-check') + .classList.remove('hidden'); + this.removeButton.classList.remove('hidden'); + this.primaryForm.querySelector('input[name=action]').value = 'remove'; + } else { + this.primaryButton + .querySelector('.activated-check') + .classList.add('hidden'); + this.removeButton.classList.add('hidden'); + this.primaryForm.querySelector('input[name=action]').value = 'add'; + } - const formButton = matchingFormElem.querySelector('button') - return formButton.textContent + this.primaryButton.classList.toggle('activated'); + this.primaryButton.classList.toggle('unactivated'); + } + + /** + * Sets that primary button's text to the given string. + * + * @param {string} newText + */ + updatePrimaryButtonText(newText) { + this.primaryButton.querySelector('.btn-text').innerText = newText; + } + + /** + * Changes value of primary form's `bookshelf_id` input to the given number. + * + * @param {number} newId + */ + updatePrimaryBookshelfId(newId) { + this.primaryForm.querySelector('input[name=bookshelf_id]').value = newId; + } + + /** + * Updates the visibility of dropdown buttons, hiding the given button. + * + * All other dropdown buttons will be visible after this method exits. + * + * @param {HTMLButtonElement} transitioningButton + */ + updateDropdownButtonVisibility(transitioningButton) { + for (const button of this.submitButtons) { + button.classList.remove('hidden'); } + + transitioningButton.classList.add('hidden'); + } + + /** + * Returns the display string used to denote the given reading log shelf ID. + * + * @param shelfId {ReadingLogShelf} + */ + getDisplayString(shelfId) { + const matchingFormElem = Array.from(this.readingLogForms).find((elem) => { + if (elem === this.primaryForm) { + return false; + } + const bookshelfInput = elem.querySelector('input[name=bookshelf_id]'); + return shelfId === bookshelfInput.value; + }); + + const formButton = matchingFormElem.querySelector('button'); + return formButton.textContent; + } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/index.js b/openlibrary/plugins/openlibrary/js/my-books/index.js index 1640be09742..5be7fe28fb5 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/index.js +++ b/openlibrary/plugins/openlibrary/js/my-books/index.js @@ -1,88 +1,99 @@ -import { CreateListForm } from './CreateListForm' -import { MyBooksDropper } from './MyBooksDropper' -import myBooksStore from './store' -import { getListPartials } from '../lists/ListService' -import { ShowcaseItem, createActiveShowcaseItem, toggleActiveShowcaseItems } from '../lists/ShowcaseItem' -import { removeChildren } from '../utils' +import { getListPartials } from '../lists/ListService'; +import { + createActiveShowcaseItem, + ShowcaseItem, + toggleActiveShowcaseItems, +} from '../lists/ShowcaseItem'; +import { removeChildren } from '../utils'; +import { CreateListForm } from './CreateListForm'; +import { MyBooksDropper } from './MyBooksDropper'; +import myBooksStore from './store'; // XXX : jsdoc // XXX : decompose export function initMyBooksAffordances(dropperElements, showcaseElements) { - const showcases = [] - for (const elem of showcaseElements) { - const showcase = new ShowcaseItem(elem) - showcase.initialize() - - showcases.push(showcase) - } - - myBooksStore.setShowcases(showcases) - - const form = document.querySelector('#create-list-form') - const createListForm = new CreateListForm(form) - createListForm.initialize() - - const droppers = [] - const seedKeys = [] - for (const dropper of dropperElements) { - const myBooksDropper = new MyBooksDropper(dropper) - myBooksDropper.initialize() - - droppers.push(myBooksDropper) - seedKeys.push(...myBooksDropper.getSeedKeys()) - } - - // Remove duplicate keys: - const seedKeySet = new Set(seedKeys) - - // Get user key from first Dropper and add to store: - const userKey = droppers[0].readingLists.userKey - myBooksStore.setUserKey(userKey) - myBooksStore.setDroppers(droppers) - - getListPartials() - .then(response => response.json()) - .then((data) => { - // XXX : convert this block to one or two function calls - const listData = data.listData - const activeShowcaseItems = [] - for (const listKey in listData) { - // Check for matches between seed keys and list members - // If match, create new active showcase item - - for (const seedKey of listData[listKey].members) { - if (seedKeySet.has(seedKey)) { - const key = listData[listKey].members[0] - const coverID = key.slice(key.indexOf('OL')) - const cover = `https://covers.openlibrary.org/b/olid/${coverID}-S.jpg` - - activeShowcaseItems.push(createActiveShowcaseItem(listKey, seedKey, listData[listKey].listName, cover)) - } - } - } - - const activeListsShowcaseElem = document.querySelector('.already-lists') - - if (activeListsShowcaseElem) { - // Remove the loading indicator: - removeChildren(activeListsShowcaseElem) - - for (const li of activeShowcaseItems) { - activeListsShowcaseElem.appendChild(li) - - const showcase = new ShowcaseItem(li) - showcase.initialize() - - showcases.push(showcase) - } - toggleActiveShowcaseItems(false) - } - - // Update dropper content: - for (const dropper of droppers) { - dropper.updateReadingLists(data['dropper']) - } - }) + const showcases = []; + for (const elem of showcaseElements) { + const showcase = new ShowcaseItem(elem); + showcase.initialize(); + + showcases.push(showcase); + } + + myBooksStore.setShowcases(showcases); + + const form = document.querySelector('#create-list-form'); + const createListForm = new CreateListForm(form); + createListForm.initialize(); + + const droppers = []; + const seedKeys = []; + for (const dropper of dropperElements) { + const myBooksDropper = new MyBooksDropper(dropper); + myBooksDropper.initialize(); + + droppers.push(myBooksDropper); + seedKeys.push(...myBooksDropper.getSeedKeys()); + } + + // Remove duplicate keys: + const seedKeySet = new Set(seedKeys); + + // Get user key from first Dropper and add to store: + const userKey = droppers[0].readingLists.userKey; + myBooksStore.setUserKey(userKey); + myBooksStore.setDroppers(droppers); + + getListPartials() + .then((response) => response.json()) + .then((data) => { + // XXX : convert this block to one or two function calls + const listData = data.listData; + const activeShowcaseItems = []; + for (const listKey in listData) { + // Check for matches between seed keys and list members + // If match, create new active showcase item + + for (const seedKey of listData[listKey].members) { + if (seedKeySet.has(seedKey)) { + const key = listData[listKey].members[0]; + const coverID = key.slice(key.indexOf('OL')); + const cover = `https://covers.openlibrary.org/b/olid/${coverID}-S.jpg`; + + activeShowcaseItems.push( + createActiveShowcaseItem( + listKey, + seedKey, + listData[listKey].listName, + cover, + ), + ); + } + } + } + + const activeListsShowcaseElem = document.querySelector('.already-lists'); + + if (activeListsShowcaseElem) { + // Remove the loading indicator: + removeChildren(activeListsShowcaseElem); + + for (const li of activeShowcaseItems) { + activeListsShowcaseElem.appendChild(li); + + const showcase = new ShowcaseItem(li); + showcase.initialize(); + + showcases.push(showcase); + } + toggleActiveShowcaseItems(false); + } + + // Update dropper content: + for (const dropper of droppers) { + dropper.updateReadingLists(data['dropper']); + } + }); } /** @@ -92,7 +103,7 @@ export function initMyBooksAffordances(dropperElements, showcaseElements) { * @returns {MyBooksDropper|undefined} */ export function findDropperForWork(workKey) { - return myBooksStore.getDroppers().find(dropper => { - return workKey === dropper.workKey - }) + return myBooksStore.getDroppers().find((dropper) => { + return workKey === dropper.workKey; + }); } diff --git a/openlibrary/plugins/openlibrary/js/my-books/store/index.js b/openlibrary/plugins/openlibrary/js/my-books/store/index.js index a867b9f7659..488129b3d8c 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/store/index.js +++ b/openlibrary/plugins/openlibrary/js/my-books/store/index.js @@ -8,76 +8,76 @@ * @class */ class MyBooksStore { - /** - * Initializes the store. - */ - constructor() { - this._store = { - droppers: [], - showcases: [], - userkey: '', - openDropper: null - } - } + /** + * Initializes the store. + */ + constructor() { + this._store = { + droppers: [], + showcases: [], + userkey: '', + openDropper: null, + }; + } - /** - * @returns {Array<MyBooksDropper>} - */ - getDroppers() { - return this._store.droppers - } + /** + * @returns {Array<MyBooksDropper>} + */ + getDroppers() { + return this._store.droppers; + } - /** - * @param {Array<MyBooksDropper>} droppers - */ - setDroppers(droppers) { - this._store.droppers = droppers - } + /** + * @param {Array<MyBooksDropper>} droppers + */ + setDroppers(droppers) { + this._store.droppers = droppers; + } - /** - * @returns {Array<ShowcaseItem>} - */ - getShowcases() { - return this._store.showcases - } + /** + * @returns {Array<ShowcaseItem>} + */ + getShowcases() { + return this._store.showcases; + } - /** - * @param {Array<ShowcaseItem>} showcases - */ - setShowcases(showcases) { - this._store.showcases = showcases - } + /** + * @param {Array<ShowcaseItem>} showcases + */ + setShowcases(showcases) { + this._store.showcases = showcases; + } - /** - * @returns {string} - */ - getUserKey() { - return this._store.userKey - } + /** + * @returns {string} + */ + getUserKey() { + return this._store.userKey; + } - /** - * @param {string} userKey - */ - setUserKey(userKey) { - this._store.userKey = userKey - } + /** + * @param {string} userKey + */ + setUserKey(userKey) { + this._store.userKey = userKey; + } - /** - * @returns {MyBooksDropper} - */ - getOpenDropper() { - return this._store.openDropper - } + /** + * @returns {MyBooksDropper} + */ + getOpenDropper() { + return this._store.openDropper; + } - /** - * @param {MyBooksDropper} dropper - */ - setOpenDropper(dropper) { - this._store.openDropper = dropper - } + /** + * @param {MyBooksDropper} dropper + */ + setOpenDropper(dropper) { + this._store.openDropper = dropper; + } } -const myBooksStore = new MyBooksStore() -Object.freeze(myBooksStore) +const myBooksStore = new MyBooksStore(); +Object.freeze(myBooksStore); -export default myBooksStore +export default myBooksStore; diff --git a/openlibrary/plugins/openlibrary/js/native-dialog/index.js b/openlibrary/plugins/openlibrary/js/native-dialog/index.js index dffbd21ff6a..e30b2ba6aac 100644 --- a/openlibrary/plugins/openlibrary/js/native-dialog/index.js +++ b/openlibrary/plugins/openlibrary/js/native-dialog/index.js @@ -7,23 +7,26 @@ * @param {HTMLCollection<HTMLDialogElement>} elems */ export function initDialogs(elems) { - for (const elem of elems) { - elem.addEventListener('click', function(event) { - - // Event target exclusions needed for FireFox, which sets mouse positions to zero on - // <select> and <option> clicks - if (isOutOfBounds(event, elem) && event.target.nodeName !== 'SELECT' && event.target.nodeName !== 'OPTION') { - elem.close() - } - }) - elem.addEventListener('close-dialog', function() { - elem.close() - }) - const closeIcon = elem.querySelector('.native-dialog--close') - closeIcon.addEventListener('click', function() { - elem.close() - }) - } + for (const elem of elems) { + elem.addEventListener('click', (event) => { + // Event target exclusions needed for FireFox, which sets mouse positions to zero on + // <select> and <option> clicks + if ( + isOutOfBounds(event, elem) && + event.target.nodeName !== 'SELECT' && + event.target.nodeName !== 'OPTION' + ) { + elem.close(); + } + }); + elem.addEventListener('close-dialog', () => { + elem.close(); + }); + const closeIcon = elem.querySelector('.native-dialog--close'); + closeIcon.addEventListener('click', () => { + elem.close(); + }); + } } /** @@ -34,11 +37,11 @@ export function initDialogs(elems) { * @returns `true` if the click was out of bounds. */ function isOutOfBounds(event, dialog) { - const rect = dialog.getBoundingClientRect() - return ( - event.clientX < rect.left || - event.clientX > rect.right || - event.clientY < rect.top || - event.clientY > rect.bottom - ); + const rect = dialog.getBoundingClientRect(); + return ( + event.clientX < rect.left || + event.clientX > rect.right || + event.clientY < rect.top || + event.clientY > rect.bottom + ); } diff --git a/openlibrary/plugins/openlibrary/js/nonjquery_utils.js b/openlibrary/plugins/openlibrary/js/nonjquery_utils.js index 15d080a9ac6..2276dea46d4 100644 --- a/openlibrary/plugins/openlibrary/js/nonjquery_utils.js +++ b/openlibrary/plugins/openlibrary/js/nonjquery_utils.js @@ -12,21 +12,21 @@ * @param {Boolean} [execAsap] * @returns {Function} */ -export function debounce(func, threshold=100, execAsap=false) { - let timeout; - return function debounced() { - const obj = this, args = arguments; - function delayed() { - if (!execAsap) - func.apply(obj, args); - timeout = null; - } +export function debounce(func, threshold = 100, execAsap = false) { + let timeout; + return function debounced() { + const obj = this, + args = arguments; + function delayed() { + if (!execAsap) func.apply(obj, args); + timeout = null; + } - if (timeout) { - clearTimeout(timeout); - } else if (execAsap) { - func.apply(obj, args); - } - timeout = setTimeout(delayed, threshold); - }; + if (timeout) { + clearTimeout(timeout); + } else if (execAsap) { + func.apply(obj, args); + } + timeout = setTimeout(delayed, threshold); + }; } diff --git a/openlibrary/plugins/openlibrary/js/offline-banner.js b/openlibrary/plugins/openlibrary/js/offline-banner.js index 917579fe51a..30622eb2f46 100644 --- a/openlibrary/plugins/openlibrary/js/offline-banner.js +++ b/openlibrary/plugins/openlibrary/js/offline-banner.js @@ -1,7 +1,6 @@ export function initOfflineBanner() { - - window.addEventListener('offline', () => { - $('#offline-info').slideDown(); - $('#offline-info').fadeTo(5000, 1).slideUp(); - }); + window.addEventListener('offline', () => { + $('#offline-info').slideDown(); + $('#offline-info').fadeTo(5000, 1).slideUp(); + }); } diff --git a/openlibrary/plugins/openlibrary/js/ol.analytics.js b/openlibrary/plugins/openlibrary/js/ol.analytics.js index e9f4e1c820a..bfd776a5072 100644 --- a/openlibrary/plugins/openlibrary/js/ol.analytics.js +++ b/openlibrary/plugins/openlibrary/js/ol.analytics.js @@ -1,56 +1,59 @@ /** -* OpenLibrary-specific convenience functions for use with Archive.org athena.js -* -* Depends on Archive.org athena.js function archive_analytics.send_ping() -* -*/ + * OpenLibrary-specific convenience functions for use with Archive.org athena.js + * + * Depends on Archive.org athena.js function archive_analytics.send_ping() + * + */ export default function initAnalytics() { - var vs, i; - var startTime = new Date(); - if (window.archive_analytics) { - // Setup analytics, depends on script loaded from CDN - window.archive_analytics.set_up_event_tracking(); + var vs, i; + var startTime = new Date(); + if (window.archive_analytics) { + // Setup analytics, depends on script loaded from CDN + window.archive_analytics.set_up_event_tracking(); - window.archive_analytics.ol_send_event_ping = function(values) { - var endTime = new Date(); - window.archive_analytics.send_ping({ - service: 'ol', - kind: 'event', - ec: values['category'], - ea: values['action'], - el: values['label'] || location.pathname, - ev: 1, - loadtime: (endTime.getTime() - startTime.getTime()), - cache_bust: Math.random() - }); - } + window.archive_analytics.ol_send_event_ping = (values) => { + var endTime = new Date(); + window.archive_analytics.send_ping({ + service: 'ol', + kind: 'event', + ec: values['category'], + ea: values['action'], + el: values['label'] || location.pathname, + ev: 1, + loadtime: endTime.getTime() - startTime.getTime(), + cache_bust: Math.random(), + }); + }; - vs = window.archive_analytics.get_data_packets(); - for (i in vs) { - vs[i]['cache_bust']=Math.random(); - vs[i]['server_ms']=$('.analytics-stats-time-calculator').data('time'); - vs[i]['server_name']='ol-web.us.archive.org'; - vs[i]['service']='ol'; - } - if (window.flights){ - window.flights.init(); - } - $(document).on('click', '[data-ol-link-track]', function() { - var category_action = $(this).attr('data-ol-link-track').split('|'); - // for testing, - // console.log(category_action[0], category_action[1]); - window.archive_analytics.ol_send_event_ping({ - category: category_action[0], - action: category_action[1], - label: category_action[2], - }); - }); + vs = window.archive_analytics.get_data_packets(); + for (i in vs) { + vs[i]['cache_bust'] = Math.random(); + vs[i]['server_ms'] = $('.analytics-stats-time-calculator').data('time'); + vs[i]['server_name'] = 'ol-web.us.archive.org'; + vs[i]['service'] = 'ol'; } - window.vs = vs; - - // NOTE: This might cause issues if this script is made async #4474 - window.addEventListener('DOMContentLoaded', function send_analytics_pageview() { - window.archive_analytics.send_pageview({}); + if (window.flights) { + window.flights.init(); + } + $(document).on('click', '[data-ol-link-track]', function () { + var category_action = $(this).attr('data-ol-link-track').split('|'); + // for testing, + // console.log(category_action[0], category_action[1]); + window.archive_analytics.ol_send_event_ping({ + category: category_action[0], + action: category_action[1], + label: category_action[2], + }); }); + } + window.vs = vs; + + // NOTE: This might cause issues if this script is made async #4474 + window.addEventListener( + 'DOMContentLoaded', + function send_analytics_pageview() { + window.archive_analytics.send_pageview({}); + }, + ); } diff --git a/openlibrary/plugins/openlibrary/js/ol.js b/openlibrary/plugins/openlibrary/js/ol.js index e9178bd77c8..a7ecd1716d2 100644 --- a/openlibrary/plugins/openlibrary/js/ol.js +++ b/openlibrary/plugins/openlibrary/js/ol.js @@ -7,49 +7,58 @@ import { SearchModeSelector, mode as searchMode } from './SearchUtils'; Sets the key in the website cookie to the specified value */ function setValueInCookie(key, value) { - document.cookie = `${key}=${value};path=/`; + document.cookie = `${key}=${value};path=/`; } export default function init() { - const urlParams = getJsonFromUrl(location.search); - if (urlParams.mode) { - searchMode.write(urlParams.mode); - } - new SearchBar($('header#header-bar .search-component'), urlParams); + const urlParams = getJsonFromUrl(location.search); + if (urlParams.mode) { + searchMode.write(urlParams.mode); + } + new SearchBar($('header#header-bar .search-component'), urlParams); - if ($('.siteSearch.olform').length) { - // Only applies to search results page (as of writing) - new SearchPage($('.siteSearch.olform'), new SearchModeSelector($('.search-mode'))); - } + if ($('.siteSearch.olform').length) { + // Only applies to search results page (as of writing) + new SearchPage( + $('.siteSearch.olform'), + new SearchModeSelector($('.search-mode')), + ); + } - initBorrowAndReadLinks(); - initWebsiteTranslationOptions(); + initBorrowAndReadLinks(); + initWebsiteTranslationOptions(); } export function initBorrowAndReadLinks() { - // LOADING ONCLICK FUNCTIONS FOR BORROW AND READ LINKS - /* eslint-disable no-unused-vars */ - // used in openlibrary/macros/AvailabilityButton.html and openlibrary/macros/LoanStatus.html - $(function(){ - $('.cta-btn--ia.cta-btn--borrow,.cta-btn--ia.cta-btn--read').on('click', function(){ - $(this).removeClass('cta-btn cta-btn--available').addClass('cta-btn cta-btn--available--load'); - }); - }); - $(function(){ - $('#waitlist_ebook').on('click', function(){ - $(this).removeClass('cta-btn cta-btn--unavailable').addClass('cta-btn cta-btn--unavailable--load'); - }); + // LOADING ONCLICK FUNCTIONS FOR BORROW AND READ LINKS + /* eslint-disable no-unused-vars */ + // used in openlibrary/macros/AvailabilityButton.html and openlibrary/macros/LoanStatus.html + $(() => { + $('.cta-btn--ia.cta-btn--borrow,.cta-btn--ia.cta-btn--read').on( + 'click', + function () { + $(this) + .removeClass('cta-btn cta-btn--available') + .addClass('cta-btn cta-btn--available--load'); + }, + ); + }); + $(() => { + $('#waitlist_ebook').on('click', function () { + $(this) + .removeClass('cta-btn cta-btn--unavailable') + .addClass('cta-btn cta-btn--unavailable--load'); }); + }); - /* eslint-enable no-unused-vars */ + /* eslint-enable no-unused-vars */ } export function initWebsiteTranslationOptions() { - $('.locale-options li a').on('click', function (event) { - event.preventDefault(); - const locale = $(this).data('lang-id'); - setValueInCookie('HTTP_LANG', locale); - location.reload(); - }); - + $('.locale-options li a').on('click', function (event) { + event.preventDefault(); + const locale = $(this).data('lang-id'); + setValueInCookie('HTTP_LANG', locale); + location.reload(); + }); } diff --git a/openlibrary/plugins/openlibrary/js/partner_ol_lib.js b/openlibrary/plugins/openlibrary/js/partner_ol_lib.js index 5748aedfd32..b14b9d1bead 100644 --- a/openlibrary/plugins/openlibrary/js/partner_ol_lib.js +++ b/openlibrary/plugins/openlibrary/js/partner_ol_lib.js @@ -2,16 +2,16 @@ * @param {string} container */ function getIsbnToElementMap(container) { - const reISBN = /(978)?[0-9]{9}[0-9X]/i; - const elements = Array.from(document.querySelectorAll(container)); - const isbnElementMap = {}; - elements.forEach((e) => { - const isbnMatches = e.outerHTML.match(reISBN); - if (isbnMatches) { - isbnElementMap[isbnMatches[0]] = e; - } - }) - return isbnElementMap; + const reISBN = /(978)?[0-9]{9}[0-9X]/i; + const elements = Array.from(document.querySelectorAll(container)); + const isbnElementMap = {}; + elements.forEach((e) => { + const isbnMatches = e.outerHTML.match(reISBN); + if (isbnMatches) { + isbnElementMap[isbnMatches[0]] = e; + } + }); + return isbnElementMap; } /** @@ -19,19 +19,19 @@ function getIsbnToElementMap(container) { * @returns {Promise<Array>} */ async function getAvailabilityDataFromOpenLibrary(isbnList) { - const apiBaseUrl = 'https://openlibrary.org/search.json'; - const apiUrl = `${apiBaseUrl}?fields=*,availability&q=isbn:${isbnList.join('+OR+')}`; - const response = await fetch(apiUrl); - const jsonResponse = await response.json(); - const olDocs = jsonResponse.docs; - const isbnToAvailabilityDataMap = {}; - olDocs.forEach((doc) => { - const isbnList = doc.isbn; - isbnList.forEach((isbn) => { - isbnToAvailabilityDataMap[isbn] = doc?.availability; - }); + const apiBaseUrl = 'https://openlibrary.org/search.json'; + const apiUrl = `${apiBaseUrl}?fields=*,availability&q=isbn:${isbnList.join('+OR+')}`; + const response = await fetch(apiUrl); + const jsonResponse = await response.json(); + const olDocs = jsonResponse.docs; + const isbnToAvailabilityDataMap = {}; + olDocs.forEach((doc) => { + const isbnList = doc.isbn; + isbnList.forEach((isbn) => { + isbnToAvailabilityDataMap[isbn] = doc?.availability; }); - return isbnToAvailabilityDataMap; + }); + return isbnToAvailabilityDataMap; } /** @@ -48,26 +48,30 @@ async function getAvailabilityDataFromOpenLibrary(isbnList) { * }); */ async function addOpenLibraryButtons(options) { - const {bookContainer, selectorToPlaceBtnIn, textOnBtn} = options - if (bookContainer === undefined) { - throw Error( - 'book container must be specified in options for open library buttons to populate!' - ) + const { bookContainer, selectorToPlaceBtnIn, textOnBtn } = options; + if (bookContainer === undefined) { + throw Error( + 'book container must be specified in options for open library buttons to populate!', + ); + } + const foundIsbnElementsMap = getIsbnToElementMap(bookContainer); + const availabilityResults = await getAvailabilityDataFromOpenLibrary( + Object.keys(foundIsbnElementsMap), + ); + Object.keys(foundIsbnElementsMap).map((isbn) => { + const availability = availabilityResults[isbn]; + if (availability && availability.status !== 'error') { + const e = foundIsbnElementsMap[isbn]; + const buttons = selectorToPlaceBtnIn + ? e.querySelector(selectorToPlaceBtnIn) + : e; + const openLibraryBtnLink = document.createElement('a'); + openLibraryBtnLink.href = `https://openlibrary.org/works/${availability.openlibrary_work}`; + openLibraryBtnLink.text = textOnBtn || 'Open Library'; + openLibraryBtnLink.classList.add('openlibrary-btn'); + buttons.append(openLibraryBtnLink); } - const foundIsbnElementsMap = getIsbnToElementMap(bookContainer); - const availabilityResults = await getAvailabilityDataFromOpenLibrary(Object.keys(foundIsbnElementsMap)) - Object.keys(foundIsbnElementsMap).map((isbn) => { - const availability = availabilityResults[isbn] - if (availability && availability.status !== 'error') { - const e = foundIsbnElementsMap[isbn] - const buttons = selectorToPlaceBtnIn ? e.querySelector(selectorToPlaceBtnIn) : e; - const openLibraryBtnLink = document.createElement('a') - openLibraryBtnLink.href = `https://openlibrary.org/works/${availability.openlibrary_work}` - openLibraryBtnLink.text = textOnBtn || 'Open Library' - openLibraryBtnLink.classList.add('openlibrary-btn') - buttons.append(openLibraryBtnLink); - } - }) + }); } // Expose globally so clients can use this method diff --git a/openlibrary/plugins/openlibrary/js/password-toggle.js b/openlibrary/plugins/openlibrary/js/password-toggle.js index dd69c2690be..c325e525f75 100644 --- a/openlibrary/plugins/openlibrary/js/password-toggle.js +++ b/openlibrary/plugins/openlibrary/js/password-toggle.js @@ -4,15 +4,16 @@ * @param {HTMLElement} elem Reference to affordance that toggles a password input's visibility */ export function initPasswordToggling(elem) { - const passwordInput = document.querySelector('input[type=password]') + const passwordInput = document.querySelector('input[type=password]'); - elem.addEventListener('click', () => { - if (passwordInput.type === 'password') { - passwordInput.type = 'text' - elem.querySelector('img').src = '/static/images/icons/icon_eye-open.svg' - } else { - passwordInput.type = 'password' - elem.querySelector('img').src = '/static/images/icons/icon_eye-closed.svg' - } - }) + elem.addEventListener('click', () => { + if (passwordInput.type === 'password') { + passwordInput.type = 'text'; + elem.querySelector('img').src = '/static/images/icons/icon_eye-open.svg'; + } else { + passwordInput.type = 'password'; + elem.querySelector('img').src = + '/static/images/icons/icon_eye-closed.svg'; + } + }); } diff --git a/openlibrary/plugins/openlibrary/js/patron_exports.js b/openlibrary/plugins/openlibrary/js/patron_exports.js index 9315fa07ba4..ed3602f4c24 100644 --- a/openlibrary/plugins/openlibrary/js/patron_exports.js +++ b/openlibrary/plugins/openlibrary/js/patron_exports.js @@ -4,8 +4,8 @@ * @param {HTMLElement} buttonElement */ function disableButton(buttonElement) { - buttonElement.setAttribute('disabled', 'true'); - buttonElement.setAttribute('aria-disabled', 'true'); + buttonElement.setAttribute('disabled', 'true'); + buttonElement.setAttribute('aria-disabled', 'true'); } /** @@ -17,10 +17,10 @@ function disableButton(buttonElement) { * @param {NodeList<HTMLFormElement>} elems */ export function initPatronExportForms(elems) { - elems.forEach((form) => { - const submitButton = form.querySelector('input[type=submit]') - form.addEventListener('submit', () => { - disableButton(submitButton); - }) - }) + elems.forEach((form) => { + const submitButton = form.querySelector('input[type=submit]'); + form.addEventListener('submit', () => { + disableButton(submitButton); + }); + }); } diff --git a/openlibrary/plugins/openlibrary/js/private-button.js b/openlibrary/plugins/openlibrary/js/private-button.js index 4ba965a02b7..cf942c4df04 100644 --- a/openlibrary/plugins/openlibrary/js/private-button.js +++ b/openlibrary/plugins/openlibrary/js/private-button.js @@ -1,12 +1,15 @@ -import { FadingToast } from './Toast' +import { FadingToast } from './Toast'; export function initPrivateButtons(buttons) { - buttons.forEach(button => { - button.addEventListener('click', (event) => { - event.preventDefault(); - const toast = new FadingToast('This patron has not enabled following', null, 3000); - toast.show(); - - }); + buttons.forEach((button) => { + button.addEventListener('click', (event) => { + event.preventDefault(); + const toast = new FadingToast( + 'This patron has not enabled following', + null, + 3000, + ); + toast.show(); }); + }); } diff --git a/openlibrary/plugins/openlibrary/js/python.js b/openlibrary/plugins/openlibrary/js/python.js index 393e6b7fe88..c59bff54be0 100644 --- a/openlibrary/plugins/openlibrary/js/python.js +++ b/openlibrary/plugins/openlibrary/js/python.js @@ -8,31 +8,31 @@ * @return {string} */ export function commify(n) { - var text = n.toString(); - var re = /(\d+)(\d{3})/; + var text = n.toString(); + var re = /(\d+)(\d{3})/; - while (re.test(text)) { - text = text.replace(re, '$1,$2'); - } + while (re.test(text)) { + text = text.replace(re, '$1,$2'); + } - return text; + return text; } // Implementation of Python urllib.urlencode in Javascript. export function urlencode(query) { - var parts = []; - var k; - for (k in query) { - parts.push(`${k}=${query[k]}`); - } - return parts.join('&'); + var parts = []; + var k; + for (k in query) { + parts.push(`${k}=${query[k]}`); + } + return parts.join('&'); } export function slice(array, begin, end) { - var a = []; - var i; - for (i=begin; i < Math.min(array.length, end); i++) { - a.push(array[i]); - } - return a; + var a = []; + var i; + for (i = begin; i < Math.min(array.length, end); i++) { + a.push(array[i]); + } + return a; } diff --git a/openlibrary/plugins/openlibrary/js/reading-goals/index.js b/openlibrary/plugins/openlibrary/js/reading-goals/index.js index f992a545132..f863dd6c248 100644 --- a/openlibrary/plugins/openlibrary/js/reading-goals/index.js +++ b/openlibrary/plugins/openlibrary/js/reading-goals/index.js @@ -1,5 +1,5 @@ -import { initDialogs } from '../native-dialog' -import { buildPartialsUrl } from '../utils' +import { initDialogs } from '../native-dialog'; +import { buildPartialsUrl } from '../utils'; /** * Adds listener to open reading goal modal. @@ -7,19 +7,19 @@ import { buildPartialsUrl } from '../utils' * @param {HTMLCollection<HTMLElement>} links Prompts for adding a reading goal */ export function initYearlyGoalPrompt(links) { - for (const link of links) { - if (!link.classList.contains('goal-set')) { - link.addEventListener('click', onYearlyGoalClick) - } + for (const link of links) { + if (!link.classList.contains('goal-set')) { + link.addEventListener('click', onYearlyGoalClick); } + } } /** * Finds and shows the yearly goal modal. */ function onYearlyGoalClick() { - const yearlyGoalModal = document.querySelector('#yearly-goal-modal') - yearlyGoalModal.showModal() + const yearlyGoalModal = document.querySelector('#yearly-goal-modal'); + yearlyGoalModal.showModal(); } /** @@ -34,13 +34,13 @@ function onYearlyGoalClick() { * @param {HTMLCollection<HTMLElement>} elems ELements which display only the current year */ export function displayLocalYear(elems) { - const localYear = new Date().getFullYear() - for (const elem of elems) { - const serverYear = Number(elem.dataset.serverYear) - if (localYear !== serverYear) { - elem.textContent = localYear - } + const localYear = new Date().getFullYear(); + for (const elem of elems) { + const serverYear = Number(elem.dataset.serverYear); + if (localYear !== serverYear) { + elem.textContent = localYear; } + } } /** @@ -49,11 +49,11 @@ export function displayLocalYear(elems) { * @param {HTMLCollection<HTMLElement>} editLinks Edit goal links */ export function initGoalEditLinks(editLinks) { - for (const link of editLinks) { - const parent = link.closest('.reading-goal-progress') - const modal = parent.querySelector('dialog') - addGoalEditClickListener(link, modal) - } + for (const link of editLinks) { + const parent = link.closest('.reading-goal-progress'); + const modal = parent.querySelector('dialog'); + addGoalEditClickListener(link, modal); + } } /** @@ -65,9 +65,9 @@ export function initGoalEditLinks(editLinks) { * @param {HTMLDialogElement} modal The modal that will be shown */ function addGoalEditClickListener(editLink, modal) { - editLink.addEventListener('click', function() { - modal.showModal() - }) + editLink.addEventListener('click', () => { + modal.showModal(); + }); } /** @@ -77,9 +77,9 @@ function addGoalEditClickListener(editLink, modal) { * @param {HTMLCollection<HTMLElement>} submitButtons Submit goal buttons */ export function initGoalSubmitButtons(submitButtons) { - for (const button of submitButtons) { - addGoalSubmissionListener(button) - } + for (const button of submitButtons) { + addGoalSubmissionListener(button); + } } /** @@ -90,68 +90,77 @@ export function initGoalSubmitButtons(submitButtons) { * @param {HTMLELement} submitButton Reading goal form submit button */ function addGoalSubmissionListener(submitButton) { - submitButton.addEventListener('click', function(event) { - event.preventDefault() + submitButton.addEventListener('click', (event) => { + event.preventDefault(); - const form = submitButton.closest('form') + const form = submitButton.closest('form'); - if (!form.checkValidity()) { - form.reportValidity() - throw new Error('Form invalid') + if (!form.checkValidity()) { + form.reportValidity(); + throw new Error('Form invalid'); + } + const formData = new FormData(form); + + fetch(form.action, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams(formData), + }).then((response) => { + if (!response.ok) { + throw new Error('Failed to set reading goal'); + } + const modal = form.closest('dialog'); + if (modal) { + modal.close(); + } + + const yearlyGoalSections = document.querySelectorAll( + '.yearly-goal-section', + ); + if (formData.get('is_update')) { + // Progress component exists on page + yearlyGoalSections.forEach((yearlyGoalSection) => { + const goalInput = form.querySelector('input[name=goal]'); + const isDeleted = Number(goalInput.value) === 0; + + if (isDeleted) { + const chipGroup = yearlyGoalSection.querySelector('.chip-group'); + const goalContainer = yearlyGoalSection.querySelector( + '#reading-goal-container', + ); + if (chipGroup) { + chipGroup.classList.remove('hidden'); + } + if (goalContainer) { + goalContainer.remove(); + } + // Restore "Set reading goal" link hidden when goal was first set + const setGoalLink = yearlyGoalSection.querySelector( + '.set-reading-goal-link', + ); + if (setGoalLink) { + setGoalLink.classList.remove('hidden'); + } + } else { + const progressComponent = modal.closest('.reading-goal-progress'); + updateProgressComponent( + progressComponent, + Number(formData.get('goal')), + ); + } + }); + } else { + const goalYear = formData.get('year'); + fetchProgressAndUpdateViews(yearlyGoalSections, goalYear); + const banner = document.querySelector('.page-banner-mybooks'); + if (banner) { + banner.remove(); } - const formData = new FormData(form) - - fetch(form.action, { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams(formData) - }) - .then((response) => { - if (!response.ok) { - throw new Error('Failed to set reading goal') - } - const modal = form.closest('dialog') - if (modal) { - modal.close() - } - - const yearlyGoalSections = document.querySelectorAll('.yearly-goal-section') - if (formData.get('is_update')) { // Progress component exists on page - yearlyGoalSections.forEach((yearlyGoalSection) => { - const goalInput = form.querySelector('input[name=goal]') - const isDeleted = Number(goalInput.value) === 0 - - if (isDeleted) { - const chipGroup = yearlyGoalSection.querySelector('.chip-group') - const goalContainer = yearlyGoalSection.querySelector('#reading-goal-container') - if (chipGroup) { - chipGroup.classList.remove('hidden') - } - if (goalContainer) { - goalContainer.remove() - } - // Restore "Set reading goal" link hidden when goal was first set - const setGoalLink = yearlyGoalSection.querySelector('.set-reading-goal-link') - if (setGoalLink) { - setGoalLink.classList.remove('hidden') - } - } else { - const progressComponent = modal.closest('.reading-goal-progress') - updateProgressComponent(progressComponent, Number(formData.get('goal'))) - } - }) - } else { - const goalYear = formData.get('year') - fetchProgressAndUpdateViews(yearlyGoalSections, goalYear) - const banner = document.querySelector('.page-banner-mybooks') - if (banner) { - banner.remove() - } - } - }) - }) + } + }); + }); } /** @@ -162,16 +171,18 @@ function addGoalSubmissionListener(submitButton) { * @param {Number} goal The new reading goal */ function updateProgressComponent(elem, goal) { - // Calculate new percentage: - const booksReadSpan = elem.querySelector('.reading-goal-progress__books-read') - const booksRead = Number(booksReadSpan.textContent) - const percentComplete = Math.floor((booksRead / goal) * 100) - - // Update view: - const goalSpan = elem.querySelector('.reading-goal-progress__goal') - const completedBar = elem.querySelector('.reading-goal-progress__completed') - goalSpan.textContent = goal - completedBar.style.width = `${Math.min(100, percentComplete)}%` + // Calculate new percentage: + const booksReadSpan = elem.querySelector( + '.reading-goal-progress__books-read', + ); + const booksRead = Number(booksReadSpan.textContent); + const percentComplete = Math.floor((booksRead / goal) * 100); + + // Update view: + const goalSpan = elem.querySelector('.reading-goal-progress__goal'); + const completedBar = elem.querySelector('.reading-goal-progress__completed'); + goalSpan.textContent = goal; + completedBar.style.width = `${Math.min(100, percentComplete)}%`; } /** @@ -184,38 +195,42 @@ function updateProgressComponent(elem, goal) { * @param {string} goalYear Year that the goal is set for. */ function fetchProgressAndUpdateViews(yearlyGoalElems, goalYear) { - fetch(buildPartialsUrl('ReadingGoalProgress', {year: goalYear})) - .then((response) => { - if (!response.ok) { - throw new Error('Failed to fetch progress element') - } - return response.json() - }) - .then(function(data) { - const html = data['partials'] - yearlyGoalElems.forEach((yearlyGoalElem) => { - const progress = document.createElement('SPAN') - progress.id = 'reading-goal-container' - progress.innerHTML = html - yearlyGoalElem.appendChild(progress) - - const link = yearlyGoalElem.querySelector('.set-reading-goal-link'); - if (link) { - if (link.classList.contains('li-title-desktop')) { - // Remove click listener in mobile views - link.removeEventListener('click', onYearlyGoalClick) - } else { - // Hide desktop "set 20XX reading goal" link - link.classList.add('hidden'); - } - } - - const progressEditLink = progress.querySelector('.edit-reading-goal-link') - const updateModal = progress.querySelector('dialog') - initDialogs([updateModal]) - addGoalEditClickListener(progressEditLink, updateModal) - const submitButton = updateModal.querySelector('.reading-goal-submit-button') - addGoalSubmissionListener(submitButton) - }) - }) + fetch(buildPartialsUrl('ReadingGoalProgress', { year: goalYear })) + .then((response) => { + if (!response.ok) { + throw new Error('Failed to fetch progress element'); + } + return response.json(); + }) + .then((data) => { + const html = data['partials']; + yearlyGoalElems.forEach((yearlyGoalElem) => { + const progress = document.createElement('SPAN'); + progress.id = 'reading-goal-container'; + progress.innerHTML = html; + yearlyGoalElem.appendChild(progress); + + const link = yearlyGoalElem.querySelector('.set-reading-goal-link'); + if (link) { + if (link.classList.contains('li-title-desktop')) { + // Remove click listener in mobile views + link.removeEventListener('click', onYearlyGoalClick); + } else { + // Hide desktop "set 20XX reading goal" link + link.classList.add('hidden'); + } + } + + const progressEditLink = progress.querySelector( + '.edit-reading-goal-link', + ); + const updateModal = progress.querySelector('dialog'); + initDialogs([updateModal]); + addGoalEditClickListener(progressEditLink, updateModal); + const submitButton = updateModal.querySelector( + '.reading-goal-submit-button', + ); + addGoalSubmissionListener(submitButton); + }); + }); } diff --git a/openlibrary/plugins/openlibrary/js/readinglog_stats.js b/openlibrary/plugins/openlibrary/js/readinglog_stats.js index c3e1f33714d..716530f2c46 100644 --- a/openlibrary/plugins/openlibrary/js/readinglog_stats.js +++ b/openlibrary/plugins/openlibrary/js/readinglog_stats.js @@ -1,13 +1,14 @@ // @ts-check + +import Chart from 'chart.js'; +import entries from 'lodash/entries'; import fromPairs from 'lodash/fromPairs'; -import isUndefined from 'lodash/isUndefined'; +import groupBy from 'lodash/groupBy'; import includes from 'lodash/includes'; +import isUndefined from 'lodash/isUndefined'; import orderBy from 'lodash/orderBy'; -import entries from 'lodash/entries'; -import groupBy from 'lodash/groupBy'; import uniq from 'lodash/uniq'; import uniqBy from 'lodash/uniqBy'; -import Chart from 'chart.js'; import 'chartjs-plugin-datalabels'; /** @@ -40,91 +41,106 @@ import 'chartjs-plugin-datalabels'; * @param {Config} config */ export function init(config) { - Chart.scaleService.updateScaleDefaults('linear', { ticks: { beginAtZero: true, stepSize: 1 } }); - const authors_by_id = fromPairs(config.authors.map(a => [a.key, a])); - - /** - * - * @param {Config} config - * @param {ChartConfig} chartConfig - * @param {Element} container - * @param {HTMLCanvasElement} canvas - */ - function createWorkChart(config, chartConfig, container, canvas) { - /** @type {{[key: string]: Work[]}} */ - const grouped = {}; - /** @type {Work[]} */ - const excluded = []; - - for (const work of config.works) { - const allKeys = getPath(work, chartConfig.key) || []; - const validKeys = uniq( - allKeys.filter(key => !isUndefined(key) && !includes(chartConfig.exclude, key)) - ); - if (!validKeys.length) { - excluded.push(work); - continue; - } - for (const key of validKeys) { - grouped[key] = grouped[key] || []; - grouped[key].push(work); - } - } + Chart.scaleService.updateScaleDefaults('linear', { + ticks: { beginAtZero: true, stepSize: 1 }, + }); + const authors_by_id = fromPairs(config.authors.map((a) => [a.key, a])); - const bars = orderBy(entries(grouped), x => x[1].length, 'desc').slice(0, 20); - canvas.height = bars.length * 20 + 5; - canvas.width= 400; - new Chart(canvas.getContext('2d'), { - type: 'horizontalBar', - data: { - labels: bars.map(b => b[0]), - datasets: [{ - backgroundColor: 'rgb(255, 99, 132)', - borderColor: 'rgb(255, 99, 132)', - borderWidth: 0, - data: bars.map(b => b[1].length) - }] - }, - options: { - responsive: false, - legend: { display: false }, - scales: { - xAxes: [{ display: false }], - yAxes: [{ barPercentage: 1, gridLines: { display: false }, stacked: true }], - }, - onClick: (e, [chartEl]) => { - if (chartEl) { - const bar = bars[chartEl._index]; - document.querySelector('.selected-works--list').innerHTML = window.render_works_list(bar[1]); - } else { - document.querySelector('.selected-works--list').innerHTML = ''; - } - }, - plugins: { - datalabels: { - color: '#FFF', - anchor: 'end', - align: 'left', - offset: 0 - } - } - } - }); + /** + * + * @param {Config} config + * @param {ChartConfig} chartConfig + * @param {Element} container + * @param {HTMLCanvasElement} canvas + */ + function createWorkChart(config, chartConfig, container, canvas) { + /** @type {{[key: string]: Work[]}} */ + const grouped = {}; + /** @type {Work[]} */ + const excluded = []; - $(window.render_excluded_works_list(excluded, config.works.length)).appendTo(container); + for (const work of config.works) { + const allKeys = getPath(work, chartConfig.key) || []; + const validKeys = uniq( + allKeys.filter( + (key) => !isUndefined(key) && !includes(chartConfig.exclude, key), + ), + ); + if (!validKeys.length) { + excluded.push(work); + continue; + } + for (const key of validKeys) { + grouped[key] = grouped[key] || []; + grouped[key].push(work); + } } - const defaultFieldRender = field => `OPTIONAL { ?x ${field.relation} ?${field.name}. }`; - - const SPARQL_FIELDS = [ - { name: 'sex', type: 'uri', relation: 'wdt:P21' }, - { name: 'dob', type: 'literal', relation: 'wdt:P569' }, - { name: 'country_of_citizenship', type: 'uri', relation: 'wdt:P27' }, - { - name: 'country_of_birth', - type: 'uri', - relation: 'wdt:P19/wdt:P131*/wdt:P17', - render: field => ` + const bars = orderBy(entries(grouped), (x) => x[1].length, 'desc').slice( + 0, + 20, + ); + canvas.height = bars.length * 20 + 5; + canvas.width = 400; + new Chart(canvas.getContext('2d'), { + type: 'horizontalBar', + data: { + labels: bars.map((b) => b[0]), + datasets: [ + { + backgroundColor: 'rgb(255, 99, 132)', + borderColor: 'rgb(255, 99, 132)', + borderWidth: 0, + data: bars.map((b) => b[1].length), + }, + ], + }, + options: { + responsive: false, + legend: { display: false }, + scales: { + xAxes: [{ display: false }], + yAxes: [ + { barPercentage: 1, gridLines: { display: false }, stacked: true }, + ], + }, + onClick: (e, [chartEl]) => { + if (chartEl) { + const bar = bars[chartEl._index]; + document.querySelector('.selected-works--list').innerHTML = + window.render_works_list(bar[1]); + } else { + document.querySelector('.selected-works--list').innerHTML = ''; + } + }, + plugins: { + datalabels: { + color: '#FFF', + anchor: 'end', + align: 'left', + offset: 0, + }, + }, + }, + }); + + $( + window.render_excluded_works_list(excluded, config.works.length), + ).appendTo(container); + } + + const defaultFieldRender = (field) => + `OPTIONAL { ?x ${field.relation} ?${field.name}. }`; + + const SPARQL_FIELDS = [ + { name: 'sex', type: 'uri', relation: 'wdt:P21' }, + { name: 'dob', type: 'literal', relation: 'wdt:P569' }, + { name: 'country_of_citizenship', type: 'uri', relation: 'wdt:P27' }, + { + name: 'country_of_birth', + type: 'uri', + relation: 'wdt:P19/wdt:P131*/wdt:P17', + render: (field) => ` OPTIONAL { ?x ${field.relation} ?${field.name}. OPTIONAL { ?country_of_birth wdt:P571 ?country_inception. } @@ -133,86 +149,96 @@ export function init(config) { # Limit to modern-day countries or the country that existed at the time # of the author's birth FILTER(!BOUND(?country_dissolution) || !BOUND(?dob) || (?dob >= ?country_inception && ?dob <= ?country_dissolution) ). - ` - }, - ]; + `, + }, + ]; - function buildSparql(authors) { - return ` + function buildSparql(authors) { + return ` SELECT DISTINCT ?x ?xLabel ?olid - ${ - SPARQL_FIELDS.map(f => `?${f.name} ${f.type === 'uri' ? `?${f.name}Label ` : ''}`).join('') -} + ${SPARQL_FIELDS.map( + (f) => + `?${f.name} ${f.type === 'uri' ? `?${f.name}Label ` : ''}`, + ).join('')} WHERE { - VALUES ?olids { ${authors.map(a => `"${a.key.split('/')[2]}"`).join(' ')} } + VALUES ?olids { ${authors.map((a) => `"${a.key.split('/')[2]}"`).join(' ')} } ?x wdt:P648 ?olids; wdt:P648 ?olid. - ${ - SPARQL_FIELDS.map(f => (f.render || defaultFieldRender)(f)) - .join('\n') -} + ${SPARQL_FIELDS.map((f) => + (f.render || defaultFieldRender)(f), + ).join('\n')} SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],${config.lang},en". } } `; - } + } - document.getElementById('wd-query-sample').href = `https://query.wikidata.org/#${encodeURIComponent(buildSparql(config.authors.slice(0, 20)))}`; - - const wdPromise = fetch('https://query.wikidata.org/sparql?format=json', { - method: 'POST', - body: new URLSearchParams({query: buildSparql(config.authors)}) - }) - .then(r => r.json()) - .then(resp => { - const bindings = resp.results.bindings; - const grouped = groupBy(bindings, o => o.x.value.split('/')[4]); - const records = entries(grouped).map(([qid, bindings]) => { - const record = { qid, olids: uniq(bindings.map(x => x.olid.value)) }; - // { qid: Q123, olids: [ { value: }, {value: }], blah: [ {value:}, {value:} ], blahLabel: [{value:}, {value:}, - for (const {name, type} of SPARQL_FIELDS) { - if (type === 'uri') { - // need to dedupe whilst keeping labels in mind - const deduped = uniqBy( - bindings - .filter(x => x[name]) - .map(x => ({ [name]: x[name], [`${name}Label`]: x[`${name}Label`] })), - x => x[name].value) - record[name] = deduped.map(x => x[name]); - record[`${name}Label`] = deduped.map(x => x[`${name}Label`]); - } else { - record[name] = uniqBy(bindings.map(x => x[name]), 'value'); - } - } - return record; - }); - - for (const record of records) { - for (const olid of record.olids) { - if (`/authors/${olid}` in authors_by_id) { - authors_by_id[`/authors/${olid}`].wd = record; - } - } - } - }); + document.getElementById('wd-query-sample').href = + `https://query.wikidata.org/#${encodeURIComponent(buildSparql(config.authors.slice(0, 20)))}`; - // Add full authors to the works objects for easy reference - for (const work of config.works) { - work.authors = work.author_keys.map(key => authors_by_id[key]); - } - - for (const container of document.querySelectorAll(config.charts_selector)) { - const chartConfig = JSON.parse(container.dataset['config']); - const canvas = document.createElement('canvas'); - container.append(canvas); + const wdPromise = fetch('https://query.wikidata.org/sparql?format=json', { + method: 'POST', + body: new URLSearchParams({ query: buildSparql(config.authors) }), + }) + .then((r) => r.json()) + .then((resp) => { + const bindings = resp.results.bindings; + const grouped = groupBy(bindings, (o) => o.x.value.split('/')[4]); + const records = entries(grouped).map(([qid, bindings]) => { + const record = { qid, olids: uniq(bindings.map((x) => x.olid.value)) }; + // { qid: Q123, olids: [ { value: }, {value: }], blah: [ {value:}, {value:} ], blahLabel: [{value:}, {value:}, + for (const { name, type } of SPARQL_FIELDS) { + if (type === 'uri') { + // need to dedupe whilst keeping labels in mind + const deduped = uniqBy( + bindings + .filter((x) => x[name]) + .map((x) => ({ + [name]: x[name], + [`${name}Label`]: x[`${name}Label`], + })), + (x) => x[name].value, + ); + record[name] = deduped.map((x) => x[name]); + record[`${name}Label`] = deduped.map((x) => x[`${name}Label`]); + } else { + record[name] = uniqBy( + bindings.map((x) => x[name]), + 'value', + ); + } + } + return record; + }); - if (chartConfig.type === 'work-chart') { - createWorkChart(config, chartConfig, container, canvas); - } else if (chartConfig.type === 'wd-chart') { - wdPromise.then(() => createWorkChart(config, chartConfig, container, canvas)); + for (const record of records) { + for (const olid of record.olids) { + if (`/authors/${olid}` in authors_by_id) { + authors_by_id[`/authors/${olid}`].wd = record; + } } + } + }); + + // Add full authors to the works objects for easy reference + for (const work of config.works) { + work.authors = work.author_keys.map((key) => authors_by_id[key]); + } + + for (const container of document.querySelectorAll(config.charts_selector)) { + const chartConfig = JSON.parse(container.dataset['config']); + const canvas = document.createElement('canvas'); + container.append(canvas); + + if (chartConfig.type === 'work-chart') { + createWorkChart(config, chartConfig, container, canvas); + } else if (chartConfig.type === 'wd-chart') { + wdPromise.then(() => + createWorkChart(config, chartConfig, container, canvas), + ); } + } } /** @@ -221,16 +247,17 @@ export function init(config) { * @return {any} */ function getPath(obj, key) { - /** - * @param {object} obj - * @param {string[]} param1 - * @return {any} - */ - function main(obj, [head, ...rest]) { - if (typeof(obj) === 'undefined') return undefined; - if (!head) return obj; - if (head.endsWith('[]')) return obj[head.slice(0, -2)].flatMap(x => main(x, rest)); - else return main(obj[head], rest); - } - return main(obj, key.split('.')); + /** + * @param {object} obj + * @param {string[]} param1 + * @return {any} + */ + function main(obj, [head, ...rest]) { + if (typeof obj === 'undefined') return undefined; + if (!head) return obj; + if (head.endsWith('[]')) + return obj[head.slice(0, -2)].flatMap((x) => main(x, rest)); + else return main(obj[head], rest); + } + return main(obj, key.split('.')); } diff --git a/openlibrary/plugins/openlibrary/js/return-form/index.js b/openlibrary/plugins/openlibrary/js/return-form/index.js index 50b083737dd..2447441c25d 100644 --- a/openlibrary/plugins/openlibrary/js/return-form/index.js +++ b/openlibrary/plugins/openlibrary/js/return-form/index.js @@ -5,12 +5,12 @@ * @param {NodeList<HTMLElement>} returnForms */ export function initReturnForms(returnForms) { - for (const form of returnForms) { - const i18nStrings = JSON.parse(form.dataset.i18n) - form.addEventListener('submit', (event) => { - if (!confirm(i18nStrings['confirm_return'])) { - event.preventDefault(); - } - }) - } + for (const form of returnForms) { + const i18nStrings = JSON.parse(form.dataset.i18n); + form.addEventListener('submit', (event) => { + if (!confirm(i18nStrings['confirm_return'])) { + event.preventDefault(); + } + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/search.js b/openlibrary/plugins/openlibrary/js/search.js index 6e11db26415..5753ae3c902 100644 --- a/openlibrary/plugins/openlibrary/js/search.js +++ b/openlibrary/plugins/openlibrary/js/search.js @@ -12,18 +12,18 @@ import { buildPartialsUrl } from './utils'; * @param {Number} facet_inc number of hidden facets to be displayed */ export function more(header, start_facet_count, facet_inc) { - const facetEntry = `div.${header} div.facetEntry` - const shown = $(`${facetEntry}:not(:hidden)`).length - const total = $(facetEntry).length - if (shown === start_facet_count) { - $(`#${header}_less`).show(); - $(`#${header}_bull`).show(); - } - if (shown + facet_inc >= total) { - $(`#${header}_more`).hide(); - $(`#${header}_bull`).hide(); - } - $(`${facetEntry}:hidden`).slice(0, facet_inc).removeClass('ui-helper-hidden'); + const facetEntry = `div.${header} div.facetEntry`; + const shown = $(`${facetEntry}:not(:hidden)`).length; + const total = $(facetEntry).length; + if (shown === start_facet_count) { + $(`#${header}_less`).show(); + $(`#${header}_bull`).show(); + } + if (shown + facet_inc >= total) { + $(`#${header}_more`).hide(); + $(`#${header}_bull`).hide(); + } + $(`${facetEntry}:hidden`).slice(0, facet_inc).removeClass('ui-helper-hidden'); } /** @@ -34,21 +34,23 @@ export function more(header, start_facet_count, facet_inc) { * @param {Number} facet_inc number of displayed facets to be hidden */ export function less(header, start_facet_count, facet_inc) { - const facetEntry = `div.${header} div.facetEntry` - const shown = $(`${facetEntry}:not(:hidden)`).length - const total = $(facetEntry).length - const increment_extra = (shown - start_facet_count) % facet_inc; - const facet_dec = (increment_extra === 0) ? facet_inc:increment_extra; - const next_shown = Math.max(start_facet_count, shown - facet_dec); - if (shown === total) { - $(`#${header}_more`).show(); - $(`#${header}_bull`).show(); - } - if (next_shown === start_facet_count) { - $(`#${header}_less`).hide(); - $(`#${header}_bull`).hide(); - } - $(`${facetEntry}:not(:hidden)`).slice(next_shown, shown).addClass('ui-helper-hidden'); + const facetEntry = `div.${header} div.facetEntry`; + const shown = $(`${facetEntry}:not(:hidden)`).length; + const total = $(facetEntry).length; + const increment_extra = (shown - start_facet_count) % facet_inc; + const facet_dec = increment_extra === 0 ? facet_inc : increment_extra; + const next_shown = Math.max(start_facet_count, shown - facet_dec); + if (shown === total) { + $(`#${header}_more`).show(); + $(`#${header}_bull`).show(); + } + if (next_shown === start_facet_count) { + $(`#${header}_less`).hide(); + $(`#${header}_bull`).hide(); + } + $(`${facetEntry}:not(:hidden)`) + .slice(next_shown, shown) + .addClass('ui-helper-hidden'); } /** @@ -65,49 +67,50 @@ export function less(header, start_facet_count, facet_inc) { * @param {HTMLElement} facetsElem Root element of the search facets sidebar component */ export async function initSearchFacets(facetsElem) { - const asyncLoad = facetsElem.dataset.asyncLoad - - if (asyncLoad) { - const param = JSON.parse(facetsElem.dataset.param) - await whenVisible(facetsElem); - - fetchPartials(param) - .then((data) => { - if (data.activeFacets) { - const activeFacetsElem = createElementFromMarkup(data.activeFacets) - const activeFacetsContainer = document.querySelector('.selected-search-facets-container') - activeFacetsContainer.replaceChildren(activeFacetsElem) - } - const newFacetsElem = createElementFromMarkup(data.sidebar) - facetsElem.replaceWith(newFacetsElem) - hydrateFacets() - - document.title = data.title - }) - .catch(() => { - // XXX : Handle case where `/partials` response is not `2XX` here - }) - } else { - hydrateFacets() - } + const asyncLoad = facetsElem.dataset.asyncLoad; + + if (asyncLoad) { + const param = JSON.parse(facetsElem.dataset.param); + await whenVisible(facetsElem); + + fetchPartials(param) + .then((data) => { + if (data.activeFacets) { + const activeFacetsElem = createElementFromMarkup(data.activeFacets); + const activeFacetsContainer = document.querySelector( + '.selected-search-facets-container', + ); + activeFacetsContainer.replaceChildren(activeFacetsElem); + } + const newFacetsElem = createElementFromMarkup(data.sidebar); + facetsElem.replaceWith(newFacetsElem); + hydrateFacets(); + + document.title = data.title; + }) + .catch(() => { + // XXX : Handle case where `/partials` response is not `2XX` here + }); + } else { + hydrateFacets(); + } } - /** * Adds click listeners to the "show more" and "show less" facet affordances. */ function hydrateFacets() { - const data_config_json = $('#searchFacets').data('config'); - const start_facet_count = data_config_json['start_facet_count']; - const facet_inc = data_config_json['facet_inc']; - - $('.header_bull').hide(); - $('.header_more').on('click', function(){ - more($(this).data('header'), start_facet_count, facet_inc); - }); - $('.header_less').on('click', function(){ - less($(this).data('header'), start_facet_count, facet_inc); - }); + const data_config_json = $('#searchFacets').data('config'); + const start_facet_count = data_config_json['start_facet_count']; + const facet_inc = data_config_json['facet_inc']; + + $('.header_bull').hide(); + $('.header_more').on('click', function () { + more($(this).data('header'), start_facet_count, facet_inc); + }); + $('.header_less').on('click', function () { + less($(this).data('header'), start_facet_count, facet_inc); + }); } /** @@ -128,19 +131,20 @@ function hydrateFacets() { * @throws Error when `/partials` response is not in 200-299 range. */ function fetchPartials(param) { - const data = { - param: param, - path: location.pathname, - query: location.search + const data = { + param: param, + path: location.pathname, + query: location.search, + }; + + return fetch( + buildPartialsUrl('SearchFacets', { data: JSON.stringify(data) }), + ).then((resp) => { + if (!resp.ok) { + throw new Error(`Failed to fetch partials. Status code: ${resp.status}`); } - - return fetch(buildPartialsUrl('SearchFacets', {data: JSON.stringify(data)})) - .then((resp) => { - if (!resp.ok) { - throw new Error(`Failed to fetch partials. Status code: ${resp.status}`) - } - return resp.json() - }) + return resp.json(); + }); } /** @@ -153,12 +157,11 @@ function fetchPartials(param) { * @returns {HTMLElement} */ function createElementFromMarkup(markup) { - const template = document.createElement('template') - template.innerHTML = markup - return template.content.children[0] + const template = document.createElement('template'); + template.innerHTML = markup; + return template.content.children[0]; } - /** * Waits until the given element is visible in the viewport, then resolves. * @@ -167,27 +170,30 @@ function createElementFromMarkup(markup) { * @returns {Promise<void>} */ async function whenVisible(elem, options = {}) { - return new Promise((resolve) => { - const intersectionObserver = new IntersectionObserver( - (entries, observer) => { - entries.forEach(entry => { - if (!entry.isIntersecting) { - return - } - - // Stop observing once the element is visible - observer.unobserve(entry.target) - observer.disconnect() - resolve() - }) - }, - Object.assign({ - root: null, - rootMargin: '200px', - threshold: 0 - }, options) - ) - - intersectionObserver.observe(elem); - }); + return new Promise((resolve) => { + const intersectionObserver = new IntersectionObserver( + (entries, observer) => { + entries.forEach((entry) => { + if (!entry.isIntersecting) { + return; + } + + // Stop observing once the element is visible + observer.unobserve(entry.target); + observer.disconnect(); + resolve(); + }); + }, + Object.assign( + { + root: null, + rootMargin: '200px', + threshold: 0, + }, + options, + ), + ); + + intersectionObserver.observe(elem); + }); } diff --git a/openlibrary/plugins/openlibrary/js/service-worker-init.js b/openlibrary/plugins/openlibrary/js/service-worker-init.js index 99f7ea3d7ee..e6cc0c41b8b 100644 --- a/openlibrary/plugins/openlibrary/js/service-worker-init.js +++ b/openlibrary/plugins/openlibrary/js/service-worker-init.js @@ -1,17 +1,18 @@ -export default function initServiceWorker(){ - if ('serviceWorker' in navigator) { - window.addEventListener('load', () => { - navigator.serviceWorker.register('/sw.js') - .then(() => { }) - .catch(error => { - // eslint-disable-next-line no-console - console.error(`Service worker registration failed: ${error}`); - }); +export default function initServiceWorker() { + if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker + .register('/sw.js') + .then(() => {}) + .catch((error) => { + // eslint-disable-next-line no-console + console.error(`Service worker registration failed: ${error}`); }); - } - - window.addEventListener('beforeinstallprompt', (e) => { - // Prevent the mini-infobar from appearing on mobile - e.preventDefault(); }); + } + + window.addEventListener('beforeinstallprompt', (e) => { + // Prevent the mini-infobar from appearing on mobile + e.preventDefault(); + }); } diff --git a/openlibrary/plugins/openlibrary/js/service-worker-matchers.js b/openlibrary/plugins/openlibrary/js/service-worker-matchers.js index 672e6fa4c5a..d9ab3297a64 100644 --- a/openlibrary/plugins/openlibrary/js/service-worker-matchers.js +++ b/openlibrary/plugins/openlibrary/js/service-worker-matchers.js @@ -6,36 +6,39 @@ It is in a is a separate file to avoid this error when writing tests: > 1 | import { ExpirationPlugin } from 'workbox-expiration'; */ - export function matchMiscFiles({ url }) { - const miscFiles = ['/favicon.ico', '/static/manifest.json', '/cdn/archive.org/athena.js', - '/cdn/archive.org/donate.js'] - return miscFiles.includes(url.pathname); + const miscFiles = [ + '/favicon.ico', + '/static/manifest.json', + '/cdn/archive.org/athena.js', + '/cdn/archive.org/donate.js', + ]; + return miscFiles.includes(url.pathname); } export function matchSmallMediumCovers({ url }) { - const regex = /-[SM].jpg$/; - return regex.test(url.pathname); + const regex = /-[SM].jpg$/; + return regex.test(url.pathname); } export function matchLargeCovers({ url }) { - const regex = /-L.jpg$/; - return regex.test(url.pathname); + const regex = /-L.jpg$/; + return regex.test(url.pathname); } export function matchStaticImages({ url }) { - const regex = /^\/images\/|^\/static\/images\//; - return regex.test(url.pathname); + const regex = /^\/images\/|^\/static\/images\//; + return regex.test(url.pathname); } export function matchStaticBuild({ url }) { - const regex = /^\/static\/build\/.*(\.js|\.css)/; - const localhost = url.origin.includes('localhost') - return !localhost && regex.test(url.pathname); + const regex = /^\/static\/build\/.*(\.js|\.css)/; + const localhost = url.origin.includes('localhost'); + return !localhost && regex.test(url.pathname); } export function matchArchiveOrgImage({ url }) { - // most importantly, to cache your profile picture from loading every time - // also caches some covers - return url.href.startsWith('https://archive.org/services/img/'); + // most importantly, to cache your profile picture from loading every time + // also caches some covers + return url.href.startsWith('https://archive.org/services/img/'); } diff --git a/openlibrary/plugins/openlibrary/js/service-worker.js b/openlibrary/plugins/openlibrary/js/service-worker.js index 8c3e992c191..773b730e096 100644 --- a/openlibrary/plugins/openlibrary/js/service-worker.js +++ b/openlibrary/plugins/openlibrary/js/service-worker.js @@ -1,119 +1,123 @@ -import { ExpirationPlugin } from 'workbox-expiration'; -import { offlineFallback } from 'workbox-recipes'; -import { setDefaultHandler, registerRoute } from 'workbox-routing'; -import { NetworkOnly, CacheFirst } from 'workbox-strategies'; import { CacheableResponsePlugin } from 'workbox-cacheable-response'; import { clientsClaim } from 'workbox-core'; -import { matchMiscFiles, matchSmallMediumCovers, matchLargeCovers, matchStaticImages, matchStaticBuild, matchArchiveOrgImage } from './service-worker-matchers'; +import { ExpirationPlugin } from 'workbox-expiration'; +import { offlineFallback } from 'workbox-recipes'; +import { registerRoute, setDefaultHandler } from 'workbox-routing'; +import { CacheFirst, NetworkOnly } from 'workbox-strategies'; +import { + matchArchiveOrgImage, + matchLargeCovers, + matchMiscFiles, + matchSmallMediumCovers, + matchStaticBuild, + matchStaticImages, +} from './service-worker-matchers'; self.skipWaiting(); clientsClaim(); // This is needed for the offline page to show -setDefaultHandler( - new NetworkOnly() -); +setDefaultHandler(new NetworkOnly()); offlineFallback({ - pageFallback: '/static/offline.html', - imageFallback: '/static/images/logo_OL-lg.png' + pageFallback: '/static/offline.html', + imageFallback: '/static/images/logo_OL-lg.png', }); - const HOUR_SECONDS = 60 * 60; const DAY_SECONDS = 24 * HOUR_SECONDS; // only cache if it the request returns 0 or 200 status const cacheableResponses = new CacheableResponsePlugin({ - statuses: [0, 200], + statuses: [0, 200], }); registerRoute( - matchMiscFiles, - new CacheFirst({ - cacheName: 'misc-files-cache', - plugins: [ - new ExpirationPlugin({ - maxAgeSeconds: DAY_SECONDS - }), - cacheableResponses - ], - }) + matchMiscFiles, + new CacheFirst({ + cacheName: 'misc-files-cache', + plugins: [ + new ExpirationPlugin({ + maxAgeSeconds: DAY_SECONDS, + }), + cacheableResponses, + ], + }), ); registerRoute( - matchStaticImages, - new CacheFirst({ - cacheName: 'static-images-cache', - plugins: [ - new ExpirationPlugin({ - maxEntries: 100, - maxAgeSeconds: DAY_SECONDS * 365, - }), - cacheableResponses - ], - }) + matchStaticImages, + new CacheFirst({ + cacheName: 'static-images-cache', + plugins: [ + new ExpirationPlugin({ + maxEntries: 100, + maxAgeSeconds: DAY_SECONDS * 365, + }), + cacheableResponses, + ], + }), ); registerRoute( - matchStaticBuild, - // This has all the JS and CSS that changes on build - // We use cache first because it rarely changes - // But we only cache it for 10 minutes in case of deploy - // TODO: We should increase this a lot and make it change on deploy (clear it out when the deploy hash changes) - // it includes a .* at the end because some items have versions ?v=123 after - new CacheFirst({ - cacheName: 'static-build-cache', - plugins: [ - new ExpirationPlugin({ - maxAgeSeconds: 60 * 10, - }), - cacheableResponses - ], - }) -) + matchStaticBuild, + // This has all the JS and CSS that changes on build + // We use cache first because it rarely changes + // But we only cache it for 10 minutes in case of deploy + // TODO: We should increase this a lot and make it change on deploy (clear it out when the deploy hash changes) + // it includes a .* at the end because some items have versions ?v=123 after + new CacheFirst({ + cacheName: 'static-build-cache', + plugins: [ + new ExpirationPlugin({ + maxAgeSeconds: 60 * 10, + }), + cacheableResponses, + ], + }), +); registerRoute( - matchSmallMediumCovers, - // S/M covers - cache 150 of them. They take up no more than 2.25mb of space. There are ~150 covers on the homepage. - new CacheFirst({ - cacheName: 'covers-small-medium-cache', - plugins: [ - new ExpirationPlugin({ - maxEntries: 150, - purgeOnQuotaError: true, - }), - cacheableResponses - ], - }) + matchSmallMediumCovers, + // S/M covers - cache 150 of them. They take up no more than 2.25mb of space. There are ~150 covers on the homepage. + new CacheFirst({ + cacheName: 'covers-small-medium-cache', + plugins: [ + new ExpirationPlugin({ + maxEntries: 150, + purgeOnQuotaError: true, + }), + cacheableResponses, + ], + }), ); registerRoute( - matchLargeCovers, - // L covers - cache 5 of them but with a very short timeout so if you go to a few pages they stay. - new CacheFirst({ - cacheName: 'covers-large-cache', - plugins: [ - new ExpirationPlugin({ - maxEntries: 5, - maxAgeSeconds: HOUR_SECONDS, - purgeOnQuotaError: true, - }), - cacheableResponses - ], - }) + matchLargeCovers, + // L covers - cache 5 of them but with a very short timeout so if you go to a few pages they stay. + new CacheFirst({ + cacheName: 'covers-large-cache', + plugins: [ + new ExpirationPlugin({ + maxEntries: 5, + maxAgeSeconds: HOUR_SECONDS, + purgeOnQuotaError: true, + }), + cacheableResponses, + ], + }), ); registerRoute( - matchArchiveOrgImage, - new CacheFirst({ - cacheName: 'archive-org-images-cache', - plugins: [ - new ExpirationPlugin({ - maxEntries: 50, - maxAgeSeconds: DAY_SECONDS, - purgeOnQuotaError: true, - }), - cacheableResponses - ], - }) + matchArchiveOrgImage, + new CacheFirst({ + cacheName: 'archive-org-images-cache', + plugins: [ + new ExpirationPlugin({ + maxEntries: 50, + maxAgeSeconds: DAY_SECONDS, + purgeOnQuotaError: true, + }), + cacheableResponses, + ], + }), ); diff --git a/openlibrary/plugins/openlibrary/js/signup.js b/openlibrary/plugins/openlibrary/js/signup.js index fb0b7b5e821..374072a0f1d 100644 --- a/openlibrary/plugins/openlibrary/js/signup.js +++ b/openlibrary/plugins/openlibrary/js/signup.js @@ -1,240 +1,280 @@ import { debounce } from './nonjquery_utils.js'; export function initSignupForm() { - const signupForm = document.querySelector('form[name=signup]'); - const submitBtn = document.querySelector('button[name=signup]'); - const rpdCheckbox = document.querySelector('#pd-request') - const pdaSelectorContainer = document.querySelector('#pda-selector') - const pdaSelector = document.querySelector('#pd_program') - const i18nStrings = JSON.parse(signupForm.dataset.i18n); - const emailLoadingIcon = $('.ol-signup-form__input--emailAddr .ol-signup-form__icon--loading'); - const usernameLoadingIcon = $('.ol-signup-form__input--username .ol-signup-form__icon--loading'); - const emailSuccessIcon = $('.ol-signup-form__input--emailAddr .ol-signup-form__icon--success'); - const usernameSuccessIcon = $('.ol-signup-form__input--username .ol-signup-form__icon--success'); - - // Keep the same with openlibrary/plugins/upstream/forms.py - const VALID_EMAIL_RE = /^.*@.*\..*$/; - const VALID_USERNAME_RE = /^[a-z0-9-_]{3,20}$/i; - const PASSWORD_MINLENGTH = 3; - const PASSWORD_MAXLENGTH = 20; - const USERNAME_MINLENGTH = 3; - const USERNAME_MAXLENGTH = 20; - - // Callback that is called when grecaptcha.execute() is successful - function submitCreateAccountForm() { - signupForm.submit(); + const signupForm = document.querySelector('form[name=signup]'); + const submitBtn = document.querySelector('button[name=signup]'); + const rpdCheckbox = document.querySelector('#pd-request'); + const pdaSelectorContainer = document.querySelector('#pda-selector'); + const pdaSelector = document.querySelector('#pd_program'); + const i18nStrings = JSON.parse(signupForm.dataset.i18n); + const emailLoadingIcon = $( + '.ol-signup-form__input--emailAddr .ol-signup-form__icon--loading', + ); + const usernameLoadingIcon = $( + '.ol-signup-form__input--username .ol-signup-form__icon--loading', + ); + const emailSuccessIcon = $( + '.ol-signup-form__input--emailAddr .ol-signup-form__icon--success', + ); + const usernameSuccessIcon = $( + '.ol-signup-form__input--username .ol-signup-form__icon--success', + ); + + // Keep the same with openlibrary/plugins/upstream/forms.py + const VALID_EMAIL_RE = /^.*@.*\..*$/; + const VALID_USERNAME_RE = /^[a-z0-9-_]{3,20}$/i; + const PASSWORD_MINLENGTH = 3; + const PASSWORD_MAXLENGTH = 20; + const USERNAME_MINLENGTH = 3; + const USERNAME_MAXLENGTH = 20; + + // Callback that is called when grecaptcha.execute() is successful + function submitCreateAccountForm() { + signupForm.submit(); + } + window.submitCreateAccountForm = submitCreateAccountForm; + + // Checks whether reportValidity exists for cross-browser compatibility + // Includes invalid input count to account for checks not covered by reportValidity + $(signupForm).on('submit', (e) => { + e.preventDefault(); + validatePDSelection(); + const numInvalidInputs = signupForm.querySelectorAll('.invalid').length; + const isFormattingValid = + !signupForm.reportValidity || signupForm.reportValidity(); + if (numInvalidInputs === 0 && isFormattingValid && window.grecaptcha) { + $(submitBtn).prop('disabled', true).text(i18nStrings['loading_text']); + window.grecaptcha.execute(); + } + }); + + $('#username').on('keyup', function () { + const value = $(this).val(); + $('#userUrl').addClass('darkgreen').text(value).css('font-weight', '700'); + }); + + /** + * Renders an error message for a given input in a given error div. + * + * @param {string} inputId The ID (incl #) of the input the error relates to + * @param {string} errorDiv The ID (incl #) of the div where the error msg will be rendered + * @param {string} errorMsg The error message text + */ + function renderError(inputId, errorDiv, errorMsg) { + $(inputId).addClass('invalid'); + $(`label[for=${inputId.slice(1)}]`).addClass('invalid'); + $(errorDiv).text(errorMsg); + } + + /** + * Clears error styling and message for a given input and error div. + * + * @param {string} inputId The ID (incl #) of the input the error relates to + * @param {string} errorDiv The ID (incl #) of the div where the error msg is currently rendered + */ + function clearError(inputId, errorDiv) { + $(inputId).removeClass('invalid'); + $(`label[for=${inputId.slice(1)}]`).removeClass('invalid'); + $(errorDiv).text(''); + } + + function validateUsername() { + const value_username = $('#username').val(); + + usernameSuccessIcon.hide(); + + if (value_username === '') { + clearError('#username', '#usernameMessage'); + return; } - window.submitCreateAccountForm = submitCreateAccountForm - - // Checks whether reportValidity exists for cross-browser compatibility - // Includes invalid input count to account for checks not covered by reportValidity - $(signupForm).on('submit', function(e) { - e.preventDefault(); - validatePDSelection() - const numInvalidInputs = signupForm.querySelectorAll('.invalid').length; - const isFormattingValid = !signupForm.reportValidity || signupForm.reportValidity(); - if (numInvalidInputs === 0 && isFormattingValid && window.grecaptcha) { - $(submitBtn).prop('disabled', true).text(i18nStrings['loading_text']); - window.grecaptcha.execute(); - } - }); - - $('#username').on('keyup', function(){ - const value = $(this).val(); - $('#userUrl').addClass('darkgreen').text(value).css('font-weight','700'); - }); - /** - * Renders an error message for a given input in a given error div. - * - * @param {string} inputId The ID (incl #) of the input the error relates to - * @param {string} errorDiv The ID (incl #) of the div where the error msg will be rendered - * @param {string} errorMsg The error message text - */ - function renderError(inputId, errorDiv, errorMsg) { - $(inputId).addClass('invalid'); - $(`label[for=${inputId.slice(1)}]`).addClass('invalid'); - $(errorDiv).text(errorMsg); + if ( + value_username.length < USERNAME_MINLENGTH || + value_username.length > USERNAME_MAXLENGTH + ) { + renderError( + '#username', + '#usernameMessage', + i18nStrings['username_length_err'], + ); + return; } - /** - * Clears error styling and message for a given input and error div. - * - * @param {string} inputId The ID (incl #) of the input the error relates to - * @param {string} errorDiv The ID (incl #) of the div where the error msg is currently rendered - */ - function clearError(inputId, errorDiv) { - $(inputId).removeClass('invalid'); - $(`label[for=${inputId.slice(1)}]`).removeClass('invalid'); - $(errorDiv).text(''); + if (!VALID_USERNAME_RE.test(value_username)) { + renderError( + '#username', + '#usernameMessage', + i18nStrings['username_char_err'], + ); + return; } - function validateUsername() { - const value_username = $('#username').val(); + usernameLoadingIcon.show(); - usernameSuccessIcon.hide(); + $.ajax({ + url: '/account/validate', + data: { username: value_username }, + type: 'GET', + success: (errors) => { + usernameLoadingIcon.hide(); - if (value_username === '') { - clearError('#username', '#usernameMessage'); - return; + if (errors.username) { + renderError('#username', '#usernameMessage', errors.username); + } else { + clearError('#username', '#usernameMessage'); + usernameSuccessIcon.show(); } + }, + }); + } - if (value_username.length < USERNAME_MINLENGTH || value_username.length > USERNAME_MAXLENGTH) { - renderError('#username', '#usernameMessage', i18nStrings['username_length_err']); - return; - } + function validateEmail() { + const value_email = $('#emailAddr').val(); - if (!(VALID_USERNAME_RE.test(value_username))) { - renderError('#username', '#usernameMessage', i18nStrings['username_char_err']); - return; - } + emailSuccessIcon.hide(); - usernameLoadingIcon.show(); - - $.ajax({ - url: '/account/validate', - data: { username: value_username }, - type: 'GET', - success: function(errors) { - usernameLoadingIcon.hide(); - - if (errors.username) { - renderError('#username', '#usernameMessage', errors.username); - } else { - clearError('#username', '#usernameMessage'); - usernameSuccessIcon.show(); - } - } - }); + if (value_email === '') { + clearError('#emailAddr', '#emailAddrMessage'); + return; } - function validateEmail() { - const value_email = $('#emailAddr').val(); - - emailSuccessIcon.hide(); - - if (value_email === '') { - clearError('#emailAddr', '#emailAddrMessage'); - return; - } - - if (!VALID_EMAIL_RE.test(value_email)) { - renderError('#emailAddr', '#emailAddrMessage', i18nStrings['invalid_email_format']); - return; - } - - emailLoadingIcon.show(); - - $.ajax({ - url: '/account/validate', - data: { email: value_email }, - type: 'GET', - success: function(errors) { - emailLoadingIcon.hide(); - - if (errors.email) { - renderError('#emailAddr', '#emailAddrMessage', errors.email); - } else { - clearError('#emailAddr', '#emailAddrMessage'); - emailSuccessIcon.show(); - } - } - }); + if (!VALID_EMAIL_RE.test(value_email)) { + renderError( + '#emailAddr', + '#emailAddrMessage', + i18nStrings['invalid_email_format'], + ); + return; } - function validatePassword() { - const value_password = $('#password').val(); + emailLoadingIcon.show(); - if (value_password === '') { - clearError('#password', '#passwordMessage'); - return; - } + $.ajax({ + url: '/account/validate', + data: { email: value_email }, + type: 'GET', + success: (errors) => { + emailLoadingIcon.hide(); - if (value_password.length < PASSWORD_MINLENGTH || value_password.length > PASSWORD_MAXLENGTH) { - renderError('#password', '#passwordMessage', i18nStrings['password_length_err']); - return; + if (errors.email) { + renderError('#emailAddr', '#emailAddrMessage', errors.email); + } else { + clearError('#emailAddr', '#emailAddrMessage'); + emailSuccessIcon.show(); } + }, + }); + } - clearError('#password', '#passwordMessage'); - } - - function validatePDSelection() { - if (!rpdCheckbox.checked) { - clearError('#pd_program', '#pd_programMessage') - pdaSelector.setAttribute('aria-invalid', 'false'); - return - } - if (pdaSelector.value === '') { - renderError('#pd_program', '#pd_programMessage', i18nStrings['missing_pda_err']) - pdaSelector.setAttribute('aria-invalid', 'true'); - return - } + function validatePassword() { + const value_password = $('#password').val(); - clearError('#pd_program', '#pd_programMessage') - pdaSelector.setAttribute('aria-invalid', 'false'); + if (value_password === '') { + clearError('#password', '#passwordMessage'); + return; } - // Maps input ID attribute to corresponding validation function - function validateInput(input) { - const id = $(input).attr('id'); - if (id === 'emailAddr') { - validateEmail(); - } else if (id === 'username') { - validateUsername(); - } else if (id === 'password') { - validatePassword(); - } else { - throw new Error('Input validation function not found'); - } + if ( + value_password.length < PASSWORD_MINLENGTH || + value_password.length > PASSWORD_MAXLENGTH + ) { + renderError( + '#password', + '#passwordMessage', + i18nStrings['password_length_err'], + ); + return; } - const $nonCheckboxInputs = $('form[name=signup] input:not([type="checkbox"])') - - // Validates input fields already marked as invalid on value change - $nonCheckboxInputs.on('input', debounce(function(){ - if ($(this).hasClass('invalid')) { - validateInput(this); - } - }, 50)); + clearError('#password', '#passwordMessage'); + } - // Validates all other input fields (i.e. not already marked as invalid) on blur - $nonCheckboxInputs.on('blur', function() { - if (!$(this).hasClass('invalid')) { - validateInput(this); - } - }); + function validatePDSelection() { + if (!rpdCheckbox.checked) { + clearError('#pd_program', '#pd_programMessage'); + pdaSelector.setAttribute('aria-invalid', 'false'); + return; + } + if (pdaSelector.value === '') { + renderError( + '#pd_program', + '#pd_programMessage', + i18nStrings['missing_pda_err'], + ); + pdaSelector.setAttribute('aria-invalid', 'true'); + return; + } - // Validates the print-disability authority selection when the selection changes - $('form[name=signup] select').on('change', function() { - validatePDSelection() - }) - - function updateSelectorVisibility() { - if (rpdCheckbox.checked) { - pdaSelectorContainer.classList.remove('hidden') - rpdCheckbox.setAttribute('aria-expanded','true') - pdaSelectorContainer.setAttribute('aria-hidden','false') - pdaSelector.setAttribute('aria-required', 'true') - } else { - pdaSelectorContainer.classList.add('hidden') - rpdCheckbox.setAttribute('aria-expanded','false') - pdaSelectorContainer.setAttribute('aria-hidden','true') - pdaSelector.setAttribute('aria-required', 'false') - } + clearError('#pd_program', '#pd_programMessage'); + pdaSelector.setAttribute('aria-invalid', 'false'); + } + + // Maps input ID attribute to corresponding validation function + function validateInput(input) { + const id = $(input).attr('id'); + if (id === 'emailAddr') { + validateEmail(); + } else if (id === 'username') { + validateUsername(); + } else if (id === 'password') { + validatePassword(); + } else { + throw new Error('Input validation function not found'); + } + } + + const $nonCheckboxInputs = $( + 'form[name=signup] input:not([type="checkbox"])', + ); + + // Validates input fields already marked as invalid on value change + $nonCheckboxInputs.on( + 'input', + debounce(function () { + if ($(this).hasClass('invalid')) { + validateInput(this); + } + }, 50), + ); + + // Validates all other input fields (i.e. not already marked as invalid) on blur + $nonCheckboxInputs.on('blur', function () { + if (!$(this).hasClass('invalid')) { + validateInput(this); + } + }); + + // Validates the print-disability authority selection when the selection changes + $('form[name=signup] select').on('change', () => { + validatePDSelection(); + }); + + function updateSelectorVisibility() { + if (rpdCheckbox.checked) { + pdaSelectorContainer.classList.remove('hidden'); + rpdCheckbox.setAttribute('aria-expanded', 'true'); + pdaSelectorContainer.setAttribute('aria-hidden', 'false'); + pdaSelector.setAttribute('aria-required', 'true'); + } else { + pdaSelectorContainer.classList.add('hidden'); + rpdCheckbox.setAttribute('aria-expanded', 'false'); + pdaSelectorContainer.setAttribute('aria-hidden', 'true'); + pdaSelector.setAttribute('aria-required', 'false'); } + } - rpdCheckbox.addEventListener('change', updateSelectorVisibility) + rpdCheckbox.addEventListener('change', updateSelectorVisibility); - // On page reload, display PD program options and validate selection - updateSelectorVisibility() - validatePDSelection() + // On page reload, display PD program options and validate selection + updateSelectorVisibility(); + validatePDSelection(); } export function initLoginForm() { - const loginForm = $('form[name=login]'); - const loadingText = loginForm.data('i18n')['loading_text']; + const loginForm = $('form[name=login]'); + const loadingText = loginForm.data('i18n')['loading_text']; - loginForm.on('submit', () => { - $('button[type=submit]').prop('disabled', true).text(loadingText); - }) + loginForm.on('submit', () => { + $('button[type=submit]').prop('disabled', true).text(loadingText); + }); } diff --git a/openlibrary/plugins/openlibrary/js/star-ratings/index.js b/openlibrary/plugins/openlibrary/js/star-ratings/index.js index 08356d3935e..ba616161ca2 100644 --- a/openlibrary/plugins/openlibrary/js/star-ratings/index.js +++ b/openlibrary/plugins/openlibrary/js/star-ratings/index.js @@ -1,73 +1,74 @@ -import { FadingToast } from '../Toast.js'; import { findDropperForWork } from '../my-books'; import { ReadingLogShelves } from '../my-books/MyBooksDropper/ReadingLogForms'; +import { FadingToast } from '../Toast.js'; export function initRatingHandlers(ratingForms) { - for (const form of ratingForms) { - form.addEventListener('submit', function(e) { - handleRatingSubmission(e, form); - }) - } + for (const form of ratingForms) { + form.addEventListener('submit', (e) => { + handleRatingSubmission(e, form); + }); + } } function handleRatingSubmission(event, form) { - event.preventDefault(); - // Continue only if selected star is different from previous rating - if (!event.submitter.classList.contains('star-selected')) { + event.preventDefault(); + // Continue only if selected star is different from previous rating + if (!event.submitter.classList.contains('star-selected')) { + // Construct form data object: + const formData = new FormData(form); + let rating; + if (event.submitter.value) { + rating = Number(event.submitter.value); + formData.append('rating', event.submitter.value); + } - // Construct form data object: - const formData = new FormData(form); - let rating; - if (event.submitter.value) { - rating = Number(event.submitter.value) - formData.append('rating', event.submitter.value) + // Make AJAX call + fetch(form.action, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams(formData), + }) + .then((response) => { + if (response.status === 401) { + throw new Error('You must be logged in to rate books'); } + if (!response.ok) { + throw new Error('Ratings update failed'); + } + // Update view to deselect all stars + form.querySelectorAll('.star-selected').forEach((elem) => { + elem.classList.remove('star-selected'); + if (elem.hasAttribute('property')) { + elem.removeAttribute('property'); + } + }); - // Make AJAX call - fetch(form.action, { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams(formData) - }) - .then((response) => { - if (response.status === 401) { - throw new Error('You must be logged in to rate books'); - } - if (!response.ok) { - throw new Error('Ratings update failed') - } - // Update view to deselect all stars - form.querySelectorAll('.star-selected').forEach((elem) => { - elem.classList.remove('star-selected'); - if (elem.hasAttribute('property')) { - elem.removeAttribute('property'); - } - }) - - const clearButton = form.querySelector('.star-messaging'); - if (rating) { // A rating was added or updated - // Update view to show patron's new star rating: - clearButton.classList.remove('hidden'); - form.querySelectorAll(`.star-${rating}`).forEach((elem) => { - elem.classList.add('star-selected'); - if (elem.tagName === 'LABEL') { - elem.setAttribute('property', 'ratingValue') - } - }) + const clearButton = form.querySelector('.star-messaging'); + if (rating) { + // A rating was added or updated + // Update view to show patron's new star rating: + clearButton.classList.remove('hidden'); + form.querySelectorAll(`.star-${rating}`).forEach((elem) => { + elem.classList.add('star-selected'); + if (elem.tagName === 'LABEL') { + elem.setAttribute('property', 'ratingValue'); + } + }); - // Find dropper that is associated with this star rating affordance: - const dropper = findDropperForWork(form.dataset.workKey) - if (dropper) { - dropper.updateShelfDisplay(ReadingLogShelves.ALREADY_READ) - } - } else { // A rating was deleted - clearButton.classList.add('hidden'); - } - }) - .catch((error) => { - new FadingToast(error.message).show(); - }) - } + // Find dropper that is associated with this star rating affordance: + const dropper = findDropperForWork(form.dataset.workKey); + if (dropper) { + dropper.updateShelfDisplay(ReadingLogShelves.ALREADY_READ); + } + } else { + // A rating was deleted + clearButton.classList.add('hidden'); + } + }) + .catch((error) => { + new FadingToast(error.message).show(); + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/stats/index.js b/openlibrary/plugins/openlibrary/js/stats/index.js index b68b22d021a..1de4cda8e44 100644 --- a/openlibrary/plugins/openlibrary/js/stats/index.js +++ b/openlibrary/plugins/openlibrary/js/stats/index.js @@ -6,22 +6,21 @@ * @see /openlibrary/templates/admin/index.html */ export async function initUniqueLoginCounts(containerElem) { - const loadingIndicator = containerElem.querySelector('.loadingIndicator') - const i18nStrings = JSON.parse(containerElem.dataset.i18n) + const loadingIndicator = containerElem.querySelector('.loadingIndicator'); + const i18nStrings = JSON.parse(containerElem.dataset.i18n); - const counts = await fetchCounts() - .then((resp) => { - if (resp.status !== 200) { - throw new Error(`Failed to fetch partials. Status code: ${resp.status}`) - } - return resp.json() - }) + const counts = await fetchCounts().then((resp) => { + if (resp.status !== 200) { + throw new Error(`Failed to fetch partials. Status code: ${resp.status}`); + } + return resp.json(); + }); - const countDiv = document.createElement('DIV') - countDiv.innerHTML = i18nStrings.uniqueLoginsCopy - const countSpan = countDiv.querySelector('.login-counts') - countSpan.textContent = counts.loginCount - loadingIndicator.replaceWith(countDiv) + const countDiv = document.createElement('DIV'); + countDiv.innerHTML = i18nStrings.uniqueLoginsCopy; + const countSpan = countDiv.querySelector('.login-counts'); + countSpan.textContent = counts.loginCount; + loadingIndicator.replaceWith(countDiv); } /** @@ -31,5 +30,5 @@ export async function initUniqueLoginCounts(containerElem) { * @see `monthly_logins` class in /openlibrary/plugins/openlibrary/api.py */ async function fetchCounts() { - return fetch('/api/monthly_logins.json') + return fetch('/api/monthly_logins.json'); } diff --git a/openlibrary/plugins/openlibrary/js/tabs.js b/openlibrary/plugins/openlibrary/js/tabs.js index 83c7335af28..afd7f0a89f4 100644 --- a/openlibrary/plugins/openlibrary/js/tabs.js +++ b/openlibrary/plugins/openlibrary/js/tabs.js @@ -2,8 +2,8 @@ const TABS_OPTIONS = { fx: { opacity: 'toggle' } }; import 'jquery-ui/ui/widgets/tabs'; export function initTabs($node) { - $node.tabs(TABS_OPTIONS); - $node.filter('.autohash').on('tabsselect', function(event, ui) { - document.location.hash = ui.panel.id; - }); + $node.tabs(TABS_OPTIONS); + $node.filter('.autohash').on('tabsselect', (event, ui) => { + document.location.hash = ui.panel.id; + }); } diff --git a/openlibrary/plugins/openlibrary/js/team.js b/openlibrary/plugins/openlibrary/js/team.js index ba1084a76df..a248daea4ea 100644 --- a/openlibrary/plugins/openlibrary/js/team.js +++ b/openlibrary/plugins/openlibrary/js/team.js @@ -1,320 +1,320 @@ import team from '../../../templates/about/team.json'; import { updateURLParameters } from './utils'; export function initTeamFilter() { - const currentYear = new Date().getFullYear().toString(); - // Photos - const default_profile_image = + const currentYear = new Date().getFullYear().toString(); + // Photos + const default_profile_image = '../../../static/images/openlibrary-180x180.png'; - const bookUrlIcon = '../../../static/images/icons/icon_book-lg.png'; - const personalUrlIcon = '../../../static/images/globe-solid.svg'; - const initialSearchParams = new URL(window.location.href).searchParams; - const initialRole = initialSearchParams.get('role') || 'All'; - const initialDepartment = initialSearchParams.get('department') || 'All'; - - // Team sorted by last name - const sortByLastName = (array) => { - array.sort((a, b) => { - const aName = a.name.split(' '); - const bName = b.name.split(' '); - const aLastName = aName[aName.length - 1]; - const bLastName = bName[bName.length - 1]; - if (aLastName < bLastName) { - return -1; - } else if (aLastName > bLastName) { - return 1; - } else { - return 0; - } - }); - }; - sortByLastName(team); - - // Match a substring in each person's role - const matchSubstring = (array, substring) => { - return array.some((item) => item.includes(substring)); - }; - - // *************************************** Team sorted by role *************************************** - // ********** STAFF ********** - const staff = team.filter((person) => matchSubstring(person.roles, 'staff')); - const staffEmeritus = staff.filter((person) => - matchSubstring(person.roles, 'emeritus') - ); - const staffCurrent = staff.filter( - (person) => !matchSubstring(person.roles, 'emeritus') - ); - - // ********** FELLOWS ********** - const fellows = team.filter( - (person) => - matchSubstring(person.roles, 'fellow') && - !matchSubstring(person.roles, 'staff') - ); - const currentFellows = fellows.filter((person) => - matchSubstring(person.roles, currentYear) - ); - const pastFellows = fellows.filter( - (person) => !matchSubstring(person.roles, currentYear) - ); - - // ********** VOLUNTEERS ********** - const volunteers = team.filter( - (person) => - matchSubstring(person.roles, 'volunteer') && - !matchSubstring(person.roles, 'fellow') - ); - - // *************************************** Selectors and eventListeners *************************************** - const roleFilter = document.getElementById('role'); - const departmentFilter = document.getElementById('department'); - roleFilter.value = initialRole; - roleFilter.addEventListener('change', (e) => { - filterTeam(e.target.value, departmentFilter.value); - updateURLParameters({ - role: e.target.value, - department: departmentFilter.value - }); + const bookUrlIcon = '../../../static/images/icons/icon_book-lg.png'; + const personalUrlIcon = '../../../static/images/globe-solid.svg'; + const initialSearchParams = new URL(window.location.href).searchParams; + const initialRole = initialSearchParams.get('role') || 'All'; + const initialDepartment = initialSearchParams.get('department') || 'All'; + + // Team sorted by last name + const sortByLastName = (array) => { + array.sort((a, b) => { + const aName = a.name.split(' '); + const bName = b.name.split(' '); + const aLastName = aName[aName.length - 1]; + const bLastName = bName[bName.length - 1]; + if (aLastName < bLastName) { + return -1; + } else if (aLastName > bLastName) { + return 1; + } else { + return 0; + } + }); + }; + sortByLastName(team); + + // Match a substring in each person's role + const matchSubstring = (array, substring) => { + return array.some((item) => item.includes(substring)); + }; + + // *************************************** Team sorted by role *************************************** + // ********** STAFF ********** + const staff = team.filter((person) => matchSubstring(person.roles, 'staff')); + const staffEmeritus = staff.filter((person) => + matchSubstring(person.roles, 'emeritus'), + ); + const staffCurrent = staff.filter( + (person) => !matchSubstring(person.roles, 'emeritus'), + ); + + // ********** FELLOWS ********** + const fellows = team.filter( + (person) => + matchSubstring(person.roles, 'fellow') && + !matchSubstring(person.roles, 'staff'), + ); + const currentFellows = fellows.filter((person) => + matchSubstring(person.roles, currentYear), + ); + const pastFellows = fellows.filter( + (person) => !matchSubstring(person.roles, currentYear), + ); + + // ********** VOLUNTEERS ********** + const volunteers = team.filter( + (person) => + matchSubstring(person.roles, 'volunteer') && + !matchSubstring(person.roles, 'fellow'), + ); + + // *************************************** Selectors and eventListeners *************************************** + const roleFilter = document.getElementById('role'); + const departmentFilter = document.getElementById('department'); + roleFilter.value = initialRole; + roleFilter.addEventListener('change', (e) => { + filterTeam(e.target.value, departmentFilter.value); + updateURLParameters({ + role: e.target.value, + department: departmentFilter.value, + }); + }); + departmentFilter.value = initialDepartment; + departmentFilter.addEventListener('change', (e) => { + filterTeam(roleFilter.value, e.target.value); + updateURLParameters({ + role: roleFilter.value, + department: departmentFilter.value, }); - departmentFilter.value = initialDepartment; - departmentFilter.addEventListener('change', (e) => { - filterTeam(roleFilter.value, e.target.value); - updateURLParameters({ - role: roleFilter.value, - department: departmentFilter.value - }); + }); + const cardsContainer = document.querySelector('.teamCards_container'); + + // *************************************** Functions *************************************** + const showError = () => { + const noResults = document.createElement('h3'); + noResults.classList = 'noResults'; + noResults.textContent = + "It looks like we don't have anyone with those specifications."; + cardsContainer.append(noResults); + }; + + const createCards = (array) => { + array.map((member) => { + // create + const teamCardContainer = document.createElement('div'); + const teamCard = document.createElement('div'); + + const teamCardPhotoContainer = document.createElement('div'); + const teamCardPhoto = document.createElement('img'); + + const teamCardDescription = document.createElement('div'); + const memberOlLink = document.createElement('a'); + const memberName = document.createElement('h2'); + // const memberRole = document.createElement('h4'); + // const memberDepartment = document.createElement('h3'); + const memberTitle = document.createElement('h3'); + + const descriptionLinks = document.createElement('div'); + + //modify + teamCardContainer.classList = 'teamCard__container'; + teamCard.classList = 'teamCard'; + + teamCardPhotoContainer.classList = 'teamCard__photoContainer'; + teamCardPhoto.classList = 'teamCard__photo'; + teamCardPhoto.src = `${ + member.photo_path ? member.photo_path : default_profile_image + }`; + + teamCardDescription.classList.add('teamCard__description'); + if (member.ol_key) { + memberOlLink.href = `https://openlibrary.org/people/${member.ol_key}`; + } + member.name.length >= 18 + ? (memberName.classList = 'description__name--length-long') + : (memberName.classList = 'description__name--length-short'); + + memberName.textContent = `${member.name}`; + memberTitle.classList = 'description__title'; + memberTitle.textContent = `${member.title}`; + + descriptionLinks.classList = 'description__links'; + if (member.personal_url) { + const memberPersonalA = document.createElement('a'); + const memberPersonalImg = document.createElement('img'); + + memberPersonalA.href = `${member.personal_url}`; + memberPersonalImg.src = personalUrlIcon; + memberPersonalImg.classList = 'links__site'; + + memberPersonalA.append(memberPersonalImg); + descriptionLinks.append(memberPersonalA); + } + + if (member.favorite_book_url) { + const memberBookA = document.createElement('a'); + const memberBookImg = document.createElement('img'); + + memberBookA.href = `${member.favorite_book_url}`; + memberBookImg.src = bookUrlIcon; + memberBookImg.classList = 'links__book'; + + memberBookA.append(memberBookImg); + descriptionLinks.append(memberBookA); + } + + // append + teamCardPhotoContainer.append(teamCardPhoto); + memberOlLink.append(memberName); + teamCardDescription.append( + memberOlLink, + // memberRole, + // memberDepartment, + memberTitle, + descriptionLinks, + ); + teamCard.append(teamCardPhotoContainer, teamCardDescription); + teamCardContainer.append(teamCard); + cardsContainer.append(teamCardContainer); }); - const cardsContainer = document.querySelector('.teamCards_container'); - - // *************************************** Functions *************************************** - const showError = () => { - const noResults = document.createElement('h3'); - noResults.classList = 'noResults'; - noResults.textContent = - 'It looks like we don\'t have anyone with those specifications.'; - cardsContainer.append(noResults); - }; - - const createCards = (array) => { - array.map((member) => { - // create - const teamCardContainer = document.createElement('div'); - const teamCard = document.createElement('div'); - - const teamCardPhotoContainer = document.createElement('div'); - const teamCardPhoto = document.createElement('img'); - - const teamCardDescription = document.createElement('div'); - const memberOlLink = document.createElement('a'); - const memberName = document.createElement('h2'); - // const memberRole = document.createElement('h4'); - // const memberDepartment = document.createElement('h3'); - const memberTitle = document.createElement('h3'); - - const descriptionLinks = document.createElement('div'); - - //modify - teamCardContainer.classList = 'teamCard__container'; - teamCard.classList = 'teamCard'; - - teamCardPhotoContainer.classList = 'teamCard__photoContainer'; - teamCardPhoto.classList = 'teamCard__photo'; - teamCardPhoto.src = `${ - member.photo_path ? member.photo_path : default_profile_image - }`; - - teamCardDescription.classList.add('teamCard__description'); - if (member.ol_key) { - memberOlLink.href = `https://openlibrary.org/people/${member.ol_key}`; - } - member.name.length >= 18 - ? (memberName.classList = 'description__name--length-long') - : (memberName.classList = 'description__name--length-short'); - - memberName.textContent = `${member.name}`; - memberTitle.classList = 'description__title'; - memberTitle.textContent = `${member.title}`; - - descriptionLinks.classList = 'description__links'; - if (member.personal_url) { - const memberPersonalA = document.createElement('a'); - const memberPersonalImg = document.createElement('img'); - - memberPersonalA.href = `${member.personal_url}`; - memberPersonalImg.src = personalUrlIcon; - memberPersonalImg.classList = 'links__site'; - - memberPersonalA.append(memberPersonalImg); - descriptionLinks.append(memberPersonalA); - } - - if (member.favorite_book_url) { - const memberBookA = document.createElement('a'); - const memberBookImg = document.createElement('img'); - - memberBookA.href = `${member.favorite_book_url}`; - memberBookImg.src = bookUrlIcon; - memberBookImg.classList = 'links__book'; - - memberBookA.append(memberBookImg); - descriptionLinks.append(memberBookA); - } - - // append - teamCardPhotoContainer.append(teamCardPhoto); - memberOlLink.append(memberName); - teamCardDescription.append( - memberOlLink, - // memberRole, - // memberDepartment, - memberTitle, - descriptionLinks - ); - teamCard.append(teamCardPhotoContainer, teamCardDescription); - teamCardContainer.append(teamCard); - cardsContainer.append(teamCardContainer); - }); - }; - - const createSectionHeading = (text) => { - const sectionSeparator = document.createElement('div'); - sectionSeparator.textContent = `${text}`; - sectionSeparator.classList = 'sectionSeparator'; - cardsContainer.append(sectionSeparator); - }; - - const createsubSection = (array, text) => { - const subsectionSeparator = document.createElement('div'); - subsectionSeparator.textContent = `${text}`; - subsectionSeparator.classList = 'subsectionSeparator'; - cardsContainer.append(subsectionSeparator); - createCards(array); - }; - - const filterTeam = (role, department) => { - cardsContainer.textContent = ''; - // **************************************** default sort ***************************************** - if (role === 'All' && department === 'All') { - createSectionHeading('Staff'); - createsubSection(staffCurrent, 'Current'); - createsubSection(staffEmeritus, 'Emeritus'); - - createSectionHeading('Fellows'); - createsubSection(currentFellows, 'Current'); - createsubSection(pastFellows, 'Past'); - - createSectionHeading('Volunteers'); - createCards(volunteers); - } - // ************************************* sort by department *************************************** - else if (role === 'All' && department !== 'All') { - role = ''; - const filteredTeam = team.filter( - (person) => - matchSubstring(person.roles, role) && - matchSubstring(person.departments, department) - ); - - const staff = filteredTeam.filter((person) => - matchSubstring(person.roles, 'staff') - ); - const staffEmeritus = staff.filter((person) => - matchSubstring(person.roles, 'emeritus') - ); - const staffCurrent = staff.filter( - (person) => !matchSubstring(person.roles, 'emeritus') - ); - - const fellows = filteredTeam.filter( - (person) => - matchSubstring(person.roles, 'fellow') && - !matchSubstring(person.roles, 'staff') - ); - const currentFellows = fellows.filter((person) => - matchSubstring(person.roles, currentYear) - ); - const pastFellows = fellows.filter( - (person) => !matchSubstring(person.roles, currentYear) - ); - - const volunteers = filteredTeam.filter( - (person) => - matchSubstring(person.roles, 'volunteer') && - !matchSubstring(person.roles, 'fellow') - ); - - staff.length && createSectionHeading('Staff'); - staffCurrent.length && createsubSection(staffCurrent, 'Current'); - staffEmeritus.length && createsubSection(staffEmeritus, 'Emeritus'); - - fellows.length && createSectionHeading('Fellows'); - currentFellows.length && createsubSection(currentFellows, 'Current'); - pastFellows.length && createsubSection(pastFellows, 'Past'); - - volunteers.length && createSectionHeading('Volunteers'); - createCards(volunteers); - } - // ****************************** sort by role and/or department ******************************* - else { - department === 'All' ? (department = '') : department; - createSectionHeading(capitalize(role)); - if (role === 'volunteer') { - const filteredVolunteers = volunteers.filter((person) => - matchSubstring(person.departments, department) - ); - filteredVolunteers.length !== 0 - ? createCards(filteredVolunteers) - : showError(); - } else if (role === 'staff') { - const filteredCurrentStaff = staffCurrent.filter((person) => - matchSubstring(person.departments, department) - ); - const filteredStaffEmeritus = staffEmeritus.filter((person) => - matchSubstring(person.departments, department) - ); - filteredCurrentStaff.length && + }; + + const createSectionHeading = (text) => { + const sectionSeparator = document.createElement('div'); + sectionSeparator.textContent = `${text}`; + sectionSeparator.classList = 'sectionSeparator'; + cardsContainer.append(sectionSeparator); + }; + + const createsubSection = (array, text) => { + const subsectionSeparator = document.createElement('div'); + subsectionSeparator.textContent = `${text}`; + subsectionSeparator.classList = 'subsectionSeparator'; + cardsContainer.append(subsectionSeparator); + createCards(array); + }; + + const filterTeam = (role, department) => { + cardsContainer.textContent = ''; + // **************************************** default sort ***************************************** + if (role === 'All' && department === 'All') { + createSectionHeading('Staff'); + createsubSection(staffCurrent, 'Current'); + createsubSection(staffEmeritus, 'Emeritus'); + + createSectionHeading('Fellows'); + createsubSection(currentFellows, 'Current'); + createsubSection(pastFellows, 'Past'); + + createSectionHeading('Volunteers'); + createCards(volunteers); + } + // ************************************* sort by department *************************************** + else if (role === 'All' && department !== 'All') { + role = ''; + const filteredTeam = team.filter( + (person) => + matchSubstring(person.roles, role) && + matchSubstring(person.departments, department), + ); + + const staff = filteredTeam.filter((person) => + matchSubstring(person.roles, 'staff'), + ); + const staffEmeritus = staff.filter((person) => + matchSubstring(person.roles, 'emeritus'), + ); + const staffCurrent = staff.filter( + (person) => !matchSubstring(person.roles, 'emeritus'), + ); + + const fellows = filteredTeam.filter( + (person) => + matchSubstring(person.roles, 'fellow') && + !matchSubstring(person.roles, 'staff'), + ); + const currentFellows = fellows.filter((person) => + matchSubstring(person.roles, currentYear), + ); + const pastFellows = fellows.filter( + (person) => !matchSubstring(person.roles, currentYear), + ); + + const volunteers = filteredTeam.filter( + (person) => + matchSubstring(person.roles, 'volunteer') && + !matchSubstring(person.roles, 'fellow'), + ); + + staff.length && createSectionHeading('Staff'); + staffCurrent.length && createsubSection(staffCurrent, 'Current'); + staffEmeritus.length && createsubSection(staffEmeritus, 'Emeritus'); + + fellows.length && createSectionHeading('Fellows'); + currentFellows.length && createsubSection(currentFellows, 'Current'); + pastFellows.length && createsubSection(pastFellows, 'Past'); + + volunteers.length && createSectionHeading('Volunteers'); + createCards(volunteers); + } + // ****************************** sort by role and/or department ******************************* + else { + department === 'All' ? (department = '') : department; + createSectionHeading(capitalize(role)); + if (role === 'volunteer') { + const filteredVolunteers = volunteers.filter((person) => + matchSubstring(person.departments, department), + ); + filteredVolunteers.length !== 0 + ? createCards(filteredVolunteers) + : showError(); + } else if (role === 'staff') { + const filteredCurrentStaff = staffCurrent.filter((person) => + matchSubstring(person.departments, department), + ); + const filteredStaffEmeritus = staffEmeritus.filter((person) => + matchSubstring(person.departments, department), + ); + filteredCurrentStaff.length && createsubSection(filteredCurrentStaff, 'Current'); - filteredStaffEmeritus.length && + filteredStaffEmeritus.length && createsubSection(filteredStaffEmeritus, 'Emeritus'); - !filteredCurrentStaff.length && + !filteredCurrentStaff.length && !filteredStaffEmeritus.length && showError(); - } else { - const filteredCurrentFellows = currentFellows.filter((person) => - matchSubstring(person.departments, department) - ); - const filteredPastFellows = pastFellows.filter((person) => - matchSubstring(person.departments, department) - ); - filteredCurrentFellows.length && + } else { + const filteredCurrentFellows = currentFellows.filter((person) => + matchSubstring(person.departments, department), + ); + const filteredPastFellows = pastFellows.filter((person) => + matchSubstring(person.departments, department), + ); + filteredCurrentFellows.length && createsubSection(filteredCurrentFellows, 'Current'); - filteredPastFellows.length && + filteredPastFellows.length && createsubSection(filteredPastFellows, 'Past'); - !filteredCurrentFellows.length && + !filteredCurrentFellows.length && !filteredPastFellows.length && showError(); - } - } - }; - - const capitalize = (text) => { - const firstLetter = text[0].toUpperCase(); - if (text === 'fellow' || text === 'volunteer') { - return `${firstLetter + text.slice(1)}s`; - } else { - return firstLetter + text.slice(1); - } - }; - - // on page load - createSectionHeading('Staff'); - createsubSection(staffCurrent, 'Current'); - createsubSection(staffEmeritus, 'Emeritus'); - - createSectionHeading('Fellows'); - createsubSection(currentFellows, 'Current'); - createsubSection(pastFellows, 'Past'); - - createSectionHeading('Volunteers'); - createCards(volunteers); - filterTeam(initialRole, initialDepartment); + } + } + }; + + const capitalize = (text) => { + const firstLetter = text[0].toUpperCase(); + if (text === 'fellow' || text === 'volunteer') { + return `${firstLetter + text.slice(1)}s`; + } else { + return firstLetter + text.slice(1); + } + }; + + // on page load + createSectionHeading('Staff'); + createsubSection(staffCurrent, 'Current'); + createsubSection(staffEmeritus, 'Emeritus'); + + createSectionHeading('Fellows'); + createsubSection(currentFellows, 'Current'); + createsubSection(pastFellows, 'Past'); + + createSectionHeading('Volunteers'); + createCards(volunteers); + filterTeam(initialRole, initialDepartment); } diff --git a/openlibrary/plugins/openlibrary/js/template.js b/openlibrary/plugins/openlibrary/js/template.js index ff1a151d308..bbd764c11f9 100644 --- a/openlibrary/plugins/openlibrary/js/template.js +++ b/openlibrary/plugins/openlibrary/js/template.js @@ -3,42 +3,39 @@ // Inspired by http://ejohn.org/blog/javascript-micro-templating/ export default function Template(tmpl_text) { - var s = []; - var js = ['var _p=[];', 'with(env) {']; - var tokens, i, t, f, g; + var s = []; + var js = ['var _p=[];', 'with(env) {']; + var tokens, i, t, f, g; - function addCode(text) { - js.push(text); - } - function addExpr(text) { - js.push(`_p.push(htmlquote(${text}));`); - } - function addText(text) { - js.push(`_p.push(__s[${s.length}]);`); - s.push(text); - } + function addCode(text) { + js.push(text); + } + function addExpr(text) { + js.push(`_p.push(htmlquote(${text}));`); + } + function addText(text) { + js.push(`_p.push(__s[${s.length}]);`); + s.push(text); + } - tokens = tmpl_text.split('<%'); + tokens = tmpl_text.split('<%'); - addText(tokens[0]); - for (i=1; i < tokens.length; i++) { - t = tokens[i].split('%>'); + addText(tokens[0]); + for (i = 1; i < tokens.length; i++) { + t = tokens[i].split('%>'); - if (t[0][0] === '=') { - addExpr(t[0].substr(1)); - } - else { - addCode(t[0]); - } - addText(t[1]); + if (t[0][0] === '=') { + addExpr(t[0].substr(1)); + } else { + addCode(t[0]); } - js.push('}', 'return _p.join(\'\');'); + addText(t[1]); + } + js.push('}', "return _p.join('');"); - f = new Function(['__s', 'env'], js.join('\n')); - g = function(env) { - return f(s, env); - }; - g.toString = function() { return tmpl_text; }; - g.toCode = function() { return f.toString(); }; - return g; + f = new Function(['__s', 'env'], js.join('\n')); + g = (env) => f(s, env); + g.toString = () => tmpl_text; + g.toCode = () => f.toString(); + return g; } diff --git a/openlibrary/plugins/openlibrary/js/type_changer.js b/openlibrary/plugins/openlibrary/js/type_changer.js index 650cc6ae546..106d9891333 100644 --- a/openlibrary/plugins/openlibrary/js/type_changer.js +++ b/openlibrary/plugins/openlibrary/js/type_changer.js @@ -3,17 +3,17 @@ */ export function initTypeChanger(elem) { - // /about?m=edit - where this code is run + // /about?m=edit - where this code is run - function changeTemplate() { - // Change the template of the page based on the selected value - const searchParams = new URLSearchParams(window.location.search); - const t = elem.value; - searchParams.set('t', t); + function changeTemplate() { + // Change the template of the page based on the selected value + const searchParams = new URLSearchParams(window.location.search); + const t = elem.value; + searchParams.set('t', t); - // Update the URL and navigate to the new page - window.location.search = searchParams.toString(); - } + // Update the URL and navigate to the new page + window.location.search = searchParams.toString(); + } - elem.addEventListener('change', changeTemplate); + elem.addEventListener('change', changeTemplate); } diff --git a/openlibrary/plugins/openlibrary/js/utils.js b/openlibrary/plugins/openlibrary/js/utils.js index 9b3288fe0a0..b43e75b8265 100644 --- a/openlibrary/plugins/openlibrary/js/utils.js +++ b/openlibrary/plugins/openlibrary/js/utils.js @@ -6,27 +6,26 @@ See: https://github.com/internetarchive/openlibrary/pull/9180#issuecomment-21079 // closes active popup export function closePopup() { - // Note we don't import colorbox here, since it's on the parent - parent.jQuery.fn.colorbox.close(); + // Note we don't import colorbox here, since it's on the parent + parent.jQuery.fn.colorbox.close(); } // used in templates/admin/imports.html export function truncate(text, limit) { - if (text.length > limit) { - return `${text.substr(0, limit)}...`; - } else { - return text; - } + if (text.length > limit) { + return `${text.substr(0, limit)}...`; + } else { + return text; + } } // used in openlibrary/templates/books/edit/excerpts.html export function cond(predicate, true_value, false_value) { - if (predicate) { - return true_value; - } - else { - return false_value; - } + if (predicate) { + return true_value; + } else { + return false_value; + } } /** @@ -35,29 +34,29 @@ export function cond(predicate, true_value, false_value) { * @param {...HTMLElement} elements */ export function removeChildren(...elements) { - for (const elem of elements) { - if (elem) { - while (elem.firstChild) { - elem.removeChild(elem.firstChild) - } - } + for (const elem of elements) { + if (elem) { + while (elem.firstChild) { + elem.removeChild(elem.firstChild); + } } + } } // Function to add or update multiple query parameters export function updateURLParameters(params) { - // Get the current URL - const url = new URL(window.location.href); + // Get the current URL + const url = new URL(window.location.href); - // Iterate over the params object and update/add each parameter - for (const key in params) { - if (params.hasOwnProperty(key)) { - url.searchParams.set(key, params[key]); - } + // Iterate over the params object and update/add each parameter + for (const key in params) { + if (Object.hasOwn(params, key)) { + url.searchParams.set(key, params[key]); } + } - // Use history.pushState to update the URL without reloading - window.history.pushState({ path: url.href }, '', url.href); + // Use history.pushState to update the URL without reloading + window.history.pushState({ path: url.href }, '', url.href); } /** @@ -65,25 +64,25 @@ export function updateURLParameters(params) { * @param string a value for document.querySelectorAll() */ export function trimInputValues(param) { - const inputs = document.querySelectorAll(param); - inputs.forEach(input => { - input.addEventListener('blur', function() { - this.value = this.value.trim(); - }); + const inputs = document.querySelectorAll(param); + inputs.forEach((input) => { + input.addEventListener('blur', function () { + this.value = this.value.trim(); }); + }); } export function buildPartialsUrl(component, params = {}) { - const curUrl = new URL(window.location.href); - const url = new URL(`${location.origin}/partials/${component}.json`); + const curUrl = new URL(window.location.href); + const url = new URL(`${location.origin}/partials/${component}.json`); - if (curUrl.searchParams.has('lang')) { - url.searchParams.set('lang', curUrl.searchParams.get('lang')); - } + if (curUrl.searchParams.has('lang')) { + url.searchParams.set('lang', curUrl.searchParams.get('lang')); + } - for (const key in params) { - url.searchParams.set(key, params[key]); - } + for (const key in params) { + url.searchParams.set(key, params[key]); + } - return url; + return url; } diff --git a/openlibrary/plugins/openlibrary/js/waitlist.js b/openlibrary/plugins/openlibrary/js/waitlist.js index 1fb8d17c5b1..1e2689eaffa 100644 --- a/openlibrary/plugins/openlibrary/js/waitlist.js +++ b/openlibrary/plugins/openlibrary/js/waitlist.js @@ -6,16 +6,14 @@ import 'jquery-ui/ui/widgets/dialog'; * @param {NodeList<HTMLElement>} leaveWaitlistLinks - NodeList of leave waitlist links */ export function initLeaveWaitlist(leaveWaitlistLinks) { - for (const link of leaveWaitlistLinks) { - link.addEventListener('click', () => { - const $link = $(link) - const title = $link.parents('tr').find('.book').text(); - $('#leave-waitinglist-dialog strong').text(title); - // We remove the hidden class here because otherwise it flashes for a moment on page load - $('#leave-waitinglist-dialog').removeClass('hidden'); - $('#leave-waitinglist-dialog') - .data('origin', $link) - .dialog('open'); - }) - } + for (const link of leaveWaitlistLinks) { + link.addEventListener('click', () => { + const $link = $(link); + const title = $link.parents('tr').find('.book').text(); + $('#leave-waitinglist-dialog strong').text(title); + // We remove the hidden class here because otherwise it flashes for a moment on page load + $('#leave-waitinglist-dialog').removeClass('hidden'); + $('#leave-waitinglist-dialog').data('origin', $link).dialog('open'); + }); + } } diff --git a/package-lock.json b/package-lock.json index 5ede5e9f958..15f837fa720 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@babel/core": "^7.24.7", "@babel/eslint-parser": "^7.24.7", "@babel/preset-env": "^7.24.7", + "@biomejs/biome": "^2.4.10", "@ericblade/quagga2": "^1.7.4", "@vitejs/plugin-legacy": "^8.0.1", "@vitejs/plugin-vue": "^6.0.5", @@ -1930,6 +1931,169 @@ "dev": true, "license": "MIT" }, + "node_modules/@biomejs/biome": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.10.tgz", + "integrity": "sha512-xxA3AphFQ1geij4JTHXv4EeSTda1IFn22ye9LdyVPoJU19fNVl0uzfEuhsfQ4Yue/0FaLs2/ccVi4UDiE7R30w==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.10", + "@biomejs/cli-darwin-x64": "2.4.10", + "@biomejs/cli-linux-arm64": "2.4.10", + "@biomejs/cli-linux-arm64-musl": "2.4.10", + "@biomejs/cli-linux-x64": "2.4.10", + "@biomejs/cli-linux-x64-musl": "2.4.10", + "@biomejs/cli-win32-arm64": "2.4.10", + "@biomejs/cli-win32-x64": "2.4.10" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.10.tgz", + "integrity": "sha512-vuzzI1cWqDVzOMIkYyHbKqp+AkQq4K7k+UCXWpkYcY/HDn1UxdsbsfgtVpa40shem8Kax4TLDLlx8kMAecgqiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.10.tgz", + "integrity": "sha512-14fzASRo+BPotwp7nWULy2W5xeUyFnTaq1V13Etrrxkrih+ez/2QfgFm5Ehtf5vSjtgx/IJycMMpn5kPd5ZNaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.10.tgz", + "integrity": "sha512-7MH1CMW5uuxQ/s7FLST63qF8B3Hgu2HRdZ7tA1X1+mk+St4JOuIrqdhIBnnyqeyWJNI+Bww7Es5QZ0wIc1Cmkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.10.tgz", + "integrity": "sha512-WrJY6UuiSD/Dh+nwK2qOTu8kdMDlLV3dLMmychIghHPAysWFq1/DGC1pVZx8POE3ZkzKR3PUUnVrtZfMfaJjyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.10.tgz", + "integrity": "sha512-tZLvEEi2u9Xu1zAqRjTcpIDGVtldigVvzug2fTuPG0ME/g8/mXpRPcNgLB22bGn6FvLJpHHnqLnwliOu8xjYrg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.10.tgz", + "integrity": "sha512-kDTi3pI6PBN6CiczsWYOyP2zk0IJI08EWEQyDMQWW221rPaaEz6FvjLhnU07KMzLv8q3qSuoB93ua6inSQ55Tw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.10.tgz", + "integrity": "sha512-umwQU6qPzH+ISTf/eHyJ/QoQnJs3V9Vpjz2OjZXe9MVBZ7prgGafMy7yYeRGnlmDAn87AKTF3Q6weLoMGpeqdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.10.tgz", + "integrity": "sha512-aW/JU5GuyH4uxMrNYpoC2kjaHlyJGLgIa3XkhPEZI0uKhZhJZU8BuEyJmvgzSPQNGozBwWjC972RaNdcJ9KyJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@cacheable/memory": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.8.tgz", diff --git a/package.json b/package.json index d52ae945224..aa75c9ec8f0 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@babel/core": "^7.24.7", "@babel/eslint-parser": "^7.24.7", "@babel/preset-env": "^7.24.7", + "@biomejs/biome": "^2.4.10", "@ericblade/quagga2": "^1.7.4", "@vitejs/plugin-legacy": "^8.0.1", "@vitejs/plugin-vue": "^6.0.5", diff --git a/scripts/gh_scripts/new_pr_labeler.mjs b/scripts/gh_scripts/new_pr_labeler.mjs index 1c7d233449e..923effe7dc3 100644 --- a/scripts/gh_scripts/new_pr_labeler.mjs +++ b/scripts/gh_scripts/new_pr_labeler.mjs @@ -17,81 +17,91 @@ * 3. Updates PR, adding same priority label as issue, and assigning the lead (if * they are not also the author) */ -import { Octokit } from "@octokit/action"; +import { Octokit } from '@octokit/action'; -const CLOSES_REGEX = /\b(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved):?\s+#(\d+)/i +const CLOSES_REGEX = + /\b(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved):?\s+#(\d+)/i; -console.log('Script starting....') -const octokit = new Octokit() -await main() -console.log('Script terminated....') +console.log('Script starting....'); +const octokit = new Octokit(); +await main(); +console.log('Script terminated....'); async function main() { - // Parse and assign all command-line variables - const {fullRepoName, prAuthor, prNumber, prBody} = parseArgs() + // Parse and assign all command-line variables + const { fullRepoName, prAuthor, prNumber, prBody } = parseArgs(); - // Look for "Closes:" statement, storing the issue number (if present) - const issueNumber = findLinkedIssue(prBody) + // Look for "Closes:" statement, storing the issue number (if present) + const issueNumber = findLinkedIssue(prBody); - if (!issueNumber) { - console.log('No linked issue found for this pull request.') - return - } + if (!issueNumber) { + console.log('No linked issue found for this pull request.'); + return; + } - // Fetch the issue - const [repoOwner, repoName] = fullRepoName.split('/') - const linkedIssue = await octokit.request('GET /repos/{owner}/{repo}/issues/{issue_number}', { - owner: repoOwner, - repo: repoName, - issue_number: issueNumber, - headers: { - 'X-GitHub-Api-Version': '2022-11-28' - } - }) - if (!linkedIssue) { - console.log(`An issue occurred while fetching issue #${issueNumber}`) - process.exit(1) - } + // Fetch the issue + const [repoOwner, repoName] = fullRepoName.split('/'); + const linkedIssue = await octokit.request( + 'GET /repos/{owner}/{repo}/issues/{issue_number}', + { + owner: repoOwner, + repo: repoName, + issue_number: issueNumber, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + if (!linkedIssue) { + console.log(`An issue occurred while fetching issue #${issueNumber}`); + process.exit(1); + } - // Check the issue's labels for the priority and lead - let leadName - let priority - for (const label of linkedIssue.data.labels) { - if (!leadName && label.name.startsWith('Lead: @')) { - leadName = label.name.split('@')[1] - } - if (!priority && label.name.match(/Priority: [012]/)) { - priority = label.name - } + // Check the issue's labels for the priority and lead + let leadName; + let priority; + for (const label of linkedIssue.data.labels) { + if (!leadName && label.name.startsWith('Lead: @')) { + leadName = label.name.split('@')[1]; + } + if (!priority && label.name.match(/Priority: [012]/)) { + priority = label.name; } + } - // Don't assign lead to PR if PR author is the issue lead - const assignLead = leadName && !(leadName === prAuthor) + // Don't assign lead to PR if PR author is the issue lead + const assignLead = leadName && !(leadName === prAuthor); - // Update PR, adding assignee and priority label - if (assignLead) { - await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', { - owner: repoOwner, - repo: repoName, - issue_number: prNumber, - assignees: [leadName], - headers: { - 'X-GitHub-Api-Version': '2022-11-28' - } - }) - } + // Update PR, adding assignee and priority label + if (assignLead) { + await octokit.request( + 'POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', + { + owner: repoOwner, + repo: repoName, + issue_number: prNumber, + assignees: [leadName], + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + } - if (priority) { - await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/labels', { - owner: repoOwner, - repo: repoName, - issue_number: prNumber, - labels: [priority], - headers: { - 'X-GitHub-Api-Version': '2022-11-28' - } - }) - } + if (priority) { + await octokit.request( + 'POST /repos/{owner}/{repo}/issues/{issue_number}/labels', + { + owner: repoOwner, + repo: repoName, + issue_number: prNumber, + labels: [priority], + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + } } /** @@ -102,17 +112,17 @@ async function main() { * @returns {Record<string, string>} */ function parseArgs() { - if (process.argv.length < 6) { - console.log('Unexpected number of arguments.') - process.exit(1) - } - const prBody = process.argv.slice(5).join(' ') - return { - fullRepoName: process.argv[2], - prAuthor: process.argv[3], - prNumber: process.argv[4], - prBody: prBody - } + if (process.argv.length < 6) { + console.log('Unexpected number of arguments.'); + process.exit(1); + } + const prBody = process.argv.slice(5).join(' '); + return { + fullRepoName: process.argv[2], + prAuthor: process.argv[3], + prNumber: process.argv[4], + prBody: prBody, + }; } /** @@ -125,6 +135,6 @@ function parseArgs() { * "Closes" statement is found. */ function findLinkedIssue(body) { - const matches = body.match(CLOSES_REGEX) - return matches?.length ? Number(matches[1]) : '' + const matches = body.match(CLOSES_REGEX); + return matches?.length ? Number(matches[1]) : ''; } diff --git a/scripts/gh_scripts/weekly_status_report.mjs b/scripts/gh_scripts/weekly_status_report.mjs index 307cc60f088..108e32a7535 100644 --- a/scripts/gh_scripts/weekly_status_report.mjs +++ b/scripts/gh_scripts/weekly_status_report.mjs @@ -1,11 +1,11 @@ -import fs from 'node:fs' -import { Octokit } from "@octokit/action"; +import fs from 'node:fs'; +import { Octokit } from '@octokit/action'; -const octokit = new Octokit() +const octokit = new Octokit(); -console.log('Starting script...') -await main() -console.log('Finishing script...') +console.log('Starting script...'); +await main(); +console.log('Finishing script...'); /** * Runs the weekly status report job. @@ -16,34 +16,40 @@ console.log('Finishing script...') * 3. Publishes the assembled message to Slack */ async function main() { - const config = getConfig() - - // Each line of the Slack message will be accumulated here: - const lines = ['Project Management Helper'] - - await prepareRecentComments(config.leads) - .then((results) => lines.push(...results)) - - await prepareReviewAssigneeIssues(config.leads) - .then((results) => lines.push(...results)) - - const openPullRequests = await fetchOpenPullRequests() - const nonDraftPullRequests = openPullRequests.filter((pull) => !pull.draft) - - if (config.publishFullDigest) { - await prepareUnassignedItems(nonDraftPullRequests) - .then((results) => lines.push(...results)) - await prepareUntriagedIssues(config.leads) - .then((results) => lines.push(...results)) - } - lines.push(...prepareAssignedPullRequests(nonDraftPullRequests, config.leads)) - if (config.publishFullDigest) { - lines.push(...prepareStaffPullRequests(nonDraftPullRequests, config.leads)) - } - lines.push(...prepareSubmitterInput(nonDraftPullRequests, config.leads)) - - // Publish message - await publishToSlack(lines, config.slackChannel) + const config = getConfig(); + + // Each line of the Slack message will be accumulated here: + const lines = ['Project Management Helper']; + + await prepareRecentComments(config.leads).then((results) => + lines.push(...results), + ); + + await prepareReviewAssigneeIssues(config.leads).then((results) => + lines.push(...results), + ); + + const openPullRequests = await fetchOpenPullRequests(); + const nonDraftPullRequests = openPullRequests.filter((pull) => !pull.draft); + + if (config.publishFullDigest) { + await prepareUnassignedItems(nonDraftPullRequests).then((results) => + lines.push(...results), + ); + await prepareUntriagedIssues(config.leads).then((results) => + lines.push(...results), + ); + } + lines.push( + ...prepareAssignedPullRequests(nonDraftPullRequests, config.leads), + ); + if (config.publishFullDigest) { + lines.push(...prepareStaffPullRequests(nonDraftPullRequests, config.leads)); + } + lines.push(...prepareSubmitterInput(nonDraftPullRequests, config.leads)); + + // Publish message + await publishToSlack(lines, config.slackChannel); } /** @@ -70,15 +76,15 @@ async function main() { * @returns {Config} */ function getConfig() { - if (process.argv.length < 3) { - throw new Error("Unexpected amount of arguments") - } - const configPath = process.argv[2] - try { - return JSON.parse(fs.readFileSync(configPath, 'utf8')) - } catch (err) { - throw err - } + if (process.argv.length < 3) { + throw new Error('Unexpected amount of arguments'); + } + const configPath = process.argv[2]; + try { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } catch (err) { + throw err; + } } /** @@ -89,15 +95,15 @@ function getConfig() { * @returns {string} The lead's Slack ID, or "UNKNOWN" */ function findSlackId(githubUsername, leads) { - for (const lead of leads) { - if (githubUsername === lead.githubUsername) { - return lead.slackId - } + for (const lead of leads) { + if (githubUsername === lead.githubUsername) { + return lead.slackId; } + } - // If we see "UNKNOWN" in the digest, our configurations - // should be updated - return 'UNKNOWN' + // If we see "UNKNOWN" in the digest, our configurations + // should be updated + return 'UNKNOWN'; } /** @@ -106,14 +112,14 @@ function findSlackId(githubUsername, leads) { * @returns {Promise<Array<Record>>} */ async function fetchOpenPullRequests() { - return octokit.paginate('GET /repos/{owner}/{repo}/pulls', { - owner: 'internetarchive', - repo: 'openlibrary', - per_page: 100, - headers: { - 'X-GitHub-Api-Version': '2022-11-28' - } - }) + return octokit.paginate('GET /repos/{owner}/{repo}/pulls', { + owner: 'internetarchive', + repo: 'openlibrary', + per_page: 100, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }); } /** @@ -123,71 +129,85 @@ async function fetchOpenPullRequests() { * @returns {Promise<Array<string>>} The recent comments, in order, line by line */ async function prepareRecentComments(leads) { - const output = ['*Recent Comments*'] - const issuesAwaitingComments = await octokit.paginate('GET /repos/{owner}/{repo}/issues', { - owner: 'internetarchive', - repo: 'openlibrary', - labels: `Needs: Response`, - per_page: 100, - headers: { - 'X-GitHub-Api-Version': '2022-11-28' - } - }) - let isUpToDate = true - - for (const lead of leads) { - const leadIssuesAwaitingComments = issuesAwaitingComments.filter((issue) => { - for (const label of issue.labels) { - if (label.name === lead.leadLabel) { - return true - } - } - return false - }) - const searchResultsUrl = `https://github.com/internetarchive/openlibrary/issues?q=is%3Aopen+label%3A%22Needs%3A+Response%22+label%3A${encodeURIComponent('"' + lead.leadLabel + '"')}` - if (leadIssuesAwaitingComments.length > 0) { - output.push(` • <${searchResultsUrl}|${leadIssuesAwaitingComments.length} issue(s)> need response from ${lead.slackId}`) - isUpToDate = false + const output = ['*Recent Comments*']; + const issuesAwaitingComments = await octokit.paginate( + 'GET /repos/{owner}/{repo}/issues', + { + owner: 'internetarchive', + repo: 'openlibrary', + labels: `Needs: Response`, + per_page: 100, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + let isUpToDate = true; + + for (const lead of leads) { + const leadIssuesAwaitingComments = issuesAwaitingComments.filter( + (issue) => { + for (const label of issue.labels) { + if (label.name === lead.leadLabel) { + return true; + } } + return false; + }, + ); + const searchResultsUrl = `https://github.com/internetarchive/openlibrary/issues?q=is%3Aopen+label%3A%22Needs%3A+Response%22+label%3A${encodeURIComponent('"' + lead.leadLabel + '"')}`; + if (leadIssuesAwaitingComments.length > 0) { + output.push( + ` • <${searchResultsUrl}|${leadIssuesAwaitingComments.length} issue(s)> need response from ${lead.slackId}`, + ); + isUpToDate = false; } + } - if (isUpToDate) { - output.push(' _No issues awaiting comments from leads_') - } + if (isUpToDate) { + output.push(' _No issues awaiting comments from leads_'); + } - return output + return output; } - /** * Prepares a message containing links to unassigned issues and pull requests. * * @param {Array<Record>} pullRequests Non-draft pull request records * @returns {Promise<Array<string>>} Messages with links to our unassigned PRs */ - async function prepareUnassignedItems(pullRequests) { - const output = ['*Needs: Lead/Assignees*'] - - const issuesNeedingLeads = await octokit.paginate('GET /repos/{owner}/{repo}/issues', { - owner: 'internetarchive', - repo: 'openlibrary', - labels: `Needs: Lead`, - per_page: 100, - headers: { - 'X-GitHub-Api-Version': '2022-11-28' - } - }) - output.push(` • <https://github.com/internetarchive/openlibrary/issues?q=is%3Aissue+is%3Aopen+label%3A%22Needs%3A+Lead%22|${issuesNeedingLeads.length} issue(s)> need leads assigned by team`) - - const unassignedPrs = pullRequests.filter((pull) => !pull.assignee) - const renovatebotPullCount = unassignedPrs.filter((pull) => pull.user.login === 'renovate[bot]').length - const unassignedCount = unassignedPrs.length - renovatebotPullCount - output.push(` • <https://github.com/internetarchive/openlibrary/pulls?q=is%3Apr+is%3Aopen+no%3Aassignee+-is%3Adraft+-author:app/renovate|${unassignedCount} unassigned PRs> + <https://github.com/internetarchive/openlibrary/pulls/app%2Frenovate|${renovatebotPullCount} renovatebot>`) - - return output +async function prepareUnassignedItems(pullRequests) { + const output = ['*Needs: Lead/Assignees*']; + + const issuesNeedingLeads = await octokit.paginate( + 'GET /repos/{owner}/{repo}/issues', + { + owner: 'internetarchive', + repo: 'openlibrary', + labels: `Needs: Lead`, + per_page: 100, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + output.push( + ` • <https://github.com/internetarchive/openlibrary/issues?q=is%3Aissue+is%3Aopen+label%3A%22Needs%3A+Lead%22|${issuesNeedingLeads.length} issue(s)> need leads assigned by team`, + ); + + const unassignedPrs = pullRequests.filter((pull) => !pull.assignee); + const renovatebotPullCount = unassignedPrs.filter( + (pull) => pull.user.login === 'renovate[bot]', + ).length; + const unassignedCount = unassignedPrs.length - renovatebotPullCount; + output.push( + ` • <https://github.com/internetarchive/openlibrary/pulls?q=is%3Apr+is%3Aopen+no%3Aassignee+-is%3Adraft+-author:app/renovate|${unassignedCount} unassigned PRs> + <https://github.com/internetarchive/openlibrary/pulls/app%2Frenovate|${renovatebotPullCount} renovatebot>`, + ); + + return output; } - /** * Finds all issues with the "Needs: Triage" label, prepares a message containing a * summary for each given lead, and returns the prepared messages. @@ -199,44 +219,51 @@ async function prepareRecentComments(leads) { * @returns {Promise<Array<string>>} Messages about untriaged issues */ async function prepareUntriagedIssues(leads) { - const output = ['*Untriaged Issues*'] - - const untriagedIssues = await octokit.paginate('GET /repos/{owner}/{repo}/issues', { - owner: 'internetarchive', - repo: 'openlibrary', - labels: `Needs: Triage`, - per_page: 100, - headers: { - 'X-GitHub-Api-Version': '2022-11-28' - } - }) - - let allIssuesTriaged = true - for (const lead of leads) { - const searchResultsUrl = `https://github.com/internetarchive/openlibrary/issues?${new URLSearchParams({ - q: `is:issue is:open label:"Needs: Triage" label:"${lead.leadLabel}"` - })}` - let issueCount = 0 - - for (const issue of untriagedIssues) { - for (const label of issue.labels) { - if (label.name === lead.leadLabel) { - allIssuesTriaged = false - ++issueCount - } - } - } - - if (issueCount > 0) { - output.push(` • <${searchResultsUrl}|${issueCount} issue(s)> need triage by ${lead.slackId}`) + const output = ['*Untriaged Issues*']; + + const untriagedIssues = await octokit.paginate( + 'GET /repos/{owner}/{repo}/issues', + { + owner: 'internetarchive', + repo: 'openlibrary', + labels: `Needs: Triage`, + per_page: 100, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + + let allIssuesTriaged = true; + for (const lead of leads) { + const searchResultsUrl = `https://github.com/internetarchive/openlibrary/issues?${new URLSearchParams( + { + q: `is:issue is:open label:"Needs: Triage" label:"${lead.leadLabel}"`, + }, + )}`; + let issueCount = 0; + + for (const issue of untriagedIssues) { + for (const label of issue.labels) { + if (label.name === lead.leadLabel) { + allIssuesTriaged = false; + ++issueCount; } + } } - if (allIssuesTriaged) { - output.push(' _No untriaged issues found_') + if (issueCount > 0) { + output.push( + ` • <${searchResultsUrl}|${issueCount} issue(s)> need triage by ${lead.slackId}`, + ); } + } + + if (allIssuesTriaged) { + output.push(' _No untriaged issues found_'); + } - return output + return output; } /** @@ -247,39 +274,46 @@ async function prepareUntriagedIssues(leads) { * @returns {Promise<Array<string>>} */ async function prepareReviewAssigneeIssues(leads) { - const output = ['*Issues needing attention (_Assignee may have abandoned or may need help_)*:'] - - const staleIssues = await octokit.paginate('GET /repos/{owner}/{repo}/issues', { - owner: 'internetarchive', - repo: 'openlibrary', - labels: 'Needs: Review Assignee', - per_page: 100, - headers: { - 'X-GitHub-Api-Version': '2022-11-28' - } - }) - - let noStaleIssuesFound = true - for (const lead of leads) { - const issuesForLead = staleIssues.filter((issue) => { - for (const label of issue.labels) { - if (label.name === lead.leadLabel) { - return true - } - } - return false - }) - const searchResultsUrl = `https://github.com/internetarchive/openlibrary/issues?q=is%3Aopen+is%3Aissue+label%3A%22Needs%3A+Review+Assignee%22+label%3A${encodeURIComponent('"' + lead.leadLabel + '"')}` - if (issuesForLead.length > 0) { - output.push(` • <${searchResultsUrl}|${issuesForLead.length} issue(s)> need follow-up from ${lead.slackId}`) - noStaleIssuesFound = false + const output = [ + '*Issues needing attention (_Assignee may have abandoned or may need help_)*:', + ]; + + const staleIssues = await octokit.paginate( + 'GET /repos/{owner}/{repo}/issues', + { + owner: 'internetarchive', + repo: 'openlibrary', + labels: 'Needs: Review Assignee', + per_page: 100, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + + let noStaleIssuesFound = true; + for (const lead of leads) { + const issuesForLead = staleIssues.filter((issue) => { + for (const label of issue.labels) { + if (label.name === lead.leadLabel) { + return true; } + } + return false; + }); + const searchResultsUrl = `https://github.com/internetarchive/openlibrary/issues?q=is%3Aopen+is%3Aissue+label%3A%22Needs%3A+Review+Assignee%22+label%3A${encodeURIComponent('"' + lead.leadLabel + '"')}`; + if (issuesForLead.length > 0) { + output.push( + ` • <${searchResultsUrl}|${issuesForLead.length} issue(s)> need follow-up from ${lead.slackId}`, + ); + noStaleIssuesFound = false; } + } - if (noStaleIssuesFound) { - output.push(' _No assigned issues found that need attention from leads_') - } - return output + if (noStaleIssuesFound) { + output.push(' _No assigned issues found that need attention from leads_'); + } + return output; } /** @@ -291,68 +325,68 @@ async function prepareReviewAssigneeIssues(leads) { * @returns {Array<string>} Messages with links to each lead's PRs */ function prepareAssignedPullRequests(pullRequests, leads) { - const output = ['*Assigned PRs*'] - - let noAssignedPullsFound = true - for (const lead of leads) { - const searchResultsUrl = `https://github.com/internetarchive/openlibrary/pulls?q=is%3Aopen+is%3Apr+-is%3Adraft+assignee%3A${lead.githubUsername}` - const assignedPulls = pullRequests.filter((pull) => { - for (const assignee of pull.assignees || []) { - if (assignee.login === lead.githubUsername) { - return true - } - } - return false - }) - - if (assignedPulls.length) { - noAssignedPullsFound = false + const output = ['*Assigned PRs*']; + + let noAssignedPullsFound = true; + for (const lead of leads) { + const searchResultsUrl = `https://github.com/internetarchive/openlibrary/pulls?q=is%3Aopen+is%3Apr+-is%3Adraft+assignee%3A${lead.githubUsername}`; + const assignedPulls = pullRequests.filter((pull) => { + for (const assignee of pull.assignees || []) { + if (assignee.login === lead.githubUsername) { + return true; } + } + return false; + }); - let p0Count = 0, - p1Count = 0, - p2Count = 0 - assignedPulls.forEach((pull) => { - for (const label of pull.labels) { - switch(label.name) { - case 'Priority: 0': - ++p0Count; - break - case 'Priority: 1': - ++p1Count - break - case 'Priority: 2': - ++p2Count - break; - } - } - }) - let statusText = ` • *${lead.githubUsername}* <${searchResultsUrl}|${assignedPulls.length} PR(s)>` - - if (p0Count || p1Count || p2Count) { - statusText += ' [' - if (p0Count) { - statusText += `<${searchResultsUrl + '+label%3A"Priority%3A+0"'}|P0:${p0Count}>, ` - } - if (p1Count) { - statusText += `<${searchResultsUrl + '+label%3A"Priority%3A+1"'}|P1:${p1Count}>, ` - } - if (p2Count) { - statusText += `<${searchResultsUrl + '+label%3A"Priority%3A+2"'}|P2:${p2Count}>` - } else { - // Remove the trailing `, ` characters: - statusText = statusText.substring(0, statusText.length - 3) - } - statusText += ']' - } - output.push(statusText) + if (assignedPulls.length) { + noAssignedPullsFound = false; } - if (noAssignedPullsFound) { - output.push(' _No assigned pull requests found._') + let p0Count = 0, + p1Count = 0, + p2Count = 0; + assignedPulls.forEach((pull) => { + for (const label of pull.labels) { + switch (label.name) { + case 'Priority: 0': + ++p0Count; + break; + case 'Priority: 1': + ++p1Count; + break; + case 'Priority: 2': + ++p2Count; + break; + } + } + }); + let statusText = ` • *${lead.githubUsername}* <${searchResultsUrl}|${assignedPulls.length} PR(s)>`; + + if (p0Count || p1Count || p2Count) { + statusText += ' ['; + if (p0Count) { + statusText += `<${searchResultsUrl + '+label%3A"Priority%3A+0"'}|P0:${p0Count}>, `; + } + if (p1Count) { + statusText += `<${searchResultsUrl + '+label%3A"Priority%3A+1"'}|P1:${p1Count}>, `; + } + if (p2Count) { + statusText += `<${searchResultsUrl + '+label%3A"Priority%3A+2"'}|P2:${p2Count}>`; + } else { + // Remove the trailing `, ` characters: + statusText = statusText.substring(0, statusText.length - 3); + } + statusText += ']'; } + output.push(statusText); + } - return output + if (noAssignedPullsFound) { + output.push(' _No assigned pull requests found._'); + } + + return output; } /** @@ -366,53 +400,56 @@ function prepareAssignedPullRequests(pullRequests, leads) { * @returns {Array<string>} Messages with the current status of each staff PR */ function prepareStaffPullRequests(pullRequests, leads) { - // Include PRs with authors that are in the `leads` configuration: - const includeAuthors = [] - leads.forEach((lead) => includeAuthors.push(lead.githubUsername)) - - // Exclude PRs that have these labels: - const excludeLabels = ['Needs: Submitter Input', 'State: Blocked'] - - const output = ['*Staff PRs*'] - for (const pull of pullRequests) { - const authorName = pull.user?.login - let highPriorityEmoji = '' - if (!includeAuthors.includes(authorName)) { - continue - } - let skipItem = false - for (const label of pull.labels) { - if (excludeLabels.includes(label.name)) { - skipItem = true - break - } - if (label.name === 'Priority: 0') { - highPriorityEmoji = '| 🚨 ' - } - if (label.name === 'Priority: 1' && !highPriorityEmoji) { // Don't clobber higher priority emoji - highPriorityEmoji = '| ❗️ ' - } - } - if (skipItem) { - continue - } + // Include PRs with authors that are in the `leads` configuration: + const includeAuthors = []; + leads.forEach((lead) => includeAuthors.push(lead.githubUsername)); + + // Exclude PRs that have these labels: + const excludeLabels = ['Needs: Submitter Input', 'State: Blocked']; + + const output = ['*Staff PRs*']; + for (const pull of pullRequests) { + const authorName = pull.user?.login; + let highPriorityEmoji = ''; + if (!includeAuthors.includes(authorName)) { + continue; + } + let skipItem = false; + for (const label of pull.labels) { + if (excludeLabels.includes(label.name)) { + skipItem = true; + break; + } + if (label.name === 'Priority: 0') { + highPriorityEmoji = '| 🚨 '; + } + if (label.name === 'Priority: 1' && !highPriorityEmoji) { + // Don't clobber higher priority emoji + highPriorityEmoji = '| ❗️ '; + } + } + if (skipItem) { + continue; + } - const assigneeName = pull.assignee?.login - // Issue title and link: - let summaryMessage = ` • <${pull.html_url}|*#${pull.number}* | ${pull.title}>` + const assigneeName = pull.assignee?.login; + // Issue title and link: + let summaryMessage = ` • <${pull.html_url}|*#${pull.number}* | ${pull.title}>`; - // Creator, assignee, and priority: - const now = Date.now() - const openedAt = Date.parse(pull.created_at) - const elapsedTime = now - openedAt // Time in milliseconds - const daysPassed = Math.floor(elapsedTime / (24 * 60 * 60 * 1000)) + // Creator, assignee, and priority: + const now = Date.now(); + const openedAt = Date.parse(pull.created_at); + const elapsedTime = now - openedAt; // Time in milliseconds + const daysPassed = Math.floor(elapsedTime / (24 * 60 * 60 * 1000)); - const assigneeSlackId = assigneeName ? findSlackId(assigneeName, leads) : '⚠️ None' - summaryMessage += ` by ${pull.user.login} ${daysPassed} days ago | Assigned: ${assigneeSlackId} ${highPriorityEmoji}` - output.push(summaryMessage) - } + const assigneeSlackId = assigneeName + ? findSlackId(assigneeName, leads) + : '⚠️ None'; + summaryMessage += ` by ${pull.user.login} ${daysPassed} days ago | Assigned: ${assigneeSlackId} ${highPriorityEmoji}`; + output.push(summaryMessage); + } - return output + return output; } /** @@ -427,32 +464,36 @@ function prepareStaffPullRequests(pullRequests, leads) { * @returns {Array<string>} Messages about PRs that require submitter input before being reviewed */ function prepareSubmitterInput(pullRequests, leads) { - const output = ['*Submitter Input for our PRs*'] - - let noPullRequestsAwaitingInput = true - for (const lead of leads) { - const searchResultsUrl = `https://github.com/internetarchive/openlibrary/pulls?q=is%3Aopen+is%3Apr+-is%3Adraft+assignee%3A${lead.githubUsername}+label%3A"Needs%3A+Submitter+Input"` - const assignedPulls = pullRequests.filter((pull) => pull.assignee?.login === lead.githubUsername) - let awaitingResponseCount = 0 - assignedPulls.forEach((pull) => { - for (const label of pull.labels) { - if (label.name === 'Needs: Response') { - ++awaitingResponseCount - break - } - } - }) - - if (awaitingResponseCount > 0) { - output.push(` • ${lead.slackId} <${searchResultsUrl}|${awaitingResponseCount} PR(s)>`) - noPullRequestsAwaitingInput = false + const output = ['*Submitter Input for our PRs*']; + + let noPullRequestsAwaitingInput = true; + for (const lead of leads) { + const searchResultsUrl = `https://github.com/internetarchive/openlibrary/pulls?q=is%3Aopen+is%3Apr+-is%3Adraft+assignee%3A${lead.githubUsername}+label%3A"Needs%3A+Submitter+Input"`; + const assignedPulls = pullRequests.filter( + (pull) => pull.assignee?.login === lead.githubUsername, + ); + let awaitingResponseCount = 0; + assignedPulls.forEach((pull) => { + for (const label of pull.labels) { + if (label.name === 'Needs: Response') { + ++awaitingResponseCount; + break; } + } + }); + + if (awaitingResponseCount > 0) { + output.push( + ` • ${lead.slackId} <${searchResultsUrl}|${awaitingResponseCount} PR(s)>`, + ); + noPullRequestsAwaitingInput = false; } + } - if (noPullRequestsAwaitingInput) { - output.push(' _No leads are awaiting submitter input for their PRs_') - } - return output + if (noPullRequestsAwaitingInput) { + output.push(' _No leads are awaiting submitter input for their PRs_'); + } + return output; } /** @@ -467,17 +508,17 @@ function prepareSubmitterInput(pullRequests, leads) { * @returns {Promise<Response>} */ async function publishToSlack(lines, slackChannel) { - const message = lines.join('\n') - const bearerToken = process.env.SLACK_TOKEN - return fetch('https://slack.com/api/chat.postMessage', { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=utf-8', - Authorization: `Bearer ${bearerToken}` - }, - body: JSON.stringify({ - channel: slackChannel, - text: message - }) - }) + const message = lines.join('\n'); + const bearerToken = process.env.SLACK_TOKEN; + return fetch('https://slack.com/api/chat.postMessage', { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + Authorization: `Bearer ${bearerToken}`, + }, + body: JSON.stringify({ + channel: slackChannel, + text: message, + }), + }); } diff --git a/scripts/solr_restarter/index.js b/scripts/solr_restarter/index.js index ff92e16ec95..6c620a15878 100644 --- a/scripts/solr_restarter/index.js +++ b/scripts/solr_restarter/index.js @@ -10,127 +10,144 @@ */ const { execSync } = require('child_process'); - /** * @param {number} ms */ async function sleep(ms) { - return new Promise(res => setTimeout(() => res(), ms)); + return new Promise((res) => setTimeout(() => res(), ms)); } class SolrRestarter { - /** Don't restart twice in 10 minutes */ - MAX_RESTART_WIN = 10*60*1000; - /** Must be unhealthy for this many minutes to trigger a refresh */ - UNHEALTHY_DURATION = 2*60*1000; - /** Check every minute */ - CHECK_FREQ = 60*1000; - /** How many times we're aloud to try restarting without going healthy before giving up */ - MAX_RESTARTS = 3; - /** Number of restarts we've done without transitioning to healthy */ - restartsRun = 0; - /** timestamp in ms */ - lastRestart = 0; - /** @type {'healthy' | 'unhealthy'} */ - state = 'healthy'; - /** timestamp in ms */ - lastStateChange = 0; - /** Number of consecutive health checks that haven't failed or succeeded, but errored. */ - healthCheckErrorRun = 0; + /** Don't restart twice in 10 minutes */ + MAX_RESTART_WIN = 10 * 60 * 1000; + /** Must be unhealthy for this many minutes to trigger a refresh */ + UNHEALTHY_DURATION = 2 * 60 * 1000; + /** Check every minute */ + CHECK_FREQ = 60 * 1000; + /** How many times we're aloud to try restarting without going healthy before giving up */ + MAX_RESTARTS = 3; + /** Number of restarts we've done without transitioning to healthy */ + restartsRun = 0; + /** timestamp in ms */ + lastRestart = 0; + /** @type {'healthy' | 'unhealthy'} */ + state = 'healthy'; + /** timestamp in ms */ + lastStateChange = 0; + /** Number of consecutive health checks that haven't failed or succeeded, but errored. */ + healthCheckErrorRun = 0; - /** The URL to fetch in our healthcheck */ - TEST_URL = process.env.TEST_URL ?? 'http://openlibrary.org/search.json?q=hello&mode=everything&limit=0'; + /** The URL to fetch in our healthcheck */ + TEST_URL = + process.env.TEST_URL ?? + 'http://openlibrary.org/search.json?q=hello&mode=everything&limit=0'; - /** Whether we should send slack messages, or just console.log */ - SEND_SLACK_MESSAGE = process.env.SEND_SLACK_MESSAGE == 'true'; + /** Whether we should send slack messages, or just console.log */ + SEND_SLACK_MESSAGE = process.env.SEND_SLACK_MESSAGE == 'true'; - /** The containers to restart */ - CONTAINER_NAMES = process.env.CONTAINER_NAMES; + /** The containers to restart */ + CONTAINER_NAMES = process.env.CONTAINER_NAMES; - async checkHealth() { - console.log(this.TEST_URL); - const resp = await Promise.race([fetch(this.TEST_URL), sleep(3000).then(() => 'timeout')]); + async checkHealth() { + console.log(this.TEST_URL); + const resp = await Promise.race([ + fetch(this.TEST_URL), + sleep(3000).then(() => 'timeout'), + ]); - if (resp == 'timeout') return false; + if (resp == 'timeout') return false; - try { - const json = await resp.json(); - return !json.error && json.numFound; - } catch (err) { - throw `Invalid response: ${await resp.text()}`; - } + try { + const json = await resp.json(); + return !json.error && json.numFound; + } catch (err) { + throw `Invalid response: ${await resp.text()}`; } + } - /** - * @param {string} text - */ - async sendSlackMessage(text) { - if (this.SEND_SLACK_MESSAGE) { - await fetch('https://slack.com/api/chat.postMessage', { - method: 'POST', - headers: { - Authorization: `Bearer ${process.env.SLACK_TOKEN}`, - "Content-Type": "application/json; charset=utf-8", - }, - body: JSON.stringify({ - text, - channel: process.env.SLACK_CHANNEL_ID, - }) - }).then(r => r.text()); - } else { - console.log(text); - } + /** + * @param {string} text + */ + async sendSlackMessage(text) { + if (this.SEND_SLACK_MESSAGE) { + await fetch('https://slack.com/api/chat.postMessage', { + method: 'POST', + headers: { + Authorization: `Bearer ${process.env.SLACK_TOKEN}`, + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify({ + text, + channel: process.env.SLACK_CHANNEL_ID, + }), + }).then((r) => r.text()); + } else { + console.log(text); } + } - async loop() { - while (true) { - let isHealthy = true; - try { - isHealthy = await this.checkHealth(); - } catch (err) { - this.healthCheckErrorRun++; - if (this.healthCheckErrorRun > 3) { - // This is an unexpected error; likely means OL is down for other reasons. - await this.sendSlackMessage(`Health check errored 3+ times with ${err}; skipping?`); - } - await sleep(this.CHECK_FREQ); - continue; - } - this.healthCheckErrorRun = 0; - const newState = isHealthy ? 'healthy' : 'unhealthy'; - if (this.state != newState) { - this.lastStateChange = Date.now(); - } - this.state = newState; - console.log(`State: ${this.state}`); + async loop() { + while (true) { + let isHealthy = true; + try { + isHealthy = await this.checkHealth(); + } catch (err) { + this.healthCheckErrorRun++; + if (this.healthCheckErrorRun > 3) { + // This is an unexpected error; likely means OL is down for other reasons. + await this.sendSlackMessage( + `Health check errored 3+ times with ${err}; skipping?`, + ); + } + await sleep(this.CHECK_FREQ); + continue; + } + this.healthCheckErrorRun = 0; + const newState = isHealthy ? 'healthy' : 'unhealthy'; + if (this.state != newState) { + this.lastStateChange = Date.now(); + } + this.state = newState; + console.log(`State: ${this.state}`); - if (!isHealthy) { - if (Date.now() - this.lastStateChange > this.UNHEALTHY_DURATION) { - const canRestart = Date.now() - this.lastRestart > this.MAX_RESTART_WIN; - if (canRestart) { - if (this.restartsRun >= this.MAX_RESTARTS) { - await this.sendSlackMessage("Hit max restarts. we're clearly not helping. Exiting."); - throw new Error("MAX_RESTARTS exceeded"); - } - await this.sendSlackMessage(`solr-restarter: Unhealthy for a few minutes; Restarting solr`); - execSync(`docker restart ${this.CONTAINER_NAMES}`, { stdio: "inherit" }); - this.restartsRun++; - this.lastRestart = Date.now(); - } else { - console.log('Cannot restart; too soon since last restart'); - } - } - } else { - // Send a message if we recently tried to restart - if (this.restartsRun) { - await this.sendSlackMessage(`solr-restarter: solr state now ${this.state} :success-kid:`); - } - this.restartsRun = 0; + if (!isHealthy) { + if (Date.now() - this.lastStateChange > this.UNHEALTHY_DURATION) { + const canRestart = + Date.now() - this.lastRestart > this.MAX_RESTART_WIN; + if (canRestart) { + if (this.restartsRun >= this.MAX_RESTARTS) { + await this.sendSlackMessage( + "Hit max restarts. we're clearly not helping. Exiting.", + ); + throw new Error('MAX_RESTARTS exceeded'); } - await sleep(this.CHECK_FREQ); + await this.sendSlackMessage( + `solr-restarter: Unhealthy for a few minutes; Restarting solr`, + ); + execSync(`docker restart ${this.CONTAINER_NAMES}`, { + stdio: 'inherit', + }); + this.restartsRun++; + this.lastRestart = Date.now(); + } else { + console.log('Cannot restart; too soon since last restart'); + } + } + } else { + // Send a message if we recently tried to restart + if (this.restartsRun) { + await this.sendSlackMessage( + `solr-restarter: solr state now ${this.state} :success-kid:`, + ); } + this.restartsRun = 0; + } + await sleep(this.CHECK_FREQ); } + } } -process.on('unhandledRejection', err => { throw err }); +process.on('unhandledRejection', (err) => { + throw err; +}); new SolrRestarter().loop(); diff --git a/static/bookmarklets/import_webbook.js b/static/bookmarklets/import_webbook.js index 15dc2923396..7eaac7a5c6b 100644 --- a/static/bookmarklets/import_webbook.js +++ b/static/bookmarklets/import_webbook.js @@ -1,7 +1,7 @@ -javascript:(async()=> { - const url = prompt('Enter the book URL you want to import:'); - if (!url) return; - const promptText = `You are an expert book metadata librarian and research assistant. Your role is to search the web and collect accurate data to produce the most useful, factual, complete, and patron-oriented book page possible for the URL: +javascript: (async () => { + const url = prompt('Enter the book URL you want to import:'); + if (!url) return; + const promptText = `You are an expert book metadata librarian and research assistant. Your role is to search the web and collect accurate data to produce the most useful, factual, complete, and patron-oriented book page possible for the URL: Book URL: ${url} @@ -97,6 +97,10 @@ Field-Specific Instructions: - "pdf", "epub", "html", "web", etc. - Always set "access": "read" and "provider_name": "Open Library Community Librarians".`; - const chatUrl = `https://chatgpt.com/?hints=search&q=${encodeURIComponent(promptText)}`; - window.open(chatUrl, '_blank', 'width=1000,height=800,menubar=no,toolbar=no,location=no,status=no,scrollbars=yes'); + const chatUrl = `https://chatgpt.com/?hints=search&q=${encodeURIComponent(promptText)}`; + window.open( + chatUrl, + '_blank', + 'width=1000,height=800,menubar=no,toolbar=no,location=no,status=no,scrollbars=yes', + ); })(); diff --git a/stories/.storybook/main.js b/stories/.storybook/main.js index d490645c4ca..b0a1bbd6083 100644 --- a/stories/.storybook/main.js +++ b/stories/.storybook/main.js @@ -1,24 +1,19 @@ -const webpackConfig = require( '../../webpack.config' ); +const webpackConfig = require('../../webpack.config'); module.exports = { webpackFinal: async (config) => { config.module.rules = config.module.rules.concat( - webpackConfig.module.rules + webpackConfig.module.rules, ); return config; }, - "framework": { - "name": '@storybook/html-webpack5' + framework: { + name: '@storybook/html-webpack5', }, - "stories": [ - "../**/*.mdx", - "../**/*.stories.@(js|jsx|ts|tsx)" - ], - "core": { + stories: ['../**/*.mdx', '../**/*.stories.@(js|jsx|ts|tsx)'], + core: { // Opt out of telemetry: https://storybook.js.org/docs/html/configure/telemetry - "disableTelemetry": true + disableTelemetry: true, }, - "addons": [ - "@storybook/addon-essentials" - ], -} \ No newline at end of file + addons: ['@storybook/addon-essentials'], +}; diff --git a/stories/.storybook/preview.js b/stories/.storybook/preview.js index 5d00c021204..b285aacafd8 100644 --- a/stories/.storybook/preview.js +++ b/stories/.storybook/preview.js @@ -1,4 +1,3 @@ - export const parameters = { - actions: { argTypesRegex: "^on[A-Z].*" }, -} \ No newline at end of file + actions: { argTypesRegex: '^on[A-Z].*' }, +}; diff --git a/stories/Button.stories.js b/stories/Button.stories.js index 9e77fab571d..41137aa1cd3 100644 --- a/stories/Button.stories.js +++ b/stories/Button.stories.js @@ -2,65 +2,68 @@ import '../static/css/components/buttonCta.css'; import '../static/css/components/buttonCta--js.css'; export default { - title: 'Legacy/Button' + title: 'Legacy/Button', }; -const ButtonTemplate = (buttonType, text, badgeCount=null) => `<div class="cta-btn${ButtonTypes[buttonType]}">${text}${badgeCount ? BadgeTemplate(badgeCount) : ''}</div>`; +const ButtonTemplate = (buttonType, text, badgeCount = null) => + `<div class="cta-btn${ButtonTypes[buttonType]}">${text}${badgeCount ? BadgeTemplate(badgeCount) : ''}</div>`; -const BadgeTemplate = (badgeCount) => ` <span class="cta-btn__badge">${badgeCount}</span>` +const BadgeTemplate = (badgeCount) => + ` <span class="cta-btn__badge">${badgeCount}</span>`; const ButtonTypes = { - default: '', - unavailable: ' cta-btn--unavailable', - available: ' cta-btn--available', - preview: ' cta-btn--shell cta-btn--preview' -} + default: '', + unavailable: ' cta-btn--unavailable', + available: ' cta-btn--available', + preview: ' cta-btn--shell cta-btn--preview', +}; -export const CtaBtn = () => ButtonTemplate('default','Leave waitlist'); +export const CtaBtn = () => ButtonTemplate('default', 'Leave waitlist'); CtaBtn.parameters = { - docs: { - source: { - code: ButtonTemplate('default', 'Leave waitlist') - } - } -} + docs: { + source: { + code: ButtonTemplate('default', 'Leave waitlist'), + }, + }, +}; -export const CtaBtnUnavailable = () => ButtonTemplate('unavailable','Join waitlist'); +export const CtaBtnUnavailable = () => + ButtonTemplate('unavailable', 'Join waitlist'); CtaBtnUnavailable.parameters = { - docs: { - source: { - code: ButtonTemplate('unavailable', 'Join waitlist') - } - } -} + docs: { + source: { + code: ButtonTemplate('unavailable', 'Join waitlist'), + }, + }, +}; -export const CtaBtnAvailable = () => ButtonTemplate('available','Borrow'); +export const CtaBtnAvailable = () => ButtonTemplate('available', 'Borrow'); CtaBtnAvailable.parameters = { - docs: { - source: { - code: ButtonTemplate('available', 'Borrow') - } - } -} + docs: { + source: { + code: ButtonTemplate('available', 'Borrow'), + }, + }, +}; -export const CtaBtnPreview = () => ButtonTemplate('preview','Preview'); +export const CtaBtnPreview = () => ButtonTemplate('preview', 'Preview'); CtaBtnPreview.parameters = { - docs: { - source: { - code: ButtonTemplate('preview', 'Preview') - } - } -} + docs: { + source: { + code: ButtonTemplate('preview', 'Preview'), + }, + }, +}; export const CtaBtnWithBadge = () => - ButtonTemplate('unavailable','Join waiting list',4); + ButtonTemplate('unavailable', 'Join waiting list', 4); CtaBtnWithBadge.parameters = { - docs: { - source: { - code: ButtonTemplate('unavailable', 'Join waiting list', 4) - } - } -} + docs: { + source: { + code: ButtonTemplate('unavailable', 'Join waiting list', 4), + }, + }, +}; export const CtaBtnGroup = () => `<div class="cta-button-group"> <a href="/borrow/ia/sevenhabitsofhi00cove?ref=ol" title="Borrow ebook from Internet Archive" id="borrow_ebook" data-ol-link-track="CTAClick|Borrow" class="cta-btn cta-btn--available">Borrow</a> diff --git a/tests/unit/js/Browser.test.js b/tests/unit/js/Browser.test.js index ac5e84f51ed..e90ad35efd7 100644 --- a/tests/unit/js/Browser.test.js +++ b/tests/unit/js/Browser.test.js @@ -1,48 +1,59 @@ -import { removeURLParameter, getJsonFromUrl } from '../../../openlibrary/plugins/openlibrary/js/Browser'; +import { + getJsonFromUrl, + removeURLParameter, +} from '../../../openlibrary/plugins/openlibrary/js/Browser'; describe('removeURLParameter', () => { - const fn = removeURLParameter; - - test('URL with no parameters', () => { - expect(fn('http://foo.com', 'x')).toBe('http://foo.com'); - }); - - test('URL with the given parameter', () => { - expect(fn('http://foo.com?x=3', 'x')).toBe('http://foo.com'); - expect(fn('http://foo.com?x=3&y=4&z=5', 'x')).toBe('http://foo.com?y=4&z=5'); - expect(fn('http://foo.com?x=3&y=4&z=5', 'y')).toBe('http://foo.com?x=3&z=5'); - expect(fn('http://foo.com?x=3&y=4&z=5', 'z')).toBe('http://foo.com?x=3&y=4'); - }); - - test('URL without the given parameter', () => { - expect(fn('http://foo.com?x=3', 'y')).toBe('http://foo.com?x=3'); - expect(fn('http://foo.com?x=3&y=4&z=5', 'w')).toBe('http://foo.com?x=3&y=4&z=5'); - }); - - test('URL with multiple occurences of param', () => { - expect(fn('http://foo.com?x=3&x=4&x=5', 'x')).toBe('http://foo.com'); - expect(fn('http://foo.com?x=3&x=4&z=5', 'x')).toBe('http://foo.com?z=5'); - }) + const fn = removeURLParameter; + + test('URL with no parameters', () => { + expect(fn('http://foo.com', 'x')).toBe('http://foo.com'); + }); + + test('URL with the given parameter', () => { + expect(fn('http://foo.com?x=3', 'x')).toBe('http://foo.com'); + expect(fn('http://foo.com?x=3&y=4&z=5', 'x')).toBe( + 'http://foo.com?y=4&z=5', + ); + expect(fn('http://foo.com?x=3&y=4&z=5', 'y')).toBe( + 'http://foo.com?x=3&z=5', + ); + expect(fn('http://foo.com?x=3&y=4&z=5', 'z')).toBe( + 'http://foo.com?x=3&y=4', + ); + }); + + test('URL without the given parameter', () => { + expect(fn('http://foo.com?x=3', 'y')).toBe('http://foo.com?x=3'); + expect(fn('http://foo.com?x=3&y=4&z=5', 'w')).toBe( + 'http://foo.com?x=3&y=4&z=5', + ); + }); + + test('URL with multiple occurences of param', () => { + expect(fn('http://foo.com?x=3&x=4&x=5', 'x')).toBe('http://foo.com'); + expect(fn('http://foo.com?x=3&x=4&z=5', 'x')).toBe('http://foo.com?z=5'); + }); }); describe('getJsonFromUrl', () => { - const fn = getJsonFromUrl; + const fn = getJsonFromUrl; - test('Handles empty strings', () => { - expect(fn('')).toEqual({}); - expect(fn('?')).toEqual({}); - }); + test('Handles empty strings', () => { + expect(fn('')).toEqual({}); + expect(fn('?')).toEqual({}); + }); - test('Handles normal params', () => { - expect(fn('?hello=world')).toEqual({hello: 'world'}); - expect(fn('?x=3&y=4&z=5')).toEqual({x: '3', y: '4', z: '5'}); - }); + test('Handles normal params', () => { + expect(fn('?hello=world')).toEqual({ hello: 'world' }); + expect(fn('?x=3&y=4&z=5')).toEqual({ x: '3', y: '4', z: '5' }); + }); - test('Decodes parameter values', () => { - expect(fn('?q=foo%20bar')).toEqual({q: 'foo bar'}); - }); + test('Decodes parameter values', () => { + expect(fn('?q=foo%20bar')).toEqual({ q: 'foo bar' }); + }); - test('Parameters override each other', () => { - expect(fn('?x=1&x=2&x=3')).toEqual({x: '3'}); - }); + test('Parameters override each other', () => { + expect(fn('?x=1&x=2&x=3')).toEqual({ x: '3' }); + }); }); diff --git a/tests/unit/js/SearchBar.test.js b/tests/unit/js/SearchBar.test.js index e52be87166a..a731ffcc667 100644 --- a/tests/unit/js/SearchBar.test.js +++ b/tests/unit/js/SearchBar.test.js @@ -1,10 +1,10 @@ import sinon from 'sinon'; +import * as nonjquery_utils from '../../../openlibrary/plugins/openlibrary/js/nonjquery_utils.js'; import { SearchBar } from '../../../openlibrary/plugins/openlibrary/js/SearchBar'; import * as SearchUtils from '../../../openlibrary/plugins/openlibrary/js/SearchUtils'; -import * as nonjquery_utils from '../../../openlibrary/plugins/openlibrary/js/nonjquery_utils.js'; describe('SearchBar', () => { - const DUMMY_COMPONENT_HTML = ` + const DUMMY_COMPONENT_HTML = ` <div> <form class="search-bar-input" action="https://openlibrary.org/search?q=foo"> <input type="text"> @@ -12,277 +12,305 @@ describe('SearchBar', () => { <ul class="search-results"></ul> </div>`; - describe('initFromUrlParams', () => { - /** @type {SearchBar} */ - let sb; - beforeEach(() => { - sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - sinon.stub(sb, 'getCurUrl').returns(new URL('https://openlibrary.org/search')); - }); - afterEach(() => localStorage.clear()); - test('Does not throw on empty params', () => { - sb.initFromUrlParams({}); - }); - - test('Updates facet from params', () => { - expect(sb.facet.read()).not.toBe('title'); - sb.initFromUrlParams({facet: 'title'}); - expect(sb.facet.read()).toBe('title'); - }); - - test('Ignore invalid facets', () => { - const originalValue = sb.facet.read(); - sb.initFromUrlParams({facet: 'spam'}); - expect(sb.facet.read()).toBe(originalValue); - }); - - test('Sets input value from q param', () => { - sb.initFromUrlParams({q: 'Harry Potter'}); - expect(sb.$input.val()).toBe('Harry Potter'); - }); - - test('Remove title prefix from q param', () => { - sb.initFromUrlParams({q: 'title:"Harry Potter"', facet: 'title'}); - expect(sb.$input.val()).toBe('Harry Potter'); - sb.initFromUrlParams({q: 'title: "Harry"', facet: 'title'}); - expect(sb.$input.val()).toBe('Harry'); - }); - - test('Persists value in url param', () => { - expect(localStorage.getItem('facet')).not.toBe('title'); - sb.initFromUrlParams({facet: 'title'}); - expect(localStorage.getItem('facet')).toBe('title'); - }); + describe('initFromUrlParams', () => { + /** @type {SearchBar} */ + let sb; + beforeEach(() => { + sb = new SearchBar($(DUMMY_COMPONENT_HTML)); + sinon + .stub(sb, 'getCurUrl') + .returns(new URL('https://openlibrary.org/search')); + }); + afterEach(() => localStorage.clear()); + test('Does not throw on empty params', () => { + sb.initFromUrlParams({}); + }); + + test('Updates facet from params', () => { + expect(sb.facet.read()).not.toBe('title'); + sb.initFromUrlParams({ facet: 'title' }); + expect(sb.facet.read()).toBe('title'); + }); + + test('Ignore invalid facets', () => { + const originalValue = sb.facet.read(); + sb.initFromUrlParams({ facet: 'spam' }); + expect(sb.facet.read()).toBe(originalValue); + }); + + test('Sets input value from q param', () => { + sb.initFromUrlParams({ q: 'Harry Potter' }); + expect(sb.$input.val()).toBe('Harry Potter'); + }); + + test('Remove title prefix from q param', () => { + sb.initFromUrlParams({ q: 'title:"Harry Potter"', facet: 'title' }); + expect(sb.$input.val()).toBe('Harry Potter'); + sb.initFromUrlParams({ q: 'title: "Harry"', facet: 'title' }); + expect(sb.$input.val()).toBe('Harry'); + }); + + test('Persists value in url param', () => { + expect(localStorage.getItem('facet')).not.toBe('title'); + sb.initFromUrlParams({ facet: 'title' }); + expect(localStorage.getItem('facet')).toBe('title'); + }); + }); + + describe('submitForm', () => { + let sb; + beforeEach(() => { + sb = new SearchBar($(DUMMY_COMPONENT_HTML)); + }); + afterEach(() => localStorage.clear()); + + test('Queries are marshalled before submit for titles', () => { + sb.initFromUrlParams({ facet: 'title' }); + const spy = sinon.spy(SearchBar, 'marshalBookSearchQuery'); + sb.submitForm(); + expect(spy.callCount).toBe(1); + spy.restore(); + }); + + test('Form action is updated on submit', () => { + sb.initFromUrlParams({ facet: 'title' }); + const originalAction = sb.$form[0].action; + sb.submitForm(); + expect(sb.$form[0].action).not.toBe(originalAction); + }); + + test('Special inputs are added to the form on submit', () => { + const spy = sinon.spy(SearchUtils, 'addModeInputsToForm'); + sb.submitForm(); + expect(spy.callCount).toBe(1); + }); + }); + + describe('toggleCollapsibleModeForSmallScreens', () => { + /** @type {SearchBar?} */ + let sb; + beforeEach(() => (sb = new SearchBar($(DUMMY_COMPONENT_HTML)))); + afterEach(() => localStorage.clear()); + + test('Only enters collapsible mode if not already there', () => { + sb.inCollapsibleMode = true; + const spy = sinon.spy(sb, 'enableCollapsibleMode'); + sb.toggleCollapsibleModeForSmallScreens(100); + expect(spy.callCount).toBe(0); + }); + + test('Only exits collapsible mode if not already exited', () => { + sb.inCollapsibleMode = false; + const spy = sinon.spy(sb, 'disableCollapsibleMode'); + sb.toggleCollapsibleModeForSmallScreens(1000); + expect(spy.callCount).toBe(0); + }); + }); + + describe('marshalBookSearchQuery', () => { + const fn = SearchBar.marshalBookSearchQuery; + test('Empty string', () => { + expect(fn('')).toBe(''); }); - describe('submitForm', () => { - let sb; - beforeEach(() => { - sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - }); - afterEach(() => localStorage.clear()); - - test('Queries are marshalled before submit for titles', () => { - sb.initFromUrlParams({facet: 'title'}); - const spy = sinon.spy(SearchBar, 'marshalBookSearchQuery'); - sb.submitForm(); - expect(spy.callCount).toBe(1); - spy.restore(); - }); - - test('Form action is updated on submit', () => { - sb.initFromUrlParams({facet: 'title'}); - const originalAction = sb.$form[0].action; - sb.submitForm(); - expect(sb.$form[0].action).not.toBe(originalAction); - }); - - test('Special inputs are added to the form on submit', () => { - const spy = sinon.spy(SearchUtils, 'addModeInputsToForm') - sb.submitForm(); - expect(spy.callCount).toBe(1); - }); + test('Adds title prefix to plain strings', () => { + expect(fn('Harry Potter')).toBe('title: "Harry Potter"'); }); - describe('toggleCollapsibleModeForSmallScreens', () => { - /** @type {SearchBar?} */ - let sb; - beforeEach(() => sb = new SearchBar($(DUMMY_COMPONENT_HTML))); - afterEach(() => localStorage.clear()); - - test('Only enters collapsible mode if not already there', () => { - sb.inCollapsibleMode = true; - const spy = sinon.spy(sb, 'enableCollapsibleMode'); - sb.toggleCollapsibleModeForSmallScreens(100); - expect(spy.callCount).toBe(0); - }); - - test('Only exits collapsible mode if not already exited', () => { - sb.inCollapsibleMode = false; - const spy = sinon.spy(sb, 'disableCollapsibleMode'); - sb.toggleCollapsibleModeForSmallScreens(1000); - expect(spy.callCount).toBe(0); - }); + test('Does not add title prefix to lucene-style queries', () => { + expect(fn('author:"Harry Potter"')).toBe('author:"Harry Potter"'); + expect(fn('"Harry Potter"')).toBe('"Harry Potter"'); }); + }); - describe('marshalBookSearchQuery', () => { - const fn = SearchBar.marshalBookSearchQuery; - test('Empty string', () => { - expect(fn('')).toBe(''); - }); + describe('Misc', () => { + const sandbox = sinon.createSandbox(); + afterEach(() => { + sandbox.restore(); + localStorage.clear(); + }); - test('Adds title prefix to plain strings', () => { - expect(fn('Harry Potter')).toBe('title: "Harry Potter"'); - }); + test('When localStorage empty, defaults to facet=all', () => { + localStorage.clear(); + const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); + expect(sb.facet.read()).toBe('all'); + }); - test('Does not add title prefix to lucene-style queries', () => { - expect(fn('author:"Harry Potter"')).toBe('author:"Harry Potter"'); - expect(fn('"Harry Potter"')).toBe('"Harry Potter"'); - }); + test('Facet persists between page loads', () => { + localStorage.setItem('facet', 'title'); + const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); + expect(sb.facet.read()).toBe('title'); + const sb2 = new SearchBar($(DUMMY_COMPONENT_HTML)); + expect(sb2.facet.read()).toBe('title'); }); - describe('Misc', () => { - const sandbox = sinon.createSandbox(); - afterEach(() => { - sandbox.restore(); - localStorage.clear(); - }); - - test('When localStorage empty, defaults to facet=all', () => { - localStorage.clear(); - const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - expect(sb.facet.read()).toBe('all'); - }); - - test('Facet persists between page loads', () => { - localStorage.setItem('facet', 'title'); - const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - expect(sb.facet.read()).toBe('title'); - const sb2 = new SearchBar($(DUMMY_COMPONENT_HTML)); - expect(sb2.facet.read()).toBe('title'); - }); - - test('Advanced facet triggers redirect', () => { - const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - const navigateToStub = sandbox.stub(sb, 'navigateTo'); - const event = Object.assign(new $.Event(), { target: { value: 'advanced' } }); - sb.handleFacetSelectChange(event); - expect(navigateToStub.callCount).toBe(1); - expect(navigateToStub.args[0]).toEqual(['/advancedsearch']); - }); - - for (const facet of ['title', 'author', 'all']) { - test(`Facet "${facet}" searches tigger autocomplete`, () => { - // Stub debounce to avoid have to manipulate time (!) - sandbox.stub(nonjquery_utils, 'debounce').callsFake(fn => fn); - const sb = new SearchBar($(DUMMY_COMPONENT_HTML), { facet }); - const getJSONStub = sandbox.stub($, 'getJSON'); - - sb.$input.val('Harry'); - sb.$input.triggerHandler('focus'); - expect(getJSONStub.callCount).toBe(1); - }); - } - - test('Title searches tigger autocomplete even if containing title: prefix', () => { - // Stub debounce to avoid have to manipulate time (!) - sandbox.stub(nonjquery_utils, 'debounce').callsFake(fn => fn); - const sb = new SearchBar($(DUMMY_COMPONENT_HTML), {facet: 'title'}); - const getJSONStub = sandbox.stub($, 'getJSON'); - sb.$input.val('title:"Harry"'); - sb.$input.triggerHandler('focus'); - expect(getJSONStub.callCount).toBe(1); - }); - - test('Focussing on input when empty does not trigger autocomplete', () => { - // Stub debounce to avoid have to manipulate time (!) - sandbox.stub(nonjquery_utils, 'debounce').callsFake(fn => fn); - const sb = new SearchBar($(DUMMY_COMPONENT_HTML), {facet: 'title'}); - const getJSONStub = sandbox.stub($, 'getJSON'); - sb.$input.val(''); - sb.$input.triggerHandler('focus'); - expect(getJSONStub.callCount).toBe(0); - }); - - for (const facet of ['lists', 'subject', 'text']) { - test(`Facet "${facet}" does not tigger autocomplete`, () => { - // Stub debounce to avoid have to manipulate time (!) - sandbox.stub(nonjquery_utils, 'debounce').callsFake(fn => fn); - const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - const getJSONStub = sandbox.stub($, 'getJSON'); - - sb.$input.val('foo bar'); - sb.facet.write(facet); - sb.$input.triggerHandler('focus'); - expect(getJSONStub.callCount).toBe(0); - }); - } - - test('Tabbing out of search input clears autocomplete results', () => { - const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - - // Spy on the clearAutocompletionResults method - const clearResultsSpy = sandbox.spy(sb, 'clearAutocompletionResults'); - - // Simulate tab keydown event on the form - const tabEvent = $.Event('keydown', { key: 'Tab' }); - sb.$form.trigger(tabEvent); - - // Verify clearAutocompletionResults was called - expect(clearResultsSpy.callCount).toBe(1); - }); - - test('Autocomplete rendering behavior depends on existing results', () => { - sandbox.stub(nonjquery_utils, 'debounce').callsFake(fn => fn); - const sb = new SearchBar($(DUMMY_COMPONENT_HTML), { facet: 'title' }); - const renderSpy = sandbox.spy(sb, 'renderAutocompletionResults'); - - // Should render when results are empty - sb.$input.triggerHandler('focus'); - expect(renderSpy.callCount).toBe(1, 'Should render when no results exist'); - - renderSpy.resetHistory(); - - // Should not render when results exist - sb.$results.append('<li>Some result</li>'); - sb.$input.triggerHandler('focus'); - expect(renderSpy.callCount).toBe(0, 'Should not render when results exist'); - }); - - test('Tabbing from search result focuses search submit button and clears results', () => { - const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - - // Add a dummy result and focus on it - sb.$results.append('<li tabindex="0">Test Result</li>'); - const $resultItem = sb.$results.children().first(); - $resultItem.trigger('focus'); - - // Spy on the clearAutocompletionResults method - const clearResultsSpy = sandbox.spy(sb, 'clearAutocompletionResults'); - - // Spy on the focus trigger for search submit - const focusSpy = sandbox.spy(sb.$searchSubmit, 'trigger'); - - // Simulate tab keydown event on the result item - const tabEvent = $.Event('keydown', { key: 'Tab', shiftKey: false }); - $resultItem.trigger(tabEvent); - - // Verify clearAutocompletionResults was called - expect(clearResultsSpy.callCount).toBe(1, 'Should clear autocomplete results'); - - // Verify search submit was focused - expect(focusSpy.calledWith('focus')).toBe(true, 'Should focus search submit button'); - - // Verify event default was prevented - expect(tabEvent.isDefaultPrevented()).toBe(true, 'Should prevent default tab behavior'); - }); - - test('Shift+tabbing from search result focuses facet select and clears results', () => { - const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - - // Add a dummy result and focus on it - sb.$results.append('<li tabindex="0">Test Result</li>'); - const $resultItem = sb.$results.children().first(); - $resultItem.trigger('focus'); - - // Spy on the clearAutocompletionResults method - const clearResultsSpy = sandbox.spy(sb, 'clearAutocompletionResults'); - - // Spy on the focus trigger for facet select - const focusSpy = sandbox.spy(sb.$facetSelect, 'trigger'); - - // Simulate shift+tab keydown event on the result item - const shiftTabEvent = $.Event('keydown', { key: 'Tab', shiftKey: true }); - $resultItem.trigger(shiftTabEvent); - - // Verify clearAutocompletionResults was called - expect(clearResultsSpy.callCount).toBe(1, 'Should clear autocomplete results'); + test('Advanced facet triggers redirect', () => { + const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); + const navigateToStub = sandbox.stub(sb, 'navigateTo'); + const event = Object.assign(new $.Event(), { + target: { value: 'advanced' }, + }); + sb.handleFacetSelectChange(event); + expect(navigateToStub.callCount).toBe(1); + expect(navigateToStub.args[0]).toEqual(['/advancedsearch']); + }); + + for (const facet of ['title', 'author', 'all']) { + test(`Facet "${facet}" searches tigger autocomplete`, () => { + // Stub debounce to avoid have to manipulate time (!) + sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); + const sb = new SearchBar($(DUMMY_COMPONENT_HTML), { facet }); + const getJSONStub = sandbox.stub($, 'getJSON'); + + sb.$input.val('Harry'); + sb.$input.triggerHandler('focus'); + expect(getJSONStub.callCount).toBe(1); + }); + } + + test('Title searches tigger autocomplete even if containing title: prefix', () => { + // Stub debounce to avoid have to manipulate time (!) + sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); + const sb = new SearchBar($(DUMMY_COMPONENT_HTML), { facet: 'title' }); + const getJSONStub = sandbox.stub($, 'getJSON'); + sb.$input.val('title:"Harry"'); + sb.$input.triggerHandler('focus'); + expect(getJSONStub.callCount).toBe(1); + }); + + test('Focussing on input when empty does not trigger autocomplete', () => { + // Stub debounce to avoid have to manipulate time (!) + sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); + const sb = new SearchBar($(DUMMY_COMPONENT_HTML), { facet: 'title' }); + const getJSONStub = sandbox.stub($, 'getJSON'); + sb.$input.val(''); + sb.$input.triggerHandler('focus'); + expect(getJSONStub.callCount).toBe(0); + }); - // Verify facet select was focused - expect(focusSpy.calledWith('focus')).toBe(true, 'Should focus facet select'); + for (const facet of ['lists', 'subject', 'text']) { + test(`Facet "${facet}" does not tigger autocomplete`, () => { + // Stub debounce to avoid have to manipulate time (!) + sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); + const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); + const getJSONStub = sandbox.stub($, 'getJSON'); + + sb.$input.val('foo bar'); + sb.facet.write(facet); + sb.$input.triggerHandler('focus'); + expect(getJSONStub.callCount).toBe(0); + }); + } + + test('Tabbing out of search input clears autocomplete results', () => { + const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); + + // Spy on the clearAutocompletionResults method + const clearResultsSpy = sandbox.spy(sb, 'clearAutocompletionResults'); + + // Simulate tab keydown event on the form + const tabEvent = $.Event('keydown', { key: 'Tab' }); + sb.$form.trigger(tabEvent); + + // Verify clearAutocompletionResults was called + expect(clearResultsSpy.callCount).toBe(1); + }); + + test('Autocomplete rendering behavior depends on existing results', () => { + sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); + const sb = new SearchBar($(DUMMY_COMPONENT_HTML), { facet: 'title' }); + const renderSpy = sandbox.spy(sb, 'renderAutocompletionResults'); + + // Should render when results are empty + sb.$input.triggerHandler('focus'); + expect(renderSpy.callCount).toBe( + 1, + 'Should render when no results exist', + ); + + renderSpy.resetHistory(); + + // Should not render when results exist + sb.$results.append('<li>Some result</li>'); + sb.$input.triggerHandler('focus'); + expect(renderSpy.callCount).toBe( + 0, + 'Should not render when results exist', + ); + }); + + test('Tabbing from search result focuses search submit button and clears results', () => { + const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); + + // Add a dummy result and focus on it + sb.$results.append('<li tabindex="0">Test Result</li>'); + const $resultItem = sb.$results.children().first(); + $resultItem.trigger('focus'); + + // Spy on the clearAutocompletionResults method + const clearResultsSpy = sandbox.spy(sb, 'clearAutocompletionResults'); + + // Spy on the focus trigger for search submit + const focusSpy = sandbox.spy(sb.$searchSubmit, 'trigger'); + + // Simulate tab keydown event on the result item + const tabEvent = $.Event('keydown', { key: 'Tab', shiftKey: false }); + $resultItem.trigger(tabEvent); + + // Verify clearAutocompletionResults was called + expect(clearResultsSpy.callCount).toBe( + 1, + 'Should clear autocomplete results', + ); + + // Verify search submit was focused + expect(focusSpy.calledWith('focus')).toBe( + true, + 'Should focus search submit button', + ); + + // Verify event default was prevented + expect(tabEvent.isDefaultPrevented()).toBe( + true, + 'Should prevent default tab behavior', + ); + }); - // Verify event default was prevented - expect(shiftTabEvent.isDefaultPrevented()).toBe(true, 'Should prevent default tab behavior'); - }); + test('Shift+tabbing from search result focuses facet select and clears results', () => { + const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); + + // Add a dummy result and focus on it + sb.$results.append('<li tabindex="0">Test Result</li>'); + const $resultItem = sb.$results.children().first(); + $resultItem.trigger('focus'); + + // Spy on the clearAutocompletionResults method + const clearResultsSpy = sandbox.spy(sb, 'clearAutocompletionResults'); + + // Spy on the focus trigger for facet select + const focusSpy = sandbox.spy(sb.$facetSelect, 'trigger'); + + // Simulate shift+tab keydown event on the result item + const shiftTabEvent = $.Event('keydown', { key: 'Tab', shiftKey: true }); + $resultItem.trigger(shiftTabEvent); + + // Verify clearAutocompletionResults was called + expect(clearResultsSpy.callCount).toBe( + 1, + 'Should clear autocomplete results', + ); + + // Verify facet select was focused + expect(focusSpy.calledWith('focus')).toBe( + true, + 'Should focus facet select', + ); + + // Verify event default was prevented + expect(shiftTabEvent.isDefaultPrevented()).toBe( + true, + 'Should prevent default tab behavior', + ); }); + }); }); diff --git a/tests/unit/js/SearchUtils.test.js b/tests/unit/js/SearchUtils.test.js index ae5924efa07..5197145a223 100644 --- a/tests/unit/js/SearchUtils.test.js +++ b/tests/unit/js/SearchUtils.test.js @@ -2,98 +2,98 @@ import sinon from 'sinon'; import * as SearchUtils from '../../../openlibrary/plugins/openlibrary/js/SearchUtils'; describe('PersistentValue', () => { - const PV = SearchUtils.PersistentValue; - afterEach(() => localStorage.clear()); + const PV = SearchUtils.PersistentValue; + afterEach(() => localStorage.clear()); - test('Saves to localStorage', () => { - const pv = new PV('foo'); - pv.write('bar'); - expect(localStorage.getItem('foo')).toBe('bar'); - }); + test('Saves to localStorage', () => { + const pv = new PV('foo'); + pv.write('bar'); + expect(localStorage.getItem('foo')).toBe('bar'); + }); - test('Reads from localStorage', () => { - localStorage.setItem('foo', 'bar'); - const pv = new PV('foo'); - expect(pv.read()).toBe('bar'); - }); + test('Reads from localStorage', () => { + localStorage.setItem('foo', 'bar'); + const pv = new PV('foo'); + expect(pv.read()).toBe('bar'); + }); - test('Writes default on init', () => { - const pv = new PV('foo', { default: 'blue' }); - expect(pv.read()).toBe('blue'); - }); + test('Writes default on init', () => { + const pv = new PV('foo', { default: 'blue' }); + expect(pv.read()).toBe('blue'); + }); - test('Does not writes default on init if already set', () => { - localStorage.setItem('foo', 'green'); - const pv = new PV('foo', { default: 'blue' }); - expect(pv.read()).toBe('green'); - }); + test('Does not writes default on init if already set', () => { + localStorage.setItem('foo', 'green'); + const pv = new PV('foo', { default: 'blue' }); + expect(pv.read()).toBe('green'); + }); - test('Writes default on invalid init', () => { - localStorage.setItem('foo', 'anything'); - const pv = new PV('foo', { - default: 'blue', - initValidation: () => false - }); - expect(pv.read()).toBe('blue'); + test('Writes default on invalid init', () => { + localStorage.setItem('foo', 'anything'); + const pv = new PV('foo', { + default: 'blue', + initValidation: () => false, }); + expect(pv.read()).toBe('blue'); + }); - test('Writes null on invalid init', () => { - localStorage.setItem('foo', 'anything'); - const pv = new PV('foo', { - initValidation: () => false - }); - expect(pv.read()).toBe(null); + test('Writes null on invalid init', () => { + localStorage.setItem('foo', 'anything'); + const pv = new PV('foo', { + initValidation: () => false, }); + expect(pv.read()).toBe(null); + }); - test('Does not writes default on valid init', () => { - localStorage.setItem('foo', 'anything'); - const pv = new PV('foo', { - default: 'blue', - initValidation: () => true - }); - expect(pv.read()).toBe('anything'); + test('Does not writes default on valid init', () => { + localStorage.setItem('foo', 'anything'); + const pv = new PV('foo', { + default: 'blue', + initValidation: () => true, }); + expect(pv.read()).toBe('anything'); + }); - test('Writing applies transformation', () => { - localStorage.setItem('foo', 'blue'); - const pv = new PV('foo', { - writeTransformation: (newVal, oldVal) => oldVal + newVal - }); - pv.write('green'); - expect(pv.read()).toBe('bluegreen'); + test('Writing applies transformation', () => { + localStorage.setItem('foo', 'blue'); + const pv = new PV('foo', { + writeTransformation: (newVal, oldVal) => oldVal + newVal, }); + pv.write('green'); + expect(pv.read()).toBe('bluegreen'); + }); - test('Writing removes when null', () => { - const pv = new PV('foo'); - pv.write(null); - expect(pv.read()).toBe(null); - }); + test('Writing removes when null', () => { + const pv = new PV('foo'); + pv.write(null); + expect(pv.read()).toBe(null); + }); - test('Writing triggers on change', () => { - const pv = new PV('foo'); - pv.write('a'); - const spy = sinon.spy(); - pv.sync(spy, false); - pv.write('b'); - expect(spy.callCount).toBe(1); - expect(spy.args[0]).toEqual(['b']); - }); + test('Writing triggers on change', () => { + const pv = new PV('foo'); + pv.write('a'); + const spy = sinon.spy(); + pv.sync(spy, false); + pv.write('b'); + expect(spy.callCount).toBe(1); + expect(spy.args[0]).toEqual(['b']); + }); - test('Writing does not trigger when same', () => { - const pv = new PV('foo'); - pv.write('a'); - const spy = sinon.spy(); - pv.sync(spy, false); - pv.write('a'); - expect(spy.callCount).toBe(0); - }); + test('Writing does not trigger when same', () => { + const pv = new PV('foo'); + pv.write('a'); + const spy = sinon.spy(); + pv.sync(spy, false); + pv.write('a'); + expect(spy.callCount).toBe(0); + }); - test('Change fires automatically if so specified', () => { - const pv = new PV('foo'); - pv.write('a'); - const spy = sinon.spy(); - pv.sync(spy, true); - expect(spy.callCount).toBe(1); - expect(spy.args[0]).toEqual(['a']); - }); + test('Change fires automatically if so specified', () => { + const pv = new PV('foo'); + pv.write('a'); + const spy = sinon.spy(); + pv.sync(spy, true); + expect(spy.callCount).toBe(1); + expect(spy.args[0]).toEqual(['a']); + }); }); diff --git a/tests/unit/js/SelectionManager.test.js b/tests/unit/js/SelectionManager.test.js index 6331ed9d5f4..a7b18b1d86c 100644 --- a/tests/unit/js/SelectionManager.test.js +++ b/tests/unit/js/SelectionManager.test.js @@ -1,87 +1,86 @@ import SelectionManager from '../../../openlibrary/plugins/openlibrary/js/ile/utils/SelectionManager/SelectionManager.js'; function createTestElementsForProcessClick() { - const listItem = document.createElement('li'); - listItem.classList.add('searchResultItem', 'ile-selectable'); + const listItem = document.createElement('li'); + listItem.classList.add('searchResultItem', 'ile-selectable'); - const link = document.createElement('a'); - listItem.appendChild(link); + const link = document.createElement('a'); + listItem.appendChild(link); - const bookTitle = document.createElement('div'); - bookTitle.classList.add('booktitle'); - const bookLink = document.createElement('a'); - bookLink.href = 'OL12345W'; // Mock href value - bookTitle.appendChild(bookLink); + const bookTitle = document.createElement('div'); + bookTitle.classList.add('booktitle'); + const bookLink = document.createElement('a'); + bookLink.href = 'OL12345W'; // Mock href value + bookTitle.appendChild(bookLink); - listItem.appendChild(bookTitle); + listItem.appendChild(bookTitle); - return {listItem,link}; + return { listItem, link }; } function setupSelectionManager() { - const sm = new SelectionManager(null, '/search'); - sm.ile = { $statusImages: { append: jest.fn() } }; - sm.selectedItems = { work: [] }; - sm.updateToolbar = jest.fn(); - return sm; + const sm = new SelectionManager(null, '/search'); + sm.ile = { $statusImages: { append: jest.fn() } }; + sm.selectedItems = { work: [] }; + sm.updateToolbar = jest.fn(); + return sm; } describe('SelectionManager', () => { - afterEach(() => { - window.sessionStorage.clear(); - }); + afterEach(() => { + window.sessionStorage.clear(); + }); - test('getSelectedItems initializes selected item types', () => { - const sm = new SelectionManager(null, '/search'); - sm.getSelectedItems(); - expect(sm.selectedItems).toEqual({ - work: [], - edition: [], - author: [], - }); + test('getSelectedItems initializes selected item types', () => { + const sm = new SelectionManager(null, '/search'); + sm.getSelectedItems(); + expect(sm.selectedItems).toEqual({ + work: [], + edition: [], + author: [], }); + }); - test('addSelectedItem', () => { - const sm = new SelectionManager(null, '/search'); - sm.getSelectedItems(); // to initialize types for push to work - sm.addSelectedItem('OL1W'); - expect(sm.selectedItems).toEqual({ - work: ['OL1W'], - edition: [], - author: [], - }); + test('addSelectedItem', () => { + const sm = new SelectionManager(null, '/search'); + sm.getSelectedItems(); // to initialize types for push to work + sm.addSelectedItem('OL1W'); + expect(sm.selectedItems).toEqual({ + work: ['OL1W'], + edition: [], + author: [], }); + }); + test('processClick - clicking on a link or button', () => { + const sm = setupSelectionManager(); + const { listItem, link } = createTestElementsForProcessClick(); - test('processClick - clicking on a link or button', () => { - const sm = setupSelectionManager(); - const { listItem,link } = createTestElementsForProcessClick(); - - link.addEventListener('click', () => { - sm.processClick({ target: link, currentTarget: listItem }); - }); - - expect(listItem.classList.contains('ile-selected')).toBe(false); - link.click(); - expect(listItem.classList.contains('ile-selected')).toBe(false); - - jest.clearAllMocks(); + link.addEventListener('click', () => { + sm.processClick({ target: link, currentTarget: listItem }); }); - test('processClick - clicking on listItem', () => { - const sm = setupSelectionManager(); - const { listItem } = createTestElementsForProcessClick(); + expect(listItem.classList.contains('ile-selected')).toBe(false); + link.click(); + expect(listItem.classList.contains('ile-selected')).toBe(false); - listItem.addEventListener('click', () => { - sm.processClick({ target: listItem, currentTarget: listItem }); - }); + jest.clearAllMocks(); + }); - expect(listItem.classList.contains('ile-selected')).toBe(false); - listItem.click(); - expect(listItem.classList.contains('ile-selected')).toBe(true); - listItem.click(); - expect(listItem.classList.contains('ile-selected')).toBe(false); + test('processClick - clicking on listItem', () => { + const sm = setupSelectionManager(); + const { listItem } = createTestElementsForProcessClick(); - jest.clearAllMocks(); + listItem.addEventListener('click', () => { + sm.processClick({ target: listItem, currentTarget: listItem }); }); + + expect(listItem.classList.contains('ile-selected')).toBe(false); + listItem.click(); + expect(listItem.classList.contains('ile-selected')).toBe(true); + listItem.click(); + expect(listItem.classList.contains('ile-selected')).toBe(false); + + jest.clearAllMocks(); + }); }); diff --git a/tests/unit/js/autocomplete.test.js b/tests/unit/js/autocomplete.test.js index 65774407426..5afc4666013 100644 --- a/tests/unit/js/autocomplete.test.js +++ b/tests/unit/js/autocomplete.test.js @@ -1,66 +1,57 @@ -import { highlight, mapApiResultsToAutocompleteSuggestions } from '../../../openlibrary/plugins/openlibrary/js/autocomplete.js'; +import { + highlight, + mapApiResultsToAutocompleteSuggestions, +} from '../../../openlibrary/plugins/openlibrary/js/autocomplete.js'; describe('highlight', () => { - - test('Highlights terms with strong tag', () => { - [ - [ - 'Jon Robson', - 'Jon', - '<strong>Jon</strong> Robson' - ], - [ - 'No match', - 'abcde', - 'No match' - ] - ].forEach((test) => { - const highlightedText = highlight(test[0], test[1]); - expect(highlightedText).toStrictEqual(test[2]); - }); - }) + test('Highlights terms with strong tag', () => { + [ + ['Jon Robson', 'Jon', '<strong>Jon</strong> Robson'], + ['No match', 'abcde', 'No match'], + ].forEach((test) => { + const highlightedText = highlight(test[0], test[1]); + expect(highlightedText).toStrictEqual(test[2]); + }); + }); }); - describe('mapApiResultsToAutocompleteSuggestions', () => { - test('API results are converted to suggestions using label function', () => { - const suggestions = mapApiResultsToAutocompleteSuggestions( - [ - { - key: 1, - name: 'Test' - } - ], - (r) => r.name - ); + test('API results are converted to suggestions using label function', () => { + const suggestions = mapApiResultsToAutocompleteSuggestions( + [ + { + key: 1, + name: 'Test', + }, + ], + (r) => r.name, + ); - expect(suggestions).toStrictEqual([ - { - key: 1, - label: 'Test', - value: 'Test' - } - ]); - }); + expect(suggestions).toStrictEqual([ + { + key: 1, + label: 'Test', + value: 'Test', + }, + ]); + }); - test('Add new item field can be added', () => { - const suggestions = mapApiResultsToAutocompleteSuggestions( - [ - { - key: 1, - name: 'Test' - } - ], - (r) => r.name, - 'Add new item' - ); + test('Add new item field can be added', () => { + const suggestions = mapApiResultsToAutocompleteSuggestions( + [ + { + key: 1, + name: 'Test', + }, + ], + (r) => r.name, + 'Add new item', + ); - expect(suggestions[1]).toStrictEqual( - { - key: '__new__', - label: 'Add new item', - value: 'Add new item' - } - ); - }) + expect(suggestions[1]).toStrictEqual({ + key: '__new__', + label: 'Add new item', + value: 'Add new item', + }); + }); }); diff --git a/tests/unit/js/droppers.test.js b/tests/unit/js/droppers.test.js index 620ef58e70d..7f1e7512dcd 100644 --- a/tests/unit/js/droppers.test.js +++ b/tests/unit/js/droppers.test.js @@ -1,305 +1,352 @@ import sinon from 'sinon'; -import { initDroppers, initGenericDroppers } from '../../../openlibrary/plugins/openlibrary/js/dropper'; -import { Dropper } from '../../../openlibrary/plugins/openlibrary/js/dropper/Dropper' -import { legacyBookDropperMarkup, openDropperMarkup, closedDropperMarkup, disabledDropperMarkup } from './sample-html/dropper-test-data' +import { + initDroppers, + initGenericDroppers, +} from '../../../openlibrary/plugins/openlibrary/js/dropper'; +import { Dropper } from '../../../openlibrary/plugins/openlibrary/js/dropper/Dropper'; import * as nonjquery_utils from '../../../openlibrary/plugins/openlibrary/js/nonjquery_utils.js'; - +import { + closedDropperMarkup, + disabledDropperMarkup, + legacyBookDropperMarkup, + openDropperMarkup, +} from './sample-html/dropper-test-data'; describe('initDroppers', () => { - test('dropdown changes arrow direction on click', () => { - // Stub debounce to avoid have to manipulate time (!) - const stub = sinon.stub(nonjquery_utils, 'debounce').callsFake(fn => fn); + test('dropdown changes arrow direction on click', () => { + // Stub debounce to avoid have to manipulate time (!) + const stub = sinon.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); - $(document.body).html(legacyBookDropperMarkup); - const $dropclick = $('.dropclick'); - const $arrow = $dropclick.find('.arrow'); - initDroppers(document.querySelectorAll('.dropper')); + $(document.body).html(legacyBookDropperMarkup); + const $dropclick = $('.dropclick'); + const $arrow = $dropclick.find('.arrow'); + initDroppers(document.querySelectorAll('.dropper')); - for (let i = 0; i < 2; i++) { - $dropclick.trigger('click'); - expect($arrow.hasClass('up')).toBe(true); + for (let i = 0; i < 2; i++) { + $dropclick.trigger('click'); + expect($arrow.hasClass('up')).toBe(true); - $dropclick.trigger('click'); - expect($arrow.hasClass('up')).toBe(false); - } + $dropclick.trigger('click'); + expect($arrow.hasClass('up')).toBe(false); + } - stub.restore(); - }); + stub.restore(); + }); }); describe('Generic Droppers', () => { - test('Clicking dropclick element toggles the dropper', () => { - // Setup - document.body.innerHTML = closedDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const dropper = new Dropper(wrapper) - dropper.initialize() - - const dropClick = document.querySelector('.generic-dropper__dropclick') - const arrow = dropClick.querySelector('.arrow') - - // Dropper should be closed at the start - expect(arrow.classList.contains('up')).toBe(false) - expect(wrapper.classList.contains('dropper-wrapper--active')).toBe(false) - - // Open dropper - dropClick.click() - expect(arrow.classList.contains('up')).toBe(true) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true) - - // Close dropper - dropClick.click() - expect(arrow.classList.contains('up')).toBe(false) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - }) - - test('Opened droppers close if they are not the target of a click', () => { - // Setup - document.body.innerHTML = openDropperMarkup.concat(openDropperMarkup, openDropperMarkup) - const wrappers = document.querySelectorAll('.generic-dropper-wrapper') - initGenericDroppers(wrappers) - - - // Ensure that all three droppers are open - expect(wrappers.length).toBe(3) - for (const wrapper of wrappers) { - const arrow = wrapper.querySelector('.arrow') - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true) - expect(arrow.classList.contains('up')).toBe(true) - } - - // After clicking the dropdown content of the first dropper: - const dropdownContent = wrappers[0].querySelector('.generic-dropper__dropdown') - dropdownContent.click() - - // First dropper should be open - expect(wrappers[0].classList.contains('generic-dropper-wrapper--active')).toBe(true) - expect(wrappers[0].querySelector('.arrow').classList.contains('up')).toBe(true) - - // ...while other droppers should be closed - for (let i = 1; i < wrappers.length; ++i) { - const arrow = wrappers[i].querySelector('.arrow') - expect(wrappers[i].classList.contains('generic-dropper-wrapper--active')).toBe(false) - expect(arrow.classList.contains('up')).toBe(false) - } - }) - - test('Disabled droppers cannot be opened nor closed', () => { - document.body.innerHTML = disabledDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const dropper = new Dropper(wrapper) - dropper.initialize() - const dropclick = wrapper.querySelector('.generic-dropper__dropclick') - const arrow = wrapper.querySelector('.arrow') - - // Sanity checks - expect(wrapper.classList.contains('generic-dropper--disabled')).toBe(true) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - expect(arrow.classList.contains('up')).toBe(false) - - // Click on the dropclick: - dropclick.click() - - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - expect(arrow.classList.contains('up')).toBe(false) - }) -}) + test('Clicking dropclick element toggles the dropper', () => { + // Setup + document.body.innerHTML = closedDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + dropper.initialize(); + + const dropClick = document.querySelector('.generic-dropper__dropclick'); + const arrow = dropClick.querySelector('.arrow'); + + // Dropper should be closed at the start + expect(arrow.classList.contains('up')).toBe(false); + expect(wrapper.classList.contains('dropper-wrapper--active')).toBe(false); + + // Open dropper + dropClick.click(); + expect(arrow.classList.contains('up')).toBe(true); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + true, + ); + + // Close dropper + dropClick.click(); + expect(arrow.classList.contains('up')).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + }); + + test('Opened droppers close if they are not the target of a click', () => { + // Setup + document.body.innerHTML = openDropperMarkup.concat( + openDropperMarkup, + openDropperMarkup, + ); + const wrappers = document.querySelectorAll('.generic-dropper-wrapper'); + initGenericDroppers(wrappers); + + // Ensure that all three droppers are open + expect(wrappers.length).toBe(3); + for (const wrapper of wrappers) { + const arrow = wrapper.querySelector('.arrow'); + expect( + wrapper.classList.contains('generic-dropper-wrapper--active'), + ).toBe(true); + expect(arrow.classList.contains('up')).toBe(true); + } + + // After clicking the dropdown content of the first dropper: + const dropdownContent = wrappers[0].querySelector( + '.generic-dropper__dropdown', + ); + dropdownContent.click(); + + // First dropper should be open + expect( + wrappers[0].classList.contains('generic-dropper-wrapper--active'), + ).toBe(true); + expect(wrappers[0].querySelector('.arrow').classList.contains('up')).toBe( + true, + ); + + // ...while other droppers should be closed + for (let i = 1; i < wrappers.length; ++i) { + const arrow = wrappers[i].querySelector('.arrow'); + expect( + wrappers[i].classList.contains('generic-dropper-wrapper--active'), + ).toBe(false); + expect(arrow.classList.contains('up')).toBe(false); + } + }); + + test('Disabled droppers cannot be opened nor closed', () => { + document.body.innerHTML = disabledDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + dropper.initialize(); + const dropclick = wrapper.querySelector('.generic-dropper__dropclick'); + const arrow = wrapper.querySelector('.arrow'); + + // Sanity checks + expect(wrapper.classList.contains('generic-dropper--disabled')).toBe(true); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + expect(arrow.classList.contains('up')).toBe(false); + + // Click on the dropclick: + dropclick.click(); + + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + expect(arrow.classList.contains('up')).toBe(false); + }); +}); describe('Dropper.js class', () => { - test('Dropper references set correctly on instantiation', () => { - document.body.innerHTML = closedDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const dropper = new Dropper(wrapper) - - // Reference to component root stored - expect(dropper.dropper === wrapper).toBe(true) - - // Dropclick reference stored - const dropClick = wrapper.querySelector('.generic-dropper__dropclick') - expect(dropper.dropClick === dropClick).toBe(true) - - // Dropper is closed - expect(dropper.isDropperOpen).toBe(false) - - // This dropper is not disabled - expect(dropper.isDropperDisabled).toBe(false) - }) - - it('is not functional until initialize() is called', () => { - document.body.innerHTML = closedDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const dropClick = wrapper.querySelector('.generic-dropper__dropclick') - const arrow = wrapper.querySelector('.arrow') - - const dropper = new Dropper(wrapper) - const spy = jest.spyOn(dropper, 'toggleDropper') - - // Dropper should be closed initially: - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - expect(arrow.classList.contains('up')).toBe(false) - - // Clicking should not do anything yet: - dropClick.click() - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - expect(arrow.classList.contains('up')).toBe(false) - expect(spy).not.toHaveBeenCalled() - - // Test again after initialization: - dropper.initialize() - dropClick.click() - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true) - expect(arrow.classList.contains('up')).toBe(true) - expect(spy).toHaveBeenCalled() - - jest.restoreAllMocks() - }) - - it('can be closed if not disabled', () => { - document.body.innerHTML = openDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const arrow = wrapper.querySelector('.arrow') - - const dropper = new Dropper(wrapper) - dropper.initialize() - - // Check initial state: - expect(dropper.isDropperDisabled).toBe(false) - expect(dropper.isDropperOpen).toBe(true) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true) - expect(arrow.classList.contains('up')).toBe(true) - - // Check again after closing: - dropper.closeDropper() - expect(dropper.isDropperOpen).toBe(false) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - expect(arrow.classList.contains('up')).toBe(false) - }) - - it('can be toggled if not disabled', () => { - document.body.innerHTML = closedDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const arrow = wrapper.querySelector('.arrow') - - const dropper = new Dropper(wrapper) - dropper.initialize() - - // Check initial state: - expect(dropper.isDropperDisabled).toBe(false) - expect(dropper.isDropperOpen).toBe(false) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - expect(arrow.classList.contains('up')).toBe(false) - - // Check after toggling open: - dropper.toggleDropper() - expect(dropper.isDropperOpen).toBe(true) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true) - expect(arrow.classList.contains('up')).toBe(true) - - // Check after toggling once more: - dropper.toggleDropper() - expect(dropper.isDropperOpen).toBe(false) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - expect(arrow.classList.contains('up')).toBe(false) - }) - - it('cannot be opened while disabled', () => { - document.body.innerHTML = disabledDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const dropper = new Dropper(wrapper) - dropper.initialize() - const arrow = wrapper.querySelector('.arrow') - - // Check initial state: - expect(dropper.isDropperDisabled).toBe(true) - expect(arrow.classList.contains('up')).toBe(false) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - - // Check state after toggling: - dropper.toggleDropper() - expect(arrow.classList.contains('up')).toBe(false) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - }) - - describe('Dropper event methods', () => { - afterEach(() => { - jest.clearAllMocks() - }) - - it('calls `onDisabledClick()` when dropper is clicked while disabled', () => { - document.body.innerHTML = disabledDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const dropper = new Dropper(wrapper) - dropper.initialize() - - const onDisabledClickFn = jest.spyOn(dropper, 'onDisabledClick') - - // Check initial state: - expect(dropper.isDropperDisabled).toBe(true) - expect(onDisabledClickFn).not.toHaveBeenCalled() - - // Check state after toggling: - dropper.toggleDropper() - expect(dropper.isDropperDisabled).toBe(true) - expect(onDisabledClickFn).toHaveBeenCalledTimes(1) - - // Check state after closing: - dropper.closeDropper() - expect(dropper.isDropperDisabled).toBe(true) - expect(onDisabledClickFn).toHaveBeenCalledTimes(2) - }) - - it('calls `onClose()` when active dropper is closed', () => { - document.body.innerHTML = openDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const dropper = new Dropper(wrapper) - dropper.initialize() - - const onCloseFn = jest.spyOn(dropper, 'onClose') - - // Check initial state: - expect(dropper.isDropperOpen).toBe(true) - expect(onCloseFn).not.toHaveBeenCalled() - - // Check state after closing: - dropper.closeDropper() - expect(dropper.isDropperOpen).toBe(false) - expect(onCloseFn).toHaveBeenCalledTimes(1) - - // Check state after toggling open then closed: - dropper.toggleDropper() - expect(dropper.isDropperOpen).toBe(true) - expect(onCloseFn).toHaveBeenCalledTimes(1) // Should not be called when dropper is closed - - dropper.toggleDropper() - expect(dropper.isDropperOpen).toBe(false) - expect(onCloseFn).toHaveBeenCalledTimes(2) - }) - - test('toggling dropper results in correct event method being called', () => { - document.body.innerHTML = closedDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const dropper = new Dropper(wrapper) - dropper.initialize() - - const onCloseFn = jest.spyOn(dropper, 'onClose') - const onOpenFn = jest.spyOn(dropper, 'onOpen') - - // Check initial state: - expect(dropper.isDropperOpen).toBe(false) - expect(onCloseFn).not.toHaveBeenCalled() - expect(onOpenFn).not.toHaveBeenCalled() - - // Check after toggling open: - dropper.toggleDropper() - expect(dropper.isDropperOpen).toBe(true) - expect(onCloseFn).toHaveBeenCalledTimes(0) - expect(onOpenFn).toHaveBeenCalledTimes(1) - - // Check after toggling closed: - dropper.toggleDropper() - expect(dropper.isDropperOpen).toBe(false) - expect(onCloseFn).toHaveBeenCalledTimes(1) - expect(onOpenFn).toHaveBeenCalledTimes(1) - }) - }) -}) + test('Dropper references set correctly on instantiation', () => { + document.body.innerHTML = closedDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + + // Reference to component root stored + expect(dropper.dropper === wrapper).toBe(true); + + // Dropclick reference stored + const dropClick = wrapper.querySelector('.generic-dropper__dropclick'); + expect(dropper.dropClick === dropClick).toBe(true); + + // Dropper is closed + expect(dropper.isDropperOpen).toBe(false); + + // This dropper is not disabled + expect(dropper.isDropperDisabled).toBe(false); + }); + + it('is not functional until initialize() is called', () => { + document.body.innerHTML = closedDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropClick = wrapper.querySelector('.generic-dropper__dropclick'); + const arrow = wrapper.querySelector('.arrow'); + + const dropper = new Dropper(wrapper); + const spy = jest.spyOn(dropper, 'toggleDropper'); + + // Dropper should be closed initially: + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + expect(arrow.classList.contains('up')).toBe(false); + + // Clicking should not do anything yet: + dropClick.click(); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + expect(arrow.classList.contains('up')).toBe(false); + expect(spy).not.toHaveBeenCalled(); + + // Test again after initialization: + dropper.initialize(); + dropClick.click(); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + true, + ); + expect(arrow.classList.contains('up')).toBe(true); + expect(spy).toHaveBeenCalled(); + + jest.restoreAllMocks(); + }); + + it('can be closed if not disabled', () => { + document.body.innerHTML = openDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const arrow = wrapper.querySelector('.arrow'); + + const dropper = new Dropper(wrapper); + dropper.initialize(); + + // Check initial state: + expect(dropper.isDropperDisabled).toBe(false); + expect(dropper.isDropperOpen).toBe(true); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + true, + ); + expect(arrow.classList.contains('up')).toBe(true); + + // Check again after closing: + dropper.closeDropper(); + expect(dropper.isDropperOpen).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + expect(arrow.classList.contains('up')).toBe(false); + }); + + it('can be toggled if not disabled', () => { + document.body.innerHTML = closedDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const arrow = wrapper.querySelector('.arrow'); + + const dropper = new Dropper(wrapper); + dropper.initialize(); + + // Check initial state: + expect(dropper.isDropperDisabled).toBe(false); + expect(dropper.isDropperOpen).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + expect(arrow.classList.contains('up')).toBe(false); + + // Check after toggling open: + dropper.toggleDropper(); + expect(dropper.isDropperOpen).toBe(true); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + true, + ); + expect(arrow.classList.contains('up')).toBe(true); + + // Check after toggling once more: + dropper.toggleDropper(); + expect(dropper.isDropperOpen).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + expect(arrow.classList.contains('up')).toBe(false); + }); + + it('cannot be opened while disabled', () => { + document.body.innerHTML = disabledDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + dropper.initialize(); + const arrow = wrapper.querySelector('.arrow'); + + // Check initial state: + expect(dropper.isDropperDisabled).toBe(true); + expect(arrow.classList.contains('up')).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + + // Check state after toggling: + dropper.toggleDropper(); + expect(arrow.classList.contains('up')).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + }); + + describe('Dropper event methods', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('calls `onDisabledClick()` when dropper is clicked while disabled', () => { + document.body.innerHTML = disabledDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + dropper.initialize(); + + const onDisabledClickFn = jest.spyOn(dropper, 'onDisabledClick'); + + // Check initial state: + expect(dropper.isDropperDisabled).toBe(true); + expect(onDisabledClickFn).not.toHaveBeenCalled(); + + // Check state after toggling: + dropper.toggleDropper(); + expect(dropper.isDropperDisabled).toBe(true); + expect(onDisabledClickFn).toHaveBeenCalledTimes(1); + + // Check state after closing: + dropper.closeDropper(); + expect(dropper.isDropperDisabled).toBe(true); + expect(onDisabledClickFn).toHaveBeenCalledTimes(2); + }); + + it('calls `onClose()` when active dropper is closed', () => { + document.body.innerHTML = openDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + dropper.initialize(); + + const onCloseFn = jest.spyOn(dropper, 'onClose'); + + // Check initial state: + expect(dropper.isDropperOpen).toBe(true); + expect(onCloseFn).not.toHaveBeenCalled(); + + // Check state after closing: + dropper.closeDropper(); + expect(dropper.isDropperOpen).toBe(false); + expect(onCloseFn).toHaveBeenCalledTimes(1); + + // Check state after toggling open then closed: + dropper.toggleDropper(); + expect(dropper.isDropperOpen).toBe(true); + expect(onCloseFn).toHaveBeenCalledTimes(1); // Should not be called when dropper is closed + + dropper.toggleDropper(); + expect(dropper.isDropperOpen).toBe(false); + expect(onCloseFn).toHaveBeenCalledTimes(2); + }); + + test('toggling dropper results in correct event method being called', () => { + document.body.innerHTML = closedDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + dropper.initialize(); + + const onCloseFn = jest.spyOn(dropper, 'onClose'); + const onOpenFn = jest.spyOn(dropper, 'onOpen'); + + // Check initial state: + expect(dropper.isDropperOpen).toBe(false); + expect(onCloseFn).not.toHaveBeenCalled(); + expect(onOpenFn).not.toHaveBeenCalled(); + + // Check after toggling open: + dropper.toggleDropper(); + expect(dropper.isDropperOpen).toBe(true); + expect(onCloseFn).toHaveBeenCalledTimes(0); + expect(onOpenFn).toHaveBeenCalledTimes(1); + + // Check after toggling closed: + dropper.toggleDropper(); + expect(dropper.isDropperOpen).toBe(false); + expect(onCloseFn).toHaveBeenCalledTimes(1); + expect(onOpenFn).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/tests/unit/js/editionEditPageClassification.test.js b/tests/unit/js/editionEditPageClassification.test.js index 88f66530c7f..b6b0666a2fc 100644 --- a/tests/unit/js/editionEditPageClassification.test.js +++ b/tests/unit/js/editionEditPageClassification.test.js @@ -1,38 +1,58 @@ -import { initClassificationValidation } from '../../../openlibrary/plugins/openlibrary/js/edit.js'; import sinon from 'sinon'; -import * as testData from './html-test-data'; -import { htmlquote } from '../../../openlibrary/plugins/openlibrary/js/jsdef'; +import { initClassificationValidation } from '../../../openlibrary/plugins/openlibrary/js/edit.js'; import { init } from '../../../openlibrary/plugins/openlibrary/js/jquery.repeat'; +import { htmlquote } from '../../../openlibrary/plugins/openlibrary/js/jsdef'; +import * as testData from './html-test-data'; let sandbox; beforeEach(() => { - // Clear session storage - sandbox = sinon.createSandbox(); - global.htmlquote = htmlquote; - // htmlquote is used inside an eval expression (yuck) so is an implied dependency - sandbox.stub(global, 'htmlquote').callsFake(htmlquote); - // setup Query repeat - init(); - // setup the HTML - $(document.body).html(testData.readClassification); - initClassificationValidation(); + // Clear session storage + sandbox = sinon.createSandbox(); + global.htmlquote = htmlquote; + // htmlquote is used inside an eval expression (yuck) so is an implied dependency + sandbox.stub(global, 'htmlquote').callsFake(htmlquote); + // setup Query repeat + init(); + // setup the HTML + $(document.body).html(testData.readClassification); + initClassificationValidation(); }); describe('initClassificationValidation', () => { - test.each([ + test.each([ // format: [testName, selectValue, classificationValue, expectedDisplay] - ['Can have a classification and any value', 'lc_classifications', 'anything at all', 'none'], - ['Cannot have both an empty classification and classification value', '', '', 'block'], - ['Cannot have an empty classification', '', 'Test', 'block'], - ['Cannot have an empty classification value', 'lc_classifications', '', 'block'], - ['Cannot have --- as a classification WITHOUT a value', '---', 'test', 'block'], - ['Cannot have --- as a classification with a value', '---', '', 'block'], - ])('Test: %s', (testName, selectValue, classificationValue, expectedDisplay) => { - $('#select-classification').val(selectValue); - $('#classification-value').val(classificationValue); - $('.repeat-add').trigger('click'); - const displayError = $('#classification-errors').css('display'); - expect(displayError).toBe(expectedDisplay); - }); + [ + 'Can have a classification and any value', + 'lc_classifications', + 'anything at all', + 'none', + ], + [ + 'Cannot have both an empty classification and classification value', + '', + '', + 'block', + ], + ['Cannot have an empty classification', '', 'Test', 'block'], + [ + 'Cannot have an empty classification value', + 'lc_classifications', + '', + 'block', + ], + [ + 'Cannot have --- as a classification WITHOUT a value', + '---', + 'test', + 'block', + ], + ['Cannot have --- as a classification with a value', '---', '', 'block'], + ])('Test: %s', (testName, selectValue, classificationValue, expectedDisplay) => { + $('#select-classification').val(selectValue); + $('#classification-value').val(classificationValue); + $('.repeat-add').trigger('click'); + const displayError = $('#classification-errors').css('display'); + expect(displayError).toBe(expectedDisplay); + }); }); diff --git a/tests/unit/js/editionsEditPage.test.js b/tests/unit/js/editionsEditPage.test.js index d8cf34f61a1..5d53fe8b737 100644 --- a/tests/unit/js/editionsEditPage.test.js +++ b/tests/unit/js/editionsEditPage.test.js @@ -1,8 +1,8 @@ -import { validateIdentifiers } from '../../../openlibrary/plugins/openlibrary/js/edit.js'; import sinon from 'sinon'; -import * as testData from './html-test-data'; -import { htmlquote } from '../../../openlibrary/plugins/openlibrary/js/jsdef'; +import { validateIdentifiers } from '../../../openlibrary/plugins/openlibrary/js/edit.js'; import { init } from '../../../openlibrary/plugins/openlibrary/js/jquery.repeat'; +import { htmlquote } from '../../../openlibrary/plugins/openlibrary/js/jsdef'; +import * as testData from './html-test-data'; let sandbox; @@ -25,199 +25,199 @@ let sandbox; // Adapted from jquery.repeat.test.js beforeEach(() => { - // Clear session storage - sandbox = sinon.createSandbox(); - global.htmlquote = htmlquote; - // htmlquote is used inside an eval expression (yuck) so is an implied dependency - sandbox.stub(global, 'htmlquote').callsFake(htmlquote); - // setup Query repeat - init(); - // setup the HTML - $(document.body).html(testData.editionIdentifiersSample); - $('#identifiers').repeat({ - vars: {prefix: 'edition--'}, - validate: function(data) {return validateIdentifiers(data)}, - }); + // Clear session storage + sandbox = sinon.createSandbox(); + global.htmlquote = htmlquote; + // htmlquote is used inside an eval expression (yuck) so is an implied dependency + sandbox.stub(global, 'htmlquote').callsFake(htmlquote); + // setup Query repeat + init(); + // setup the HTML + $(document.body).html(testData.editionIdentifiersSample); + $('#identifiers').repeat({ + vars: { prefix: 'edition--' }, + validate: (data) => validateIdentifiers(data), + }); }); // Per the test data used, and beforeEach(), the length always starts out at 5. describe('initIdentifierValidation', () => { - // ISBN 10 - it('does add a valid ISBN 10 ending in X', () => { - $('#select-id').val('isbn_10'); - $('#id-value').val('0-8044-2957-X'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - it('does add a valid ISBN 10 NOT ending in X', () => { - $('#select-id').val('isbn_10'); - $('#id-value').val('0596520689'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - it('does NOT add an invalid ISBN 10 with a failed check digit', () => { - $('#select-id').val('isbn_10'); - $('#id-value').val('1234567890'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(5); - }); - - it('does NOT prompt to add a formally invalid ISBN 10', () => { - $('#select-id').val('isbn_10'); - $('#id-value').val('12345'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(5); - expect($('.repeat-add').length).toBe(1); - const errorDivText = $('#id-errors').text(); - const expected = 'Add it anyway?'; - expect(errorDivText).toEqual(expect.not.stringContaining(expected)); - }); - - it('clears the invalid ISBN 10 error prompt and does not add an ISBN if a user clicks no', () => { - $('#select-id').val('isbn_10'); - $('#id-value').val('2121212121'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(5); - expect($('.repeat-add').length).toBe(2); - $('#do-not-add-isbn').trigger('click'); - expect($('.repeat-item').length).toBe(5); - const cssDisplay = $('#id-errors').css('display'); - expect(cssDisplay).toEqual('none') - }); - - it('does NOT add a duplicate ISBN 10', () => { - $('#select-id').val('isbn_10'); - $('#id-value').val('0063162024'); - $('.repeat-add').trigger('click'); - $('#select-id').val('isbn_10'); - $('#id-value').val('0063162024'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - it('does properly strip spaces and hypens from a valid ISBN 10 and add it', () => { - $('#select-id').val('isbn_10'); - $('#id-value').val('09- 8478---2869 '); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }) - - it('does count identical stripped and unstripped ISBN 10s as the same ISBN', () => { - $('#select-id').val('isbn_10'); - $('#id-value').val(' 144--93-55730 '); - $('.repeat-add').trigger('click'); - $('#select-id').val('isbn_10'); - $('#id-value').val('1449355730'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - // ISBN 13 - it('does add a valid ISBN 13', () => { - $('#select-id').val('isbn_13'); - $('#id-value').val('9781789801217'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - it('does NOT add an invalid ISBN 13', () => { - $('#select-id').val('isbn_13'); - $('#id-value').val('1111111111111'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(5); - }); - - it('does NOT prompt to add a formally invalid ISBN 13', () => { - $('#select-id').val('isbn_13'); - $('#id-value').val('12345'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(5); - expect($('.repeat-add').length).toBe(1); - const errorDivText = $('#id-errors').text(); - const expected = 'Add it anyway?'; - expect(errorDivText).toEqual(expect.not.stringContaining(expected)); - }); - - it('clears the invalid ISBN 13 error prompt and does not add an ISBN if a user clicks no', () => { - $('#select-id').val('isbn_13'); - $('#id-value').val('0123456789123'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(5); - expect($('.repeat-add').length).toBe(2); - $('#do-not-add-isbn').trigger('click'); - expect($('.repeat-item').length).toBe(5); - const cssDisplay = $('#id-errors').css('display'); - expect(cssDisplay).toEqual('none') - }); - - it('does NOT add a duplicate ISBN 13', () => { - $('#select-id').val('isbn_13'); - $('#id-value').val('9780984782857'); - $('.repeat-add').trigger('click'); - $('#select-id').val('isbn_13'); - $('#id-value').val('9780984782857'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - it('does properly strip spaces and hypens from a valid ISBN 13 and add it', () => { - $('#select-id').val('isbn_13'); - $('#id-value').val('978-16172--95 980 '); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }) - - it('does count identical stripped and unstripped ISBN 13s as the same ISBN', () => { - $('#select-id').val('isbn_13'); - $('#id-value').val('-979-86 -64653403 '); - $('.repeat-add').trigger('click'); - $('#select-id').val('isbn_13'); - $('#id-value').val('9798664653403'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - //LCCN - it('does add a valid LCCN', () => { - $('#select-id').val('lccn'); - $('#id-value').val('n78-890351'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - it('does NOT add an invalid LCCN', () => { - $('#select-id').val('lccn'); - $('#id-value').val('12345'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(5); - }); - - it('does NOT add a duplicate LCCN', () => { - $('#select-id').val('lccn'); - $('#id-value').val('n78-890351'); - $('.repeat-add').trigger('click'); - $('#select-id').val('lccn'); - $('#id-value').val('n78-890351'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - it('does properly normalize a valid LCCN and add it', () => { - $('#select-id').val('lccn'); - $('#id-value').val(' 75-425165//r75'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }) - - it('does count identical normalized and non-normalized LCCNs as the same LCCN', () => { - $('#select-id').val('lccn'); - $('#id-value').val(' 75-425165//r75'); - $('.repeat-add').trigger('click'); - $('#select-id').val('lccn'); - $('#id-value').val('75425165'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); + // ISBN 10 + it('does add a valid ISBN 10 ending in X', () => { + $('#select-id').val('isbn_10'); + $('#id-value').val('0-8044-2957-X'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does add a valid ISBN 10 NOT ending in X', () => { + $('#select-id').val('isbn_10'); + $('#id-value').val('0596520689'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does NOT add an invalid ISBN 10 with a failed check digit', () => { + $('#select-id').val('isbn_10'); + $('#id-value').val('1234567890'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(5); + }); + + it('does NOT prompt to add a formally invalid ISBN 10', () => { + $('#select-id').val('isbn_10'); + $('#id-value').val('12345'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(5); + expect($('.repeat-add').length).toBe(1); + const errorDivText = $('#id-errors').text(); + const expected = 'Add it anyway?'; + expect(errorDivText).toEqual(expect.not.stringContaining(expected)); + }); + + it('clears the invalid ISBN 10 error prompt and does not add an ISBN if a user clicks no', () => { + $('#select-id').val('isbn_10'); + $('#id-value').val('2121212121'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(5); + expect($('.repeat-add').length).toBe(2); + $('#do-not-add-isbn').trigger('click'); + expect($('.repeat-item').length).toBe(5); + const cssDisplay = $('#id-errors').css('display'); + expect(cssDisplay).toEqual('none'); + }); + + it('does NOT add a duplicate ISBN 10', () => { + $('#select-id').val('isbn_10'); + $('#id-value').val('0063162024'); + $('.repeat-add').trigger('click'); + $('#select-id').val('isbn_10'); + $('#id-value').val('0063162024'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does properly strip spaces and hypens from a valid ISBN 10 and add it', () => { + $('#select-id').val('isbn_10'); + $('#id-value').val('09- 8478---2869 '); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does count identical stripped and unstripped ISBN 10s as the same ISBN', () => { + $('#select-id').val('isbn_10'); + $('#id-value').val(' 144--93-55730 '); + $('.repeat-add').trigger('click'); + $('#select-id').val('isbn_10'); + $('#id-value').val('1449355730'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + // ISBN 13 + it('does add a valid ISBN 13', () => { + $('#select-id').val('isbn_13'); + $('#id-value').val('9781789801217'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does NOT add an invalid ISBN 13', () => { + $('#select-id').val('isbn_13'); + $('#id-value').val('1111111111111'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(5); + }); + + it('does NOT prompt to add a formally invalid ISBN 13', () => { + $('#select-id').val('isbn_13'); + $('#id-value').val('12345'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(5); + expect($('.repeat-add').length).toBe(1); + const errorDivText = $('#id-errors').text(); + const expected = 'Add it anyway?'; + expect(errorDivText).toEqual(expect.not.stringContaining(expected)); + }); + + it('clears the invalid ISBN 13 error prompt and does not add an ISBN if a user clicks no', () => { + $('#select-id').val('isbn_13'); + $('#id-value').val('0123456789123'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(5); + expect($('.repeat-add').length).toBe(2); + $('#do-not-add-isbn').trigger('click'); + expect($('.repeat-item').length).toBe(5); + const cssDisplay = $('#id-errors').css('display'); + expect(cssDisplay).toEqual('none'); + }); + + it('does NOT add a duplicate ISBN 13', () => { + $('#select-id').val('isbn_13'); + $('#id-value').val('9780984782857'); + $('.repeat-add').trigger('click'); + $('#select-id').val('isbn_13'); + $('#id-value').val('9780984782857'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does properly strip spaces and hypens from a valid ISBN 13 and add it', () => { + $('#select-id').val('isbn_13'); + $('#id-value').val('978-16172--95 980 '); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does count identical stripped and unstripped ISBN 13s as the same ISBN', () => { + $('#select-id').val('isbn_13'); + $('#id-value').val('-979-86 -64653403 '); + $('.repeat-add').trigger('click'); + $('#select-id').val('isbn_13'); + $('#id-value').val('9798664653403'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + //LCCN + it('does add a valid LCCN', () => { + $('#select-id').val('lccn'); + $('#id-value').val('n78-890351'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does NOT add an invalid LCCN', () => { + $('#select-id').val('lccn'); + $('#id-value').val('12345'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(5); + }); + + it('does NOT add a duplicate LCCN', () => { + $('#select-id').val('lccn'); + $('#id-value').val('n78-890351'); + $('.repeat-add').trigger('click'); + $('#select-id').val('lccn'); + $('#id-value').val('n78-890351'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does properly normalize a valid LCCN and add it', () => { + $('#select-id').val('lccn'); + $('#id-value').val(' 75-425165//r75'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does count identical normalized and non-normalized LCCNs as the same LCCN', () => { + $('#select-id').val('lccn'); + $('#id-value').val(' 75-425165//r75'); + $('.repeat-add').trigger('click'); + $('#select-id').val('lccn'); + $('#id-value').val('75425165'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); }); diff --git a/tests/unit/js/html-test-data.js b/tests/unit/js/html-test-data.js index 3548c53842c..9aec3a671fc 100644 --- a/tests/unit/js/html-test-data.js +++ b/tests/unit/js/html-test-data.js @@ -100,7 +100,7 @@ export const clamperSample = ` <a>orphans</a> <a>fantasy fiction</a> <a>England in fiction</a> - </span>` + </span>`; export const readClassification = ` <fieldset class="major" id="classifications" data-config="{"Please select a classification.": "Please select a classification.", "You need to give a value to CLASS.": "You need to give a value to CLASS."}"> @@ -193,4 +193,4 @@ export const readClassification = ` </div> </div> </fieldset> -` +`; diff --git a/tests/unit/js/idValidation.test.js b/tests/unit/js/idValidation.test.js index 82fa3364745..298e40e4c25 100644 --- a/tests/unit/js/idValidation.test.js +++ b/tests/unit/js/idValidation.test.js @@ -1,157 +1,157 @@ import { - parseIsbn, - parseLccn, - isChecksumValidIsbn10, - isChecksumValidIsbn13, - isFormatValidIsbn10, - isFormatValidIsbn13, - isValidLccn + isChecksumValidIsbn10, + isChecksumValidIsbn13, + isFormatValidIsbn10, + isFormatValidIsbn13, + isValidLccn, + parseIsbn, + parseLccn, } from '../../../openlibrary/plugins/openlibrary/js/idValidation.js'; describe('parseIsbn', () => { - it('correctly parses ISBN 10 with dashes', () => { - expect(parseIsbn('0-553-38168-7')).toBe('0553381687'); - }); - it('correctly parses ISBN 13 with dashes', () => { - expect(parseIsbn('978-0-553-38168-9')).toBe('9780553381689'); - }); -}) + it('correctly parses ISBN 10 with dashes', () => { + expect(parseIsbn('0-553-38168-7')).toBe('0553381687'); + }); + it('correctly parses ISBN 13 with dashes', () => { + expect(parseIsbn('978-0-553-38168-9')).toBe('9780553381689'); + }); +}); // testing from examples listed here: // https://www.loc.gov/marc/lccn-namespace.html describe('parseLccn', () => { - it('correctly parses LCCN example 1', () => { - expect(parseLccn('n78-890351')).toBe('n78890351'); - }); - it('correctly parses LCCN example 2', () => { - expect(parseLccn('n78-89035')).toBe('n78089035'); - }); - it('correctly parses LCCN example 3', () => { - expect(parseLccn('n 78890351 ')).toBe('n78890351'); - }); - it('correctly parses LCCN example 4', () => { - expect(parseLccn(' 85000002')).toBe('85000002'); - }); - it('correctly parses LCCN example 5', () => { - expect(parseLccn('85-2 ')).toBe('85000002'); - }); - it('correctly parses LCCN example 6', () => { - expect(parseLccn('2001-000002')).toBe('2001000002'); - }); - it('correctly parses LCCN example 7', () => { - expect(parseLccn('75-425165//r75')).toBe('75425165'); - }); - it('correctly parses LCCN example 8', () => { - expect(parseLccn(' 79139101 /AC/r932')).toBe('79139101'); - }); -}) + it('correctly parses LCCN example 1', () => { + expect(parseLccn('n78-890351')).toBe('n78890351'); + }); + it('correctly parses LCCN example 2', () => { + expect(parseLccn('n78-89035')).toBe('n78089035'); + }); + it('correctly parses LCCN example 3', () => { + expect(parseLccn('n 78890351 ')).toBe('n78890351'); + }); + it('correctly parses LCCN example 4', () => { + expect(parseLccn(' 85000002')).toBe('85000002'); + }); + it('correctly parses LCCN example 5', () => { + expect(parseLccn('85-2 ')).toBe('85000002'); + }); + it('correctly parses LCCN example 6', () => { + expect(parseLccn('2001-000002')).toBe('2001000002'); + }); + it('correctly parses LCCN example 7', () => { + expect(parseLccn('75-425165//r75')).toBe('75425165'); + }); + it('correctly parses LCCN example 8', () => { + expect(parseLccn(' 79139101 /AC/r932')).toBe('79139101'); + }); +}); describe('isChecksumValidIsbn10', () => { - it('returns true with valid ISBN 10 (X check character)', () => { - expect(isChecksumValidIsbn10('080442957X')).toBe(true); - }); - it('returns true with valid ISBN 10 (numerical check character, check 1)', () => { - expect(isChecksumValidIsbn10('1593279280')).toBe(true); - }); - it('returns true with valid ISBN 10 (numerical check character, check 2)', () => { - expect(isChecksumValidIsbn10('1617295981')).toBe(true); - }); + it('returns true with valid ISBN 10 (X check character)', () => { + expect(isChecksumValidIsbn10('080442957X')).toBe(true); + }); + it('returns true with valid ISBN 10 (numerical check character, check 1)', () => { + expect(isChecksumValidIsbn10('1593279280')).toBe(true); + }); + it('returns true with valid ISBN 10 (numerical check character, check 2)', () => { + expect(isChecksumValidIsbn10('1617295981')).toBe(true); + }); - it('returns false with an invalid ISBN 10', () => { - expect(isChecksumValidIsbn10('1234567890')).toBe(false); - }); -}) + it('returns false with an invalid ISBN 10', () => { + expect(isChecksumValidIsbn10('1234567890')).toBe(false); + }); +}); describe('isChecksumValidIsbn13', () => { - it('returns true with valid ISBN 13 (check 1)', () => { - expect(isChecksumValidIsbn13('9781789801217')).toBe(true); - }); - it('returns true with valid ISBN 13 (check 2)', () => { - expect(isChecksumValidIsbn13('9798430918002')).toBe(true); - }); + it('returns true with valid ISBN 13 (check 1)', () => { + expect(isChecksumValidIsbn13('9781789801217')).toBe(true); + }); + it('returns true with valid ISBN 13 (check 2)', () => { + expect(isChecksumValidIsbn13('9798430918002')).toBe(true); + }); - it('returns false with an invalid ISBN 13 (check 1)', () => { - expect(isChecksumValidIsbn13('1234567890123')).toBe(false); - }); - it('returns false with an invalid ISBN 13 (check 2)', () => { - expect(isChecksumValidIsbn13('9790000000000')).toBe(false); - }); -}) + it('returns false with an invalid ISBN 13 (check 1)', () => { + expect(isChecksumValidIsbn13('1234567890123')).toBe(false); + }); + it('returns false with an invalid ISBN 13 (check 2)', () => { + expect(isChecksumValidIsbn13('9790000000000')).toBe(false); + }); +}); describe('isFormatValidIsbn10', () => { - it('returns true with valid ISBN 10 (X check character)', () => { - expect(isFormatValidIsbn10('080442957X')).toBe(true); - }); - it('returns true with valid ISBN 10', () => { - expect(isFormatValidIsbn10('1593279280')).toBe(true); - }); + it('returns true with valid ISBN 10 (X check character)', () => { + expect(isFormatValidIsbn10('080442957X')).toBe(true); + }); + it('returns true with valid ISBN 10', () => { + expect(isFormatValidIsbn10('1593279280')).toBe(true); + }); - it('returns false with invalid ISBN 10', () => { - expect(isFormatValidIsbn10('a234567890')).toBe(false); - }); - it('returns false with blank value', () => { - expect(isFormatValidIsbn10('')).toBe(false); - }); -}) + it('returns false with invalid ISBN 10', () => { + expect(isFormatValidIsbn10('a234567890')).toBe(false); + }); + it('returns false with blank value', () => { + expect(isFormatValidIsbn10('')).toBe(false); + }); +}); describe('isFormatValidIsbn13', () => { - it('returns true with valid ISBN 13', () => { - expect(isFormatValidIsbn13('9781789801217')).toBe(true); - }); + it('returns true with valid ISBN 13', () => { + expect(isFormatValidIsbn13('9781789801217')).toBe(true); + }); - it('returns false with invalid ISBN 13 (too long)', () => { - expect(isFormatValidIsbn13('97918430918002')).toBe(false); - }); - it('returns false with invalid ISBN 13 (too short)', () => { - expect(isFormatValidIsbn13('979843091802')).toBe(false); - }); - it('returns false with invalis ISBN 13 (non-numeric)', () => { - expect(isFormatValidIsbn13('979a430918002')).toBe(false); - }); -}) + it('returns false with invalid ISBN 13 (too long)', () => { + expect(isFormatValidIsbn13('97918430918002')).toBe(false); + }); + it('returns false with invalid ISBN 13 (too short)', () => { + expect(isFormatValidIsbn13('979843091802')).toBe(false); + }); + it('returns false with invalis ISBN 13 (non-numeric)', () => { + expect(isFormatValidIsbn13('979a430918002')).toBe(false); + }); +}); // testing from examples listed here: // https://www.loc.gov/marc/lccn-namespace.html // https://www.oclc.org/bibformats/en/0xx/010.html describe('isValidLccn', () => { - it('returns true for LCCN of length 8', () => { - expect(isValidLccn('85000002')).toBe(true); - }); - it('returns true for LCCN of length 9', () => { - expect(isValidLccn('n78890351')).toBe(true); - }); - it('returns true for LCCN of length 10 (all digits)', () => { - expect(isValidLccn('2001000002')).toBe(true); - }); - it('returns true for LCCN of length 10 (alpha prefix)', () => { - expect(isValidLccn('sn85000678')).toBe(true); - }); - it('returns true for LCCN of length 11 (alpha-numeric prefix)', () => { - expect(isValidLccn('a2500000003')).toBe(true); - }); - it('returns true for LCCN of length 11 (alpha prefix)', () => { - expect(isValidLccn('agr25000003')).toBe(true); - }); - it('returns true for LCCN of length 12', () => { - expect(isValidLccn('mm2002084896')).toBe(true); - }); + it('returns true for LCCN of length 8', () => { + expect(isValidLccn('85000002')).toBe(true); + }); + it('returns true for LCCN of length 9', () => { + expect(isValidLccn('n78890351')).toBe(true); + }); + it('returns true for LCCN of length 10 (all digits)', () => { + expect(isValidLccn('2001000002')).toBe(true); + }); + it('returns true for LCCN of length 10 (alpha prefix)', () => { + expect(isValidLccn('sn85000678')).toBe(true); + }); + it('returns true for LCCN of length 11 (alpha-numeric prefix)', () => { + expect(isValidLccn('a2500000003')).toBe(true); + }); + it('returns true for LCCN of length 11 (alpha prefix)', () => { + expect(isValidLccn('agr25000003')).toBe(true); + }); + it('returns true for LCCN of length 12', () => { + expect(isValidLccn('mm2002084896')).toBe(true); + }); - it('returns false for LCCN below minimum length', () => { - expect(isValidLccn('8500002')).toBe(false); - }); - it('returns false for LCCN of length 9 with all digits', () => { - expect(isValidLccn('178890351')).toBe(false); - }); - it('returns false for LCCN of length 10 with alpha characters', () => { - expect(isValidLccn('a001000002')).toBe(false); - }); - it('returns false for LCCN of length 11 with all digits', () => { - expect(isValidLccn('12500000003')).toBe(false); - }); - it('returns false for LCCN of length 12 with all digits', () => { - expect(isValidLccn('125000000003')).toBe(false); - }); - it('returns false for LCCN of length 13', () => { - expect(isValidLccn('1250000000003')).toBe(false); - }); -}) + it('returns false for LCCN below minimum length', () => { + expect(isValidLccn('8500002')).toBe(false); + }); + it('returns false for LCCN of length 9 with all digits', () => { + expect(isValidLccn('178890351')).toBe(false); + }); + it('returns false for LCCN of length 10 with alpha characters', () => { + expect(isValidLccn('a001000002')).toBe(false); + }); + it('returns false for LCCN of length 11 with all digits', () => { + expect(isValidLccn('12500000003')).toBe(false); + }); + it('returns false for LCCN of length 12 with all digits', () => { + expect(isValidLccn('125000000003')).toBe(false); + }); + it('returns false for LCCN of length 13', () => { + expect(isValidLccn('1250000000003')).toBe(false); + }); +}); diff --git a/tests/unit/js/jquery.repeat.test.js b/tests/unit/js/jquery.repeat.test.js index 94f3d3a86a7..97db55c4d4a 100644 --- a/tests/unit/js/jquery.repeat.test.js +++ b/tests/unit/js/jquery.repeat.test.js @@ -1,41 +1,43 @@ import sinon from 'sinon'; -import * as testData from './html-test-data'; -import { htmlquote } from '../../../openlibrary/plugins/openlibrary/js/jsdef'; import { init } from '../../../openlibrary/plugins/openlibrary/js/jquery.repeat'; +import { htmlquote } from '../../../openlibrary/plugins/openlibrary/js/jsdef'; +import * as testData from './html-test-data'; let sandbox; beforeEach(() => { - sandbox = sinon.createSandbox(); - global.htmlquote = htmlquote; - // htmlquote is used inside an eval expression (yuck) so is an implied dependency - sandbox.stub(global, 'htmlquote').callsFake(htmlquote); + sandbox = sinon.createSandbox(); + global.htmlquote = htmlquote; + // htmlquote is used inside an eval expression (yuck) so is an implied dependency + sandbox.stub(global, 'htmlquote').callsFake(htmlquote); }); test('identifiers of repeated elements are never the same.', () => { - // setup Query repeat - init(); - // setup the HTML - $(document.body).html(testData.editionIdentifiersSample); - // turn on jQuery repeat - $('#identifiers').repeat({ - vars: { - prefix: 'edition--' - }, - validate: () => {} - }); + // setup Query repeat + init(); + // setup the HTML + $(document.body).html(testData.editionIdentifiersSample); + // turn on jQuery repeat + $('#identifiers').repeat({ + vars: { + prefix: 'edition--', + }, + validate: () => {}, + }); - expect($('.repeat-item').length).toBe(5); - $('#select-id').val('google'); - $('#id-value').text('fo4rzdaHDAwC'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - $('#identifiers--3 .repeat-remove').trigger('click') - expect($('.repeat-item').length).toBe(5); - $('#select-id').val('goodreads'); - $('#id-value').text('44415839'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - const ids = $('[id]').map((_, node) => node.getAttribute('id')).toArray(); - expect(ids.length).toBe(new Set(ids).size); + expect($('.repeat-item').length).toBe(5); + $('#select-id').val('google'); + $('#id-value').text('fo4rzdaHDAwC'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + $('#identifiers--3 .repeat-remove').trigger('click'); + expect($('.repeat-item').length).toBe(5); + $('#select-id').val('goodreads'); + $('#id-value').text('44415839'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + const ids = $('[id]') + .map((_, node) => node.getAttribute('id')) + .toArray(); + expect(ids.length).toBe(new Set(ids).size); }); diff --git a/tests/unit/js/jsdef.test.js b/tests/unit/js/jsdef.test.js index f1f69ed93ae..d1dcee0e01f 100644 --- a/tests/unit/js/jsdef.test.js +++ b/tests/unit/js/jsdef.test.js @@ -1,57 +1,64 @@ -import { foreach, range, join, len, htmlquote, enumerate, - websafe } from '../../../openlibrary/plugins/openlibrary/js/jsdef'; +import { + enumerate, + foreach, + htmlquote, + join, + len, + range, + websafe, +} from '../../../openlibrary/plugins/openlibrary/js/jsdef'; test('jsdef: python range function', () => { - expect(range(2, 5)).toEqual([2, 3, 4]); - expect(range(5)).toEqual([0, 1, 2, 3, 4]); - expect(range(0, 10, 2)).toEqual([0, 2, 4, 6, 8]); + expect(range(2, 5)).toEqual([2, 3, 4]); + expect(range(5)).toEqual([0, 1, 2, 3, 4]); + expect(range(0, 10, 2)).toEqual([0, 2, 4, 6, 8]); }); test('jsdef: enumerate', () => { - expect(enumerate([1, 2, 3])).toEqual([ - ['0', 1], - ['1', 2], - ['2', 3] - ]); + expect(enumerate([1, 2, 3])).toEqual([ + ['0', 1], + ['1', 2], + ['2', 3], + ]); }); test('jsdef: foreach', () => { - let called = 0; - const loop = []; - const listToLoop = [1, 2, 3]; - expect.assertions(1); - return new Promise((resolve) => { - foreach(listToLoop, loop, function () { - called += 1; - if (called === 3) { - expect(called).toBe(3); - resolve(); - } - }) + let called = 0; + const loop = []; + const listToLoop = [1, 2, 3]; + expect.assertions(1); + return new Promise((resolve) => { + foreach(listToLoop, loop, () => { + called += 1; + if (called === 3) { + expect(called).toBe(3); + resolve(); + } }); + }); }); test('jsdef: join', () => { - const str = '-'; - const joinFn = join.bind(str); - expect(joinFn(['1', '2'])).toBe('1-2'); + const str = '-'; + const joinFn = join.bind(str); + expect(joinFn(['1', '2'])).toBe('1-2'); }); test('jsdef: len', () => { - expect(len(['1', '2'])).toBe(2); + expect(len(['1', '2'])).toBe(2); }); test('jsdef: htmlquote', () => { - expect(htmlquote(5)).toBe('5'); - expect(htmlquote('<foo>')).toBe('<foo>'); - expect(htmlquote('\'foo\': "bar"')).toBe(''foo': "bar"'); - expect(htmlquote('a&b')).toBe('a&b'); + expect(htmlquote(5)).toBe('5'); + expect(htmlquote('<foo>')).toBe('<foo>'); + expect(htmlquote('\'foo\': "bar"')).toBe(''foo': "bar"'); + expect(htmlquote('a&b')).toBe('a&b'); }); test('jsdef: websafe', () => { - expect(websafe('<script>')).toBe('<script>'); - // not sure if these are really necessary, but they document the current behaviour - expect(websafe(undefined)).toBe(''); - expect(websafe(null)).toBe(''); - expect(websafe({toString: undefined})).toBe(''); + expect(websafe('<script>')).toBe('<script>'); + // not sure if these are really necessary, but they document the current behaviour + expect(websafe(undefined)).toBe(''); + expect(websafe(null)).toBe(''); + expect(websafe({ toString: undefined })).toBe(''); }); diff --git a/tests/unit/js/lists.test.js b/tests/unit/js/lists.test.js index 7e76994b28d..43fc7f228f3 100644 --- a/tests/unit/js/lists.test.js +++ b/tests/unit/js/lists.test.js @@ -1,195 +1,269 @@ -import { createActiveShowcaseItem, ShowcaseItem } from '../../../openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js'; -import { CreateListForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js' -import { listCreationForm, filledListCreationForm, showcaseI18nInput, subjectShowcase, authorShowcase, workShowcase, editionShowcase, activeListShowcase, listsSectionShowcase } from './sample-html/lists-test-data' +import { + createActiveShowcaseItem, + ShowcaseItem, +} from '../../../openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js'; +import { CreateListForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js'; +import { + activeListShowcase, + authorShowcase, + editionShowcase, + filledListCreationForm, + listCreationForm, + listsSectionShowcase, + showcaseI18nInput, + subjectShowcase, + workShowcase, +} from './sample-html/lists-test-data'; describe('CreateListForm class tests', () => { - test('CreateListForm fields correctly set', () => { - document.body.innerHTML = listCreationForm - const formElem = document.querySelector('form') - const listForm = new CreateListForm(formElem) - - const createListButton = document.querySelector('#create-list-button') - expect(listForm.createListButton === createListButton).toBe(true) - - const listTitleInput = document.querySelector('#list_label') - expect(listForm.listTitleInput === listTitleInput).toBe(true) - - const listDescriptionInput = document.querySelector('#list_desc') - expect(listForm.listDescriptionInput === listDescriptionInput).toBe(true) - }) - - test('`resetForm()` clears a filled form', () => { - document.body.innerHTML = listCreationForm - const formElem = document.querySelector('form') - const listForm = new CreateListForm(formElem) - - // Initial checks - expect(listForm.listTitleInput.value).not.toBeTruthy() - expect(listForm.listDescriptionInput.value).not.toBeTruthy() - - // After setting input values - listForm.listTitleInput.value = 'New List' - listForm.listDescriptionInput.value = 'My new list.' - expect(listForm.listTitleInput.value).toBeTruthy() - expect(listForm.listDescriptionInput.value).toBeTruthy() - - // After clearing the form: - listForm.resetForm() - expect(listForm.listTitleInput.value).not.toBeTruthy() - expect(listForm.listDescriptionInput.value).not.toBeTruthy() - }) - - it('should have empty inputs after instantiation', () => { - document.body.innerHTML = filledListCreationForm - const formElem = document.querySelector('form') - const titleInput = formElem.querySelector('#list_label') - const descriptionInput = formElem.querySelector('#list_desc') - - // Form is initially filled - expect(titleInput.value).toBeTruthy() - expect(descriptionInput.value).toBeTruthy() - - // Creating new CreateListForm should clear the form - // eslint-disable-next-line no-unused-vars - const listForm = new CreateListForm(formElem) - expect(titleInput.value).not.toBeTruthy() - expect(descriptionInput.value).not.toBeTruthy() - }) -}) + test('CreateListForm fields correctly set', () => { + document.body.innerHTML = listCreationForm; + const formElem = document.querySelector('form'); + const listForm = new CreateListForm(formElem); + + const createListButton = document.querySelector('#create-list-button'); + expect(listForm.createListButton === createListButton).toBe(true); + + const listTitleInput = document.querySelector('#list_label'); + expect(listForm.listTitleInput === listTitleInput).toBe(true); + + const listDescriptionInput = document.querySelector('#list_desc'); + expect(listForm.listDescriptionInput === listDescriptionInput).toBe(true); + }); + + test('`resetForm()` clears a filled form', () => { + document.body.innerHTML = listCreationForm; + const formElem = document.querySelector('form'); + const listForm = new CreateListForm(formElem); + + // Initial checks + expect(listForm.listTitleInput.value).not.toBeTruthy(); + expect(listForm.listDescriptionInput.value).not.toBeTruthy(); + + // After setting input values + listForm.listTitleInput.value = 'New List'; + listForm.listDescriptionInput.value = 'My new list.'; + expect(listForm.listTitleInput.value).toBeTruthy(); + expect(listForm.listDescriptionInput.value).toBeTruthy(); + + // After clearing the form: + listForm.resetForm(); + expect(listForm.listTitleInput.value).not.toBeTruthy(); + expect(listForm.listDescriptionInput.value).not.toBeTruthy(); + }); + + it('should have empty inputs after instantiation', () => { + document.body.innerHTML = filledListCreationForm; + const formElem = document.querySelector('form'); + const titleInput = formElem.querySelector('#list_label'); + const descriptionInput = formElem.querySelector('#list_desc'); + + // Form is initially filled + expect(titleInput.value).toBeTruthy(); + expect(descriptionInput.value).toBeTruthy(); + + // Creating new CreateListForm should clear the form + // eslint-disable-next-line no-unused-vars + const listForm = new CreateListForm(formElem); + expect(titleInput.value).not.toBeTruthy(); + expect(descriptionInput.value).not.toBeTruthy(); + }); +}); describe('createActiveShowcaseItem() tests', () => { - test('createActiveShowcaseItem() results are as expected', () => { - document.body.innerHTML = showcaseI18nInput - const listKey = '/people/openlibrary/lists/OL1L' - const seedKey = '/books/OL3421846M' - const listTitle = 'My First List' - const coverUrl = '/images/icons/avatar_book-sm.png' - - const li = createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl) - const anchors = li.querySelectorAll('a') - const [imageLink, titleLink, removeLink] = anchors - const inputs = li.querySelectorAll('input') - const [titleInput, seedKeyInput, seedTypeInput] = inputs - - - // Must have `actionable-item` class - expect(li.classList.contains('actionable-item')).toBe(true) - - // List key has been set - expect(removeLink.dataset.listKey === listKey).toBe(true) - expect(imageLink.href.endsWith(listKey)).toBe(true) - expect(titleLink.href.endsWith(listKey)).toBe(true) - expect(removeLink.href.endsWith(listKey)).toBe(true) - - // Seed key has been set - expect(seedKeyInput.value === seedKey).toBe(true) - expect(seedTypeInput.value === 'edition').toBe(true) - - // List title has been set - expect(titleLink.dataset.listTitle === listTitle).toBe(true) - expect(titleLink.textContent === listTitle).toBe(true) - expect(titleInput.value === listTitle).toBe(true) - - // Cover URL has been set - expect(imageLink.children[0].src.endsWith(coverUrl)).toBe(true) - }) - - test('createActiveShowcaseItem() sets the correct seed type', () => { - const listKey = '/people/openlibrary/lists/OL1L' - const listTitle = 'My First List' - const coverUrl = '/images/icons/avatar_book-sm.png' - - const editionKey = '/books/OL3421846M' - const workKey = '/works/OL54120W' - const authorKey = '/authors/OL18319A' - const subjectKey = 'quotations' - const bogusKey = '/bogus/OL38475839B' - - const editionItem = createActiveShowcaseItem(listKey, editionKey, listTitle, coverUrl) - expect(editionItem.querySelector('input[name=seed-type]').value).toBe('edition') - - const workItem = createActiveShowcaseItem(listKey, workKey, listTitle, coverUrl) - expect(workItem.querySelector('input[name=seed-type]').value).toBe('work') - - const authorItem = createActiveShowcaseItem(listKey, authorKey, listTitle, coverUrl) - expect(authorItem.querySelector('input[name=seed-type]').value).toBe('author') - - const subjectItem = createActiveShowcaseItem(listKey, subjectKey, listTitle, coverUrl) - expect(subjectItem.querySelector('input[name=seed-type]').value).toBe('subject') - - const bogusItem = createActiveShowcaseItem(listKey, bogusKey, listTitle, coverUrl) - expect(bogusItem.querySelector('input[name=seed-type]').value).toBe('undefined') - }) - - it('sets the correct default value for `coverUrl`', () => { - document.body.innerHTML = showcaseI18nInput - const listKey = '/people/openlibrary/lists/OL1L' - const seedKey = '/books/OL3421846M' - const listTitle = 'My First List' - - const li = createActiveShowcaseItem(listKey, seedKey, listTitle) - const coverImage = li.querySelector('img') - - const expectedCoverUrl = '/images/icons/avatar_book-sm.png' - expect(coverImage.src.endsWith(expectedCoverUrl)).toBe(true) - }) -}) + test('createActiveShowcaseItem() results are as expected', () => { + document.body.innerHTML = showcaseI18nInput; + const listKey = '/people/openlibrary/lists/OL1L'; + const seedKey = '/books/OL3421846M'; + const listTitle = 'My First List'; + const coverUrl = '/images/icons/avatar_book-sm.png'; + + const li = createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl); + const anchors = li.querySelectorAll('a'); + const [imageLink, titleLink, removeLink] = anchors; + const inputs = li.querySelectorAll('input'); + const [titleInput, seedKeyInput, seedTypeInput] = inputs; + + // Must have `actionable-item` class + expect(li.classList.contains('actionable-item')).toBe(true); + + // List key has been set + expect(removeLink.dataset.listKey === listKey).toBe(true); + expect(imageLink.href.endsWith(listKey)).toBe(true); + expect(titleLink.href.endsWith(listKey)).toBe(true); + expect(removeLink.href.endsWith(listKey)).toBe(true); + + // Seed key has been set + expect(seedKeyInput.value === seedKey).toBe(true); + expect(seedTypeInput.value === 'edition').toBe(true); + + // List title has been set + expect(titleLink.dataset.listTitle === listTitle).toBe(true); + expect(titleLink.textContent === listTitle).toBe(true); + expect(titleInput.value === listTitle).toBe(true); + + // Cover URL has been set + expect(imageLink.children[0].src.endsWith(coverUrl)).toBe(true); + }); + + test('createActiveShowcaseItem() sets the correct seed type', () => { + const listKey = '/people/openlibrary/lists/OL1L'; + const listTitle = 'My First List'; + const coverUrl = '/images/icons/avatar_book-sm.png'; + + const editionKey = '/books/OL3421846M'; + const workKey = '/works/OL54120W'; + const authorKey = '/authors/OL18319A'; + const subjectKey = 'quotations'; + const bogusKey = '/bogus/OL38475839B'; + + const editionItem = createActiveShowcaseItem( + listKey, + editionKey, + listTitle, + coverUrl, + ); + expect(editionItem.querySelector('input[name=seed-type]').value).toBe( + 'edition', + ); + + const workItem = createActiveShowcaseItem( + listKey, + workKey, + listTitle, + coverUrl, + ); + expect(workItem.querySelector('input[name=seed-type]').value).toBe('work'); + + const authorItem = createActiveShowcaseItem( + listKey, + authorKey, + listTitle, + coverUrl, + ); + expect(authorItem.querySelector('input[name=seed-type]').value).toBe( + 'author', + ); + + const subjectItem = createActiveShowcaseItem( + listKey, + subjectKey, + listTitle, + coverUrl, + ); + expect(subjectItem.querySelector('input[name=seed-type]').value).toBe( + 'subject', + ); + + const bogusItem = createActiveShowcaseItem( + listKey, + bogusKey, + listTitle, + coverUrl, + ); + expect(bogusItem.querySelector('input[name=seed-type]').value).toBe( + 'undefined', + ); + }); + + it('sets the correct default value for `coverUrl`', () => { + document.body.innerHTML = showcaseI18nInput; + const listKey = '/people/openlibrary/lists/OL1L'; + const seedKey = '/books/OL3421846M'; + const listTitle = 'My First List'; + + const li = createActiveShowcaseItem(listKey, seedKey, listTitle); + const coverImage = li.querySelector('img'); + + const expectedCoverUrl = '/images/icons/avatar_book-sm.png'; + expect(coverImage.src.endsWith(expectedCoverUrl)).toBe(true); + }); +}); describe('ShowcaseItem class tests', () => { - test('ShowcaseItem fields correctly set', () => { - document.body.innerHTML = activeListShowcase - const showcaseElem = document.querySelector('.actionable-item') - const showcase = new ShowcaseItem(showcaseElem) - const removeAffordance = showcaseElem.querySelector('.remove-from-list') - - expect(showcase.showcaseElem === showcaseElem).toBe(true) - expect(showcase.isActiveShowcase).toBe(true) - expect(showcase.removeFromListAffordance === removeAffordance).toBe(true) - expect(showcase.listKey === '/people/openlibrary/lists/OL1L').toBe(true) - expect(showcase.seedKey === '/works/OL54120W').toBe(true) - expect(showcase.type).toBe('work') - expect(showcase.seed).toMatchObject({key: '/works/OL54120W'}) - }) - - it('correctly infers if it is an active showcase', () => { - document.body.innerHTML = activeListShowcase + listsSectionShowcase - const [activeShowcaseElem, otherShowcaseElem] = document.querySelectorAll('.actionable-item') - const activeShowcase = new ShowcaseItem(activeShowcaseElem) - const otherShowcase = new ShowcaseItem(otherShowcaseElem) - - expect(activeShowcase.isActiveShowcase).toBe(true) - expect(otherShowcase.isActiveShowcase).toBe(false) - }) - - describe('Seed type inference', () => { - const cases = [ - {markup: subjectShowcase, expectedType: 'subject', expectedIsWorkValue: false, expectedIsSubjectValue: true}, - {markup: authorShowcase, expectedType: 'author', expectedIsWorkValue: false, expectedIsSubjectValue: false}, - {markup: workShowcase, expectedType: 'work', expectedIsWorkValue: true, expectedIsSubjectValue: false}, - {markup: editionShowcase, expectedType: 'edition', expectedIsWorkValue: false, expectedIsSubjectValue: false} - ] - - test.each(cases)('Type is $expectedType', ({markup, expectedType}) => { - document.body.innerHTML = markup - const showcaseElem = document.querySelector('.actionable-item') - const showcase = new ShowcaseItem(showcaseElem) - expect(showcase.type).toBe(expectedType) - }) - - test.each(cases)('`isWork` value expected to be $expectedIsWorkValue', ({markup, expectedIsWorkValue}) => { - document.body.innerHTML = markup - const showcaseElem = document.querySelector('.actionable-item') - const showcase = new ShowcaseItem(showcaseElem) - expect(showcase.isWork).toBe(expectedIsWorkValue) - }) - - test.each(cases)('`isSubject` value expected to be $expectedIsSubjectValue', ({markup, expectedIsSubjectValue}) => { - document.body.innerHTML = markup - const showcaseElem = document.querySelector('.actionable-item') - const showcase = new ShowcaseItem(showcaseElem) - expect(showcase.isSubject).toBe(expectedIsSubjectValue) - }) - }) - - // XXX : test : removeSelf() fails safely when myBooksStore has not been created? -}) + test('ShowcaseItem fields correctly set', () => { + document.body.innerHTML = activeListShowcase; + const showcaseElem = document.querySelector('.actionable-item'); + const showcase = new ShowcaseItem(showcaseElem); + const removeAffordance = showcaseElem.querySelector('.remove-from-list'); + + expect(showcase.showcaseElem === showcaseElem).toBe(true); + expect(showcase.isActiveShowcase).toBe(true); + expect(showcase.removeFromListAffordance === removeAffordance).toBe(true); + expect(showcase.listKey === '/people/openlibrary/lists/OL1L').toBe(true); + expect(showcase.seedKey === '/works/OL54120W').toBe(true); + expect(showcase.type).toBe('work'); + expect(showcase.seed).toMatchObject({ key: '/works/OL54120W' }); + }); + + it('correctly infers if it is an active showcase', () => { + document.body.innerHTML = activeListShowcase + listsSectionShowcase; + const [activeShowcaseElem, otherShowcaseElem] = + document.querySelectorAll('.actionable-item'); + const activeShowcase = new ShowcaseItem(activeShowcaseElem); + const otherShowcase = new ShowcaseItem(otherShowcaseElem); + + expect(activeShowcase.isActiveShowcase).toBe(true); + expect(otherShowcase.isActiveShowcase).toBe(false); + }); + + describe('Seed type inference', () => { + const cases = [ + { + markup: subjectShowcase, + expectedType: 'subject', + expectedIsWorkValue: false, + expectedIsSubjectValue: true, + }, + { + markup: authorShowcase, + expectedType: 'author', + expectedIsWorkValue: false, + expectedIsSubjectValue: false, + }, + { + markup: workShowcase, + expectedType: 'work', + expectedIsWorkValue: true, + expectedIsSubjectValue: false, + }, + { + markup: editionShowcase, + expectedType: 'edition', + expectedIsWorkValue: false, + expectedIsSubjectValue: false, + }, + ]; + + test.each(cases)('Type is $expectedType', ({ markup, expectedType }) => { + document.body.innerHTML = markup; + const showcaseElem = document.querySelector('.actionable-item'); + const showcase = new ShowcaseItem(showcaseElem); + expect(showcase.type).toBe(expectedType); + }); + + test.each(cases)('`isWork` value expected to be $expectedIsWorkValue', ({ + markup, + expectedIsWorkValue, + }) => { + document.body.innerHTML = markup; + const showcaseElem = document.querySelector('.actionable-item'); + const showcase = new ShowcaseItem(showcaseElem); + expect(showcase.isWork).toBe(expectedIsWorkValue); + }); + + test.each( + cases, + )('`isSubject` value expected to be $expectedIsSubjectValue', ({ + markup, + expectedIsSubjectValue, + }) => { + document.body.innerHTML = markup; + const showcaseElem = document.querySelector('.actionable-item'); + const showcase = new ShowcaseItem(showcaseElem); + expect(showcase.isSubject).toBe(expectedIsSubjectValue); + }); + }); + + // XXX : test : removeSelf() fails safely when myBooksStore has not been created? +}); diff --git a/tests/unit/js/my-books.test.js b/tests/unit/js/my-books.test.js index 037d0e9ffb1..e58d0a12c00 100644 --- a/tests/unit/js/my-books.test.js +++ b/tests/unit/js/my-books.test.js @@ -1,151 +1,149 @@ -import { listCreationForm } from './sample-html/lists-test-data' -import { checkInForm } from './sample-html/checkIns-test-data' -import { CreateListForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/CreateListForm' -import { CheckInForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents' +import { CreateListForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/CreateListForm'; +import { CheckInForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents'; +import { checkInForm } from './sample-html/checkIns-test-data'; +import { listCreationForm } from './sample-html/lists-test-data'; jest.mock('jquery-ui/ui/widgets/dialog', () => {}); describe('CreateListForm.js class', () => { - let form - let formElem - - beforeEach(() => { - document.body.innerHTML = listCreationForm - formElem = document.querySelector('form') - form = new CreateListForm(formElem) - }) - - test('References are set correctly', () => { - const createListButton = formElem.querySelector('#create-list-button') - const nameInput = formElem.querySelector('#list_label') - const descriptionInput = formElem.querySelector('#list_desc') - - expect(createListButton === form.createListButton).toBe(true) - expect(nameInput === form.listTitleInput).toBe(true) - expect(descriptionInput === form.listDescriptionInput).toBe(true) - }) - - it('it clears the form after a resetForm() call', () => { - const nameInput = document.querySelector('#list_label') - const descriptionInput = document.querySelector('#list_desc') - - // Form should be empty initially - expect(nameInput.value.length).toBe(0) - expect(descriptionInput.value.length).toBe(0) - - // Add values to each input - nameInput.value = 'My New List' - descriptionInput.value = 'The best list ever' - expect(nameInput.value.length).toBeGreaterThan(0) - expect(descriptionInput.value.length).toBeGreaterThan(0) - - // After clearing the form - form.resetForm() - expect(nameInput.value.length).toBe(0) - expect(descriptionInput.value.length).toBe(0) - }) -}) + let form; + let formElem; + + beforeEach(() => { + document.body.innerHTML = listCreationForm; + formElem = document.querySelector('form'); + form = new CreateListForm(formElem); + }); + + test('References are set correctly', () => { + const createListButton = formElem.querySelector('#create-list-button'); + const nameInput = formElem.querySelector('#list_label'); + const descriptionInput = formElem.querySelector('#list_desc'); + + expect(createListButton === form.createListButton).toBe(true); + expect(nameInput === form.listTitleInput).toBe(true); + expect(descriptionInput === form.listDescriptionInput).toBe(true); + }); + + it('it clears the form after a resetForm() call', () => { + const nameInput = document.querySelector('#list_label'); + const descriptionInput = document.querySelector('#list_desc'); + + // Form should be empty initially + expect(nameInput.value.length).toBe(0); + expect(descriptionInput.value.length).toBe(0); + + // Add values to each input + nameInput.value = 'My New List'; + descriptionInput.value = 'The best list ever'; + expect(nameInput.value.length).toBeGreaterThan(0); + expect(descriptionInput.value.length).toBeGreaterThan(0); + + // After clearing the form + form.resetForm(); + expect(nameInput.value.length).toBe(0); + expect(descriptionInput.value.length).toBe(0); + }); +}); describe('CheckInForm class', () => { - let formElem = undefined - let submitButton = undefined - let yearSelect = undefined - let monthSelect = undefined - let daySelect = undefined - - const workOlid = 'OL123W' - const editionKey = '/books/OL456M' - - - - beforeEach(() => { - document.body.innerHTML = checkInForm - formElem = document.querySelector('form') - submitButton = document.querySelector('.check-in__submit-btn') - yearSelect = document.querySelector('select[name=year]') - monthSelect = document.querySelector('select[name=month]') - daySelect = document.querySelector('select[name=day]') - }) - - test('Submit button, month select, and day select are initially disabled when read date is absent', () => { - const form = new CheckInForm(formElem, workOlid, editionKey) - form.initialize() - expect(submitButton.disabled).toBe(true) - expect(monthSelect.disabled).toBe(true) - expect(daySelect.disabled).toBe(true) - - expect(yearSelect.disabled).toBe(false) - expect(yearSelect.value).toBe('') - }) - - it('Sets correct values and enables selects and submit button', () => { - const form = new CheckInForm(formElem, workOlid, editionKey) - form.initialize() - form.updateSelectedDate(2022, 1, 31) - expect(submitButton.disabled).toBe(false) - expect(monthSelect.disabled).toBe(false) - expect(daySelect.disabled).toBe(false) - - expect(yearSelect.value).toBe('2022') - expect(monthSelect.value).toBe('1') - expect(daySelect.value).toBe('31') - }) - - it('Hides impossible day options', () => { - const form = new CheckInForm(formElem, workOlid, editionKey) - form.initialize() - form.updateSelectedDate(2022, 2, 20) - - // The 28th day should be visible: - expect(daySelect.options[28].classList.contains('hidden')).toBe(false) - - // Subsequent days should not be visible - expect(daySelect.options[29].classList.contains('hidden')).toBe(true) - expect(daySelect.options[30].classList.contains('hidden')).toBe(true) - expect(daySelect.options[31].classList.contains('hidden')).toBe(true) - }) - - it('Shows 29 days in February when there is a leap year', () => { - const form = new CheckInForm(formElem, workOlid, editionKey) - form.initialize() - form.updateSelectedDate(2020, 2, 1) - - expect(daySelect.options[29].classList.contains('hidden')).toBe(false) - expect(daySelect.options[30].classList.contains('hidden')).toBe(true) - expect(daySelect.options[31].classList.contains('hidden')).toBe(true) - }) - - it('Associates labels with select elements during initialization', () => { - const form = new CheckInForm(formElem, workOlid, editionKey) - - // Get reference to each label: - const yearLabel = formElem.querySelector('.check-in__year-label') - const monthLabel = formElem.querySelector('.check-in__month-label') - const dayLabel = formElem.querySelector('.check-in__day-label') - - // Verify labels have no `for` initially: - expect(yearLabel.htmlFor).toBe('') - expect(monthLabel.htmlFor).toBe('') - expect(dayLabel.htmlFor).toBe('') - - // Verify select elements have no `id` initially: - expect(yearSelect.id).toBe('') - expect(monthSelect.id).toBe('') - expect(daySelect.id).toBe('') - - // Verify labels associated with selects after initialization: - form.initialize() - - const expectedYearId = `year-select-${workOlid}` - const expectedMonthId = `month-select-${workOlid}` - const expectedDayId = `day-select-${workOlid}` - - expect(yearLabel.htmlFor).toBe(expectedYearId) - expect(monthLabel.htmlFor).toBe(expectedMonthId) - expect(dayLabel.htmlFor).toBe(expectedDayId) - - expect(yearSelect.id).toBe(expectedYearId) - expect(monthSelect.id).toBe(expectedMonthId) - expect(daySelect.id).toBe(expectedDayId) - }) -}) + let formElem; + let submitButton; + let yearSelect; + let monthSelect; + let daySelect; + + const workOlid = 'OL123W'; + const editionKey = '/books/OL456M'; + + beforeEach(() => { + document.body.innerHTML = checkInForm; + formElem = document.querySelector('form'); + submitButton = document.querySelector('.check-in__submit-btn'); + yearSelect = document.querySelector('select[name=year]'); + monthSelect = document.querySelector('select[name=month]'); + daySelect = document.querySelector('select[name=day]'); + }); + + test('Submit button, month select, and day select are initially disabled when read date is absent', () => { + const form = new CheckInForm(formElem, workOlid, editionKey); + form.initialize(); + expect(submitButton.disabled).toBe(true); + expect(monthSelect.disabled).toBe(true); + expect(daySelect.disabled).toBe(true); + + expect(yearSelect.disabled).toBe(false); + expect(yearSelect.value).toBe(''); + }); + + it('Sets correct values and enables selects and submit button', () => { + const form = new CheckInForm(formElem, workOlid, editionKey); + form.initialize(); + form.updateSelectedDate(2022, 1, 31); + expect(submitButton.disabled).toBe(false); + expect(monthSelect.disabled).toBe(false); + expect(daySelect.disabled).toBe(false); + + expect(yearSelect.value).toBe('2022'); + expect(monthSelect.value).toBe('1'); + expect(daySelect.value).toBe('31'); + }); + + it('Hides impossible day options', () => { + const form = new CheckInForm(formElem, workOlid, editionKey); + form.initialize(); + form.updateSelectedDate(2022, 2, 20); + + // The 28th day should be visible: + expect(daySelect.options[28].classList.contains('hidden')).toBe(false); + + // Subsequent days should not be visible + expect(daySelect.options[29].classList.contains('hidden')).toBe(true); + expect(daySelect.options[30].classList.contains('hidden')).toBe(true); + expect(daySelect.options[31].classList.contains('hidden')).toBe(true); + }); + + it('Shows 29 days in February when there is a leap year', () => { + const form = new CheckInForm(formElem, workOlid, editionKey); + form.initialize(); + form.updateSelectedDate(2020, 2, 1); + + expect(daySelect.options[29].classList.contains('hidden')).toBe(false); + expect(daySelect.options[30].classList.contains('hidden')).toBe(true); + expect(daySelect.options[31].classList.contains('hidden')).toBe(true); + }); + + it('Associates labels with select elements during initialization', () => { + const form = new CheckInForm(formElem, workOlid, editionKey); + + // Get reference to each label: + const yearLabel = formElem.querySelector('.check-in__year-label'); + const monthLabel = formElem.querySelector('.check-in__month-label'); + const dayLabel = formElem.querySelector('.check-in__day-label'); + + // Verify labels have no `for` initially: + expect(yearLabel.htmlFor).toBe(''); + expect(monthLabel.htmlFor).toBe(''); + expect(dayLabel.htmlFor).toBe(''); + + // Verify select elements have no `id` initially: + expect(yearSelect.id).toBe(''); + expect(monthSelect.id).toBe(''); + expect(daySelect.id).toBe(''); + + // Verify labels associated with selects after initialization: + form.initialize(); + + const expectedYearId = `year-select-${workOlid}`; + const expectedMonthId = `month-select-${workOlid}`; + const expectedDayId = `day-select-${workOlid}`; + + expect(yearLabel.htmlFor).toBe(expectedYearId); + expect(monthLabel.htmlFor).toBe(expectedMonthId); + expect(dayLabel.htmlFor).toBe(expectedDayId); + + expect(yearSelect.id).toBe(expectedYearId); + expect(monthSelect.id).toBe(expectedMonthId); + expect(daySelect.id).toBe(expectedDayId); + }); +}); diff --git a/tests/unit/js/nonjquery_utils.test.js b/tests/unit/js/nonjquery_utils.test.js index 908ebd346b2..f35b8f0669b 100644 --- a/tests/unit/js/nonjquery_utils.test.js +++ b/tests/unit/js/nonjquery_utils.test.js @@ -2,55 +2,55 @@ import sinon from 'sinon'; import { debounce } from '../../../openlibrary/plugins/openlibrary/js/nonjquery_utils.js'; describe('debounce', () => { - test('func not called during initialization', () => { - const spy = sinon.spy(); - debounce(spy, 100, false); - expect(spy.callCount).toBe(0); - }); + test('func not called during initialization', () => { + const spy = sinon.spy(); + debounce(spy, 100, false); + expect(spy.callCount).toBe(0); + }); - test('func called after threshold when !execAsap', () => { - const clock = sinon.useFakeTimers(); - const spy = sinon.spy(); - const debouncedSpy = debounce(spy, 100, false); - debouncedSpy(); - expect(spy.callCount).toBe(0); - clock.tick(99); - expect(spy.callCount).toBe(0); - clock.tick(1); - expect(spy.callCount).toBe(1); - clock.restore(); - }); + test('func called after threshold when !execAsap', () => { + const clock = sinon.useFakeTimers(); + const spy = sinon.spy(); + const debouncedSpy = debounce(spy, 100, false); + debouncedSpy(); + expect(spy.callCount).toBe(0); + clock.tick(99); + expect(spy.callCount).toBe(0); + clock.tick(1); + expect(spy.callCount).toBe(1); + clock.restore(); + }); - test('func called immediately when execAsap', () => { - const clock = sinon.useFakeTimers(); - const spy = sinon.spy(); - const debouncedSpy = debounce(spy, 100, true); - debouncedSpy(); - expect(spy.callCount).toBe(1); - clock.tick(100); - expect(spy.callCount).toBe(1); - clock.restore(); - }); + test('func called immediately when execAsap', () => { + const clock = sinon.useFakeTimers(); + const spy = sinon.spy(); + const debouncedSpy = debounce(spy, 100, true); + debouncedSpy(); + expect(spy.callCount).toBe(1); + clock.tick(100); + expect(spy.callCount).toBe(1); + clock.restore(); + }); - test('func called with correct context and arguments', () => { - const spy = sinon.spy(); - const debouncedSpy = debounce(spy, 100, true); - const context = {}; - debouncedSpy.call(context, 1, 2, 3); - expect(spy.thisValues[0]).toBe(context); - expect(spy.args[0]).toEqual([1, 2, 3]); - }); + test('func called with correct context and arguments', () => { + const spy = sinon.spy(); + const debouncedSpy = debounce(spy, 100, true); + const context = {}; + debouncedSpy.call(context, 1, 2, 3); + expect(spy.thisValues[0]).toBe(context); + expect(spy.args[0]).toEqual([1, 2, 3]); + }); - test('func only called once when spammed', () => { - const clock = sinon.useFakeTimers(); - const spy = sinon.spy(); - const debouncedSpy = debounce(spy, 100, false); - for (let i = 0; i < 10; i++) { - debouncedSpy(); - expect(spy.callCount).toBe(0); - } - clock.tick(100); - expect(spy.callCount).toBe(1); - clock.restore(); - }); + test('func only called once when spammed', () => { + const clock = sinon.useFakeTimers(); + const spy = sinon.spy(); + const debouncedSpy = debounce(spy, 100, false); + for (let i = 0; i < 10; i++) { + debouncedSpy(); + expect(spy.callCount).toBe(0); + } + clock.tick(100); + expect(spy.callCount).toBe(1); + clock.restore(); + }); }); diff --git a/tests/unit/js/python.test.js b/tests/unit/js/python.test.js index f645cb0d064..4674f95e90d 100644 --- a/tests/unit/js/python.test.js +++ b/tests/unit/js/python.test.js @@ -1,39 +1,45 @@ -import { commify, urlencode, slice } from '../../../openlibrary/plugins/openlibrary/js/python'; +import { + commify, + slice, + urlencode, +} from '../../../openlibrary/plugins/openlibrary/js/python'; test('commify', () => { - expect(commify('5443232')).toBe('5,443,232'); - expect(commify('50')).toBe('50'); - expect(commify('5000')).toBe('5,000'); - expect(commify(['1','2','3','45'])).toBe('1,2,3,45'); - expect(commify([1, 20, 3])).toBe('1,20,3'); + expect(commify('5443232')).toBe('5,443,232'); + expect(commify('50')).toBe('50'); + expect(commify('5000')).toBe('5,000'); + expect(commify(['1', '2', '3', '45'])).toBe('1,2,3,45'); + expect(commify([1, 20, 3])).toBe('1,20,3'); }); describe('urlencode', () => { - test('empty array', () => { - expect(urlencode([])).toEqual(''); - }); - test('array of 1', () => { - expect(urlencode(['apple'])).toEqual('0=apple'); - }); - test('array of 3', () => { - expect(urlencode(['apple', 'grapes', 'orange'])).toEqual('0=apple&1=grapes&2=orange'); - }); + test('empty array', () => { + expect(urlencode([])).toEqual(''); + }); + test('array of 1', () => { + expect(urlencode(['apple'])).toEqual('0=apple'); + }); + test('array of 3', () => { + expect(urlencode(['apple', 'grapes', 'orange'])).toEqual( + '0=apple&1=grapes&2=orange', + ); + }); }); describe('slice', () => { - test('empty array', () => { - expect(slice([], 0, 0)).toEqual([]); - }); - test('array of 2', () => { - expect(slice([1, 2], 0, 1)).toEqual([1]); - }); - test('arr length less than end', () => { - expect(slice([1, 2, 3], 0, 5)).toEqual([1, 2, 3]); - }); - test('beginning greater than end', () => { - expect(slice([1, 2, 3, 4, 5], 4, 3)).toEqual([]); - }); - test('array of 5', () => { - expect(slice([1, 2, 3, 4, 5], 0, 3)).toEqual([1, 2, 3]); - }); + test('empty array', () => { + expect(slice([], 0, 0)).toEqual([]); + }); + test('array of 2', () => { + expect(slice([1, 2], 0, 1)).toEqual([1]); + }); + test('arr length less than end', () => { + expect(slice([1, 2, 3], 0, 5)).toEqual([1, 2, 3]); + }); + test('beginning greater than end', () => { + expect(slice([1, 2, 3, 4, 5], 4, 3)).toEqual([]); + }); + test('array of 5', () => { + expect(slice([1, 2, 3, 4, 5], 0, 3)).toEqual([1, 2, 3]); + }); }); diff --git a/tests/unit/js/sample-html/checkIns-test-data.js b/tests/unit/js/sample-html/checkIns-test-data.js index f2ab53499a1..a653c1b4db1 100644 --- a/tests/unit/js/sample-html/checkIns-test-data.js +++ b/tests/unit/js/sample-html/checkIns-test-data.js @@ -83,4 +83,4 @@ export const checkInForm = ` </span> </form> </div> -` +`; diff --git a/tests/unit/js/sample-html/dropper-test-data.js b/tests/unit/js/sample-html/dropper-test-data.js index 584c186269e..da9b83e515a 100644 --- a/tests/unit/js/sample-html/dropper-test-data.js +++ b/tests/unit/js/sample-html/dropper-test-data.js @@ -5,28 +5,28 @@ export const legacyBookDropperMarkup = ` <div class="arrow arrow-unactivated"></div> </a> </div> -` +`; -export const openDropperMarkup = generateDropperMarkup(true) +export const openDropperMarkup = generateDropperMarkup(true); -export const closedDropperMarkup = generateDropperMarkup(false) +export const closedDropperMarkup = generateDropperMarkup(false); -export const disabledDropperMarkup = generateDropperMarkup(false, true) +export const disabledDropperMarkup = generateDropperMarkup(false, true); function generateDropperMarkup(isDropperOpen, isDropperDisabled = false) { - let wrapperClasses = 'generic-dropper-wrapper' - let arrowClasses = 'arrow' + let wrapperClasses = 'generic-dropper-wrapper'; + let arrowClasses = 'arrow'; - if (isDropperOpen) { - wrapperClasses += ' generic-dropper-wrapper--active' - arrowClasses += ' up' - } + if (isDropperOpen) { + wrapperClasses += ' generic-dropper-wrapper--active'; + arrowClasses += ' up'; + } - if (isDropperDisabled) { - wrapperClasses += ' generic-dropper--disabled' - } + if (isDropperDisabled) { + wrapperClasses += ' generic-dropper--disabled'; + } - return ` + return ` <div class="${wrapperClasses}"> <div class="generic-dropper"> <div class="generic-dropper__actions"> @@ -42,5 +42,5 @@ function generateDropperMarkup(isDropperOpen, isDropperDisabled = false) { </div> </div> </div> - ` + `; } diff --git a/tests/unit/js/sample-html/lists-test-data.js b/tests/unit/js/sample-html/lists-test-data.js index f5719b6bafe..504cf5cd1f3 100644 --- a/tests/unit/js/sample-html/lists-test-data.js +++ b/tests/unit/js/sample-html/lists-test-data.js @@ -1,8 +1,8 @@ function createListFormMarkup(isFilled) { - const listName = isFilled ? 'My New List' : '' - const listDescription = isFilled ? 'A list for all of my books' : '' + const listName = isFilled ? 'My New List' : ''; + const listDescription = isFilled ? 'A list for all of my books' : ''; - return ` + return ` <form method="post" class="floatform" name="new-list" id="new-list"> <div class="formElement"> <div class="label"> @@ -28,15 +28,16 @@ function createListFormMarkup(isFilled) { </div> </div> </form> - ` + `; } -export const listCreationForm = createListFormMarkup(false) -export const filledListCreationForm = createListFormMarkup(true) +export const listCreationForm = createListFormMarkup(false); +export const filledListCreationForm = createListFormMarkup(true); -export const showcaseI18nInput = '<input type="hidden" name="list-i18n-strings" value="{"cover_of": "Cover of: ", "see_this_list": "See this list", "remove_from_list": "Remove from your list?", "from": "from", "you": "You"}"></input>' +export const showcaseI18nInput = + '<input type="hidden" name="list-i18n-strings" value="{"cover_of": "Cover of: ", "see_this_list": "See this list", "remove_from_list": "Remove from your list?", "from": "from", "you": "You"}"></input>'; -const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png' +const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; /** * @typedef {Object} ShowcaseDetails @@ -52,13 +53,15 @@ const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png' * @param {Array<ShowcaseDetails>} showcaseData */ function createShowcaseMarkup(isActiveShowcase, showcaseData) { - const listId = isActiveShowcase ? 'already-lists' : 'list-lists' - const listClasses = 'listLists'.concat(isActiveShowcase ? ' already-lists' : '') + const listId = isActiveShowcase ? 'already-lists' : 'list-lists'; + const listClasses = 'listLists'.concat( + isActiveShowcase ? ' already-lists' : '', + ); - let showcaseMarkup = '' + let showcaseMarkup = ''; - for (const data of showcaseData) { - showcaseMarkup += `<li class="actionable-item"> + for (const data of showcaseData) { + showcaseMarkup += `<li class="actionable-item"> <span class="image"> <a href="${data.listKey}"><img src="${DEFAULT_COVER_URL}" alt="Cover of: ${data.listTitle}" title="Cover of: ${data.listTitle}"></a> </span> @@ -74,59 +77,71 @@ function createShowcaseMarkup(isActiveShowcase, showcaseData) { <span class="owner">from <a href="${data.listOwner}">You</a></span> </span> </li> - ` - } + `; + } - return `<ul id="${listId}" class="${listClasses}"> + return `<ul id="${listId}" class="${listClasses}"> ${showcaseMarkup} </ul> - ` + `; } export const showcaseDetailsData = [ - { - listKey: '/people/openlibrary/lists/OL1L', - seedKey: '/works/OL54120W', - listTitle: 'My First List', - listOwner: '/people/openlibrary', - seedType: 'work' - }, - { - listKey: '/people/openlibrary/lists/OL1L', - seedKey: '/books/OL3421846M', - listTitle: 'My First List', - listOwner: '/people/openlibrary', - seedType: 'edition' - }, - { - listKey: '/people/openlibrary/lists/OL2L', - seedKey: '/works/OL54120W', - listTitle: 'Another List', - listOwner: '/people/openlibrary', - seedType: 'work' - }, - { - listKey: '/people/openlibrary/lists/OL1L', - seedKey: '/authors/OL18319A', - listTitle: 'My First List', - listOwner: '/people/openlibrary', - seedType: 'author' - }, - { - listKey: '/people/openlibrary/lists/OL1L', - seedKey: 'quotations', - listTitle: 'My First List', - listOwner: '/people/openlibrary', - seedType: 'subject' - }, -] - -export const multipleShowcasesOnPage = createShowcaseMarkup(true, [showcaseDetailsData[0], showcaseDetailsData[2]]) + createShowcaseMarkup(false, [showcaseDetailsData[0], showcaseDetailsData[1]]) -export const activeListShowcase = createShowcaseMarkup(true, [showcaseDetailsData[0]]) -export const listsSectionShowcase = createShowcaseMarkup(false, [showcaseDetailsData[0]]) -export const subjectShowcase = createShowcaseMarkup(false, [showcaseDetailsData[4]]) -export const authorShowcase = createShowcaseMarkup(false, [showcaseDetailsData[3]]) -export const workShowcase = createShowcaseMarkup(false, [showcaseDetailsData[0]]) -export const editionShowcase = createShowcaseMarkup(false, [showcaseDetailsData[1]]) - + { + listKey: '/people/openlibrary/lists/OL1L', + seedKey: '/works/OL54120W', + listTitle: 'My First List', + listOwner: '/people/openlibrary', + seedType: 'work', + }, + { + listKey: '/people/openlibrary/lists/OL1L', + seedKey: '/books/OL3421846M', + listTitle: 'My First List', + listOwner: '/people/openlibrary', + seedType: 'edition', + }, + { + listKey: '/people/openlibrary/lists/OL2L', + seedKey: '/works/OL54120W', + listTitle: 'Another List', + listOwner: '/people/openlibrary', + seedType: 'work', + }, + { + listKey: '/people/openlibrary/lists/OL1L', + seedKey: '/authors/OL18319A', + listTitle: 'My First List', + listOwner: '/people/openlibrary', + seedType: 'author', + }, + { + listKey: '/people/openlibrary/lists/OL1L', + seedKey: 'quotations', + listTitle: 'My First List', + listOwner: '/people/openlibrary', + seedType: 'subject', + }, +]; +export const multipleShowcasesOnPage = + createShowcaseMarkup(true, [showcaseDetailsData[0], showcaseDetailsData[2]]) + + createShowcaseMarkup(false, [showcaseDetailsData[0], showcaseDetailsData[1]]); +export const activeListShowcase = createShowcaseMarkup(true, [ + showcaseDetailsData[0], +]); +export const listsSectionShowcase = createShowcaseMarkup(false, [ + showcaseDetailsData[0], +]); +export const subjectShowcase = createShowcaseMarkup(false, [ + showcaseDetailsData[4], +]); +export const authorShowcase = createShowcaseMarkup(false, [ + showcaseDetailsData[3], +]); +export const workShowcase = createShowcaseMarkup(false, [ + showcaseDetailsData[0], +]); +export const editionShowcase = createShowcaseMarkup(false, [ + showcaseDetailsData[1], +]); diff --git a/tests/unit/js/sample-html/utils-test-data.js b/tests/unit/js/sample-html/utils-test-data.js index bc49363c86f..8426b2551e0 100644 --- a/tests/unit/js/sample-html/utils-test-data.js +++ b/tests/unit/js/sample-html/utils-test-data.js @@ -1,17 +1,17 @@ // removeChildren() test data: // Single element, no children -export const childlessElem = '<div class="remove-tests"></div>' +export const childlessElem = '<div class="remove-tests"></div>'; // Single element, multiple children export const multiChildElem = `<div class="remove-tests"> <div>Child one</div> <div>Child two</div> -</div>` +</div>`; // Single element, child with children export const elemWithDescendants = `<div class="remove-tests"> <div> <div>Ancestor</div> </div> -</div>` +</div>`; diff --git a/tests/unit/js/search.test.js b/tests/unit/js/search.test.js index b06dc134641..481b0894197 100644 --- a/tests/unit/js/search.test.js +++ b/tests/unit/js/search.test.js @@ -1,4 +1,7 @@ -import { more, less } from '../../../openlibrary/plugins/openlibrary/js/search.js'; +import { + less, + more, +} from '../../../openlibrary/plugins/openlibrary/js/search.js'; /** Creates a dummy search facets section with a list of 'facetEntry' element and a * 'facetMoreLess' section. @@ -8,29 +11,33 @@ import { more, less } from '../../../openlibrary/plugins/openlibrary/js/search.j * @param {Number} minVisibleFacet minimum number of visible facet * @return {String} HTML search facets section */ -function createSearchFacets(totalFacet = 2, visibleFacet = 2, minVisibleFacet = 2) { - const divSearchFacets = document.createElement('DIV'); - divSearchFacets.setAttribute('id', 'searchFacets'); - divSearchFacets.innerHTML = ` +function createSearchFacets( + totalFacet = 2, + visibleFacet = 2, + minVisibleFacet = 2, +) { + const divSearchFacets = document.createElement('DIV'); + divSearchFacets.setAttribute('id', 'searchFacets'); + divSearchFacets.innerHTML = ` <div class="facet test"> <h4 class="facetHead">Facet Label</h4> </div> - ` + `; - const divTestFacet = divSearchFacets.querySelector('div.test'); - for (let i = 0; i < totalFacet; i++) { - const facetNb = i + 1; - divTestFacet.innerHTML += ` + const divTestFacet = divSearchFacets.querySelector('div.test'); + for (let i = 0; i < totalFacet; i++) { + const facetNb = i + 1; + divTestFacet.innerHTML += ` <div class="facetEntry"> <span><a>facet_${facetNb}</a></span> </div> `; - if (i >= visibleFacet) { - divTestFacet.lastElementChild.classList.add('ui-helper-hidden'); - } + if (i >= visibleFacet) { + divTestFacet.lastElementChild.classList.add('ui-helper-hidden'); } + } - divTestFacet.innerHTML += ` + divTestFacet.innerHTML += ` <div class="facetMoreLess"> <span class="header_more small" data-header="test"> <a id="test_more">more</a> @@ -42,16 +49,16 @@ function createSearchFacets(totalFacet = 2, visibleFacet = 2, minVisibleFacet = </div> `; - if (visibleFacet === minVisibleFacet) { - divTestFacet.querySelector('#test_bull').style.display = 'none'; - divTestFacet.querySelector('#test_less').style.display = 'none'; - } - if (visibleFacet === totalFacet) { - divTestFacet.querySelector('#test_more').style.display = 'none'; - divTestFacet.querySelector('#test_bull').style.display = 'none'; - } + if (visibleFacet === minVisibleFacet) { + divTestFacet.querySelector('#test_bull').style.display = 'none'; + divTestFacet.querySelector('#test_less').style.display = 'none'; + } + if (visibleFacet === totalFacet) { + divTestFacet.querySelector('#test_more').style.display = 'none'; + divTestFacet.querySelector('#test_bull').style.display = 'none'; + } - return divSearchFacets.outerHTML; + return divSearchFacets.outerHTML; } /** Runs visibility tests for all 'facetEntry' elements in document. @@ -60,23 +67,27 @@ function createSearchFacets(totalFacet = 2, visibleFacet = 2, minVisibleFacet = * @param {Number} expectedVisibleFacet expected number of visible facet */ function checkFacetVisibility(totalFacet, expectedVisibleFacet) { - const facetEntryList = document.getElementsByClassName('facetEntry'); - - test('facetEntry element number', () => { - expect(facetEntryList).toHaveLength(totalFacet); - }); - - for (let i = 0; i < totalFacet; i++) { - if (i < expectedVisibleFacet) { - test(`element "facet_${i+1}" displayed`, () => { - expect(facetEntryList[i].classList.contains('ui-helper-hidden')).toBe(false); - }); - } else { - test(`element "facet_${i+1}" hidden`, () => { - expect(facetEntryList[i].classList.contains('ui-helper-hidden')).toBe(true); - }); - } + const facetEntryList = document.getElementsByClassName('facetEntry'); + + test('facetEntry element number', () => { + expect(facetEntryList).toHaveLength(totalFacet); + }); + + for (let i = 0; i < totalFacet; i++) { + if (i < expectedVisibleFacet) { + test(`element "facet_${i + 1}" displayed`, () => { + expect(facetEntryList[i].classList.contains('ui-helper-hidden')).toBe( + false, + ); + }); + } else { + test(`element "facet_${i + 1}" hidden`, () => { + expect(facetEntryList[i].classList.contains('ui-helper-hidden')).toBe( + true, + ); + }); } + } } /** Runs visibility tests for 'less', 'bull' and 'more' elements in document @@ -85,105 +96,124 @@ function checkFacetVisibility(totalFacet, expectedVisibleFacet) { * @param {Number} minVisibleFacet minimum visible facet number * @param {Number} expectedVisibleFacet expected number of visible facet */ -function checkFacetMoreLessVisibility(totalFacet, minVisibleFacet, expectedVisibleFacet) { - if (expectedVisibleFacet <= minVisibleFacet) { - test('element "test_more"', () => { - expect(document.getElementById('test_more').style.display).not.toBe('none'); - }); - test('element "test_bull"', () => { - expect(document.getElementById('test_bull').style.display).toBe('none'); - }); - test('element "test_less"', () => { - expect(document.getElementById('test_less').style.display).toBe('none'); - }); - } else if (expectedVisibleFacet >= totalFacet) { - test('element "test_more"', () => { - expect(document.getElementById('test_more').style.display).toBe('none'); - }); - test('element "test_bull"', () => { - expect(document.getElementById('test_bull').style.display).toBe('none'); - }); - test('element "test_less"', () => { - expect(document.getElementById('test_less').style.display).not.toBe('none'); - }); - } else { - test('element "test_more"', () => { - expect(document.getElementById('test_more').style.display).not.toBe('none'); - }); - test('element "test_bull"', () => { - expect(document.getElementById('test_bull').style.display).not.toBe('none'); - }); - test('element "test_less"', () => { - expect(document.getElementById('test_less').style.display).not.toBe('none'); - }); - } +function checkFacetMoreLessVisibility( + totalFacet, + minVisibleFacet, + expectedVisibleFacet, +) { + if (expectedVisibleFacet <= minVisibleFacet) { + test('element "test_more"', () => { + expect(document.getElementById('test_more').style.display).not.toBe( + 'none', + ); + }); + test('element "test_bull"', () => { + expect(document.getElementById('test_bull').style.display).toBe('none'); + }); + test('element "test_less"', () => { + expect(document.getElementById('test_less').style.display).toBe('none'); + }); + } else if (expectedVisibleFacet >= totalFacet) { + test('element "test_more"', () => { + expect(document.getElementById('test_more').style.display).toBe('none'); + }); + test('element "test_bull"', () => { + expect(document.getElementById('test_bull').style.display).toBe('none'); + }); + test('element "test_less"', () => { + expect(document.getElementById('test_less').style.display).not.toBe( + 'none', + ); + }); + } else { + test('element "test_more"', () => { + expect(document.getElementById('test_more').style.display).not.toBe( + 'none', + ); + }); + test('element "test_bull"', () => { + expect(document.getElementById('test_bull').style.display).not.toBe( + 'none', + ); + }); + test('element "test_less"', () => { + expect(document.getElementById('test_less').style.display).not.toBe( + 'none', + ); + }); + } } const _originalGetClientRects = window.Element.prototype.getClientRects; // Stubbed getClientRects to enable jQuery ':hidden' selector used by 'more' and 'less' functions -const _stubbedGetClientRects = function() { - let node = this; - while (node) { - if (node === document) { - break; - } - if (!node.style || node.style.display === 'none' || node.style.visibility === 'hidden' || node.classList.contains('ui-helper-hidden')) { - return []; - } - node = node.parentNode; +const _stubbedGetClientRects = function () { + let node = this; + while (node) { + if (node === document) { + break; + } + if ( + !node.style || + node.style.display === 'none' || + node.style.visibility === 'hidden' || + node.classList.contains('ui-helper-hidden') + ) { + return []; } - return [{width: 1, height: 1}]; + node = node.parentNode; + } + return [{ width: 1, height: 1 }]; }; describe('more', () => { - [ - /*[ totalFacet, minVisibleFacet, facetInc, visibleFacet, expectedVisibleFacet ]*/ - [ 7, 2, 3, 2, 5 ], - [ 9, 2, 3, 5, 8 ], - [ 7, 2, 3, 5, 7 ], - [ 7, 2, 3, 7, 7 ] - ].forEach((test) => { - const label = `Facet setup [total: ${test[0]}, visible: ${test[3]}, min: ${test[1]}]`; - describe(label, () => { - beforeAll(() => { - document.body.innerHTML = createSearchFacets(test[0], test[3], test[1]); - window.Element.prototype.getClientRects = _stubbedGetClientRects; - more('test', test[1], test[2]); - }); - - afterAll(() => { - window.Element.prototype.getClientRects = _originalGetClientRects; - }); - - checkFacetVisibility(test[0], test[4]); - checkFacetMoreLessVisibility(test[0], test[1], test[4]); - }); + [ + /*[ totalFacet, minVisibleFacet, facetInc, visibleFacet, expectedVisibleFacet ]*/ + [7, 2, 3, 2, 5], + [9, 2, 3, 5, 8], + [7, 2, 3, 5, 7], + [7, 2, 3, 7, 7], + ].forEach((test) => { + const label = `Facet setup [total: ${test[0]}, visible: ${test[3]}, min: ${test[1]}]`; + describe(label, () => { + beforeAll(() => { + document.body.innerHTML = createSearchFacets(test[0], test[3], test[1]); + window.Element.prototype.getClientRects = _stubbedGetClientRects; + more('test', test[1], test[2]); + }); + + afterAll(() => { + window.Element.prototype.getClientRects = _originalGetClientRects; + }); + + checkFacetVisibility(test[0], test[4]); + checkFacetMoreLessVisibility(test[0], test[1], test[4]); }); + }); }); describe('less', () => { - [ - /*[ totalFacet, minVisibleFacet, facetInc, visibleFacet, expectedVisibleFacet ]*/ - [ 5, 2, 3, 2, 2 ], - [ 7, 2, 3, 5, 2 ], - [ 9, 2, 3, 8, 5 ], - [ 7, 2, 3, 7, 5 ] - ].forEach((test) => { - const label = `Facet setup [total: ${test[0]}, visible: ${test[3]}, min: ${test[1]}]`; - describe(label, () => { - beforeAll(() => { - document.body.innerHTML = createSearchFacets(test[0], test[3], test[1]); - window.Element.prototype.getClientRects = _stubbedGetClientRects; - less('test', test[1], test[2]); - }); - - afterAll(() => { - window.Element.prototype.getClientRects = _originalGetClientRects; - }); - - checkFacetVisibility(test[0], test[4]); - checkFacetMoreLessVisibility(test[0], test[1], test[4]); - }); + [ + /*[ totalFacet, minVisibleFacet, facetInc, visibleFacet, expectedVisibleFacet ]*/ + [5, 2, 3, 2, 2], + [7, 2, 3, 5, 2], + [9, 2, 3, 8, 5], + [7, 2, 3, 7, 5], + ].forEach((test) => { + const label = `Facet setup [total: ${test[0]}, visible: ${test[3]}, min: ${test[1]}]`; + describe(label, () => { + beforeAll(() => { + document.body.innerHTML = createSearchFacets(test[0], test[3], test[1]); + window.Element.prototype.getClientRects = _stubbedGetClientRects; + less('test', test[1], test[2]); + }); + + afterAll(() => { + window.Element.prototype.getClientRects = _originalGetClientRects; + }); + + checkFacetVisibility(test[0], test[4]); + checkFacetMoreLessVisibility(test[0], test[1], test[4]); }); + }); }); diff --git a/tests/unit/js/service-worker-matchers.test.js b/tests/unit/js/service-worker-matchers.test.js index 93765655ce3..659f65140a3 100644 --- a/tests/unit/js/service-worker-matchers.test.js +++ b/tests/unit/js/service-worker-matchers.test.js @@ -1,79 +1,169 @@ -import { matchMiscFiles, matchSmallMediumCovers, matchLargeCovers, matchStaticImages, matchStaticBuild, matchArchiveOrgImage } from '../../../openlibrary/plugins/openlibrary/js/service-worker-matchers'; - +import { + matchArchiveOrgImage, + matchLargeCovers, + matchMiscFiles, + matchSmallMediumCovers, + matchStaticBuild, + matchStaticImages, +} from '../../../openlibrary/plugins/openlibrary/js/service-worker-matchers'; // Helper function to create a URL object function _u(url) { - return { url: new URL(url) } + return { url: new URL(url) }; } // Group related tests together describe('URL Matchers', () => { - describe('matchMiscFiles', () => { - test('matches miscellaneous files', () => { - expect(matchMiscFiles(_u('https://openlibrary.org/favicon.ico'))).toBe(true); - expect(matchMiscFiles(_u('https://openlibrary.org/static/manifest.json'))).toBe(true); - }); + describe('matchMiscFiles', () => { + test('matches miscellaneous files', () => { + expect(matchMiscFiles(_u('https://openlibrary.org/favicon.ico'))).toBe( + true, + ); + expect( + matchMiscFiles(_u('https://openlibrary.org/static/manifest.json')), + ).toBe(true); + }); - test('does not match homepage', () => { - expect(matchMiscFiles(_u('https://openlibrary.org/'))).toBe(false); - }); + test('does not match homepage', () => { + expect(matchMiscFiles(_u('https://openlibrary.org/'))).toBe(false); }); + }); - describe('matchSmallMediumCovers', () => { - test('matches small and medium cover sizes', () => { - expect(matchSmallMediumCovers(_u('https://covers.openlibrary.org/a/id/6257045-M.jpg'))).toBe(true); - expect(matchSmallMediumCovers(_u('https://covers.openlibrary.org/a/id/6257045-S.jpg'))).toBe(true); - expect(matchSmallMediumCovers(_u('https://covers.openlibrary.org/b/id/1852327-M.jpg'))).toBe(true); - expect(matchSmallMediumCovers(_u('https://covers.openlibrary.org/a/olid/OL2838765A-M.jpg'))).toBe(true); - }); + describe('matchSmallMediumCovers', () => { + test('matches small and medium cover sizes', () => { + expect( + matchSmallMediumCovers( + _u('https://covers.openlibrary.org/a/id/6257045-M.jpg'), + ), + ).toBe(true); + expect( + matchSmallMediumCovers( + _u('https://covers.openlibrary.org/a/id/6257045-S.jpg'), + ), + ).toBe(true); + expect( + matchSmallMediumCovers( + _u('https://covers.openlibrary.org/b/id/1852327-M.jpg'), + ), + ).toBe(true); + expect( + matchSmallMediumCovers( + _u('https://covers.openlibrary.org/a/olid/OL2838765A-M.jpg'), + ), + ).toBe(true); + }); - test('does not match large covers', () => { - expect(matchSmallMediumCovers(_u('https://covers.openlibrary.org/a/id/6257045-L.jpg'))).toBe(false); - }); + test('does not match large covers', () => { + expect( + matchSmallMediumCovers( + _u('https://covers.openlibrary.org/a/id/6257045-L.jpg'), + ), + ).toBe(false); }); + }); - describe('matchLargeCovers', () => { - test('matches large cover sizes', () => { - expect(matchLargeCovers(_u('https://covers.openlibrary.org/a/id/6257045-L.jpg'))).toBe(true); - }); + describe('matchLargeCovers', () => { + test('matches large cover sizes', () => { + expect( + matchLargeCovers( + _u('https://covers.openlibrary.org/a/id/6257045-L.jpg'), + ), + ).toBe(true); + }); - test('does not match small or medium covers', () => { - expect(matchLargeCovers(_u('https://covers.openlibrary.org/a/id/6257045-S.jpg'))).toBe(false); - expect(matchLargeCovers(_u('https://covers.openlibrary.org/a/id/6257045-M.jpg'))).toBe(false); - expect(matchLargeCovers(_u('https://covers.openlibrary.org/a/id/6257045.jpg'))).toBe(false); - }); + test('does not match small or medium covers', () => { + expect( + matchLargeCovers( + _u('https://covers.openlibrary.org/a/id/6257045-S.jpg'), + ), + ).toBe(false); + expect( + matchLargeCovers( + _u('https://covers.openlibrary.org/a/id/6257045-M.jpg'), + ), + ).toBe(false); + expect( + matchLargeCovers(_u('https://covers.openlibrary.org/a/id/6257045.jpg')), + ).toBe(false); }); + }); - describe('matchStaticImages', () => { - test('matches static images', () => { - expect(matchStaticImages(_u('https://openlibrary.org/static/images/down-arrow.png'))).toBe(true); - expect(matchStaticImages(_u('https://testing.openlibrary.org/static/images/icons/barcode_scanner.svg'))).toBe(true); - }); + describe('matchStaticImages', () => { + test('matches static images', () => { + expect( + matchStaticImages( + _u('https://openlibrary.org/static/images/down-arrow.png'), + ), + ).toBe(true); + expect( + matchStaticImages( + _u( + 'https://testing.openlibrary.org/static/images/icons/barcode_scanner.svg', + ), + ), + ).toBe(true); + }); - test('does not match other URLs', () => { - expect(matchStaticImages(_u('https://openlibrary.org/static/build/js/4290.a0ae80aacde14696d322.js'))).toBe(false); - expect(matchStaticImages(_u('https://covers.openlibrary.org/w/id/14348537-L.jpg'))).toBe(false); - }); + test('does not match other URLs', () => { + expect( + matchStaticImages( + _u( + 'https://openlibrary.org/static/build/js/4290.a0ae80aacde14696d322.js', + ), + ), + ).toBe(false); + expect( + matchStaticImages( + _u('https://covers.openlibrary.org/w/id/14348537-L.jpg'), + ), + ).toBe(false); }); + }); - describe('matchStaticBuild', () => { - test('matches static build files', () => { - expect(matchStaticBuild(_u('https://openlibrary.org/static/build/js/4290.a0ae80aacde14696d322.js'))).toBe(true); - expect(matchStaticBuild(_u('https://testing.openlibrary.org/static/build/css/page-book.css?v=097b69dc350c972d96da0c70cebe7b75'))).toBe(true); - }); + describe('matchStaticBuild', () => { + test('matches static build files', () => { + expect( + matchStaticBuild( + _u( + 'https://openlibrary.org/static/build/js/4290.a0ae80aacde14696d322.js', + ), + ), + ).toBe(true); + expect( + matchStaticBuild( + _u( + 'https://testing.openlibrary.org/static/build/css/page-book.css?v=097b69dc350c972d96da0c70cebe7b75', + ), + ), + ).toBe(true); + }); - test('does not match localhost URLs', () => { - expect(matchStaticBuild(_u('http://localhost:8080/static/build/js/4290.a0ae80aacde14696d322.js'))).toBe(false); - }); + test('does not match localhost URLs', () => { + expect( + matchStaticBuild( + _u( + 'http://localhost:8080/static/build/js/4290.a0ae80aacde14696d322.js', + ), + ), + ).toBe(false); }); + }); - describe('matchArchiveOrgImage', () => { - test('matches archive.org images', () => { - expect(matchArchiveOrgImage(_u('https://archive.org/services/img/@raybb'))).toBe(true); - expect(matchArchiveOrgImage(_u('https://archive.org/services/img/courtofmistfury0000maas'))).toBe(true); - }); + describe('matchArchiveOrgImage', () => { + test('matches archive.org images', () => { + expect( + matchArchiveOrgImage(_u('https://archive.org/services/img/@raybb')), + ).toBe(true); + expect( + matchArchiveOrgImage( + _u('https://archive.org/services/img/courtofmistfury0000maas'), + ), + ).toBe(true); + }); - test('does not match other URLs', () => { - expect(matchArchiveOrgImage(_u('https://archive.org/services/'))).toBe(false); - }); + test('does not match other URLs', () => { + expect(matchArchiveOrgImage(_u('https://archive.org/services/'))).toBe( + false, + ); }); + }); }); diff --git a/tests/unit/js/setup.js b/tests/unit/js/setup.js index 58000d42630..058bb960716 100644 --- a/tests/unit/js/setup.js +++ b/tests/unit/js/setup.js @@ -2,10 +2,11 @@ // Make jQuery available globally for tests import $ from 'jquery'; + window.jQuery = $; window.$ = $; // Improve error reporting for unhandled promise rejections process.on('unhandledRejection', (error) => { - throw error; + throw error; }); diff --git a/tests/unit/js/signup.test.js b/tests/unit/js/signup.test.js index de61b41b07e..6d358719b4f 100644 --- a/tests/unit/js/signup.test.js +++ b/tests/unit/js/signup.test.js @@ -1,7 +1,7 @@ import { initSignupForm } from '../../../openlibrary/plugins/openlibrary/js/signup'; beforeEach(() => { - document.body.innerHTML = ` + document.body.innerHTML = ` <form id="signup" name="signup" data-i18n={}> <label for="emailAddr">Email</label> <div id="emailAddrMessage" class="ol-signup-form__error"></div> @@ -26,218 +26,217 @@ beforeEach(() => { }); describe('Email tests', () => { - let emailLabel, emailField + let emailLabel, emailField; - beforeEach(() => { - // call the function - initSignupForm(); + beforeEach(() => { + // call the function + initSignupForm(); - //declare the elements - emailLabel = document.querySelector('label[for="emailAddr"]'); - emailField = document.getElementById('emailAddr'); - }) + //declare the elements + emailLabel = document.querySelector('label[for="emailAddr"]'); + emailField = document.getElementById('emailAddr'); + }); - test('validateEmail should update elements correctly on success', () => { - // set the email value - emailField.value = 'testemail@archive.org'; + test('validateEmail should update elements correctly on success', () => { + // set the email value + emailField.value = 'testemail@archive.org'; - // Trigger the blur event on the email field - emailField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the email field + emailField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(emailField.classList.contains('invalid')).toBe(false); - expect(emailLabel.classList.contains('invalid')).toBe(false); - }); + // Assert that the elements have the expected classes + expect(emailField.classList.contains('invalid')).toBe(false); + expect(emailLabel.classList.contains('invalid')).toBe(false); + }); - test('validateEmail should update elements correctly for empty fields', () => { - // set the email value - emailField.value = ''; + test('validateEmail should update elements correctly for empty fields', () => { + // set the email value + emailField.value = ''; - // Trigger the blur event on the email field - emailField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the email field + emailField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(emailField.classList.contains('invalid')).toBe(false); - expect(emailLabel.classList.contains('invalid')).toBe(false); - }); + // Assert that the elements have the expected classes + expect(emailField.classList.contains('invalid')).toBe(false); + expect(emailLabel.classList.contains('invalid')).toBe(false); + }); - test('validateEmail should update elements correctly for emails with plus signs', () => { - // set the email value - emailField.value = 'testemail+01@archive.org'; + test('validateEmail should update elements correctly for emails with plus signs', () => { + // set the email value + emailField.value = 'testemail+01@archive.org'; - // Trigger the blur event on the email field - emailField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the email field + emailField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(emailField.classList.contains('invalid')).toBe(false); - expect(emailLabel.classList.contains('invalid')).toBe(false); - }); + // Assert that the elements have the expected classes + expect(emailField.classList.contains('invalid')).toBe(false); + expect(emailLabel.classList.contains('invalid')).toBe(false); + }); - test('validateEmail should update elements correctly for emails with no punctuation', () => { - // set the password values - emailField.value = 'testemail'; + test('validateEmail should update elements correctly for emails with no punctuation', () => { + // set the password values + emailField.value = 'testemail'; - // Trigger the blur event on the email fields - emailField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the email fields + emailField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(emailField.classList.contains('invalid')).toBe(true); - expect(emailLabel.classList.contains('invalid')).toBe(true); - }); + // Assert that the elements have the expected classes + expect(emailField.classList.contains('invalid')).toBe(true); + expect(emailLabel.classList.contains('invalid')).toBe(true); + }); - test('validateEmail should update elements correctly for emails with invalid punctuation', () => { - // set the email values - emailField.value = 'testemail@archive-org'; + test('validateEmail should update elements correctly for emails with invalid punctuation', () => { + // set the email values + emailField.value = 'testemail@archive-org'; - // Trigger the blur event on the email fields - emailField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the email fields + emailField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(emailField.classList.contains('invalid')).toBe(true); - expect(emailLabel.classList.contains('invalid')).toBe(true); - }); + // Assert that the elements have the expected classes + expect(emailField.classList.contains('invalid')).toBe(true); + expect(emailLabel.classList.contains('invalid')).toBe(true); + }); }); describe('Username tests', () => { - let usernameLabel, usernameField + let usernameLabel, usernameField; - beforeEach(() => { - // call the function - initSignupForm(); + beforeEach(() => { + // call the function + initSignupForm(); - //declare the elements - usernameLabel = document.querySelector('label[for="username"]'); - usernameField = document.getElementById('username'); - }) + //declare the elements + usernameLabel = document.querySelector('label[for="username"]'); + usernameField = document.getElementById('username'); + }); - test('validateUsername should update elements correctly on success', () => { - // set the username value - usernameField.value = 'username123'; + test('validateUsername should update elements correctly on success', () => { + // set the username value + usernameField.value = 'username123'; - // Trigger the blur event on the username field - usernameField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the username field + usernameField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(usernameField.classList.contains('invalid')).toBe(false); - expect(usernameLabel.classList.contains('invalid')).toBe(false); - }); + // Assert that the elements have the expected classes + expect(usernameField.classList.contains('invalid')).toBe(false); + expect(usernameLabel.classList.contains('invalid')).toBe(false); + }); - test('validateUsername should update elements correctly for empty fields', () => { - // set the username value - usernameField.value = ''; + test('validateUsername should update elements correctly for empty fields', () => { + // set the username value + usernameField.value = ''; - // Trigger the blur event on the username field - usernameField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the username field + usernameField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(usernameField.classList.contains('invalid')).toBe(false); - expect(usernameLabel.classList.contains('invalid')).toBe(false); - }); + // Assert that the elements have the expected classes + expect(usernameField.classList.contains('invalid')).toBe(false); + expect(usernameLabel.classList.contains('invalid')).toBe(false); + }); - test('validateUsername should update elements correctly for usernames over 20 chars', () => { - // set the username values - usernameField.value = 'username1234567891011'; + test('validateUsername should update elements correctly for usernames over 20 chars', () => { + // set the username values + usernameField.value = 'username1234567891011'; - // Trigger the blur event on the username fields - usernameField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the username fields + usernameField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(usernameField.classList.contains('invalid')).toBe(true); - expect(usernameLabel.classList.contains('invalid')).toBe(true); - }); + // Assert that the elements have the expected classes + expect(usernameField.classList.contains('invalid')).toBe(true); + expect(usernameLabel.classList.contains('invalid')).toBe(true); + }); - test('validateusername should update elements correctly for usernames under 3 chars', () => { - // set the username values - usernameField.value = 'us'; + test('validateusername should update elements correctly for usernames under 3 chars', () => { + // set the username values + usernameField.value = 'us'; - // Trigger the blur event on the username fields - usernameField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the username fields + usernameField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(usernameField.classList.contains('invalid')).toBe(true); - expect(usernameLabel.classList.contains('invalid')).toBe(true); - }); + // Assert that the elements have the expected classes + expect(usernameField.classList.contains('invalid')).toBe(true); + expect(usernameLabel.classList.contains('invalid')).toBe(true); + }); }); - describe('Password tests', () => { - let passwordLabel, passwordField + let passwordLabel, passwordField; - beforeEach(() => { - // call the function - initSignupForm(); + beforeEach(() => { + // call the function + initSignupForm(); - //declare the elements - passwordLabel = document.querySelector('label[for="password"]'); - passwordField = document.getElementById('password'); - }) + //declare the elements + passwordLabel = document.querySelector('label[for="password"]'); + passwordField = document.getElementById('password'); + }); - test('validatePassword should update elements correctly on success', () => { - // set the password value - passwordField.value = 'password123'; + test('validatePassword should update elements correctly on success', () => { + // set the password value + passwordField.value = 'password123'; - // Trigger the blur event on the password field - passwordField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the password field + passwordField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(passwordField.classList.contains('invalid')).toBe(false); - expect(passwordLabel.classList.contains('invalid')).toBe(false); - }); + // Assert that the elements have the expected classes + expect(passwordField.classList.contains('invalid')).toBe(false); + expect(passwordLabel.classList.contains('invalid')).toBe(false); + }); - test('validatePassword should update elements correctly for empty fields', () => { - // set the password value - passwordField.value = ''; + test('validatePassword should update elements correctly for empty fields', () => { + // set the password value + passwordField.value = ''; - // Trigger the blur event on the password field - passwordField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the password field + passwordField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(passwordField.classList.contains('invalid')).toBe(false); - expect(passwordLabel.classList.contains('invalid')).toBe(false); - }); + // Assert that the elements have the expected classes + expect(passwordField.classList.contains('invalid')).toBe(false); + expect(passwordLabel.classList.contains('invalid')).toBe(false); + }); - test('validatePassword should update elements correctly for passwords over 20 chars', () => { - // set the password values - passwordField.value = 'password1234567891011'; + test('validatePassword should update elements correctly for passwords over 20 chars', () => { + // set the password values + passwordField.value = 'password1234567891011'; - // Trigger the blur event on the password fields - passwordField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the password fields + passwordField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(passwordField.classList.contains('invalid')).toBe(true); - expect(passwordLabel.classList.contains('invalid')).toBe(true); - }); + // Assert that the elements have the expected classes + expect(passwordField.classList.contains('invalid')).toBe(true); + expect(passwordLabel.classList.contains('invalid')).toBe(true); + }); - test('validatePassword should update elements correctly for passwords under 3 chars', () => { - // set the password values - passwordField.value = 'pa'; + test('validatePassword should update elements correctly for passwords under 3 chars', () => { + // set the password values + passwordField.value = 'pa'; - // Trigger the blur event on the password fields - passwordField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the password fields + passwordField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(passwordField.classList.contains('invalid')).toBe(true); - expect(passwordLabel.classList.contains('invalid')).toBe(true); - }); + // Assert that the elements have the expected classes + expect(passwordField.classList.contains('invalid')).toBe(true); + expect(passwordLabel.classList.contains('invalid')).toBe(true); + }); }); describe('Print disability tests', () => { - let checkbox, selector; + let checkbox, selector; - beforeEach(() => { - initSignupForm(); + beforeEach(() => { + initSignupForm(); - checkbox = document.querySelector('#pd-request'); - selector = document.querySelector('#pda-selector') - }) + checkbox = document.querySelector('#pd-request'); + selector = document.querySelector('#pda-selector'); + }); - test('Qualifying authority selector only visible when PD checkbox is checked', () => { - checkbox.checked = false - checkbox.dispatchEvent(new Event('change', { bubbles: true })); - expect(selector.classList.contains('hidden')).toBe(true); + test('Qualifying authority selector only visible when PD checkbox is checked', () => { + checkbox.checked = false; + checkbox.dispatchEvent(new Event('change', { bubbles: true })); + expect(selector.classList.contains('hidden')).toBe(true); - checkbox.checked = true - checkbox.dispatchEvent(new Event('change', { bubbles: true })); - expect(selector.classList.contains('hidden')).toBe(false); - }) -}) + checkbox.checked = true; + checkbox.dispatchEvent(new Event('change', { bubbles: true })); + expect(selector.classList.contains('hidden')).toBe(false); + }); +}); diff --git a/tests/unit/js/utils.test.js b/tests/unit/js/utils.test.js index b9205bfdfd4..58c14739f67 100644 --- a/tests/unit/js/utils.test.js +++ b/tests/unit/js/utils.test.js @@ -1,65 +1,69 @@ -import { childlessElem, multiChildElem, elemWithDescendants } from './sample-html/utils-test-data'; import { removeChildren } from '../../../openlibrary/plugins/openlibrary/js/utils'; +import { + childlessElem, + elemWithDescendants, + multiChildElem, +} from './sample-html/utils-test-data'; describe('`removeChildren()` tests', () => { - it('changes nothing if element has no children', () => { - document.body.innerHTML = childlessElem - const elem = document.querySelector('.remove-tests') - const clonedElem = elem.cloneNode(true) + it('changes nothing if element has no children', () => { + document.body.innerHTML = childlessElem; + const elem = document.querySelector('.remove-tests'); + const clonedElem = elem.cloneNode(true); - // Initial checks - expect(elem.childElementCount).toBe(0) - expect(elem.isEqualNode(clonedElem)).toBe(true) + // Initial checks + expect(elem.childElementCount).toBe(0); + expect(elem.isEqualNode(clonedElem)).toBe(true); - // Element should be unchanged after function call - removeChildren(elem) - expect(elem.childElementCount).toBe(0) - expect(elem.isEqualNode(clonedElem)).toBe(true) - }) + // Element should be unchanged after function call + removeChildren(elem); + expect(elem.childElementCount).toBe(0); + expect(elem.isEqualNode(clonedElem)).toBe(true); + }); - it('removes all of an element\'s children', () => { - document.body.innerHTML = multiChildElem - const elem = document.querySelector('.remove-tests') - const clonedElem = elem.cloneNode(true) + it("removes all of an element's children", () => { + document.body.innerHTML = multiChildElem; + const elem = document.querySelector('.remove-tests'); + const clonedElem = elem.cloneNode(true); - // Initial checks - expect(elem.childElementCount).toBe(2) - expect(elem.isEqualNode(clonedElem)).toBe(true) + // Initial checks + expect(elem.childElementCount).toBe(2); + expect(elem.isEqualNode(clonedElem)).toBe(true); - // After removing children - removeChildren(elem) - expect(elem.childElementCount).toBe(0) - expect(elem.isEqualNode(clonedElem)).toBe(false) - }) + // After removing children + removeChildren(elem); + expect(elem.childElementCount).toBe(0); + expect(elem.isEqualNode(clonedElem)).toBe(false); + }); - it('removes children if they have children of their own', () => { - document.body.innerHTML = elemWithDescendants - const elem = document.querySelector('.remove-tests') - const clonedElem = elem.cloneNode(true) + it('removes children if they have children of their own', () => { + document.body.innerHTML = elemWithDescendants; + const elem = document.querySelector('.remove-tests'); + const clonedElem = elem.cloneNode(true); - // Inital checks - expect(elem.childElementCount).toBe(1) - expect(elem.children[0].childElementCount).toBe(1) - expect(elem.isEqualNode(clonedElem)).toBe(true) + // Inital checks + expect(elem.childElementCount).toBe(1); + expect(elem.children[0].childElementCount).toBe(1); + expect(elem.isEqualNode(clonedElem)).toBe(true); - // After removing children - removeChildren(elem) - expect(elem.childElementCount).toBe(0) - expect(elem.isEqualNode(clonedElem)).toBe(false) - }) + // After removing children + removeChildren(elem); + expect(elem.childElementCount).toBe(0); + expect(elem.isEqualNode(clonedElem)).toBe(false); + }); - it('handles multiple parameters correctly', () => { - document.body.innerHTML = elemWithDescendants + multiChildElem - const elems = document.querySelectorAll('.remove-tests') + it('handles multiple parameters correctly', () => { + document.body.innerHTML = elemWithDescendants + multiChildElem; + const elems = document.querySelectorAll('.remove-tests'); - // Initial checks: - expect(elems.length).toBe(2) - expect(elems[0].childElementCount).toBe(1) - expect(elems[1].childElementCount).toBe(2) + // Initial checks: + expect(elems.length).toBe(2); + expect(elems[0].childElementCount).toBe(1); + expect(elems[1].childElementCount).toBe(2); - // After removing children: - removeChildren(...elems) - expect(elems[0].childElementCount).toBe(0) - expect(elems[1].childElementCount).toBe(0) - }) -}) + // After removing children: + removeChildren(...elems); + expect(elems[0].childElementCount).toBe(0); + expect(elems[1].childElementCount).toBe(0); + }); +}); diff --git a/vue.config.js b/vue.config.js index c7db816a370..55f6e0dd791 100644 --- a/vue.config.js +++ b/vue.config.js @@ -1,5 +1,5 @@ /* eslint-env node, es6 */ module.exports = { - lintOnSave: false, - publicPath: '/static/components/' + lintOnSave: false, + publicPath: '/static/components/', }; diff --git a/webpack.config.css.js b/webpack.config.css.js index 8f8dceb976c..cdc2e23f4af 100644 --- a/webpack.config.css.js +++ b/webpack.config.css.js @@ -12,82 +12,90 @@ const path = require('path'); const glob = require('glob'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); -const distDir = path.resolve(__dirname, process.env.BUILD_DIR || 'static/build/css'); +const distDir = path.resolve( + __dirname, + process.env.BUILD_DIR || 'static/build/css', +); // Find all CSS entry files matching static/css/page-*.css const cssFiles = glob.sync('./static/css/page-*.css'); const entries = { - // Design tokens — compiled from static/css/tokens/ into a single file - tokens: './static/css/tokens.css', + // Design tokens — compiled from static/css/tokens/ into a single file + tokens: './static/css/tokens.css', }; -cssFiles.forEach(file => { - const name = path.basename(file, '.css'); - entries[name] = file; +cssFiles.forEach((file) => { + const name = path.basename(file, '.css'); + entries[name] = file; }); module.exports = { - context: __dirname, - entry: entries, - output: { - path: distDir, - // Output only CSS, JS is not needed - filename: '[name].css.js', // dummy, CSS will be extracted - clean: true, - }, - module: { - rules: [ - { - test: /\.css$/, - use: [ - MiniCssExtractPlugin.loader, - { - loader: 'css-loader', - options: { - url: false, - import: true // Enable @import resolution - } - } - ] - } - ] - }, - plugins: [ - new MiniCssExtractPlugin({ - filename: '[name].css', - }), - // Inline plugin to remove intermediary JS assets - { - apply: (compiler) => { - compiler.hooks.thisCompilation.tap('RemoveJSAssetsPlugin', (compilation) => { - compilation.hooks.processAssets.tap( - { - name: 'RemoveJSAssetsPlugin', - stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, - }, - (assets) => { - Object.keys(assets) - .filter((asset) => asset.endsWith('.js')) - .forEach((asset) => { - compilation.deleteAsset(asset); - }); - } - ); - }); - } - } - ], - optimization: { - minimizer: [ - new CssMinimizerPlugin(), + context: __dirname, + entry: entries, + output: { + path: distDir, + // Output only CSS, JS is not needed + filename: '[name].css.js', // dummy, CSS will be extracted + clean: true, + }, + module: { + rules: [ + { + test: /\.css$/, + use: [ + MiniCssExtractPlugin.loader, + { + loader: 'css-loader', + options: { + url: false, + import: true, // Enable @import resolution + }, + }, ], - runtimeChunk: false, - splitChunks: false, + }, + ], + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: '[name].css', + }), + // Inline plugin to remove intermediary JS assets + { + apply: (compiler) => { + compiler.hooks.thisCompilation.tap( + 'RemoveJSAssetsPlugin', + (compilation) => { + compilation.hooks.processAssets.tap( + { + name: 'RemoveJSAssetsPlugin', + stage: + compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, + }, + (assets) => { + Object.keys(assets) + .filter((asset) => asset.endsWith('.js')) + .forEach((asset) => { + compilation.deleteAsset(asset); + }); + }, + ); + }, + ); + }, }, - // Useful for developing in docker/windows, which doesn't support file watchers - watchOptions: process.env.FORCE_POLLING === 'true' ? { - poll: 1000, // Check for changes every second - aggregateTimeout: 300, // Delay before rebuilding - ignored: /node_modules/ - } : undefined, + ], + optimization: { + minimizer: [new CssMinimizerPlugin()], + runtimeChunk: false, + splitChunks: false, + }, + // Useful for developing in docker/windows, which doesn't support file watchers + watchOptions: + process.env.FORCE_POLLING === 'true' + ? { + poll: 1000, // Check for changes every second + aggregateTimeout: 300, // Delay before rebuilding + ignored: /node_modules/, + } + : undefined, }; diff --git a/webpack.config.js b/webpack.config.js index db7a8b565d4..903f1218348 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,111 +1,115 @@ /* eslint-env node, es6 */ // https://webpack.js.org/configuration -const - webpack = require('webpack'), - path = require('path'), - prod = process.env.NODE_ENV === 'production', - // The output directory for all build artifacts. Only absolute paths are accepted by - // output.path. - distDir = path.resolve(__dirname, process.env.BUILD_DIR || 'static/build/js'); +const webpack = require('webpack'), + path = require('path'), + prod = process.env.NODE_ENV === 'production', + // The output directory for all build artifacts. Only absolute paths are accepted by + // output.path. + distDir = path.resolve(__dirname, process.env.BUILD_DIR || 'static/build/js'); module.exports = { - // Fail on the first build error instead of tolerating it for prod builds. This seems to - // correspond to optimization.noEmitOnErrors. - bail: prod, + // Fail on the first build error instead of tolerating it for prod builds. This seems to + // correspond to optimization.noEmitOnErrors. + bail: prod, - // Specify that all paths are relative the Webpack configuration directory not the current - // working directory. - context: __dirname, + // Specify that all paths are relative the Webpack configuration directory not the current + // working directory. + context: __dirname, - // A map of ResourceLoader module / entry chunk names to JavaScript files to pack. - entry: { - all: './openlibrary/plugins/openlibrary/js/index.js', - partnerLib: './openlibrary/plugins/openlibrary/js/partner_ol_lib.js', - sw: './openlibrary/plugins/openlibrary/js/service-worker.js', - }, - - resolve: { - alias: {} - }, - plugins: [ - new webpack.ProvidePlugin({ - $: 'jquery', - jQuery: 'jquery' - }), - ], - module: { - rules: [{ - test: /\.js$/, - exclude: /node_modules/, - use: { - loader: 'babel-loader', - options: { - // Beware of https://github.com/babel/babel-loader/issues/690. - // Changes to browsers require manual invalidation. - cacheDirectory: true - } - } - }, { - test: /\.css$/, - use: [ - { - loader: 'style-loader' - }, - { - loader: 'css-loader', - options: { - url: false - } - } - ] - }] - }, - optimization: { - splitChunks: { - cacheGroups: { - // Turn off webpack's default 'vendors' cache group. If this is desired - // later on, we can explicitly turn this on for clarity. - // https://webpack.js.org/plugins/split-chunks-plugin/#optimization-splitchunks - vendors: false, + // A map of ResourceLoader module / entry chunk names to JavaScript files to pack. + entry: { + all: './openlibrary/plugins/openlibrary/js/index.js', + partnerLib: './openlibrary/plugins/openlibrary/js/partner_ol_lib.js', + sw: './openlibrary/plugins/openlibrary/js/service-worker.js', + }, - } + resolve: { + alias: {}, + }, + plugins: [ + new webpack.ProvidePlugin({ + $: 'jquery', + jQuery: 'jquery', + }), + ], + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + // Beware of https://github.com/babel/babel-loader/issues/690. + // Changes to browsers require manual invalidation. + cacheDirectory: true, + }, }, - // Don't produce production output when a build error occurs. - emitOnErrors: !prod + }, + { + test: /\.css$/, + use: [ + { + loader: 'style-loader', + }, + { + loader: 'css-loader', + options: { + url: false, + }, + }, + ], + }, + ], + }, + optimization: { + splitChunks: { + cacheGroups: { + // Turn off webpack's default 'vendors' cache group. If this is desired + // later on, we can explicitly turn this on for clarity. + // https://webpack.js.org/plugins/split-chunks-plugin/#optimization-splitchunks + vendors: false, + }, }, + // Don't produce production output when a build error occurs. + emitOnErrors: !prod, + }, - output: { - // Specify the destination of all build products. - path: distDir, - // base path for build products when referenced from production - // (see https://webpack.js.org/guides/public-path/) - publicPath: '/static/build/js/', + output: { + // Specify the destination of all build products. + path: distDir, + // base path for build products when referenced from production + // (see https://webpack.js.org/guides/public-path/) + publicPath: '/static/build/js/', - // Store outputs per module in files named after the modules. For the JavaScript entry - // itself, append .js to each ResourceLoader module entry name. - filename: '[name].js', + // Store outputs per module in files named after the modules. For the JavaScript entry + // itself, append .js to each ResourceLoader module entry name. + filename: '[name].js', - // This option determines the name of **non-entry** chunk files. - chunkFilename: '[name].[contenthash].js', - }, + // This option determines the name of **non-entry** chunk files. + chunkFilename: '[name].[contenthash].js', + }, - // Accurate source maps at the expense of build time. - // The source map is intentionally exposed - // to users via sourceMapFilename for prod debugging. - devtool: 'source-map', - mode: prod ? 'production' : 'development', + // Accurate source maps at the expense of build time. + // The source map is intentionally exposed + // to users via sourceMapFilename for prod debugging. + devtool: 'source-map', + mode: prod ? 'production' : 'development', - performance: { - maxAssetSize: 703 * 1024, - maxEntrypointSize: 703 * 1024, - // Size violations for prod builds fail; development builds are unchecked. - hints: prod ? 'error' : false - }, + performance: { + maxAssetSize: 703 * 1024, + maxEntrypointSize: 703 * 1024, + // Size violations for prod builds fail; development builds are unchecked. + hints: prod ? 'error' : false, + }, - // Useful for developing in docker/windows, which doesn't support file watchers - watchOptions: process.env.FORCE_POLLING === 'true' ? { - poll: 1000, // Check for changes every second - aggregateTimeout: 300, // Delay before rebuilding - ignored: /node_modules/ - } : undefined, + // Useful for developing in docker/windows, which doesn't support file watchers + watchOptions: + process.env.FORCE_POLLING === 'true' + ? { + poll: 1000, // Check for changes every second + aggregateTimeout: 300, // Delay before rebuilding + ignored: /node_modules/, + } + : undefined, }; From 10ad5c52be96ed188ac57d5212ea5c93d06bb5d1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 05:30:28 +0000 Subject: [PATCH 02/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- openlibrary/plugins/openlibrary/js/Browser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlibrary/plugins/openlibrary/js/Browser.js b/openlibrary/plugins/openlibrary/js/Browser.js index 41d464b914d..7a615de63cf 100644 --- a/openlibrary/plugins/openlibrary/js/Browser.js +++ b/openlibrary/plugins/openlibrary/js/Browser.js @@ -32,7 +32,7 @@ export function removeURLParameter(url, parameter) { params = query.split(/[&;]/g); //reverse iteration as may be destructive - for (i = params.length; i-- > 0; ) { + for (i = params.length; i-- > 0;) { //idiom for string.startsWith if (params[i].lastIndexOf(paramPrefix, 0) !== -1) { params.splice(i, 1); From fe3f9b9510e88ca407c7ee8d9b1d54272f55ef0b Mon Sep 17 00:00:00 2001 From: harshgupta2125 <harsh2125gupta@gmail.com> Date: Mon, 6 Apr 2026 11:15:48 +0530 Subject: [PATCH 03/15] Fix: Address Copilot AI review feedback - Updated ESLint glob patterns to properly target subdirectories. - Reverted Object.hasOwn to Object.prototype.hasOwnProperty for browser compatibility. - Fixed jQuery 'this' context in carousel arrow function. - Fixed tagType typo in bulk-tagger. - Resolved empty catch blocks and unsafe href attributes. --- .eslintrc.json | 2 +- .../plugins/openlibrary/js/bulk-tagger/models/Tag.js | 2 +- openlibrary/plugins/openlibrary/js/carousel/index.js | 2 +- openlibrary/plugins/openlibrary/js/covers.js | 4 +++- openlibrary/plugins/openlibrary/js/dialog.js | 7 +++++-- openlibrary/plugins/openlibrary/js/service-worker-init.js | 1 - openlibrary/plugins/openlibrary/js/utils.js | 2 +- 7 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 807d8b11fc0..3544d8ca375 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -48,7 +48,7 @@ }, "overrides": [ { - "files": ["*.js", "*.mjs", "*.cjs"], + "files": ["**/*.js", "**/*.mjs", "**/*.cjs"], "rules": { "semi": "off", "quotes": "off", diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js index 820527a0f46..5eb561450cf 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js @@ -44,7 +44,7 @@ export function compare(tagA, tagB) { } else { if (lowerA.tagType < lowerB.tagType) { return -1; - } else if (lowerA.tagType > lowerB.tagtype) { + } else if (lowerA.tagType > lowerB.tagType) { return 1; } } diff --git a/openlibrary/plugins/openlibrary/js/carousel/index.js b/openlibrary/plugins/openlibrary/js/carousel/index.js index 7a4d91ab9cc..f3e6a25b975 100644 --- a/openlibrary/plugins/openlibrary/js/carousel/index.js +++ b/openlibrary/plugins/openlibrary/js/carousel/index.js @@ -7,7 +7,7 @@ export function initialzeCarousels(elems) { elemSlides.forEach((slide) => { const $slide = $(slide); if ($slide.attr('aria-describedby') !== undefined) { - $slide.attr('id', $(this).attr('aria-describedby')); + $slide.attr('id', $slide.attr('aria-describedby')); } }); }); diff --git a/openlibrary/plugins/openlibrary/js/covers.js b/openlibrary/plugins/openlibrary/js/covers.js index 0e4e0e39cba..ea7da2e9bac 100644 --- a/openlibrary/plugins/openlibrary/js/covers.js +++ b/openlibrary/plugins/openlibrary/js/covers.js @@ -175,7 +175,9 @@ async function pasteImage() { return formData; } alert('No image found in clipboard'); - } catch (error) {} + } catch { + return null; + } } export function initPasteForm(coverForm) { diff --git a/openlibrary/plugins/openlibrary/js/dialog.js b/openlibrary/plugins/openlibrary/js/dialog.js index c46a03aef93..dfb25f59a44 100644 --- a/openlibrary/plugins/openlibrary/js/dialog.js +++ b/openlibrary/plugins/openlibrary/js/dialog.js @@ -105,8 +105,11 @@ export function initDialogs() { // This will close the dialog in the current page. $('.dialog--close') - .attr('href', 'javascript:;') - .on('click', () => $.fn.colorbox.close()); + .attr('href', '#') + .on('click', (e) => { + e.preventDefault(); + $.fn.colorbox.close(); + }); // This will close the colorbox from the parent. $('.dialog--close-parent').on('click', () => parent.$.fn.colorbox.close()); } diff --git a/openlibrary/plugins/openlibrary/js/service-worker-init.js b/openlibrary/plugins/openlibrary/js/service-worker-init.js index e6cc0c41b8b..e0827ebc291 100644 --- a/openlibrary/plugins/openlibrary/js/service-worker-init.js +++ b/openlibrary/plugins/openlibrary/js/service-worker-init.js @@ -3,7 +3,6 @@ export default function initServiceWorker() { window.addEventListener('load', () => { navigator.serviceWorker .register('/sw.js') - .then(() => {}) .catch((error) => { // eslint-disable-next-line no-console console.error(`Service worker registration failed: ${error}`); diff --git a/openlibrary/plugins/openlibrary/js/utils.js b/openlibrary/plugins/openlibrary/js/utils.js index b43e75b8265..615bf000925 100644 --- a/openlibrary/plugins/openlibrary/js/utils.js +++ b/openlibrary/plugins/openlibrary/js/utils.js @@ -50,7 +50,7 @@ export function updateURLParameters(params) { // Iterate over the params object and update/add each parameter for (const key in params) { - if (Object.hasOwn(params, key)) { + if (Object.prototype.hasOwnProperty.call(params, key)) { url.searchParams.set(key, params[key]); } } From 55e4c9169f285435eb7df14efe0354ea03de1fd1 Mon Sep 17 00:00:00 2001 From: harshgupta2125 <harsh2125gupta@gmail.com> Date: Mon, 6 Apr 2026 11:22:00 +0530 Subject: [PATCH 04/15] Fix: Add ESLint override for unused variables in BulkSearch --- openlibrary/components/BulkSearch/utils/classes.js | 1 + 1 file changed, 1 insertion(+) diff --git a/openlibrary/components/BulkSearch/utils/classes.js b/openlibrary/components/BulkSearch/utils/classes.js index b52aed77991..e84a12ee0db 100644 --- a/openlibrary/components/BulkSearch/utils/classes.js +++ b/openlibrary/components/BulkSearch/utils/classes.js @@ -24,6 +24,7 @@ class AbstractExtractor { * @param {string} _text * @returns {Promise<BookMatch[]>} */ +// eslint-disable-next-line no-unused-vars async run(_extractOptions, _text) { //eslint-disable-line no-unused-vars throw new Error('Not Implemented Error'); From 82bdb3f7966ee73ba42133d39410cf4129df9b42 Mon Sep 17 00:00:00 2001 From: harshgupta2125 <harsh2125gupta@gmail.com> Date: Mon, 6 Apr 2026 18:57:28 +0530 Subject: [PATCH 05/15] Fix: Address of Copilot AI feedback (userKey, CSP buttons, i18n) --- openlibrary/plugins/openlibrary/js/affiliate-links.js | 2 +- .../plugins/openlibrary/js/fulltext-search-suggestion.js | 2 +- openlibrary/plugins/openlibrary/js/my-books/store/index.js | 2 +- openlibrary/plugins/openlibrary/js/private-button.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openlibrary/plugins/openlibrary/js/affiliate-links.js b/openlibrary/plugins/openlibrary/js/affiliate-links.js index 622004a6cfa..f30fcce14b2 100644 --- a/openlibrary/plugins/openlibrary/js/affiliate-links.js +++ b/openlibrary/plugins/openlibrary/js/affiliate-links.js @@ -102,5 +102,5 @@ async function getPartials(data, affiliateLinksSections) { * @returns {string} HTML for a retry link. */ function renderRetryLink() { - return '<span class="affiliate-links-section__retry">Failed to fetch affiliate links. <a href="javascript:;">Retry?</a></span>'; + return '<span class="affiliate-links-section__retry">Failed to fetch affiliate links. <button type="button" style="border:none;background:none;color:blue;text-decoration:underline;cursor:pointer;">Retry?</button></span>'; } diff --git a/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js b/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js index 47ad82df2cd..2342abafb01 100644 --- a/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js +++ b/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js @@ -69,5 +69,5 @@ async function getPartials(fulltextSearchSuggestion, query) { * @returns {string} HTML for a retry link. */ function renderRetryLink() { - return '<span class="fulltext-suggestions__retry">Failed to fetch fulltext search suggestions. <a href="javascript:;">Retry?</a></span>'; + return '<span class="fulltext-suggestions__retry">Failed to fetch fulltext search suggestions. <button type="button" style="border:none;background:none;color:blue;text-decoration:underline;cursor:pointer;">Retry?</button></span>'; } diff --git a/openlibrary/plugins/openlibrary/js/my-books/store/index.js b/openlibrary/plugins/openlibrary/js/my-books/store/index.js index 488129b3d8c..35246e04c3e 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/store/index.js +++ b/openlibrary/plugins/openlibrary/js/my-books/store/index.js @@ -15,7 +15,7 @@ class MyBooksStore { this._store = { droppers: [], showcases: [], - userkey: '', + userKey: '', openDropper: null, }; } diff --git a/openlibrary/plugins/openlibrary/js/private-button.js b/openlibrary/plugins/openlibrary/js/private-button.js index cf942c4df04..07a8ddc42aa 100644 --- a/openlibrary/plugins/openlibrary/js/private-button.js +++ b/openlibrary/plugins/openlibrary/js/private-button.js @@ -5,7 +5,7 @@ export function initPrivateButtons(buttons) { button.addEventListener('click', (event) => { event.preventDefault(); const toast = new FadingToast( - 'This patron has not enabled following', + window.$_('This patron has not enabled following'), null, 3000, ); From 67781f28dfa9c05870ebaee52f499eb2e7a5168e Mon Sep 17 00:00:00 2001 From: harshgupta2125 <harsh2125gupta@gmail.com> Date: Wed, 8 Apr 2026 22:59:01 +0530 Subject: [PATCH 06/15] Apply ESLint v9 auto-fixes to all JS files --- .../BarcodeScanner/utils/classes.js | 146 +- .../components/BulkSearch/utils/classes.js | 392 +++--- .../components/BulkSearch/utils/samples.js | 40 +- .../BulkSearch/utils/searchUtils.js | 76 +- .../IdentifiersInput/utils/utils.js | 156 +-- .../components/LibraryExplorer/utils.js | 106 +- .../components/LibraryExplorer/utils/lcc.js | 62 +- .../ObservationForm/ObservationService.js | 44 +- .../components/ObservationForm/Utils.js | 12 +- openlibrary/components/configs.js | 38 +- openlibrary/components/dev/vite.config.js | 2 +- openlibrary/components/lit/OLChip.js | 88 +- openlibrary/components/lit/OLChipGroup.js | 32 +- openlibrary/components/lit/OLReadMore.js | 170 +-- openlibrary/components/lit/OlPagination.js | 402 +++--- openlibrary/components/lit/OlPopover.js | 1014 +++++++------- openlibrary/components/rollupInputCore.js | 24 +- openlibrary/plugins/openlibrary/js/Browser.js | 56 +- .../plugins/openlibrary/js/SearchBar.js | 724 +++++----- .../plugins/openlibrary/js/SearchPage.js | 22 +- .../plugins/openlibrary/js/SearchUtils.js | 146 +- openlibrary/plugins/openlibrary/js/Toast.js | 86 +- .../plugins/openlibrary/js/add-book.js | 292 ++-- .../plugins/openlibrary/js/add_provider.js | 64 +- openlibrary/plugins/openlibrary/js/admin.js | 60 +- .../plugins/openlibrary/js/affiliate-links.js | 118 +- .../plugins/openlibrary/js/autocomplete.js | 550 ++++---- .../plugins/openlibrary/js/banner/index.js | 56 +- .../plugins/openlibrary/js/book-page-lists.js | 126 +- .../openlibrary/js/breadcrumb_select/index.js | 38 +- .../openlibrary/js/bulk-tagger/BulkTagger.js | 944 ++++++------- .../js/bulk-tagger/BulkTagger/MenuOption.js | 196 +-- .../BulkTagger/SortedMenuOptionContainer.js | 140 +- .../openlibrary/js/bulk-tagger/index.js | 2 +- .../openlibrary/js/bulk-tagger/models/Tag.js | 114 +- .../openlibrary/js/carousel/Carousel.js | 310 ++-- .../plugins/openlibrary/js/carousel/index.js | 18 +- .../plugins/openlibrary/js/clampers.js | 36 +- .../openlibrary/js/compact-title/index.js | 84 +- openlibrary/plugins/openlibrary/js/dialog.js | 174 +-- .../plugins/openlibrary/js/dropper/Dropper.js | 108 +- .../plugins/openlibrary/js/dropper/index.js | 96 +- openlibrary/plugins/openlibrary/js/edit.js | 976 ++++++------- .../openlibrary/js/edition-nav-bar/index.js | 14 +- .../openlibrary/js/editions-table/index.js | 198 +-- .../plugins/openlibrary/js/following.js | 68 +- .../js/fulltext-search-suggestion.js | 104 +- .../plugins/openlibrary/js/go-back-links.js | 18 +- .../openlibrary/js/goodreads_import.js | 338 ++--- .../plugins/openlibrary/js/graphs/index.js | 40 +- .../plugins/openlibrary/js/graphs/options.js | 94 +- .../plugins/openlibrary/js/graphs/plot.js | 490 +++---- openlibrary/plugins/openlibrary/js/i18n.js | 16 +- .../openlibrary/js/ia_thirdparty_logins.js | 32 +- .../plugins/openlibrary/js/idValidation.js | 98 +- .../plugins/openlibrary/js/ile/index.js | 114 +- .../plugins/openlibrary/js/ile/utils/ol.js | 66 +- openlibrary/plugins/openlibrary/js/index.js | 1248 ++++++++--------- .../plugins/openlibrary/js/interstitial.js | 36 +- .../plugins/openlibrary/js/isbnOverride.js | 20 +- .../plugins/openlibrary/js/jquery.repeat.js | 164 +-- openlibrary/plugins/openlibrary/js/jsdef.js | 180 +-- .../plugins/openlibrary/js/lazy-carousel.js | 134 +- .../openlibrary/js/lazy-thing-preview.js | 208 +-- .../js/librarian-dashboard/index.js | 140 +- .../plugins/openlibrary/js/list_books.js | 50 +- .../openlibrary/js/lists/ListService.js | 64 +- .../openlibrary/js/lists/ListViewBody.js | 208 +-- .../openlibrary/js/lists/ShowcaseItem.js | 258 ++-- .../openlibrary/js/markdown-editor/index.js | 26 +- .../MergeRequestService.js | 96 +- .../merge-request-table/MergeRequestTable.js | 46 +- .../MergeRequestTable/TableHeader.js | 140 +- .../MergeRequestTable/TableRow.js | 318 ++--- .../js/merge-request-table/index.js | 4 +- openlibrary/plugins/openlibrary/js/merge.js | 166 +-- .../plugins/openlibrary/js/modals/index.js | 542 +++---- .../openlibrary/js/my-books/CreateListForm.js | 124 +- .../openlibrary/js/my-books/MyBooksDropper.js | 226 +-- .../MyBooksDropper/CheckInComponents.js | 1022 +++++++------- .../my-books/MyBooksDropper/ReadingLists.js | 484 +++---- .../MyBooksDropper/ReadingLogForms.js | 334 ++--- .../plugins/openlibrary/js/my-books/index.js | 176 +-- .../openlibrary/js/my-books/store/index.js | 82 +- .../openlibrary/js/native-dialog/index.js | 44 +- .../plugins/openlibrary/js/nonjquery_utils.js | 30 +- .../plugins/openlibrary/js/offline-banner.js | 8 +- .../plugins/openlibrary/js/ol.analytics.js | 90 +- .../plugins/openlibrary/js/partner_ol_lib.js | 90 +- .../plugins/openlibrary/js/password-toggle.js | 20 +- .../plugins/openlibrary/js/patron_exports.js | 14 +- .../plugins/openlibrary/js/private-button.js | 20 +- openlibrary/plugins/openlibrary/js/python.js | 36 +- .../openlibrary/js/reading-goals/index.js | 272 ++-- .../openlibrary/js/readinglog_stats.js | 326 ++--- .../openlibrary/js/return-form/index.js | 16 +- openlibrary/plugins/openlibrary/js/search.js | 206 +-- .../openlibrary/js/service-worker-init.js | 24 +- .../openlibrary/js/service-worker-matchers.js | 38 +- .../plugins/openlibrary/js/service-worker.js | 164 +-- openlibrary/plugins/openlibrary/js/signup.js | 456 +++--- .../openlibrary/js/star-ratings/index.js | 122 +- .../plugins/openlibrary/js/stats/index.js | 28 +- openlibrary/plugins/openlibrary/js/tabs.js | 8 +- openlibrary/plugins/openlibrary/js/team.js | 596 ++++---- .../plugins/openlibrary/js/template.js | 58 +- .../plugins/openlibrary/js/type_changer.js | 18 +- .../plugins/openlibrary/js/waitlist.js | 20 +- stories/Button.stories.js | 58 +- tests/unit/js/Browser.test.js | 94 +- tests/unit/js/SearchBar.test.js | 578 ++++---- tests/unit/js/SearchUtils.test.js | 156 +-- tests/unit/js/SelectionManager.test.js | 120 +- tests/unit/js/autocomplete.test.js | 88 +- tests/unit/js/droppers.test.js | 634 ++++----- .../js/editionEditPageClassification.test.js | 88 +- tests/unit/js/editionsEditPage.test.js | 382 ++--- tests/unit/js/idValidation.test.js | 248 ++-- tests/unit/js/jquery.repeat.test.js | 60 +- tests/unit/js/jsdef.test.js | 80 +- tests/unit/js/lists.test.js | 498 +++---- tests/unit/js/my-books.test.js | 274 ++-- tests/unit/js/nonjquery_utils.test.js | 94 +- tests/unit/js/python.test.js | 68 +- .../unit/js/sample-html/dropper-test-data.js | 20 +- tests/unit/js/sample-html/lists-test-data.js | 106 +- tests/unit/js/search.test.js | 286 ++-- tests/unit/js/service-worker-matchers.test.js | 286 ++-- tests/unit/js/setup.js | 2 +- tests/unit/js/signup.test.js | 280 ++-- tests/unit/js/utils.test.js | 104 +- webpack.config.css.js | 136 +- 132 files changed, 11985 insertions(+), 11985 deletions(-) diff --git a/openlibrary/components/BarcodeScanner/utils/classes.js b/openlibrary/components/BarcodeScanner/utils/classes.js index a708f39385c..b9de19c49fd 100644 --- a/openlibrary/components/BarcodeScanner/utils/classes.js +++ b/openlibrary/components/BarcodeScanner/utils/classes.js @@ -3,105 +3,105 @@ import { createScheduler, createWorker } from 'tesseract.js'; export class OCRScanner { - constructor() { - this.scheduler = createScheduler(); - /** @type {number | null} */ - this.timerId = null; + constructor() { + this.scheduler = createScheduler(); + /** @type {number | null} */ + this.timerId = null; - this.listeners = { - /** @type {Array<(isbn: string) => void>} */ - onISBNDetected: [], - }; - } - - /** @param {(isbn: string) => void} callback */ - onISBNDetected(callback) { - this.listeners.onISBNDetected.push(callback); - } + this.listeners = { + /** @type {Array<(isbn: string) => void>} */ + onISBNDetected: [], + }; + } - async init() { - this._initPromise = this._initPromise || this._init(); - await this._initPromise; - } + /** @param {(isbn: string) => void} callback */ + onISBNDetected(callback) { + this.listeners.onISBNDetected.push(callback); + } - async _init() { - console.log('Initializing Tesseract.js'); - for (let i = 0; i < 1; i++) { - const worker = await createWorker(); - await worker.load(); - await worker.loadLanguage('eng'); - await worker.initialize('eng'); - this.scheduler.addWorker(worker); - console.log(`Loaded worker ${i}`); + async init() { + this._initPromise = this._initPromise || this._init(); + await this._initPromise; } - console.log('Tesseract.js initialized'); - } + async _init() { + console.log('Initializing Tesseract.js'); + for (let i = 0; i < 1; i++) { + const worker = await createWorker(); + await worker.load(); + await worker.loadLanguage('eng'); + await worker.initialize('eng'); + this.scheduler.addWorker(worker); + console.log(`Loaded worker ${i}`); + } + + console.log('Tesseract.js initialized'); + } - /** + /** * @param {HTMLCanvasElement} canvas */ - async doOCR(canvas) { - const { - data: { lines }, - } = await this.scheduler.addJob('recognize', canvas); - const textLines = lines.map((l) => l.text.trim()).filter((line) => line); - console.log(textLines.join('\n')); - for (const line of textLines) { - const sanitizedLine = line.replace(/[\s-'.–—]/g, ''); - if (!/\d{2}/.test(sanitizedLine)) continue; - console.log(sanitizedLine); - if ( - sanitizedLine.includes('isbn') || + async doOCR(canvas) { + const { + data: { lines }, + } = await this.scheduler.addJob('recognize', canvas); + const textLines = lines.map((l) => l.text.trim()).filter((line) => line); + console.log(textLines.join('\n')); + for (const line of textLines) { + const sanitizedLine = line.replace(/[\s-'.–—]/g, ''); + if (!/\d{2}/.test(sanitizedLine)) continue; + console.log(sanitizedLine); + if ( + sanitizedLine.includes('isbn') || /97[0-9]{10}[0-9x]/i.test(sanitizedLine) || /[0-9]{9}[0-9x]/i.test(sanitizedLine) - ) { - const isbn = sanitizedLine.match( - /(97[0-9]{10}[0-9x]|[0-9]{9}[0-9x])/i, - )[0]; - console.log(`ISBN detected: ${isbn}`); - this.listeners.onISBNDetected.forEach((callback) => callback(isbn)); - } + ) { + const isbn = sanitizedLine.match( + /(97[0-9]{10}[0-9x]|[0-9]{9}[0-9x])/i, + )[0]; + console.log(`ISBN detected: ${isbn}`); + this.listeners.onISBNDetected.forEach((callback) => callback(isbn)); + } + } } - } } /** * @template {(...args: any) => void} TFunc */ export class ThrottleGrouping { - /** + /** * @param {object} param0 * @param {TFunc} param0.func * @param {function(Parameters<TFunc>[]): Parameters<TFunc>} param0.reducer * @param {number} param0.wait */ - constructor({ func, reducer, wait = 100 }) { - this.func = func; - this.reducer = reducer; - this.wait = wait; - /** @type {Parameters<TFunc>[]} */ - this.curGroup = []; - this.timeout = null; - } + constructor({ func, reducer, wait = 100 }) { + this.func = func; + this.reducer = reducer; + this.wait = wait; + /** @type {Parameters<TFunc>[]} */ + this.curGroup = []; + this.timeout = null; + } - submitGroup() { - this.timeout = null; - this.func(...this.reducer(this.curGroup)); - this.curGroup = []; - } + submitGroup() { + this.timeout = null; + this.func(...this.reducer(this.curGroup)); + this.curGroup = []; + } - /** + /** * @param {Parameters<TFunc>} args */ - takeNext(...args) { - this.curGroup.push(args); - if (!this.timeout) { - this.timeout = setTimeout(this.submitGroup.bind(this), this.wait); + takeNext(...args) { + this.curGroup.push(args); + if (!this.timeout) { + this.timeout = setTimeout(this.submitGroup.bind(this), this.wait); + } } - } - asFunction() { - return this.takeNext.bind(this); - } + asFunction() { + return this.takeNext.bind(this); + } } diff --git a/openlibrary/components/BulkSearch/utils/classes.js b/openlibrary/components/BulkSearch/utils/classes.js index e84a12ee0db..964ceab5be8 100644 --- a/openlibrary/components/BulkSearch/utils/classes.js +++ b/openlibrary/components/BulkSearch/utils/classes.js @@ -1,274 +1,274 @@ //@ts-check export class ExtractedBook { - constructor(title = '', author = '', isbn = '') { + constructor(title = '', author = '', isbn = '') { /** @type {string} */ - this.title = title; - /**@type {string} */ - this.author = author; - /**@type {string} */ - this.isbn = isbn; - } + this.title = title; + /**@type {string} */ + this.author = author; + /**@type {string} */ + this.isbn = isbn; + } } class AbstractExtractor { - /** + /** * @param {string} label */ - constructor(label) { + constructor(label) { /** @type {string} */ - this.label = label; - } - /** + this.label = label; + } + /** * @param {ExtractionOptions} _extractOptions * @param {string} _text * @returns {Promise<BookMatch[]>} */ -// eslint-disable-next-line no-unused-vars - async run(_extractOptions, _text) { - //eslint-disable-line no-unused-vars - throw new Error('Not Implemented Error'); - } + // eslint-disable-next-line no-unused-vars + async run(_extractOptions, _text) { + + throw new Error('Not Implemented Error'); + } } export class RegexExtractor extends AbstractExtractor { - name = 'regex_extractor'; - /** + name = 'regex_extractor'; + /** * * @param {string} label * @param {string} pattern */ - constructor(label, pattern) { - super(label); - /** @type {RegExp} */ - this.pattern = new RegExp(pattern, 'gmu'); - } + constructor(label, pattern) { + super(label); + /** @type {RegExp} */ + this.pattern = new RegExp(pattern, 'gmu'); + } - /** + /** * @param {ExtractionOptions} _extractOptions * @param {string} text * @returns {Promise<BookMatch[]>} */ - async run(_extractOptions, text) { - const data = [...text.matchAll(this.pattern)]; - const extractedBooks = data.map( - (entry) => - new ExtractedBook( - entry.groups?.title, - entry.groups?.author, - entry.groups?.isbn, - ), - ); - const matchedBooks = extractedBooks.map( - (entry) => new BookMatch(entry, []), - ); - return matchedBooks; - } + async run(_extractOptions, text) { + const data = [...text.matchAll(this.pattern)]; + const extractedBooks = data.map( + (entry) => + new ExtractedBook( + entry.groups?.title, + entry.groups?.author, + entry.groups?.isbn, + ), + ); + const matchedBooks = extractedBooks.map( + (entry) => new BookMatch(entry, []), + ); + return matchedBooks; + } } export class AiExtractor extends AbstractExtractor { - name = 'ai_extractor'; - /** + name = 'ai_extractor'; + /** * @param {string} label * @param {string} model */ - constructor(label, model) { - super(label); - /** @type {string} */ - this.model = model; - } + constructor(label, model) { + super(label); + /** @type {string} */ + this.model = model; + } - /** + /** * * @param {ExtractionOptions} extractOptions * @param {string} text * @returns {Promise<BookMatch[]>} */ - async run(extractOptions, text) { - const request = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${extractOptions.openaiApiKey}`, - }, - body: JSON.stringify({ - model: this.model, - response_format: { type: 'json_object' }, - messages: [ - { - role: 'system', - content: + async run(extractOptions, text) { + const request = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${extractOptions.openaiApiKey}`, + }, + body: JSON.stringify({ + model: this.model, + response_format: { type: 'json_object' }, + messages: [ + { + role: 'system', + content: 'You are a book extraction system. You will be given a free form passage of text containing references to books, and you will need to extract the book titles, author, and optionally ISBN in a JSON array.', - }, - { - role: 'user', - content: `Please extract the books from the following text:\n\n${text}`, - }, - ], - }), - }; - try { - const resp = await fetch( - 'https://api.openai.com/v1/chat/completions', - request, - ); + }, + { + role: 'user', + content: `Please extract the books from the following text:\n\n${text}`, + }, + ], + }), + }; + try { + const resp = await fetch( + 'https://api.openai.com/v1/chat/completions', + request, + ); - if (!resp.ok) { - const status = resp.status; - let errorMessage = 'Network response was not okay.'; - if (status === 401) { - errorMessage = `${errorMessage} Error: Incorrect Authorization key.`; + if (!resp.ok) { + const status = resp.status; + let errorMessage = 'Network response was not okay.'; + if (status === 401) { + errorMessage = `${errorMessage} Error: Incorrect Authorization key.`; + } + throw new Error(errorMessage); + } + const data = await resp.json(); + return JSON.parse(data.choices[0].message.content)['books'].map( + (entry) => + new BookMatch( + new ExtractedBook(entry?.title, entry?.author, entry?.isbn), + {}, + ), + ); + } catch (error) { + return []; } - throw new Error(errorMessage); - } - const data = await resp.json(); - return JSON.parse(data.choices[0].message.content)['books'].map( - (entry) => - new BookMatch( - new ExtractedBook(entry?.title, entry?.author, entry?.isbn), - {}, - ), - ); - } catch (error) { - return []; } - } } export class TableExtractor extends AbstractExtractor { - name = 'table_extractor'; - /** + name = 'table_extractor'; + /** * * @param {string} label */ - constructor(label) { - super(label); - /** @type {string} */ - this.authorColumn = 'author'; - /** @type {string} */ - this.titleColumn = 'title'; - } + constructor(label) { + super(label); + /** @type {string} */ + this.authorColumn = 'author'; + /** @type {string} */ + this.titleColumn = 'title'; + } - /** + /** * @param {ExtractionOptions} extractionOptions * @param {string} text * @return {Promise<BookMatch[]>} */ - async run(extractionOptions, text) { + async run(extractionOptions, text) { /** @type {string[]} */ - const lines = text.split('\n'); - /** @type {string[][]} */ - const cells = lines.map((line) => line.split('\t')); - /** @type {{columns: String[], rows: {columnName: string}[]}} */ - const tableData = { - columns: cells[0], - rows: [], - }; - for (let i = 1; i < cells.length; i++) { - const row = {}; - for (let j = 0; j < tableData.columns.length; j++) { - row[tableData.columns[j].trim().toLowerCase()] = cells[i][j]; - } - // @ts-expect-error - tableData.rows.push(row); + const lines = text.split('\n'); + /** @type {string[][]} */ + const cells = lines.map((line) => line.split('\t')); + /** @type {{columns: String[], rows: {columnName: string}[]}} */ + const tableData = { + columns: cells[0], + rows: [], + }; + for (let i = 1; i < cells.length; i++) { + const row = {}; + for (let j = 0; j < tableData.columns.length; j++) { + row[tableData.columns[j].trim().toLowerCase()] = cells[i][j]; + } + // @ts-expect-error + tableData.rows.push(row); + } + return tableData.rows.map( + (row) => + new BookMatch( + new ExtractedBook( + row[this.titleColumn] || '', + row[this.authorColumn] || '', + row['isbn'] || '', + ), + {}, + ), + ); } - return tableData.rows.map( - (row) => - new BookMatch( - new ExtractedBook( - row[this.titleColumn] || '', - row[this.authorColumn] || '', - row['isbn'] || '', - ), - {}, - ), - ); - } } class ExtractionOptions { - constructor() { + constructor() { /** @type {string} */ - this.openaiApiKey = ''; - } + this.openaiApiKey = ''; + } } class MatchOptions { - constructor() { + constructor() { /** @type {boolean} */ - this.includeAuthor = true; - } + this.includeAuthor = true; + } } export class BookMatch { - /** + /** * * @param {ExtractedBook} extractedBook * @param {*} solrDocs */ - constructor(extractedBook, solrDocs) { + constructor(extractedBook, solrDocs) { /** @type {ExtractedBook} */ - this.extractedBook = extractedBook; - this.solrDocs = solrDocs; - } + this.extractedBook = extractedBook; + this.solrDocs = solrDocs; + } } const BASE_LIST_URL = '/account/lists/add?seeds='; export class BulkSearchState { - constructor() { + constructor() { /** @type {string} */ - this.inputText = ''; - /** @type {BookMatch[]} */ - this.matchedBooks = []; - /** @type {MatchOptions} */ - this.matchOptions = new MatchOptions(); - /** @type {ExtractionOptions} */ - this.extractionOptions = new ExtractionOptions(); - /** @type {AbstractExtractor[]} */ - this.extractors = [ - new RegexExtractor( - 'Pattern: Title by Author', - '(^|>)(?<title>[A-Za-z][\\p{L}0-9\\- ,]{1,250})\\s+(by|[-\u2013\u2014\\t])\\s+(?<author>[\\p{L}][\\p{L}\\.\\- ]{3,70})( \\(.*)?($|<\\/)', - ), - new RegexExtractor( - 'Pattern: Author - Title', - '(^|>)(?<author>[A-Za-z][\\p{L}0-9\\- ,]{1,250})\\s+[,-\u2013\u2014\\t]\\s+(?<title>[\\p{L}][\\p{L}\\.\\- ]{3,70})( \\(.*)?($|<\\/)', - ), - new RegexExtractor( - 'Pattern: Title - Author', - '(^|>)(?<title>[A-Za-z][\\p{L}0-9\\- ,]{1,250})\\s+[,-\u2013\u2014\\t]\\s+(?<author>[\\p{L}][\\p{L}\\.\\- ]{3,70})( \\(.*)?($|<\\/)', - ), - new RegexExtractor( - 'Pattern: Title (Author)', - '^(?<title>[\\p{L}].{1,250})\\s\\(?<author>(.{3,70})\\)$$', - ), - new RegexExtractor( - 'Wikipedia Citation Pattern: (e.g. Baum, Frank L. (1994). The Wizard of Oz)', - '^(?<author>[^.()]+).*?\\)\\. (?<title>[^.]+)', - ), - new AiExtractor('✨ AI Extraction (Beta)', 'gpt-4o-mini'), - new TableExtractor('Extract from a Table/Spreadsheet'), - ]; - /** @type {Number} */ - this._activeExtractorIndex = 0; - } + this.inputText = ''; + /** @type {BookMatch[]} */ + this.matchedBooks = []; + /** @type {MatchOptions} */ + this.matchOptions = new MatchOptions(); + /** @type {ExtractionOptions} */ + this.extractionOptions = new ExtractionOptions(); + /** @type {AbstractExtractor[]} */ + this.extractors = [ + new RegexExtractor( + 'Pattern: Title by Author', + '(^|>)(?<title>[A-Za-z][\\p{L}0-9\\- ,]{1,250})\\s+(by|[-\u2013\u2014\\t])\\s+(?<author>[\\p{L}][\\p{L}\\.\\- ]{3,70})( \\(.*)?($|<\\/)', + ), + new RegexExtractor( + 'Pattern: Author - Title', + '(^|>)(?<author>[A-Za-z][\\p{L}0-9\\- ,]{1,250})\\s+[,-\u2013\u2014\\t]\\s+(?<title>[\\p{L}][\\p{L}\\.\\- ]{3,70})( \\(.*)?($|<\\/)', + ), + new RegexExtractor( + 'Pattern: Title - Author', + '(^|>)(?<title>[A-Za-z][\\p{L}0-9\\- ,]{1,250})\\s+[,-\u2013\u2014\\t]\\s+(?<author>[\\p{L}][\\p{L}\\.\\- ]{3,70})( \\(.*)?($|<\\/)', + ), + new RegexExtractor( + 'Pattern: Title (Author)', + '^(?<title>[\\p{L}].{1,250})\\s\\(?<author>(.{3,70})\\)$$', + ), + new RegexExtractor( + 'Wikipedia Citation Pattern: (e.g. Baum, Frank L. (1994). The Wizard of Oz)', + '^(?<author>[^.()]+).*?\\)\\. (?<title>[^.]+)', + ), + new AiExtractor('✨ AI Extraction (Beta)', 'gpt-4o-mini'), + new TableExtractor('Extract from a Table/Spreadsheet'), + ]; + /** @type {Number} */ + this._activeExtractorIndex = 0; + } - /**@type {AbstractExtractor} */ - get activeExtractor() { - return this.extractors[this._activeExtractorIndex]; - } - /**@type {String} */ - get listUrl() { - return ( - BASE_LIST_URL + + /**@type {AbstractExtractor} */ + get activeExtractor() { + return this.extractors[this._activeExtractorIndex]; + } + /**@type {String} */ + get listUrl() { + return ( + BASE_LIST_URL + this.matchedBooks - .map((bm) => bm.solrDocs?.docs?.[0]?.key.split('/')[2]) - .filter((key) => key) - ); - } - /**@type {String} */ - get listString() { - return `${this.matchedBooks - .map((bm) => bm.solrDocs?.docs?.[0]?.key.split('/')[2]) - .filter((key) => key)}`; - } + .map((bm) => bm.solrDocs?.docs?.[0]?.key.split('/')[2]) + .filter((key) => key) + ); + } + /**@type {String} */ + get listString() { + return `${this.matchedBooks + .map((bm) => bm.solrDocs?.docs?.[0]?.key.split('/')[2]) + .filter((key) => key)}`; + } } diff --git a/openlibrary/components/BulkSearch/utils/samples.js b/openlibrary/components/BulkSearch/utils/samples.js index 075e2a94372..bddd30802ea 100644 --- a/openlibrary/components/BulkSearch/utils/samples.js +++ b/openlibrary/components/BulkSearch/utils/samples.js @@ -1,23 +1,23 @@ export const sampleData = [ - { - name: 'Try a Sample...', - source: '', - text: '', - }, - { - name: '1927 Books', - source: 'https://en.wikipedia.org/wiki/1927_in_literature#New_books', - text: 'Djamaluddin Adinegoro - Darah Muda (Young Blood)\nIon Agârbiceanu - Legea minții\nAnthony Berkeley - Cicely Disappears\nArthur Bernède - Belphégor\nTjoe Hong Bok - Setangan Berloemoer Darah (A Glove Covered in Blood)\nJames Boyd - Marching On\nLynn Brock - The Kink\nEdgar Rice Burroughs - The Outlaw of Torn\nJames Branch Cabell - Something About Eve\nWilla Cather - Death Comes for the Archbishop\nBlaise Cendrars - La Confession de Dan Yack\nAgatha Christie - The Big Four\nJ.J. Connington Murder in the Maze Tragedy at Ravensthorpe\nJaime de Angulo - The Lariat\nMazo de la Roche - Jalna\nWarwick Deeping - Kitty', - }, - { - name: '2023 Public Domain Day', - source: 'https://web.law.duke.edu/cspd/publicdomainday/2023/', - text: 'To the Lighthouse - Virginia Woolf\nThe Case-Book of Sherlock Holmes - Arthur Conan Doyle\nDeath Comes for the Archbishop - Willa Cather\nCopper Sun - Countee Cullen\nNow We Are Six - illustrations by E. H. Shepard - A. A. Milne\nThe Bridge of San Luis Rey - Thornton Wilder\nMen Without Women - Ernest Hemingway\nMosquitoes - William Faulkner\nThe Big Four - Agatha Christie\nTwilight Sleep - Edith Wharton\nThe Gangs of New York - Herbert Asbury\nThe Tower Treasure - Franklin W. Dixon (pseudonym)\nDer Steppenwolf - Hermann Hesse\nAmerika - Franz Kafka', - }, - { - name: 'Holocaust Wikipedia citations', - source: + { + name: 'Try a Sample...', + source: '', + text: '', + }, + { + name: '1927 Books', + source: 'https://en.wikipedia.org/wiki/1927_in_literature#New_books', + text: 'Djamaluddin Adinegoro - Darah Muda (Young Blood)\nIon Agârbiceanu - Legea minții\nAnthony Berkeley - Cicely Disappears\nArthur Bernède - Belphégor\nTjoe Hong Bok - Setangan Berloemoer Darah (A Glove Covered in Blood)\nJames Boyd - Marching On\nLynn Brock - The Kink\nEdgar Rice Burroughs - The Outlaw of Torn\nJames Branch Cabell - Something About Eve\nWilla Cather - Death Comes for the Archbishop\nBlaise Cendrars - La Confession de Dan Yack\nAgatha Christie - The Big Four\nJ.J. Connington Murder in the Maze Tragedy at Ravensthorpe\nJaime de Angulo - The Lariat\nMazo de la Roche - Jalna\nWarwick Deeping - Kitty', + }, + { + name: '2023 Public Domain Day', + source: 'https://web.law.duke.edu/cspd/publicdomainday/2023/', + text: 'To the Lighthouse - Virginia Woolf\nThe Case-Book of Sherlock Holmes - Arthur Conan Doyle\nDeath Comes for the Archbishop - Willa Cather\nCopper Sun - Countee Cullen\nNow We Are Six - illustrations by E. H. Shepard - A. A. Milne\nThe Bridge of San Luis Rey - Thornton Wilder\nMen Without Women - Ernest Hemingway\nMosquitoes - William Faulkner\nThe Big Four - Agatha Christie\nTwilight Sleep - Edith Wharton\nThe Gangs of New York - Herbert Asbury\nThe Tower Treasure - Franklin W. Dixon (pseudonym)\nDer Steppenwolf - Hermann Hesse\nAmerika - Franz Kafka', + }, + { + name: 'Holocaust Wikipedia citations', + source: 'https://en.wikipedia.org/wiki/Bibliography_of_the_Holocaust#Historical_studies', - text: 'Bauer, Yehuda (1994). Jews for Sale? Nazi-Jewish Negotiations 1933-1945. Yale University Press. ISBN 0-300-06852-2.\nBerenbaum, Michael (1990). A Mosaic of Victims: Non-Jews Persecuted and Murdered by the Nazis.\nBergen, Doris (2009). War and Genocide: Concise History of the Holocaust.\nBlack, Edwin (2010). The Farhud: The Arab-Nazi Alliance in the Holocaust. Dialog Press. ISBN 0-914153-14-5.\nBraham, Randolph (1994) [1981]. The Politics of Genocide: The Holocaust in Hungary. Columbia University Press. ISBN 0-88033-247-6.\nBraham, Randolph (2011). The Auschwitz Reports and the Holocaust in Hungary. Columbia University Press.\nChalmers, Beverley (2015). Birth, Sex and Abuse: Women\'s Voices Under Nazi Rule. Grosvenor House Publishing Ltd. ISBN 1781483531.\nDavies, Norman; Lukas, Richard C. (2001) [1996]. Forgotten Holocaust: The Poles Under German Occupation.\nDean, Martin (2008). Robbing the Jews: The Confiscation of Jewish Property in the Holocaust. Cambridge University Press.\nEvans, Suzanne (2004). Forgotten Crimes: The Holocaust and People with Disabilities.\nFriedländer, Saul (1998). The Years of Persecution: Nazi Germany and the Jews, 1933-1939. Vol. 1.\nGrau, Gunter; Shoppmann, Claudia (1995). The Hidden Holocaust?: Gay and Lesbian Persecution in Germany 1933-45.\nHedgepeth, Sonja; Saidel, Rochelle (2010). Sexual Violence against Jewish Women during the Holocaust.\nPeukert, Detlev (1994). "The Genesis of the \'Final Solution\' from the Spirit of Science". In Thomas Childers; Jane Caplan (eds.). Reevaluating the Third Reich. New York: Holmes & Meier. pp. 234–252. ISBN 0-8419-1178-9.\nRees, Laurence (2017). The Holocaust: A New History. London: Viking Press. ISBN 978-1610398442.\nAméry, Jean (1980). At the Mind\'s Limits: Contemplations by a Survivor on Auschwitz and its Realities.\nBlitz Konig, Nanette (2018). Holocaust Memoirs of a Bergen-Belsen Survivor and Classmate of Anne Frank. Amsterdam Publishers. ISBN 9789492371614.\nDittman, Anita (2005). Trapped in Hitler\'s Hell. ISBN 0-9721512-8-1.\nGerrard, Mady. Full Circle. KLPM. ISBN 978-0955865008.\nKogon, Eugen (1974). Der SS-Staat. Das System der deutschen Konzentrationslager (in German).\nRittner, Carol; Roth, John K. (1998). Different Voices: Women and the Holocaust.\nStojka, Ceija (1988). We Live in Seclusion: The Memories of a Romni.\nSteinberg, Manny (2014). Outcry: Holocaust Memoirs. Amsterdam Publishers. ISBN 978-9-082103137.\nWetzler, Alfréd (2007). Escape from Hell: The True Story of the Auschwitz Protocol. Berghahn Books.\nWinter, Walter (2004). Winter Time: Memoirs of a German Sinto who Survived Auschwitz.\nCzech, Danuta (1999). Auschwitz Chronicle: 1939-1945.\nDean, Martin (1999). Collaboration in the Holocaust: Crimes of the Local Police in Belorussia and Ukraine.\nFings, Karola; Kenrick, Donald, eds. (1999). The Gypsies During the Second World War.\nHogan, David J.; Aretha, David, eds. (2000). The Holocaust Chronicle: A History in Words and Pictures. Lincolnwood, IL: Publications International.\nPressac, Jean-Claude (1989). Auschwitz: Technique and operation of the gas chambers.\nAgamben, Giorgio (1999). Remnants of Auschwitz: The Witness and the Archive.\nBloxham, Donald (2009). The Final Solution: A Genocide.\nLawson, Tom (2010). Debates on the Holocaust. University of Manchester Press.\nLeff, Laurel (2005). Buried By The Times: The Holocaust And America\'s Most Important Newspaper. Cambridge University Press. ISBN 0-521-81287-9.\nMason, Timothy. "Intention and Explanation: A Current Controversy about the Interpretation of National Socialism". In Marrus, Michael R. (ed.). The Nazi Holocaust Part 3, The "Final Solution": The Implementation of Mass Murder. Vol. 1. Westpoint, CT: Mecler. pp. 3–20.\nNiewyk, Donald L. (1992). Holocaust: Problems & Perspective of Interpretation.', - }, + text: 'Bauer, Yehuda (1994). Jews for Sale? Nazi-Jewish Negotiations 1933-1945. Yale University Press. ISBN 0-300-06852-2.\nBerenbaum, Michael (1990). A Mosaic of Victims: Non-Jews Persecuted and Murdered by the Nazis.\nBergen, Doris (2009). War and Genocide: Concise History of the Holocaust.\nBlack, Edwin (2010). The Farhud: The Arab-Nazi Alliance in the Holocaust. Dialog Press. ISBN 0-914153-14-5.\nBraham, Randolph (1994) [1981]. The Politics of Genocide: The Holocaust in Hungary. Columbia University Press. ISBN 0-88033-247-6.\nBraham, Randolph (2011). The Auschwitz Reports and the Holocaust in Hungary. Columbia University Press.\nChalmers, Beverley (2015). Birth, Sex and Abuse: Women\'s Voices Under Nazi Rule. Grosvenor House Publishing Ltd. ISBN 1781483531.\nDavies, Norman; Lukas, Richard C. (2001) [1996]. Forgotten Holocaust: The Poles Under German Occupation.\nDean, Martin (2008). Robbing the Jews: The Confiscation of Jewish Property in the Holocaust. Cambridge University Press.\nEvans, Suzanne (2004). Forgotten Crimes: The Holocaust and People with Disabilities.\nFriedländer, Saul (1998). The Years of Persecution: Nazi Germany and the Jews, 1933-1939. Vol. 1.\nGrau, Gunter; Shoppmann, Claudia (1995). The Hidden Holocaust?: Gay and Lesbian Persecution in Germany 1933-45.\nHedgepeth, Sonja; Saidel, Rochelle (2010). Sexual Violence against Jewish Women during the Holocaust.\nPeukert, Detlev (1994). "The Genesis of the \'Final Solution\' from the Spirit of Science". In Thomas Childers; Jane Caplan (eds.). Reevaluating the Third Reich. New York: Holmes & Meier. pp. 234–252. ISBN 0-8419-1178-9.\nRees, Laurence (2017). The Holocaust: A New History. London: Viking Press. ISBN 978-1610398442.\nAméry, Jean (1980). At the Mind\'s Limits: Contemplations by a Survivor on Auschwitz and its Realities.\nBlitz Konig, Nanette (2018). Holocaust Memoirs of a Bergen-Belsen Survivor and Classmate of Anne Frank. Amsterdam Publishers. ISBN 9789492371614.\nDittman, Anita (2005). Trapped in Hitler\'s Hell. ISBN 0-9721512-8-1.\nGerrard, Mady. Full Circle. KLPM. ISBN 978-0955865008.\nKogon, Eugen (1974). Der SS-Staat. Das System der deutschen Konzentrationslager (in German).\nRittner, Carol; Roth, John K. (1998). Different Voices: Women and the Holocaust.\nStojka, Ceija (1988). We Live in Seclusion: The Memories of a Romni.\nSteinberg, Manny (2014). Outcry: Holocaust Memoirs. Amsterdam Publishers. ISBN 978-9-082103137.\nWetzler, Alfréd (2007). Escape from Hell: The True Story of the Auschwitz Protocol. Berghahn Books.\nWinter, Walter (2004). Winter Time: Memoirs of a German Sinto who Survived Auschwitz.\nCzech, Danuta (1999). Auschwitz Chronicle: 1939-1945.\nDean, Martin (1999). Collaboration in the Holocaust: Crimes of the Local Police in Belorussia and Ukraine.\nFings, Karola; Kenrick, Donald, eds. (1999). The Gypsies During the Second World War.\nHogan, David J.; Aretha, David, eds. (2000). The Holocaust Chronicle: A History in Words and Pictures. Lincolnwood, IL: Publications International.\nPressac, Jean-Claude (1989). Auschwitz: Technique and operation of the gas chambers.\nAgamben, Giorgio (1999). Remnants of Auschwitz: The Witness and the Archive.\nBloxham, Donald (2009). The Final Solution: A Genocide.\nLawson, Tom (2010). Debates on the Holocaust. University of Manchester Press.\nLeff, Laurel (2005). Buried By The Times: The Holocaust And America\'s Most Important Newspaper. Cambridge University Press. ISBN 0-521-81287-9.\nMason, Timothy. "Intention and Explanation: A Current Controversy about the Interpretation of National Socialism". In Marrus, Michael R. (ed.). The Nazi Holocaust Part 3, The "Final Solution": The Implementation of Mass Murder. Vol. 1. Westpoint, CT: Mecler. pp. 3–20.\nNiewyk, Donald L. (1992). Holocaust: Problems & Perspective of Interpretation.', + }, ]; diff --git a/openlibrary/components/BulkSearch/utils/searchUtils.js b/openlibrary/components/BulkSearch/utils/searchUtils.js index c4f35c80158..b95d98f4f5e 100644 --- a/openlibrary/components/BulkSearch/utils/searchUtils.js +++ b/openlibrary/components/BulkSearch/utils/searchUtils.js @@ -9,49 +9,49 @@ const OL_SEARCH_BASE = 'openlibrary.org'; * @param {MatchOptions} matchOptions */ export function buildSearchUrl(extractedBook, matchOptions, json = true) { - let title = extractedBook.title?.split(/[:(?]/)[0].replace(/’/g, "'"); - const author = extractedBook.author; - // Remove leading articles from title; these can sometimes be missing from OL records, - // and will hence cause a failed match. - // Taken from https://github.com/internetarchive/openlibrary/blob/4d880c1bf3e2391dd001c7818052fd639d38ff58/conf/solr/conf/managed-schema.xml#L526 - title = title - .replace( - /^(an? |the |l[aeo]s? |l'|de la |el |il |un[ae]? |du |de[imrst]? |das |ein |eine[mnrs]? |bir )/i, - '', - ) - .trim(); - const query = []; + let title = extractedBook.title?.split(/[:(?]/)[0].replace(/’/g, '\''); + const author = extractedBook.author; + // Remove leading articles from title; these can sometimes be missing from OL records, + // and will hence cause a failed match. + // Taken from https://github.com/internetarchive/openlibrary/blob/4d880c1bf3e2391dd001c7818052fd639d38ff58/conf/solr/conf/managed-schema.xml#L526 + title = title + .replace( + /^(an? |the |l[aeo]s? |l'|de la |el |il |un[ae]? |du |de[imrst]? |das |ein |eine[mnrs]? |bir )/i, + '', + ) + .trim(); + const query = []; - if (title) { - query.push(`title:"${title}"`); - } - if ( - matchOptions.includeAuthor && + if (title) { + query.push(`title:"${title}"`); + } + if ( + matchOptions.includeAuthor && author && author.toLowerCase() !== 'null' && author.toLowerCase() !== 'unknown' - ) { - const authorParts = author - .replace(/^\S+\./, '') - .trim() - .split(/\s/); - const authorLastName = author.includes(',') - ? author.replace(/,.*/, '') - : authorParts[authorParts.length - 1]; - query.push(`author:${authorLastName}`); - } + ) { + const authorParts = author + .replace(/^\S+\./, '') + .trim() + .split(/\s/); + const authorLastName = author.includes(',') + ? author.replace(/,.*/, '') + : authorParts[authorParts.length - 1]; + query.push(`author:${authorLastName}`); + } - if (extractedBook.isbn) { - query.push(`isbn:${extractedBook.isbn}`); - } + if (extractedBook.isbn) { + query.push(`isbn:${extractedBook.isbn}`); + } - let path = `https://${OL_SEARCH_BASE}/search`; - if (json) path += '.json'; - const url = `${path}?${new URLSearchParams({ - q: query.join(' '), - mode: 'everything', - fields: + let path = `https://${OL_SEARCH_BASE}/search`; + if (json) path += '.json'; + const url = `${path}?${new URLSearchParams({ + q: query.join(' '), + mode: 'everything', + fields: 'key,title,author_name,cover_i,first_publish_year,edition_count,ebook_access', - })}`; - return url; + })}`; + return url; } diff --git a/openlibrary/components/IdentifiersInput/utils/utils.js b/openlibrary/components/IdentifiersInput/utils/utils.js index 98b18458806..bc2fe044e5b 100644 --- a/openlibrary/components/IdentifiersInput/utils/utils.js +++ b/openlibrary/components/IdentifiersInput/utils/utils.js @@ -1,100 +1,100 @@ import { - isChecksumValidIsbn10, - isChecksumValidIsbn13, - isFormatValidIsbn10, - isFormatValidIsbn13, - isValidLccn, - parseIsbn, - parseLccn, + isChecksumValidIsbn10, + isChecksumValidIsbn13, + isFormatValidIsbn10, + isFormatValidIsbn13, + isValidLccn, + parseIsbn, + parseLccn, } from '../../../plugins/openlibrary/js/idValidation.js'; export function errorDisplay(message, error_output) { - let errorSelector; - if (error_output === '#hiddenAuthorIdentifiers') { - errorSelector = document.querySelector('#id-errors-author'); - } else if (error_output === '#hiddenWorkIdentifiers') { - errorSelector = document.querySelector('#id-errors-work'); - } else if (error_output === '#hiddenEditionIdentifiers') { - errorSelector = document.querySelector('#id-errors-edition'); - } - if (message) { - errorSelector.style.display = ''; - errorSelector.innerHTML = `<div>${message}</div>`; - } else { - errorSelector.style.display = 'none'; - errorSelector.innerHTML = ''; - } + let errorSelector; + if (error_output === '#hiddenAuthorIdentifiers') { + errorSelector = document.querySelector('#id-errors-author'); + } else if (error_output === '#hiddenWorkIdentifiers') { + errorSelector = document.querySelector('#id-errors-work'); + } else if (error_output === '#hiddenEditionIdentifiers') { + errorSelector = document.querySelector('#id-errors-edition'); + } + if (message) { + errorSelector.style.display = ''; + errorSelector.innerHTML = `<div>${message}</div>`; + } else { + errorSelector.style.display = 'none'; + errorSelector.innerHTML = ''; + } } function validateIsbn10(value) { - const isbn10_value = parseIsbn(value); - if (!isFormatValidIsbn10(isbn10_value)) { - errorDisplay( - 'ID must be exactly 10 characters [0-9] or X.', - '#hiddenEditionIdentifiers', - ); - return false; - } else if ( - isFormatValidIsbn10(isbn10_value) && + const isbn10_value = parseIsbn(value); + if (!isFormatValidIsbn10(isbn10_value)) { + errorDisplay( + 'ID must be exactly 10 characters [0-9] or X.', + '#hiddenEditionIdentifiers', + ); + return false; + } else if ( + isFormatValidIsbn10(isbn10_value) && !isChecksumValidIsbn10(isbn10_value) - ) { - errorDisplay( - `ISBN ${isbn10_value} may be invalid. Please confirm if you'd like to add it before saving all changes`, - '#hiddenEditionIdentifiers', - ); - } - return true; + ) { + errorDisplay( + `ISBN ${isbn10_value} may be invalid. Please confirm if you'd like to add it before saving all changes`, + '#hiddenEditionIdentifiers', + ); + } + return true; } function validateIsbn13(value) { - const isbn13_value = parseIsbn(value); + const isbn13_value = parseIsbn(value); - if (!isFormatValidIsbn13(isbn13_value)) { - errorDisplay( - 'ID must be exactly 13 digits [0-9]. For example: 978-1-56619-909-4', - '#hiddenEditionIdentifiers', - ); - return false; - } else if ( - isFormatValidIsbn13(isbn13_value) && + if (!isFormatValidIsbn13(isbn13_value)) { + errorDisplay( + 'ID must be exactly 13 digits [0-9]. For example: 978-1-56619-909-4', + '#hiddenEditionIdentifiers', + ); + return false; + } else if ( + isFormatValidIsbn13(isbn13_value) && !isChecksumValidIsbn13(isbn13_value) - ) { - errorDisplay( - `ISBN ${isbn13_value} may be invalid. Please confirm if you'd like to add it before saving all changes`, - '#hiddenEditionIdentifiers', - ); - } - return true; + ) { + errorDisplay( + `ISBN ${isbn13_value} may be invalid. Please confirm if you'd like to add it before saving all changes`, + '#hiddenEditionIdentifiers', + ); + } + return true; } function validateLccn(value) { - const lccn_value = parseLccn(value); + const lccn_value = parseLccn(value); - if (!isValidLccn(lccn_value)) { - errorDisplay('Invalid ID format', '#hiddenEditionIdentifiers'); - return false; - } - return true; + if (!isValidLccn(lccn_value)) { + errorDisplay('Invalid ID format', '#hiddenEditionIdentifiers'); + return false; + } + return true; } export function validateIdentifiers(name, value, entries, error_output) { - let validId = true; - errorDisplay('', error_output); - if (name === '' || name === '---') { + let validId = true; + errorDisplay('', error_output); + if (name === '' || name === '---') { // if somehow an invalid identifier is passed through - errorDisplay('Invalid identifier', error_output); - return false; - } - if (name === 'isbn_10') { - validId = validateIsbn10(value); - } else if (name === 'isbn_13') { - validId = validateIsbn13(value); - } else if (name === 'lccn') { - validId = validateLccn(value); - } - if (Array.from(entries).some((entry) => entry === value) === true) { - validId = false; - errorDisplay('That ID already exists for an identifier.', error_output); - } - return validId; + errorDisplay('Invalid identifier', error_output); + return false; + } + if (name === 'isbn_10') { + validId = validateIsbn10(value); + } else if (name === 'isbn_13') { + validId = validateIsbn13(value); + } else if (name === 'lccn') { + validId = validateLccn(value); + } + if (Array.from(entries).some((entry) => entry === value) === true) { + validId = false; + errorDisplay('That ID already exists for an identifier.', error_output); + } + return validId; } diff --git a/openlibrary/components/LibraryExplorer/utils.js b/openlibrary/components/LibraryExplorer/utils.js index 2712c5cfb9c..a87c0c6c8be 100644 --- a/openlibrary/components/LibraryExplorer/utils.js +++ b/openlibrary/components/LibraryExplorer/utils.js @@ -6,13 +6,13 @@ * @param {(node: T) => void} fn */ export function recurForEach(node, fn) { - if (!node) return; - fn(node); - if (!node.children) return; - for (const child of node.children) { - recurForEach(child, fn); - } - return node; + if (!node) return; + fn(node); + if (!node.children) return; + for (const child of node.children) { + recurForEach(child, fn); + } + return node; } /** @@ -20,11 +20,11 @@ export function recurForEach(node, fn) { * @param {string} str */ export function hashCode(str) { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - } - return hash; + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + return hash; } /* @@ -35,12 +35,12 @@ export function hashCode(str) { * @returns {T[]} */ export function hierarchyFind(node, predicate) { - if (!predicate(node)) return []; - for (const child of node.children || []) { - const childResult = hierarchyFind(child, predicate); - if (childResult.length) return [node, ...childResult]; - } - return [node]; + if (!predicate(node)) return []; + for (const child of node.children || []) { + const childResult = hierarchyFind(child, predicate); + if (childResult.length) return [node, ...childResult]; + } + return [node]; } /** @@ -51,14 +51,14 @@ export function hierarchyFind(node, predicate) { * @param {string} string */ export function testLuceneSyntax(pattern, string) { - if (pattern.endsWith('*')) { - return string.startsWith(pattern.slice(0, -1)); - } else if (pattern.endsWith(']')) { - const [lo, hi] = pattern.slice(1, -1).split(' TO '); - return string >= lo && string <= hi; - } else { - throw new Error(`Unsupported lucene syntax: ${pattern}`); - } + if (pattern.endsWith('*')) { + return string.startsWith(pattern.slice(0, -1)); + } else if (pattern.endsWith(']')) { + const [lo, hi] = pattern.slice(1, -1).split(' TO '); + return string >= lo && string <= hi; + } else { + throw new Error(`Unsupported lucene syntax: ${pattern}`); + } } /** @@ -67,29 +67,29 @@ export function testLuceneSyntax(pattern, string) { * @param {string} string */ export function decrementStringSolr( - string, - caseSensitive = true, - numeric = false, + string, + caseSensitive = true, + numeric = false, ) { - const lastChar = caseSensitive - ? string[string.length - 1] - : string[string.length - 1].toUpperCase(); - // Anything < '.' will likely cause query issues, so assume it's - // the end of the that prefix. - // Also append Z; this is the equivalent of going back one, and then expanding (e.g. 0.123 decremented is not 0.122, it's 0.12999999) - const maxTail = (numeric ? '9' : 'z').repeat(5); - const newLastChar = + const lastChar = caseSensitive + ? string[string.length - 1] + : string[string.length - 1].toUpperCase(); + // Anything < '.' will likely cause query issues, so assume it's + // the end of the that prefix. + // Also append Z; this is the equivalent of going back one, and then expanding (e.g. 0.123 decremented is not 0.122, it's 0.12999999) + const maxTail = (numeric ? '9' : 'z').repeat(5); + const newLastChar = lastChar === '.' - ? '' - : lastChar === '0' - ? `.${maxTail}` - : lastChar === 'A' - ? `9${maxTail}` - : lastChar === 'a' - ? `Z${maxTail}` - : `${String.fromCharCode(lastChar.charCodeAt(0) - 1)}${maxTail}`; + ? '' + : lastChar === '0' + ? `.${maxTail}` + : lastChar === 'A' + ? `9${maxTail}` + : lastChar === 'a' + ? `Z${maxTail}` + : `${String.fromCharCode(lastChar.charCodeAt(0) - 1)}${maxTail}`; - return string.slice(0, -1) + newLastChar; + return string.slice(0, -1) + newLastChar; } /** @@ -100,13 +100,13 @@ export function decrementStringSolr( * @returns {Promise<T | undefined>} - A promise that resolves to the truthy value returned by the function, or undefined if the timeout is reached. */ export async function pollUntilTruthy(fn, { timeout = 1000, step = 100 } = {}) { - const start = Date.now(); - while (Date.now() - start <= timeout) { - const val = fn(); - if (val) return val; - await new Promise((resolve) => setTimeout(resolve, step)); - } - return undefined; + const start = Date.now(); + while (Date.now() - start <= timeout) { + const val = fn(); + if (val) return val; + await new Promise((resolve) => setTimeout(resolve, step)); + } + return undefined; } /** diff --git a/openlibrary/components/LibraryExplorer/utils/lcc.js b/openlibrary/components/LibraryExplorer/utils/lcc.js index 67a9ed81802..1043ff32ac0 100644 --- a/openlibrary/components/LibraryExplorer/utils/lcc.js +++ b/openlibrary/components/LibraryExplorer/utils/lcc.js @@ -4,7 +4,7 @@ */ const LCC_PARTS_RE = new RegExp( - String.raw` + String.raw` ^ (?<letters>[A-HJ-NP-VWZ][A-Z-]{0,2}) \s? @@ -12,41 +12,41 @@ const LCC_PARTS_RE = new RegExp( (?<cutter1>\s*\.\s*[^\d\s\[]{1,3}\d*\S*)? (?<rest>\s.*)? $`.replace(/\s/g, ''), - 'i', + 'i', ); export function short_lcc_to_sortable_lcc(lcc) { - const m = clean_raw_lcc(lcc).match(LCC_PARTS_RE); - if (!m) return null; + const m = clean_raw_lcc(lcc).match(LCC_PARTS_RE); + if (!m) return null; - const letters = m.groups.letters.toUpperCase().padEnd(3, '-'); - const number = parseFloat(m.groups.number || 0); - const cutter1 = m.groups.cutter1 - ? `.${m.groups.cutter1.replace(/^[ .]+/, '')}` - : ''; - const rest = m.groups.rest ? ` ${m.groups.rest}` : ''; + const letters = m.groups.letters.toUpperCase().padEnd(3, '-'); + const number = parseFloat(m.groups.number || 0); + const cutter1 = m.groups.cutter1 + ? `.${m.groups.cutter1.replace(/^[ .]+/, '')}` + : ''; + const rest = m.groups.rest ? ` ${m.groups.rest}` : ''; - // There will often be a CPB Box No (whatever that is) in the LCC field; - // E.g. "CPB Box no. 1516 vol. 17" - // Although this might be useful to search by, it's not really an LCC, - // so considering it invalid here. - if (letters === 'CPB') return null; + // There will often be a CPB Box No (whatever that is) in the LCC field; + // E.g. "CPB Box no. 1516 vol. 17" + // Although this might be useful to search by, it's not really an LCC, + // so considering it invalid here. + if (letters === 'CPB') return null; - return `${letters}${number.toFixed(8).padStart(13, '0')}${cutter1}${rest}`; + return `${letters}${number.toFixed(8).padStart(13, '0')}${cutter1}${rest}`; } /** * @param {string} lcc */ export function sortable_lcc_to_short_lcc(lcc) { - const m = lcc.match(LCC_PARTS_RE); - const parts = { - letters: m.groups.letters.replace(/-+/, ''), - number: parseFloat(m.groups.number), - cutter1: m.groups.cutter1 ? m.groups.cutter1.trim() : '', - rest: m.groups.rest ? ` ${m.groups.rest}` : '', - }; - return `${parts.letters}${parts.number}${parts.cutter1}${parts.rest}`; + const m = lcc.match(LCC_PARTS_RE); + const parts = { + letters: m.groups.letters.replace(/-+/, ''), + number: parseFloat(m.groups.number), + cutter1: m.groups.cutter1 ? m.groups.cutter1.trim() : '', + rest: m.groups.rest ? ` ${m.groups.rest}` : '', + }; + return `${parts.letters}${parts.number}${parts.cutter1}${parts.rest}`; } /** @@ -55,12 +55,12 @@ export function sortable_lcc_to_short_lcc(lcc) { * @return {string} */ export function clean_raw_lcc(raw_lcc) { - let lcc = raw_lcc.replace(/\\/g, ' ').trim(); - if ( - (lcc.startsWith('[') && lcc.endsWith(']')) || + let lcc = raw_lcc.replace(/\\/g, ' ').trim(); + if ( + (lcc.startsWith('[') && lcc.endsWith(']')) || (lcc.startsWith('(') && lcc.endsWith(')')) - ) { - lcc = lcc.slice(1, -1); - } - return lcc; + ) { + lcc = lcc.slice(1, -1); + } + return lcc; } diff --git a/openlibrary/components/ObservationForm/ObservationService.js b/openlibrary/components/ObservationForm/ObservationService.js index 899ce97238e..c9fbcc97f53 100644 --- a/openlibrary/components/ObservationForm/ObservationService.js +++ b/openlibrary/components/ObservationForm/ObservationService.js @@ -10,23 +10,23 @@ * @returns A Promise representing the state of the POST request. */ export function updateObservation(action, type, value, workKey, username) { - const data = constructDataObject(type, value, username, action); + const data = constructDataObject(type, value, username, action); - return fetch(`${workKey}/observations`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }) - .then((response) => { - if (!response.ok) { - throw new Error('Server response was not ok'); - } + return fetch(`${workKey}/observations`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), }) - .catch((error) => { - throw error; - }); + .then((response) => { + if (!response.ok) { + throw new Error('Server response was not ok'); + } + }) + .catch((error) => { + throw error; + }); } /** @@ -48,13 +48,13 @@ export function updateObservation(action, type, value, workKey, username) { * @returns An object that represents the observation update that will be made. */ function constructDataObject(type, value, username, action) { - const data = { - username: username, - action: action, - observation: {}, - }; + const data = { + username: username, + action: action, + observation: {}, + }; - data.observation[type] = value; + data.observation[type] = value; - return data; + return data; } diff --git a/openlibrary/components/ObservationForm/Utils.js b/openlibrary/components/ObservationForm/Utils.js index 19a48afb01e..5f56344a50c 100644 --- a/openlibrary/components/ObservationForm/Utils.js +++ b/openlibrary/components/ObservationForm/Utils.js @@ -5,7 +5,7 @@ * @returns A JavaScript object */ export function decodeAndParseJSON(str) { - return JSON.parse(decodeURIComponent(str)); + return JSON.parse(decodeURIComponent(str)); } /* @@ -13,11 +13,11 @@ export function decodeAndParseJSON(str) { window.$.colorbox is a jQuery plugin */ export function resizeColorbox() { - if ( - window.$ && + if ( + window.$ && window.$.colorbox && typeof window.$.colorbox.resize === 'function' - ) { - window.$.colorbox.resize(); - } + ) { + window.$.colorbox.resize(); + } } diff --git a/openlibrary/components/configs.js b/openlibrary/components/configs.js index ae7514394f7..4fe49f387ac 100644 --- a/openlibrary/components/configs.js +++ b/openlibrary/components/configs.js @@ -8,29 +8,29 @@ const OL_BASE_DEFAULT = urlParams.get('ol_base') || (IS_VUE_APP ? 'openlibrary.org' : ''); const CONFIGS = { - OL_BASE_COVERS: urlParams.get('ol_base_covers') || 'covers.openlibrary.org', - OL_BASE_SEARCH: urlParams.get('ol_base_search') || OL_BASE_DEFAULT || '', - OL_BASE_BOOKS: urlParams.get('ol_base_books') || OL_BASE_DEFAULT || '', - OL_BASE_LANGS: urlParams.get('ol_base_langs') || OL_BASE_DEFAULT || '', - // Make the save location explicitly different from ol_base to avoid - // accidentally triggering saves to prod (which shouldn't work anyways - // due to cookies, but just in case!) - OL_BASE_SAVES: urlParams.get('ol_base_saves') || '', - OL_BASE_PUBLIC: urlParams.get('ol_base') || 'openlibrary.org', - DEBUG_MODE: urlParams.get('debug') === 'true', - LANG: urlParams.get('lang'), + OL_BASE_COVERS: urlParams.get('ol_base_covers') || 'covers.openlibrary.org', + OL_BASE_SEARCH: urlParams.get('ol_base_search') || OL_BASE_DEFAULT || '', + OL_BASE_BOOKS: urlParams.get('ol_base_books') || OL_BASE_DEFAULT || '', + OL_BASE_LANGS: urlParams.get('ol_base_langs') || OL_BASE_DEFAULT || '', + // Make the save location explicitly different from ol_base to avoid + // accidentally triggering saves to prod (which shouldn't work anyways + // due to cookies, but just in case!) + OL_BASE_SAVES: urlParams.get('ol_base_saves') || '', + OL_BASE_PUBLIC: urlParams.get('ol_base') || 'openlibrary.org', + DEBUG_MODE: urlParams.get('debug') === 'true', + LANG: urlParams.get('lang'), }; for (const key of [ - 'OL_BASE_COVERS', - 'OL_BASE_SEARCH', - 'OL_BASE_BOOKS', - 'OL_BASE_LANGS', - 'OL_BASE_SAVES', + 'OL_BASE_COVERS', + 'OL_BASE_SEARCH', + 'OL_BASE_BOOKS', + 'OL_BASE_LANGS', + 'OL_BASE_SAVES', ]) { - if (CONFIGS[key] && !CONFIGS[key].startsWith('http')) { - CONFIGS[key] = `https://${CONFIGS[key]}`; - } + if (CONFIGS[key] && !CONFIGS[key].startsWith('http')) { + CONFIGS[key] = `https://${CONFIGS[key]}`; + } } export default CONFIGS; diff --git a/openlibrary/components/dev/vite.config.js b/openlibrary/components/dev/vite.config.js index 3e70f22e767..872d385d734 100644 --- a/openlibrary/components/dev/vite.config.js +++ b/openlibrary/components/dev/vite.config.js @@ -8,5 +8,5 @@ import { defineConfig } from 'vite'; // https://vite.dev/config/ export default defineConfig({ - plugins: [vue()], + plugins: [vue()], }); diff --git a/openlibrary/components/lit/OLChip.js b/openlibrary/components/lit/OLChip.js index 7511ed60d3e..613bfda0b6a 100644 --- a/openlibrary/components/lit/OLChip.js +++ b/openlibrary/components/lit/OLChip.js @@ -24,15 +24,15 @@ import { css, html, LitElement, nothing } from 'lit'; * <ol-chip size="small" count="76" href="/subjects/fiction">Fiction</ol-chip> */ export class OLChip extends LitElement { - static properties = { - selected: { type: Boolean, reflect: true }, - size: { type: String, reflect: true }, - href: { type: String }, - count: { type: String }, - accessibleLabel: { type: String, attribute: 'accessible-label' }, - }; - - static styles = css` + static properties = { + selected: { type: Boolean, reflect: true }, + size: { type: String, reflect: true }, + href: { type: String }, + count: { type: String }, + accessibleLabel: { type: String, attribute: 'accessible-label' }, + }; + + static styles = css` :host { --chip-padding-block: 6px; --chip-padding-inline: 12px; @@ -127,29 +127,29 @@ export class OLChip extends LitElement { } `; - constructor() { - super(); - this.selected = false; - this.size = 'medium'; - this.href = null; - this.count = null; - this.accessibleLabel = null; - } - - _handleClick() { - this.dispatchEvent( - new CustomEvent('ol-chip-select', { - bubbles: true, - composed: true, - detail: { selected: !this.selected }, - }), - ); - } - - _renderIcons() { - if (!this.selected) return nothing; - - return html` + constructor() { + super(); + this.selected = false; + this.size = 'medium'; + this.href = null; + this.count = null; + this.accessibleLabel = null; + } + + _handleClick() { + this.dispatchEvent( + new CustomEvent('ol-chip-select', { + bubbles: true, + composed: true, + detail: { selected: !this.selected }, + }), + ); + } + + _renderIcons() { + if (!this.selected) return nothing; + + return html` <span class="icon-slot"> <svg class="icon" @@ -165,32 +165,32 @@ export class OLChip extends LitElement { </svg> </span> `; - } + } - _renderCount() { - if (this.count === null) return nothing; + _renderCount() { + if (this.count === null) return nothing; - return html`<span class="count">${this.count}</span>`; - } + return html`<span class="count">${this.count}</span>`; + } - render() { - const content = html` + render() { + const content = html` ${this._renderIcons()} <slot></slot> ${this._renderCount()} `; - if (this.href) { - return html` + if (this.href) { + return html` <a class="chip" href=${this.href} aria-label=${this.accessibleLabel || nothing} @click=${this._handleClick}> ${content} </a> `; - } + } - return html` + return html` <button class="chip" type="button" aria-label=${this.accessibleLabel || nothing} aria-pressed=${this.selected} @@ -198,7 +198,7 @@ export class OLChip extends LitElement { ${content} </button> `; - } + } } customElements.define('ol-chip', OLChip); diff --git a/openlibrary/components/lit/OLChipGroup.js b/openlibrary/components/lit/OLChipGroup.js index 94fc4becf55..78bf876dfa3 100644 --- a/openlibrary/components/lit/OLChipGroup.js +++ b/openlibrary/components/lit/OLChipGroup.js @@ -15,11 +15,11 @@ import { css, html, LitElement } from 'lit'; * </ol-chip-group> */ export class OLChipGroup extends LitElement { - static properties = { - gap: { type: String, reflect: true }, - }; + static properties = { + gap: { type: String, reflect: true }, + }; - static styles = css` + static styles = css` :host { display: flex; flex-wrap: wrap; @@ -35,21 +35,21 @@ export class OLChipGroup extends LitElement { } `; - constructor() { - super(); - this.gap = 'medium'; - } + constructor() { + super(); + this.gap = 'medium'; + } - connectedCallback() { - super.connectedCallback(); - if (!this.hasAttribute('role')) { - this.setAttribute('role', 'group'); + connectedCallback() { + super.connectedCallback(); + if (!this.hasAttribute('role')) { + this.setAttribute('role', 'group'); + } } - } - render() { - return html`<slot></slot>`; - } + render() { + return html`<slot></slot>`; + } } customElements.define('ol-chip-group', OLChipGroup); diff --git a/openlibrary/components/lit/OLReadMore.js b/openlibrary/components/lit/OLReadMore.js index c304705d449..b2d7df97577 100644 --- a/openlibrary/components/lit/OLReadMore.js +++ b/openlibrary/components/lit/OLReadMore.js @@ -25,18 +25,18 @@ import { css, html, LitElement } from 'lit'; * </ol-read-more> */ export class OLReadMore extends LitElement { - static properties = { - maxHeight: { type: String, attribute: 'max-height' }, - moreText: { type: String, attribute: 'more-text' }, - lessText: { type: String, attribute: 'less-text' }, - backgroundColor: { type: String, attribute: 'background-color' }, - labelSize: { type: String, attribute: 'label-size' }, - // Internal state - _expanded: { type: Boolean, state: true }, - _unnecessary: { type: Boolean, state: true }, - }; - - static styles = css` + static properties = { + maxHeight: { type: String, attribute: 'max-height' }, + moreText: { type: String, attribute: 'more-text' }, + lessText: { type: String, attribute: 'less-text' }, + backgroundColor: { type: String, attribute: 'background-color' }, + labelSize: { type: String, attribute: 'label-size' }, + // Internal state + _expanded: { type: Boolean, state: true }, + _unnecessary: { type: Boolean, state: true }, + }; + + static styles = css` :host { display: block; position: relative; @@ -113,91 +113,91 @@ export class OLReadMore extends LitElement { } `; - constructor() { - super(); - this.maxHeight = '80px'; - this.moreText = 'Read More'; - this.lessText = 'Read Less'; - this.backgroundColor = null; - this.labelSize = 'medium'; - this._expanded = false; - this._unnecessary = false; - } - - firstUpdated() { - this._checkIfTruncationNeeded(); - this._updateBackgroundColor(); - // Remove styles that were used to prevent layout shift - // Now that the component has rendered, it can size naturally - this.style.minHeight = 'auto'; - this.style.visibility = 'visible'; - this.style.overflow = 'visible'; - } - - updated(changedProperties) { - if (changedProperties.has('backgroundColor')) { - this._updateBackgroundColor(); + constructor() { + super(); + this.maxHeight = '80px'; + this.moreText = 'Read More'; + this.lessText = 'Read Less'; + this.backgroundColor = null; + this.labelSize = 'medium'; + this._expanded = false; + this._unnecessary = false; } - } - - _updateBackgroundColor() { - if (this.backgroundColor) { - this.style.setProperty( - '--ol-readmore-gradient-color', - this.backgroundColor, - ); - } - } - _checkIfTruncationNeeded() { - const content = this.shadowRoot.querySelector('.content-wrapper'); - if (!content) return; + firstUpdated() { + this._checkIfTruncationNeeded(); + this._updateBackgroundColor(); + // Remove styles that were used to prevent layout shift + // Now that the component has rendered, it can size naturally + this.style.minHeight = 'auto'; + this.style.visibility = 'visible'; + this.style.overflow = 'visible'; + } - const isOverflowing = content.scrollHeight > content.clientHeight; - this._unnecessary = !isOverflowing; + updated(changedProperties) { + if (changedProperties.has('backgroundColor')) { + this._updateBackgroundColor(); + } + } - if (this._unnecessary) { - this._expanded = true; + _updateBackgroundColor() { + if (this.backgroundColor) { + this.style.setProperty( + '--ol-readmore-gradient-color', + this.backgroundColor, + ); + } } - } - - _handleMoreClick() { - if (this._unnecessary) return; - this._expanded = true; - } - - _handleLessClick() { - if (this._unnecessary) return; - this._expanded = false; - - // Scroll back to top when collapsing if component is off-screen - const rect = this.getBoundingClientRect(); - if (rect.top < 0) { - this.scrollIntoView({ - behavior: 'smooth', - block: 'start', - }); + + _checkIfTruncationNeeded() { + const content = this.shadowRoot.querySelector('.content-wrapper'); + if (!content) return; + + const isOverflowing = content.scrollHeight > content.clientHeight; + this._unnecessary = !isOverflowing; + + if (this._unnecessary) { + this._expanded = true; + } } - } - _getContentStyle() { - if (this._expanded) { - return ''; + _handleMoreClick() { + if (this._unnecessary) return; + this._expanded = true; } - if (this.maxHeight) { - return `max-height: ${this.maxHeight}`; + _handleLessClick() { + if (this._unnecessary) return; + this._expanded = false; + + // Scroll back to top when collapsing if component is off-screen + const rect = this.getBoundingClientRect(); + if (rect.top < 0) { + this.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + } } - return ''; - } + _getContentStyle() { + if (this._expanded) { + return ''; + } + + if (this.maxHeight) { + return `max-height: ${this.maxHeight}`; + } + + return ''; + } - render() { - const showMoreBtn = !this._expanded && !this._unnecessary; - const showLessBtn = this._expanded && !this._unnecessary; - const sizeClass = this.labelSize === 'small' ? 'small' : ''; + render() { + const showMoreBtn = !this._expanded && !this._unnecessary; + const showLessBtn = this._expanded && !this._unnecessary; + const sizeClass = this.labelSize === 'small' ? 'small' : ''; - return html` + return html` <div class="content-wrapper ${this._expanded ? 'expanded' : ''}" style="${this._getContentStyle()}" @@ -223,7 +223,7 @@ export class OLReadMore extends LitElement { <svg class="chevron up" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg> </button> `; - } + } } customElements.define('ol-read-more', OLReadMore); diff --git a/openlibrary/components/lit/OlPagination.js b/openlibrary/components/lit/OlPagination.js index faa3c5049ad..e092cf6727c 100644 --- a/openlibrary/components/lit/OlPagination.js +++ b/openlibrary/components/lit/OlPagination.js @@ -56,21 +56,21 @@ import { css, html, LitElement } from 'lit'; * ></ol-pagination> */ export class OlPagination extends LitElement { - static properties = { - mode: { type: String }, - totalPages: { type: Number, attribute: 'total-pages' }, - currentPage: { type: Number, attribute: 'current-page' }, - hasNextPage: { type: Boolean, attribute: 'has-next-page' }, - baseUrl: { type: String, attribute: 'base-url' }, - labelPreviousPage: { type: String, attribute: 'label-previous-page' }, - labelNextPage: { type: String, attribute: 'label-next-page' }, - labelGoToPage: { type: String, attribute: 'label-go-to-page' }, - labelCurrentPage: { type: String, attribute: 'label-current-page' }, - labelPagination: { type: String, attribute: 'label-pagination' }, - _focusedIndex: { type: Number, state: true }, - }; - - static styles = css` + static properties = { + mode: { type: String }, + totalPages: { type: Number, attribute: 'total-pages' }, + currentPage: { type: Number, attribute: 'current-page' }, + hasNextPage: { type: Boolean, attribute: 'has-next-page' }, + baseUrl: { type: String, attribute: 'base-url' }, + labelPreviousPage: { type: String, attribute: 'label-previous-page' }, + labelNextPage: { type: String, attribute: 'label-next-page' }, + labelGoToPage: { type: String, attribute: 'label-go-to-page' }, + labelCurrentPage: { type: String, attribute: 'label-current-page' }, + labelPagination: { type: String, attribute: 'label-pagination' }, + _focusedIndex: { type: Number, state: true }, + }; + + static styles = css` :host { display: block; font-family: var(--font-family-body); @@ -141,64 +141,64 @@ export class OlPagination extends LitElement { } `; - /** Left chevron arrow icon */ - static _leftArrowIcon = - html`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>`; - - /** Right chevron arrow icon */ - static _rightArrowIcon = - html`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>`; - - constructor() { - super(); - this.mode = 'full'; - this.totalPages = 1; - this.currentPage = 1; - this.hasNextPage = false; - this.baseUrl = ''; - this._focusedIndex = -1; - - // Translatable label defaults (English) - this.labelPreviousPage = 'Go to previous page'; - this.labelNextPage = 'Go to next page'; - this.labelGoToPage = 'Go to page {page}'; - this.labelCurrentPage = 'Page {page}, current page'; - this.labelPagination = 'Pagination'; - } - - /** + /** Left chevron arrow icon */ + static _leftArrowIcon = + html`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>`; + + /** Right chevron arrow icon */ + static _rightArrowIcon = + html`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>`; + + constructor() { + super(); + this.mode = 'full'; + this.totalPages = 1; + this.currentPage = 1; + this.hasNextPage = false; + this.baseUrl = ''; + this._focusedIndex = -1; + + // Translatable label defaults (English) + this.labelPreviousPage = 'Go to previous page'; + this.labelNextPage = 'Go to next page'; + this.labelGoToPage = 'Go to page {page}'; + this.labelCurrentPage = 'Page {page}, current page'; + this.labelPagination = 'Pagination'; + } + + /** * Interpolate a label template by replacing {key} placeholders with values. * @param {String} template - The label template (e.g., "Go to page {page}") * @param {Object} values - Key-value pairs to substitute (e.g., { page: 5 }) * @returns {String} The interpolated string */ - _interpolateLabel(template, values) { - return template.replace(/\{(\w+)\}/g, (_, key) => values[key] ?? ''); - } + _interpolateLabel(template, values) { + return template.replace(/\{(\w+)\}/g, (_, key) => values[key] ?? ''); + } - /** + /** * Build URL for a specific page number. * Uses baseUrl if provided, otherwise falls back to the current window location. * This preserves all existing query parameters (like changequery() does). * @param {Number} page - The page number * @returns {String|null} The URL for the page */ - _getPageUrl(page) { - try { - const base = this.baseUrl || window.location.href; - const url = new URL(base, window.location.origin); - if (page === 1) { - url.searchParams.delete('page'); - } else { - url.searchParams.set('page', page); - } - return url.pathname + url.search; - } catch { - return null; + _getPageUrl(page) { + try { + const base = this.baseUrl || window.location.href; + const url = new URL(base, window.location.origin); + if (page === 1) { + url.searchParams.delete('page'); + } else { + url.searchParams.set('page', page); + } + return url.pathname + url.search; + } catch { + return null; + } } - } - /** + /** * Calculate which page numbers to display based on current page and total pages. * Always shows exactly 5 page numbers max, adjusting position based on current page: * - Near start: 1, 2, 3, 4 ... last (5 total) @@ -206,114 +206,114 @@ export class OlPagination extends LitElement { * - Near end: 1 ... last-3, last-2, last-1, last (5 total) * @returns {Array} Array of page numbers and 'ellipsis' markers */ - _getVisiblePages() { - const total = this.totalPages; - const current = this.currentPage; - - if (total <= 5) return [...Array(total)].map((_, i) => i + 1); - if (current <= 3) return [1, 2, 3, 4, 'ellipsis-right', total]; - if (current >= total - 2) - return [1, 'ellipsis-left', total - 3, total - 2, total - 1, total]; - - return [ - 1, - 'ellipsis-left', - current - 1, - current, - current + 1, - 'ellipsis-right', - total, - ]; - } - - /** + _getVisiblePages() { + const total = this.totalPages; + const current = this.currentPage; + + if (total <= 5) return [...Array(total)].map((_, i) => i + 1); + if (current <= 3) return [1, 2, 3, 4, 'ellipsis-right', total]; + if (current >= total - 2) + return [1, 'ellipsis-left', total - 3, total - 2, total - 1, total]; + + return [ + 1, + 'ellipsis-left', + current - 1, + current, + current + 1, + 'ellipsis-right', + total, + ]; + } + + /** * Get all focusable elements in the pagination * @returns {Array} Array of focusable elements (buttons or anchors) */ - _getFocusableElements() { - return Array.from( - this.shadowRoot.querySelectorAll( - '.pagination-item:not([aria-disabled="true"])', - ), - ); - } - - /** + _getFocusableElements() { + return Array.from( + this.shadowRoot.querySelectorAll( + '.pagination-item:not([aria-disabled="true"])', + ), + ); + } + + /** * Handle keyboard navigation within the pagination * @param {KeyboardEvent} e */ - _handleKeyDown(e) { - const focusable = this._getFocusableElements(); - const currentIndex = focusable.indexOf(this.shadowRoot.activeElement); - - switch (e.key) { - case 'ArrowLeft': - e.preventDefault(); - if (currentIndex > 0) { - focusable[currentIndex - 1].focus(); - } - break; - case 'ArrowRight': - e.preventDefault(); - if (currentIndex < focusable.length - 1) { - focusable[currentIndex + 1].focus(); + _handleKeyDown(e) { + const focusable = this._getFocusableElements(); + const currentIndex = focusable.indexOf(this.shadowRoot.activeElement); + + switch (e.key) { + case 'ArrowLeft': + e.preventDefault(); + if (currentIndex > 0) { + focusable[currentIndex - 1].focus(); + } + break; + case 'ArrowRight': + e.preventDefault(); + if (currentIndex < focusable.length - 1) { + focusable[currentIndex + 1].focus(); + } + break; + case 'Home': + e.preventDefault(); + focusable[0]?.focus(); + break; + case 'End': + e.preventDefault(); + focusable[focusable.length - 1]?.focus(); + break; } - break; - case 'Home': - e.preventDefault(); - focusable[0]?.focus(); - break; - case 'End': - e.preventDefault(); - focusable[focusable.length - 1]?.focus(); - break; } - } - /** + /** * Navigate to a specific page * @param {Number} page - The page number to navigate to */ - _goToPage(page) { - const maxPage = this.mode === 'arrows' ? Infinity : this.totalPages; - if (page < 1 || page > maxPage || page === this.currentPage) { - return; - } + _goToPage(page) { + const maxPage = this.mode === 'arrows' ? Infinity : this.totalPages; + if (page < 1 || page > maxPage || page === this.currentPage) { + return; + } - const event = new CustomEvent('ol-pagination-change', { - detail: { page }, - bubbles: true, - composed: true, - cancelable: true, - }); - this.dispatchEvent(event); - if (event.defaultPrevented) return; + const event = new CustomEvent('ol-pagination-change', { + detail: { page }, + bubbles: true, + composed: true, + cancelable: true, + }); + this.dispatchEvent(event); + if (event.defaultPrevented) return; - this.currentPage = page; - } + this.currentPage = page; + } - /** + /** * Handle click on anchor-based page links. * Dispatches the ol-pagination-change event to allow interception. * If the event is cancelled via preventDefault(), anchor navigation is also prevented. * @param {Event} e - Click event * @param {Number} page - The page number */ - _handlePageClick(e, page) { - const event = new CustomEvent('ol-pagination-change', { - detail: { page }, - bubbles: true, - composed: true, - cancelable: true, - }); - this.dispatchEvent(event); - if (event.defaultPrevented) { - e.preventDefault(); - this.currentPage = page; + _handlePageClick(e, page) { + const event = new CustomEvent('ol-pagination-change', { + detail: { page }, + bubbles: true, + composed: true, + cancelable: true, + }); + this.dispatchEvent(event); + if (event.defaultPrevented) { + e.preventDefault(); + this.currentPage = page; + } } - } - /** + /** * Render a pagination item (button or anchor based on URL mode) * @param {Object} options - Render options * @param {Number} options.page - Target page number @@ -322,13 +322,13 @@ export class OlPagination extends LitElement { * @param {TemplateResult} options.content - Content to render inside the item * @returns {TemplateResult} Lit template for the button or anchor */ - _renderPaginationItem({ page, label, className = '', content }) { - const url = this._getPageUrl(page); - const isCurrent = page === this.currentPage; - const ariaCurrent = isCurrent ? 'page' : 'false'; + _renderPaginationItem({ page, label, className = '', content }) { + const url = this._getPageUrl(page); + const isCurrent = page === this.currentPage; + const ariaCurrent = isCurrent ? 'page' : 'false'; - if (url) { - return html` + if (url) { + return html` <a href=${url} class="pagination-item ${className}" @@ -337,9 +337,9 @@ export class OlPagination extends LitElement { @click=${(e) => this._handlePageClick(e, page)} >${content}</a> `; - } + } - return html` + return html` <button class="pagination-item ${className}" aria-label=${label} @@ -347,74 +347,74 @@ export class OlPagination extends LitElement { @click=${() => this._goToPage(page)} >${content}</button> `; - } + } - /** + /** * Render a single page button/link or ellipsis * @param {Number|String} page - Page number or 'ellipsis-left'/'ellipsis-right' * @returns {TemplateResult} Lit template for the button or anchor */ - _renderPageButton(page) { - if (typeof page === 'string' && page.startsWith('ellipsis')) { - return html`<span class="ellipsis" aria-hidden="true">•••</span>`; - } + _renderPageButton(page) { + if (typeof page === 'string' && page.startsWith('ellipsis')) { + return html`<span class="ellipsis" aria-hidden="true">•••</span>`; + } - const isCurrent = page === this.currentPage; - const label = isCurrent - ? this._interpolateLabel(this.labelCurrentPage, { page }) - : this._interpolateLabel(this.labelGoToPage, { page }); + const isCurrent = page === this.currentPage; + const label = isCurrent + ? this._interpolateLabel(this.labelCurrentPage, { page }) + : this._interpolateLabel(this.labelGoToPage, { page }); - return this._renderPaginationItem({ page, label, content: page }); - } + return this._renderPaginationItem({ page, label, content: page }); + } - /** + /** * Render a navigation arrow (previous or next) * @param {String} direction - 'prev' or 'next' * @returns {TemplateResult} Lit template for the arrow */ - _renderNavArrow(direction) { - const isPrev = direction === 'prev'; - const isDisabled = isPrev - ? this.currentPage === 1 - : this.mode === 'arrows' - ? !this.hasNextPage - : this.currentPage === this.totalPages; - - if (isDisabled && this.mode !== 'arrows') return html``; - - if (isDisabled) { - const label = isPrev ? this.labelPreviousPage : this.labelNextPage; - const icon = isPrev - ? OlPagination._leftArrowIcon - : OlPagination._rightArrowIcon; - return html` + _renderNavArrow(direction) { + const isPrev = direction === 'prev'; + const isDisabled = isPrev + ? this.currentPage === 1 + : this.mode === 'arrows' + ? !this.hasNextPage + : this.currentPage === this.totalPages; + + if (isDisabled && this.mode !== 'arrows') return html``; + + if (isDisabled) { + const label = isPrev ? this.labelPreviousPage : this.labelNextPage; + const icon = isPrev + ? OlPagination._leftArrowIcon + : OlPagination._rightArrowIcon; + return html` <span class="pagination-item pagination-arrow" aria-disabled="true" aria-label=${label} >${icon}</span> `; + } + + const page = isPrev ? this.currentPage - 1 : this.currentPage + 1; + const label = isPrev ? this.labelPreviousPage : this.labelNextPage; + const icon = isPrev + ? OlPagination._leftArrowIcon + : OlPagination._rightArrowIcon; + + return this._renderPaginationItem({ + page, + label, + className: 'pagination-arrow', + content: icon, + }); } - const page = isPrev ? this.currentPage - 1 : this.currentPage + 1; - const label = isPrev ? this.labelPreviousPage : this.labelNextPage; - const icon = isPrev - ? OlPagination._leftArrowIcon - : OlPagination._rightArrowIcon; - - return this._renderPaginationItem({ - page, - label, - className: 'pagination-arrow', - content: icon, - }); - } - - render() { - const isArrows = this.mode === 'arrows'; - const visiblePages = isArrows ? [] : this._getVisiblePages(); - - return html` + render() { + const isArrows = this.mode === 'arrows'; + const visiblePages = isArrows ? [] : this._getVisiblePages(); + + return html` <nav class="pagination" role="navigation" @@ -426,7 +426,7 @@ export class OlPagination extends LitElement { ${this._renderNavArrow('next')} </nav> `; - } + } } customElements.define('ol-pagination', OlPagination); diff --git a/openlibrary/components/lit/OlPopover.js b/openlibrary/components/lit/OlPopover.js index dc303035d9c..0dc0737c0eb 100644 --- a/openlibrary/components/lit/OlPopover.js +++ b/openlibrary/components/lit/OlPopover.js @@ -46,24 +46,24 @@ const FOCUSABLE = * </ol-popover> */ export class OlPopover extends LitElement { - static properties = { - open: { type: Boolean, reflect: true }, - placement: { type: String }, - offset: { type: Number }, - accessibleLabel: { type: String, attribute: 'accessible-label' }, - autoClose: { type: Boolean, attribute: 'auto-close' }, - _position: { state: true }, - _transformOrigin: { state: true }, - _animState: { state: true }, - _mobile: { state: true }, - }; - - // Animation states: closed → preparing → entering → open → exiting → closed - // "preparing" renders the panel in the DOM at its start position (opacity 0, - // scale 0.95) without a transition so the browser paints it. We measure the - // panel here for collision detection, then move to "entering". - - static styles = css` + static properties = { + open: { type: Boolean, reflect: true }, + placement: { type: String }, + offset: { type: Number }, + accessibleLabel: { type: String, attribute: 'accessible-label' }, + autoClose: { type: Boolean, attribute: 'auto-close' }, + _position: { state: true }, + _transformOrigin: { state: true }, + _animState: { state: true }, + _mobile: { state: true }, + }; + + // Animation states: closed → preparing → entering → open → exiting → closed + // "preparing" renders the panel in the DOM at its start position (opacity 0, + // scale 0.95) without a transition so the browser paints it. We measure the + // panel here for collision detection, then move to "entering". + + static styles = css` :host { display: inline-flex; align-items: center; @@ -238,55 +238,55 @@ export class OlPopover extends LitElement { } `; - constructor() { - super(); - this.open = false; - this.placement = 'bottom-center'; - this.offset = 4; - this.accessibleLabel = ''; - this.autoClose = true; - this._position = { top: 0, left: 0 }; - this._transformOrigin = 'top left'; - this._animState = 'closed'; - this._mobile = false; - this._panelId = `ol-popover-${++_idCounter}`; - this._prevFocus = null; - this._rafId = null; - this._savedOverflow = null; - - // Touch drag state - this._touchStartY = 0; - this._touchStartTime = 0; - this._isDragging = false; - this._isHandleDrag = false; - this._lastDragY = 0; - - this._onOutsideClick = this._onOutsideClick.bind(this); - this._onKeydownGlobal = this._onKeydownGlobal.bind(this); - this._onScrollResize = this._onScrollResize.bind(this); - this._onTouchStart = this._onTouchStart.bind(this); - this._onTouchMove = this._onTouchMove.bind(this); - this._onTouchEnd = this._onTouchEnd.bind(this); - } - - render() { - const showPanel = this._animState !== 'closed'; - return html` + constructor() { + super(); + this.open = false; + this.placement = 'bottom-center'; + this.offset = 4; + this.accessibleLabel = ''; + this.autoClose = true; + this._position = { top: 0, left: 0 }; + this._transformOrigin = 'top left'; + this._animState = 'closed'; + this._mobile = false; + this._panelId = `ol-popover-${++_idCounter}`; + this._prevFocus = null; + this._rafId = null; + this._savedOverflow = null; + + // Touch drag state + this._touchStartY = 0; + this._touchStartTime = 0; + this._isDragging = false; + this._isHandleDrag = false; + this._lastDragY = 0; + + this._onOutsideClick = this._onOutsideClick.bind(this); + this._onKeydownGlobal = this._onKeydownGlobal.bind(this); + this._onScrollResize = this._onScrollResize.bind(this); + this._onTouchStart = this._onTouchStart.bind(this); + this._onTouchMove = this._onTouchMove.bind(this); + this._onTouchEnd = this._onTouchEnd.bind(this); + } + + render() { + const showPanel = this._animState !== 'closed'; + return html` <slot name="trigger"></slot> ${ - showPanel - ? html` + showPanel + ? html` ${ - this._mobile - ? html` + this._mobile + ? html` <div class="backdrop" data-state="${this._animState}" @click="${this._onBackdropClick}" ></div> ` - : nothing - } + : nothing +} <div id="${this._panelId}" class="panel ${this._mobile ? 'tray' : ''}" @@ -296,14 +296,14 @@ export class OlPopover extends LitElement { aria-label="${ifDefined(this.accessibleLabel || undefined)}" tabindex="-1" style="${ - this._mobile - ? '' - : ` + this._mobile + ? '' + : ` top: ${this._position.top}px; left: ${this._position.left}px; transform-origin: ${this._transformOrigin}; ` - }" +}" @transitionend="${this._onTransitionEnd}" > <span @@ -314,14 +314,14 @@ export class OlPopover extends LitElement { @focus="${this._onSentinelFocus}" ></span> ${ - this._mobile - ? html` + this._mobile + ? html` <div class="tray-handle" aria-hidden="true"> <div class="tray-handle-bar"></div> </div> ` - : nothing - } + : nothing +} <slot></slot> <span class="focus-sentinel" @@ -332,525 +332,525 @@ export class OlPopover extends LitElement { ></span> </div> ` - : nothing - } + : nothing +} `; - } - - firstUpdated() { - const triggerSlot = this.shadowRoot.querySelector('slot[name="trigger"]'); - triggerSlot?.addEventListener('slotchange', () => this._syncTriggerAria()); - } - - updated(changed) { - if (changed.has('open')) { - this._syncTriggerAria(); - if (this.open) { - this._show(); - } else if (changed.get('open') === true) { - this._hide(); - } } - } - // ── Show / Hide ───────────────────────────────────────────── + firstUpdated() { + const triggerSlot = this.shadowRoot.querySelector('slot[name="trigger"]'); + triggerSlot?.addEventListener('slotchange', () => this._syncTriggerAria()); + } - _show() { - this._prevFocus = document.activeElement; + updated(changed) { + if (changed.has('open')) { + this._syncTriggerAria(); + if (this.open) { + this._show(); + } else if (changed.get('open') === true) { + this._hide(); + } + } + } - document.addEventListener('click', this._onOutsideClick, true); - document.addEventListener('keydown', this._onKeydownGlobal); + // ── Show / Hide ───────────────────────────────────────────── - this._mobile = window.matchMedia('(max-width: 767px)').matches; - const reducedMotion = window.matchMedia( - '(prefers-reduced-motion: reduce)', - ).matches; + _show() { + this._prevFocus = document.activeElement; - if (this._mobile) { - this._lockBodyScroll(); - } + document.addEventListener('click', this._onOutsideClick, true); + document.addEventListener('keydown', this._onKeydownGlobal); - // On desktop, render panel off-screen first so we can measure it. - // On mobile, CSS positions the tray at the bottom automatically. - if (!this._mobile) { - this._position = { top: -9999, left: -9999 }; - } - this._animState = reducedMotion ? 'open' : 'preparing'; - - this.updateComplete.then(() => { - const panel = this.shadowRoot.querySelector('.panel'); - if (!panel) return; - - // Desktop: measure and position relative to trigger. - // Use offsetWidth/Height — getBoundingClientRect includes the - // scale(0.95) transform from the preparing state, under-reporting - // the true layout size by 5%. - if (!this._mobile) { - this._computePosition(panel.offsetWidth, panel.offsetHeight); - } - - // Add scroll/resize listeners for repositioning (desktop) - this._addScrollResizeListeners(); - - // Add touch listeners for swipe-to-dismiss (mobile) - if (this._mobile) { - panel.addEventListener('touchstart', this._onTouchStart, { - passive: true, - }); - panel.addEventListener('touchmove', this._onTouchMove, { - passive: false, - }); - panel.addEventListener('touchend', this._onTouchEnd, { passive: true }); - } + this._mobile = window.matchMedia('(max-width: 767px)').matches; + const reducedMotion = window.matchMedia( + '(prefers-reduced-motion: reduce)', + ).matches; - // Focus the panel for screen reader context - panel.focus({ preventScroll: true }); + if (this._mobile) { + this._lockBodyScroll(); + } - if (reducedMotion) { - this.dispatchEvent( - new CustomEvent('ol-popover-open', { - bubbles: true, - composed: true, - detail: { placement: this.placement }, - }), - ); - return; - } - - // Force reflow so the browser paints the start position - panel.getBoundingClientRect(); - - this._animState = 'entering'; - this.dispatchEvent( - new CustomEvent('ol-popover-open', { - bubbles: true, - composed: true, - detail: { placement: this.placement }, - }), - ); - }); - } - - _hide() { - if (this._animState === 'closed') return; - - const reducedMotion = window.matchMedia( - '(prefers-reduced-motion: reduce)', - ).matches; - if (reducedMotion) { - this._animState = 'closed'; - this._cleanup(); - return; + // On desktop, render panel off-screen first so we can measure it. + // On mobile, CSS positions the tray at the bottom automatically. + if (!this._mobile) { + this._position = { top: -9999, left: -9999 }; + } + this._animState = reducedMotion ? 'open' : 'preparing'; + + this.updateComplete.then(() => { + const panel = this.shadowRoot.querySelector('.panel'); + if (!panel) return; + + // Desktop: measure and position relative to trigger. + // Use offsetWidth/Height — getBoundingClientRect includes the + // scale(0.95) transform from the preparing state, under-reporting + // the true layout size by 5%. + if (!this._mobile) { + this._computePosition(panel.offsetWidth, panel.offsetHeight); + } + + // Add scroll/resize listeners for repositioning (desktop) + this._addScrollResizeListeners(); + + // Add touch listeners for swipe-to-dismiss (mobile) + if (this._mobile) { + panel.addEventListener('touchstart', this._onTouchStart, { + passive: true, + }); + panel.addEventListener('touchmove', this._onTouchMove, { + passive: false, + }); + panel.addEventListener('touchend', this._onTouchEnd, { passive: true }); + } + + // Focus the panel for screen reader context + panel.focus({ preventScroll: true }); + + if (reducedMotion) { + this.dispatchEvent( + new CustomEvent('ol-popover-open', { + bubbles: true, + composed: true, + detail: { placement: this.placement }, + }), + ); + return; + } + + // Force reflow so the browser paints the start position + panel.getBoundingClientRect(); + + this._animState = 'entering'; + this.dispatchEvent( + new CustomEvent('ol-popover-open', { + bubbles: true, + composed: true, + detail: { placement: this.placement }, + }), + ); + }); } - this._animState = 'exiting'; - } + _hide() { + if (this._animState === 'closed') return; + + const reducedMotion = window.matchMedia( + '(prefers-reduced-motion: reduce)', + ).matches; + if (reducedMotion) { + this._animState = 'closed'; + this._cleanup(); + return; + } + + this._animState = 'exiting'; + } - _onTransitionEnd(e) { - if (e.target !== e.currentTarget) return; + _onTransitionEnd(e) { + if (e.target !== e.currentTarget) return; - if (this._animState === 'entering') { - this._animState = 'open'; - } else if (this._animState === 'exiting') { - this._animState = 'closed'; - this._cleanup(); + if (this._animState === 'entering') { + this._animState = 'open'; + } else if (this._animState === 'exiting') { + this._animState = 'closed'; + this._cleanup(); + } } - } - /** + /** * Central cleanup called when the popover finishes closing. * Removes all global listeners, unlocks scroll, and restores focus. */ - _cleanup() { - this._removeListeners(); - this._unlockBodyScroll(); - this._restoreFocus(); - } - - _restoreFocus() { - if (this._prevFocus && typeof this._prevFocus.focus === 'function') { - this._prevFocus.focus({ preventScroll: true }); + _cleanup() { + this._removeListeners(); + this._unlockBodyScroll(); + this._restoreFocus(); } - this._prevFocus = null; - } - - // ── Trigger ARIA ──────────────────────────────────────────── - - _syncTriggerAria() { - const trigger = this._triggerEl; - if (!trigger) return; - trigger.setAttribute('aria-haspopup', 'dialog'); - trigger.setAttribute('aria-expanded', String(this.open)); - if (this.open) { - trigger.setAttribute('aria-controls', this._panelId); - } else { - trigger.removeAttribute('aria-controls'); - } - } - // ── Focus trap ────────────────────────────────────────────── + _restoreFocus() { + if (this._prevFocus && typeof this._prevFocus.focus === 'function') { + this._prevFocus.focus({ preventScroll: true }); + } + this._prevFocus = null; + } - _getFocusableElements() { - const slot = this.shadowRoot?.querySelector('.panel slot:not([name])'); - if (!slot) return []; - const elements = []; - for (const node of slot.assignedElements({ flatten: true })) { - if (node.matches?.(FOCUSABLE)) elements.push(node); - elements.push(...node.querySelectorAll(FOCUSABLE)); + // ── Trigger ARIA ──────────────────────────────────────────── + + _syncTriggerAria() { + const trigger = this._triggerEl; + if (!trigger) return; + trigger.setAttribute('aria-haspopup', 'dialog'); + trigger.setAttribute('aria-expanded', String(this.open)); + if (this.open) { + trigger.setAttribute('aria-controls', this._panelId); + } else { + trigger.removeAttribute('aria-controls'); + } } - return elements; - } - - _onSentinelFocus(e) { - const edge = e.target.dataset.edge; - const focusable = this._getFocusableElements(); - if (focusable.length === 0) { - // No focusable children — keep focus on the panel itself - this.shadowRoot.querySelector('.panel')?.focus({ preventScroll: true }); - return; + + // ── Focus trap ────────────────────────────────────────────── + + _getFocusableElements() { + const slot = this.shadowRoot?.querySelector('.panel slot:not([name])'); + if (!slot) return []; + const elements = []; + for (const node of slot.assignedElements({ flatten: true })) { + if (node.matches?.(FOCUSABLE)) elements.push(node); + elements.push(...node.querySelectorAll(FOCUSABLE)); + } + return elements; } - if (edge === 'start') { - focusable[focusable.length - 1].focus({ preventScroll: true }); - } else { - focusable[0].focus({ preventScroll: true }); + + _onSentinelFocus(e) { + const edge = e.target.dataset.edge; + const focusable = this._getFocusableElements(); + if (focusable.length === 0) { + // No focusable children — keep focus on the panel itself + this.shadowRoot.querySelector('.panel')?.focus({ preventScroll: true }); + return; + } + if (edge === 'start') { + focusable[focusable.length - 1].focus({ preventScroll: true }); + } else { + focusable[0].focus({ preventScroll: true }); + } } - } - // ── Positioning ───────────────────────────────────────────── + // ── Positioning ───────────────────────────────────────────── - /** + /** * Compute the final position of the popover panel, flipping and shifting * as needed to keep it within the viewport. */ - _computePosition(panelW, panelH) { - const trigger = this._triggerEl; - if (!trigger) return; - - const anchor = trigger.getBoundingClientRect(); - const gap = this.offset; - const viewW = window.innerWidth; - const viewH = window.innerHeight; - const pad = 8; // minimum distance from viewport edge - - // Parse requested placement - const [reqSide, reqAlign] = this._parsePlacement(this.placement); - - // Determine side (top or bottom), flipping if it would overflow - let side = reqSide; - const spaceBelow = viewH - anchor.bottom - gap; - const spaceAbove = anchor.top - gap; - - if (side === 'bottom' && panelH > spaceBelow && spaceAbove > spaceBelow) { - side = 'top'; - } else if ( - side === 'top' && + _computePosition(panelW, panelH) { + const trigger = this._triggerEl; + if (!trigger) return; + + const anchor = trigger.getBoundingClientRect(); + const gap = this.offset; + const viewW = window.innerWidth; + const viewH = window.innerHeight; + const pad = 8; // minimum distance from viewport edge + + // Parse requested placement + const [reqSide, reqAlign] = this._parsePlacement(this.placement); + + // Determine side (top or bottom), flipping if it would overflow + let side = reqSide; + const spaceBelow = viewH - anchor.bottom - gap; + const spaceAbove = anchor.top - gap; + + if (side === 'bottom' && panelH > spaceBelow && spaceAbove > spaceBelow) { + side = 'top'; + } else if ( + side === 'top' && panelH > spaceAbove && spaceBelow > spaceAbove - ) { - side = 'bottom'; - } + ) { + side = 'bottom'; + } - // Vertical position - let top; - if (side === 'bottom') { - top = anchor.bottom + gap; - } else { - top = anchor.top - gap - panelH; - } + // Vertical position + let top; + if (side === 'bottom') { + top = anchor.bottom + gap; + } else { + top = anchor.top - gap - panelH; + } + + // Horizontal position based on alignment + let left; + const anchorCenter = anchor.left + anchor.width / 2; + + switch (reqAlign) { + case 'center': + left = anchorCenter - panelW / 2; + break; + case 'end': + left = anchor.right - panelW; + break; + case 'start': + default: + left = anchor.left; + break; + } + + // Shift horizontally to keep within viewport + if (left + panelW > viewW - pad) { + left = viewW - pad - panelW; + } + if (left < pad) { + left = pad; + } + + // Shift vertically to keep within viewport + if (top + panelH > viewH - pad) { + top = viewH - pad - panelH; + } + if (top < pad) { + top = pad; + } - // Horizontal position based on alignment - let left; - const anchorCenter = anchor.left + anchor.width / 2; - - switch (reqAlign) { - case 'center': - left = anchorCenter - panelW / 2; - break; - case 'end': - left = anchor.right - panelW; - break; - case 'start': - default: - left = anchor.left; - break; + // Compute transform-origin so the animation radiates from the trigger. + // The origin is expressed relative to the panel's top-left corner. + const originY = side === 'bottom' ? 'top' : 'bottom'; + + // Find where the anchor center falls within the panel horizontally + const anchorCenterInPanel = anchorCenter - left; + const originX = `${anchorCenterInPanel}px`; + + this._position = { top, left }; + this._transformOrigin = `${originX} ${originY}`; } - // Shift horizontally to keep within viewport - if (left + panelW > viewW - pad) { - left = viewW - pad - panelW; + _parsePlacement(placement) { + const parts = (placement || 'bottom-center').split('-'); + const side = parts[0] === 'top' ? 'top' : 'bottom'; + const align = ['start', 'center', 'end'].includes(parts[1]) + ? parts[1] + : 'center'; + return [side, align]; } - if (left < pad) { - left = pad; + + get _triggerEl() { + const slot = this.shadowRoot?.querySelector('slot[name="trigger"]'); + return slot?.assignedElements()[0] ?? null; } - // Shift vertically to keep within viewport - if (top + panelH > viewH - pad) { - top = viewH - pad - panelH; + // ── Scroll / resize repositioning ─────────────────────────── + + _addScrollResizeListeners() { + window.addEventListener('scroll', this._onScrollResize, { + capture: true, + passive: true, + }); + window.addEventListener('resize', this._onScrollResize, { passive: true }); } - if (top < pad) { - top = pad; + + _removeScrollResizeListeners() { + window.removeEventListener('scroll', this._onScrollResize, { + capture: true, + }); + window.removeEventListener('resize', this._onScrollResize); + if (this._rafId) { + cancelAnimationFrame(this._rafId); + this._rafId = null; + } } - // Compute transform-origin so the animation radiates from the trigger. - // The origin is expressed relative to the panel's top-left corner. - const originY = side === 'bottom' ? 'top' : 'bottom'; - - // Find where the anchor center falls within the panel horizontally - const anchorCenterInPanel = anchorCenter - left; - const originX = `${anchorCenterInPanel}px`; - - this._position = { top, left }; - this._transformOrigin = `${originX} ${originY}`; - } - - _parsePlacement(placement) { - const parts = (placement || 'bottom-center').split('-'); - const side = parts[0] === 'top' ? 'top' : 'bottom'; - const align = ['start', 'center', 'end'].includes(parts[1]) - ? parts[1] - : 'center'; - return [side, align]; - } - - get _triggerEl() { - const slot = this.shadowRoot?.querySelector('slot[name="trigger"]'); - return slot?.assignedElements()[0] ?? null; - } - - // ── Scroll / resize repositioning ─────────────────────────── - - _addScrollResizeListeners() { - window.addEventListener('scroll', this._onScrollResize, { - capture: true, - passive: true, - }); - window.addEventListener('resize', this._onScrollResize, { passive: true }); - } - - _removeScrollResizeListeners() { - window.removeEventListener('scroll', this._onScrollResize, { - capture: true, - }); - window.removeEventListener('resize', this._onScrollResize); - if (this._rafId) { - cancelAnimationFrame(this._rafId); - this._rafId = null; + _onScrollResize() { + if (this._rafId) return; + this._rafId = requestAnimationFrame(() => { + this._rafId = null; + if (this._mobile) return; + if (this._animState !== 'open' && this._animState !== 'entering') return; + const panel = this.shadowRoot?.querySelector('.panel'); + if (panel) { + this._computePosition(panel.offsetWidth, panel.offsetHeight); + } + }); } - } - - _onScrollResize() { - if (this._rafId) return; - this._rafId = requestAnimationFrame(() => { - this._rafId = null; - if (this._mobile) return; - if (this._animState !== 'open' && this._animState !== 'entering') return; - const panel = this.shadowRoot?.querySelector('.panel'); - if (panel) { - this._computePosition(panel.offsetWidth, panel.offsetHeight); - } - }); - } - - // ── Outside click / keyboard ──────────────────────────────── - - _onOutsideClick(e) { - if (!this.autoClose) return; - if (this._animState === 'closed' || this._animState === 'exiting') return; - const path = e.composedPath(); - if (!path.includes(this)) { - this._requestClose('outside-click'); + + // ── Outside click / keyboard ──────────────────────────────── + + _onOutsideClick(e) { + if (!this.autoClose) return; + if (this._animState === 'closed' || this._animState === 'exiting') return; + const path = e.composedPath(); + if (!path.includes(this)) { + this._requestClose('outside-click'); + } } - } - _onBackdropClick() { - if (this.autoClose) { - this._requestClose('outside-click'); + _onBackdropClick() { + if (this.autoClose) { + this._requestClose('outside-click'); + } } - } - _onKeydownGlobal(e) { - if (e.key === 'Escape' && this.open) { - e.preventDefault(); - this._requestClose('escape'); + _onKeydownGlobal(e) { + if (e.key === 'Escape' && this.open) { + e.preventDefault(); + this._requestClose('escape'); + } } - } - - _requestClose(reason) { - this.dispatchEvent( - new CustomEvent('ol-popover-close', { - bubbles: true, - composed: true, - detail: { reason }, - }), - ); - } - - // ── Mobile touch / swipe-to-dismiss ───────────────────────── - - _onTouchStart(e) { - const handle = this.shadowRoot.querySelector('.tray-handle'); - const panel = this.shadowRoot.querySelector('.panel'); - const touch = e.touches[0]; - - this._touchStartY = touch.clientY; - this._touchStartTime = Date.now(); - this._isDragging = false; - this._lastDragY = 0; - this._isHandleDrag = !!(handle && e.composedPath().includes(handle)); - this._touchScrollTop = panel?.scrollTop ?? 0; - } - - _onTouchMove(e) { - const touch = e.touches[0]; - const deltaY = touch.clientY - this._touchStartY; - - if (!this._isDragging) { - // Start drag if touching handle, or at scroll-top and swiping down - if (this._isHandleDrag || (this._touchScrollTop <= 0 && deltaY > 5)) { - this._isDragging = true; - } else { - return; // Let normal scroll happen - } + + _requestClose(reason) { + this.dispatchEvent( + new CustomEvent('ol-popover-close', { + bubbles: true, + composed: true, + detail: { reason }, + }), + ); } - const dragY = Math.max(0, deltaY); - this._lastDragY = dragY; - e.preventDefault(); + // ── Mobile touch / swipe-to-dismiss ───────────────────────── - const panel = this.shadowRoot.querySelector('.panel'); - if (panel) { - panel.style.transform = `translateY(${dragY}px)`; - panel.style.transition = 'none'; + _onTouchStart(e) { + const handle = this.shadowRoot.querySelector('.tray-handle'); + const panel = this.shadowRoot.querySelector('.panel'); + const touch = e.touches[0]; + + this._touchStartY = touch.clientY; + this._touchStartTime = Date.now(); + this._isDragging = false; + this._lastDragY = 0; + this._isHandleDrag = !!(handle && e.composedPath().includes(handle)); + this._touchScrollTop = panel?.scrollTop ?? 0; } - const backdrop = this.shadowRoot.querySelector('.backdrop'); - if (backdrop) { - const progress = Math.min(dragY / 300, 1); - backdrop.style.opacity = String(1 - progress); - backdrop.style.transition = 'none'; + _onTouchMove(e) { + const touch = e.touches[0]; + const deltaY = touch.clientY - this._touchStartY; + + if (!this._isDragging) { + // Start drag if touching handle, or at scroll-top and swiping down + if (this._isHandleDrag || (this._touchScrollTop <= 0 && deltaY > 5)) { + this._isDragging = true; + } else { + return; // Let normal scroll happen + } + } + + const dragY = Math.max(0, deltaY); + this._lastDragY = dragY; + e.preventDefault(); + + const panel = this.shadowRoot.querySelector('.panel'); + if (panel) { + panel.style.transform = `translateY(${dragY}px)`; + panel.style.transition = 'none'; + } + + const backdrop = this.shadowRoot.querySelector('.backdrop'); + if (backdrop) { + const progress = Math.min(dragY / 300, 1); + backdrop.style.opacity = String(1 - progress); + backdrop.style.transition = 'none'; + } } - } - _onTouchEnd() { - if (!this._isDragging) return; + _onTouchEnd() { + if (!this._isDragging) return; - const dragY = this._lastDragY; - const elapsed = Date.now() - this._touchStartTime; - const velocity = dragY / Math.max(elapsed, 1); + const dragY = this._lastDragY; + const elapsed = Date.now() - this._touchStartTime; + const velocity = dragY / Math.max(elapsed, 1); - this._isDragging = false; - this._lastDragY = 0; + this._isDragging = false; + this._lastDragY = 0; - const panel = this.shadowRoot.querySelector('.panel'); - const backdrop = this.shadowRoot.querySelector('.backdrop'); + const panel = this.shadowRoot.querySelector('.panel'); + const backdrop = this.shadowRoot.querySelector('.backdrop'); - const DISMISS_THRESHOLD = 80; - const VELOCITY_THRESHOLD = 0.5; + const DISMISS_THRESHOLD = 80; + const VELOCITY_THRESHOLD = 0.5; - if (dragY > DISMISS_THRESHOLD || velocity > VELOCITY_THRESHOLD) { - // Swipe dismiss — animate to off-screen, then close - if (panel) { - panel.style.transition = + if (dragY > DISMISS_THRESHOLD || velocity > VELOCITY_THRESHOLD) { + // Swipe dismiss — animate to off-screen, then close + if (panel) { + panel.style.transition = 'transform 200ms cubic-bezier(0.23, 1, 0.32, 1)'; - panel.style.transform = 'translateY(100%)'; - } - if (backdrop) { - backdrop.style.transition = + panel.style.transform = 'translateY(100%)'; + } + if (backdrop) { + backdrop.style.transition = 'opacity 200ms cubic-bezier(0.23, 1, 0.32, 1)'; - backdrop.style.opacity = '0'; - } + backdrop.style.opacity = '0'; + } - const onDone = () => { - panel?.removeEventListener('transitionend', onDone); - this._clearDragStyles(); - this._animState = 'closed'; - this._cleanup(); - this.dispatchEvent( - new CustomEvent('ol-popover-close', { - bubbles: true, - composed: true, - detail: { reason: 'swipe' }, - }), - ); - }; - - if (panel) { - panel.addEventListener('transitionend', onDone, { once: true }); - } else { - onDone(); - } - } else { - // Snap back to open position - if (panel) { - panel.style.transition = + const onDone = () => { + panel?.removeEventListener('transitionend', onDone); + this._clearDragStyles(); + this._animState = 'closed'; + this._cleanup(); + this.dispatchEvent( + new CustomEvent('ol-popover-close', { + bubbles: true, + composed: true, + detail: { reason: 'swipe' }, + }), + ); + }; + + if (panel) { + panel.addEventListener('transitionend', onDone, { once: true }); + } else { + onDone(); + } + } else { + // Snap back to open position + if (panel) { + panel.style.transition = 'transform 200ms cubic-bezier(0.23, 1, 0.32, 1)'; - panel.style.transform = ''; - } - if (backdrop) { - backdrop.style.transition = + panel.style.transform = ''; + } + if (backdrop) { + backdrop.style.transition = 'opacity 200ms cubic-bezier(0.23, 1, 0.32, 1)'; - backdrop.style.opacity = ''; - } + backdrop.style.opacity = ''; + } - const onDone = () => { - panel?.removeEventListener('transitionend', onDone); - this._clearDragStyles(); - }; + const onDone = () => { + panel?.removeEventListener('transitionend', onDone); + this._clearDragStyles(); + }; - if (panel) { - panel.addEventListener('transitionend', onDone, { once: true }); - } - } - } - - _clearDragStyles() { - const panel = this.shadowRoot?.querySelector('.panel'); - const backdrop = this.shadowRoot?.querySelector('.backdrop'); - if (panel) { - panel.style.transition = ''; - panel.style.transform = ''; + if (panel) { + panel.addEventListener('transitionend', onDone, { once: true }); + } + } } - if (backdrop) { - backdrop.style.transition = ''; - backdrop.style.opacity = ''; + + _clearDragStyles() { + const panel = this.shadowRoot?.querySelector('.panel'); + const backdrop = this.shadowRoot?.querySelector('.backdrop'); + if (panel) { + panel.style.transition = ''; + panel.style.transform = ''; + } + if (backdrop) { + backdrop.style.transition = ''; + backdrop.style.opacity = ''; + } } - } - // ── Body scroll lock ──────────────────────────────────────── + // ── Body scroll lock ──────────────────────────────────────── - _lockBodyScroll() { - this._savedOverflow = document.body.style.overflow; - document.body.style.overflow = 'hidden'; - } + _lockBodyScroll() { + this._savedOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + } - _unlockBodyScroll() { - if (this._savedOverflow !== null) { - document.body.style.overflow = this._savedOverflow; - this._savedOverflow = null; + _unlockBodyScroll() { + if (this._savedOverflow !== null) { + document.body.style.overflow = this._savedOverflow; + this._savedOverflow = null; + } } - } - // ── Listener management ───────────────────────────────────── + // ── Listener management ───────────────────────────────────── - _removeListeners() { - document.removeEventListener('click', this._onOutsideClick, true); - document.removeEventListener('keydown', this._onKeydownGlobal); - this._removeScrollResizeListeners(); + _removeListeners() { + document.removeEventListener('click', this._onOutsideClick, true); + document.removeEventListener('keydown', this._onKeydownGlobal); + this._removeScrollResizeListeners(); - // Remove touch listeners from panel - const panel = this.shadowRoot?.querySelector('.panel'); - if (panel) { - panel.removeEventListener('touchstart', this._onTouchStart); - panel.removeEventListener('touchmove', this._onTouchMove); - panel.removeEventListener('touchend', this._onTouchEnd); + // Remove touch listeners from panel + const panel = this.shadowRoot?.querySelector('.panel'); + if (panel) { + panel.removeEventListener('touchstart', this._onTouchStart); + panel.removeEventListener('touchmove', this._onTouchMove); + panel.removeEventListener('touchend', this._onTouchEnd); + } } - } - disconnectedCallback() { - super.disconnectedCallback(); - this._removeListeners(); - this._unlockBodyScroll(); - } + disconnectedCallback() { + super.disconnectedCallback(); + this._removeListeners(); + this._unlockBodyScroll(); + } } customElements.define('ol-popover', OlPopover); diff --git a/openlibrary/components/rollupInputCore.js b/openlibrary/components/rollupInputCore.js index da169be020d..77a9664d9c0 100644 --- a/openlibrary/components/rollupInputCore.js +++ b/openlibrary/components/rollupInputCore.js @@ -3,18 +3,18 @@ import { defineCustomElement } from 'vue'; import AsyncComputed from 'vue-async-computed'; export const createWebComponentSimple = (rootComponent, name) => { - // This is the name we use in the DOM like: <ol-barcode-scanner></ol-barcode-scanner> - const elementName = `ol-${kebabCase(name)}`; + // This is the name we use in the DOM like: <ol-barcode-scanner></ol-barcode-scanner> + const elementName = `ol-${kebabCase(name)}`; - const WebComponent = defineCustomElement(rootComponent, { - configureApp(app) { - if (elementName === 'ol-merge-ui') { - app.use(AsyncComputed); - } - }, - }); + const WebComponent = defineCustomElement(rootComponent, { + configureApp(app) { + if (elementName === 'ol-merge-ui') { + app.use(AsyncComputed); + } + }, + }); - if (!customElements.get(elementName)) { - customElements.define(elementName, WebComponent); - } + if (!customElements.get(elementName)) { + customElements.define(elementName, WebComponent); + } }; diff --git a/openlibrary/plugins/openlibrary/js/Browser.js b/openlibrary/plugins/openlibrary/js/Browser.js index 7a615de63cf..62086529777 100644 --- a/openlibrary/plugins/openlibrary/js/Browser.js +++ b/openlibrary/plugins/openlibrary/js/Browser.js @@ -6,15 +6,15 @@ * @returns {UrlParams} */ export function getJsonFromUrl(urlSearch) { - const query = urlSearch.substr(1); - const result = {}; - if (query) { - query.split('&').forEach((part) => { - const item = part.split('='); - result[item[0]] = decodeURIComponent(item[1]); - }); - } - return result; + const query = urlSearch.substr(1); + const result = {}; + if (query) { + query.split('&').forEach((part) => { + const item = part.split('='); + result[item[0]] = decodeURIComponent(item[1]); + }); + } + return result; } /** @@ -23,25 +23,25 @@ export function getJsonFromUrl(urlSearch) { * @returns {String} */ export function removeURLParameter(url, parameter) { - var urlparts = url.split('?'); - var prefix = urlparts[0]; - var query, paramPrefix, params, i; - if (urlparts.length >= 2) { - query = urlparts[1]; - paramPrefix = `${encodeURIComponent(parameter)}=`; - params = query.split(/[&;]/g); + var urlparts = url.split('?'); + var prefix = urlparts[0]; + var query, paramPrefix, params, i; + if (urlparts.length >= 2) { + query = urlparts[1]; + paramPrefix = `${encodeURIComponent(parameter)}=`; + params = query.split(/[&;]/g); - //reverse iteration as may be destructive - for (i = params.length; i-- > 0;) { - //idiom for string.startsWith - if (params[i].lastIndexOf(paramPrefix, 0) !== -1) { - params.splice(i, 1); - } - } + //reverse iteration as may be destructive + for (i = params.length; i-- > 0;) { + //idiom for string.startsWith + if (params[i].lastIndexOf(paramPrefix, 0) !== -1) { + params.splice(i, 1); + } + } - url = prefix + (params.length > 0 ? `?${params.join('&')}` : ''); - return url; - } else { - return url; - } + url = prefix + (params.length > 0 ? `?${params.join('&')}` : ''); + return url; + } else { + return url; + } } diff --git a/openlibrary/plugins/openlibrary/js/SearchBar.js b/openlibrary/plugins/openlibrary/js/SearchBar.js index c7dcbbc831d..200fb02a8ae 100644 --- a/openlibrary/plugins/openlibrary/js/SearchBar.js +++ b/openlibrary/plugins/openlibrary/js/SearchBar.js @@ -6,35 +6,35 @@ import { PersistentValue } from './SearchUtils'; /** Mapping of search bar facets to search endpoints */ const FACET_TO_ENDPOINT = { - title: '/search', - author: '/search/authors', - lists: '/search/lists', - subject: '/search/subjects', - all: '/search', - text: '/search/inside', + title: '/search', + author: '/search/authors', + lists: '/search/lists', + subject: '/search/subjects', + all: '/search', + text: '/search/inside', }; const DEFAULT_FACET = 'all'; const DEFAULT_JSON_FIELDS = [ - 'key', - 'cover_i', - 'title', - 'subtitle', - 'author_name', - 'editions', - // This is for authors autocomplete; we mix them all up here for simplicity - 'name', + 'key', + 'cover_i', + 'title', + 'subtitle', + 'author_name', + 'editions', + // This is for authors autocomplete; we mix them all up here for simplicity + 'name', ]; /** Functions that render autocomplete results */ const RENDER_AUTOCOMPLETE_RESULT = { - ['/search'](work) { - const book = work.editions?.docs?.[0] || work; - const author_name = work.author_name ? work.author_name[0] : ''; - // See _get_safepath_re in openlibrary/core/helpers.py - let link = `${work.key}/${encodeURIComponent(work.title.replace(/[;/?:@&=+$,\s<>#%"{}|\\^[\]`]+/g, '_'))}`; - if (book !== work) { - link += `?edition=key:${book.key}`; - } - return ` + ['/search'](work) { + const book = work.editions?.docs?.[0] || work; + const author_name = work.author_name ? work.author_name[0] : ''; + // See _get_safepath_re in openlibrary/core/helpers.py + let link = `${work.key}/${encodeURIComponent(work.title.replace(/[;/?:@&=+$,\s<>#%"{}|\\^[\]`]+/g, '_'))}`; + if (book !== work) { + link += `?edition=key:${book.key}`; + } + return ` <li tabindex=0> <a href="${link}"> <img @@ -49,9 +49,9 @@ const RENDER_AUTOCOMPLETE_RESULT = { </span> </a> </li>`; - }, - ['/search/authors'](author) { - return ` + }, + ['/search/authors'](author) { + return ` <li> <a href="/authors/${author.key}"> <img @@ -62,239 +62,239 @@ const RENDER_AUTOCOMPLETE_RESULT = { <span class="author-desc"><div class="author-name">${websafe(author.name)}</div></span> </a> </li>`; - }, + }, }; /** * Manages the interactions associated with the search bar in the header */ export class SearchBar { - /** + /** * @param {JQuery} $component * @param {Object?} urlParams */ - constructor($component, urlParams = {}) { + constructor($component, urlParams = {}) { /** UI Elements */ - this.$component = $component; - this.$form = this.$component.find('form.search-bar-input'); - this.$input = this.$form.find('input[type="text"]'); - this.$results = this.$component.find('ul.search-results'); - this.$facetSelect = this.$component.find('.search-facet-selector select'); - this.$barcodeScanner = this.$component.find('#barcode_scanner_link'); - this.$searchSubmit = this.$component.find('.search-bar-submit'); - - /** State */ - /** Whether the bar is in collapsible mode */ - this.inCollapsibleMode = false; - /** Whether the search bar is currently collapsed */ - this.collapsed = false; - /** Selected facet (persisted) */ - this.facet = new PersistentValue('facet', { - default: DEFAULT_FACET, - initValidation(val) { - return val in FACET_TO_ENDPOINT; - }, - }); - - this.initFromUrlParams(urlParams); - this.initCollapsibleMode(); - // Stop renderAutoCompletionResults from firing when ESC is pressed in results list - this.escapeInput = false; - - // Bind to changes in the search state - SearchUtils.mode.sync(this.handleSearchModeChange.bind(this)); - this.facet.sync(this.handleFacetValueChange.bind(this)); - this.$facetSelect.on('change', this.handleFacetSelectChange.bind(this)); - this.$form.on('submit', this.submitForm.bind(this)); - - // Shift + Tabbing out of the search facet to clear results list - this.$facetSelect.on('keydown', (e) => { - if (e.key === 'Tab' && e.shiftKey) { - this.clearAutocompletionResults(); - } - }); - - this.$input.on('keydown', (e) => { - if (e.key === 'ArrowUp') { - this.$results.children().last().trigger('focus'); - return false; - } else if (e.key === 'ArrowDown') { - this.$results.children().first().trigger('focus'); - return false; - } else if (e.key === 'Escape') { - this.clearAutocompletionResults(); - } - }); - - this.$barcodeScanner.on('keydown', (e) => { - if (e.key === 'Tab') { - this.clearAutocompletionResults(); - } - }); - - this.$results.on('keydown', (e) => { - if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { - // On arrow keys focus on the next item unless there is none, then focus on input - const direction = + this.$component = $component; + this.$form = this.$component.find('form.search-bar-input'); + this.$input = this.$form.find('input[type="text"]'); + this.$results = this.$component.find('ul.search-results'); + this.$facetSelect = this.$component.find('.search-facet-selector select'); + this.$barcodeScanner = this.$component.find('#barcode_scanner_link'); + this.$searchSubmit = this.$component.find('.search-bar-submit'); + + /** State */ + /** Whether the bar is in collapsible mode */ + this.inCollapsibleMode = false; + /** Whether the search bar is currently collapsed */ + this.collapsed = false; + /** Selected facet (persisted) */ + this.facet = new PersistentValue('facet', { + default: DEFAULT_FACET, + initValidation(val) { + return val in FACET_TO_ENDPOINT; + }, + }); + + this.initFromUrlParams(urlParams); + this.initCollapsibleMode(); + // Stop renderAutoCompletionResults from firing when ESC is pressed in results list + this.escapeInput = false; + + // Bind to changes in the search state + SearchUtils.mode.sync(this.handleSearchModeChange.bind(this)); + this.facet.sync(this.handleFacetValueChange.bind(this)); + this.$facetSelect.on('change', this.handleFacetSelectChange.bind(this)); + this.$form.on('submit', this.submitForm.bind(this)); + + // Shift + Tabbing out of the search facet to clear results list + this.$facetSelect.on('keydown', (e) => { + if (e.key === 'Tab' && e.shiftKey) { + this.clearAutocompletionResults(); + } + }); + + this.$input.on('keydown', (e) => { + if (e.key === 'ArrowUp') { + this.$results.children().last().trigger('focus'); + return false; + } else if (e.key === 'ArrowDown') { + this.$results.children().first().trigger('focus'); + return false; + } else if (e.key === 'Escape') { + this.clearAutocompletionResults(); + } + }); + + this.$barcodeScanner.on('keydown', (e) => { + if (e.key === 'Tab') { + this.clearAutocompletionResults(); + } + }); + + this.$results.on('keydown', (e) => { + if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { + // On arrow keys focus on the next item unless there is none, then focus on input + const direction = e.key === 'ArrowUp' ? 'previousElementSibling' : 'nextElementSibling'; - if (!e.target[direction]) { - this.$input.trigger('focus'); - return false; - } else { - $(e.target[direction]).trigger('focus'); - return false; - } - } else if (e.key === 'Tab') { - // On tab, always go to the next selector (instead of next result), like wikipedia - this.clearAutocompletionResults(); - if (e.shiftKey) { - this.$facetSelect.trigger('focus'); - return false; - } else { - this.$searchSubmit.trigger('focus'); - return false; - } - } else if (e.key === 'Enter') { - e.target.firstElementChild.click(); - } else if (e.key === 'Escape') { - this.$input.trigger('focus'); - this.escapeInput = true; - this.clearAutocompletionResults(); - } - }); - - this.$form.on('keydown', (e) => { - if (e.key === 'Tab') { - this.clearAutocompletionResults(); - } - }); - - this.initAutocompletionLogic(); - } - - /** @type {String} The endpoint of the active facet */ - get facetEndpoint() { - return FACET_TO_ENDPOINT[this.facet.read()]; - } - - /** + if (!e.target[direction]) { + this.$input.trigger('focus'); + return false; + } else { + $(e.target[direction]).trigger('focus'); + return false; + } + } else if (e.key === 'Tab') { + // On tab, always go to the next selector (instead of next result), like wikipedia + this.clearAutocompletionResults(); + if (e.shiftKey) { + this.$facetSelect.trigger('focus'); + return false; + } else { + this.$searchSubmit.trigger('focus'); + return false; + } + } else if (e.key === 'Enter') { + e.target.firstElementChild.click(); + } else if (e.key === 'Escape') { + this.$input.trigger('focus'); + this.escapeInput = true; + this.clearAutocompletionResults(); + } + }); + + this.$form.on('keydown', (e) => { + if (e.key === 'Tab') { + this.clearAutocompletionResults(); + } + }); + + this.initAutocompletionLogic(); + } + + /** @type {String} The endpoint of the active facet */ + get facetEndpoint() { + return FACET_TO_ENDPOINT[this.facet.read()]; + } + + /** * Update internal state from url parameters * @param {Object} urlParams */ - initFromUrlParams(urlParams) { - if (urlParams.facet in FACET_TO_ENDPOINT) { - this.facet.write(urlParams.facet); - } + initFromUrlParams(urlParams) { + if (urlParams.facet in FACET_TO_ENDPOINT) { + this.facet.write(urlParams.facet); + } - if (urlParams.q && this.getCurUrl().pathname.match(/^\/search/)) { - let q = urlParams.q.replace(/\+/g, ' '); - if (this.facet.read() === 'title' && q.indexOf('title:') !== -1) { - const parts = q.split('"'); - if (parts.length === 3) { - q = parts[1]; + if (urlParams.q && this.getCurUrl().pathname.match(/^\/search/)) { + let q = urlParams.q.replace(/\+/g, ' '); + if (this.facet.read() === 'title' && q.indexOf('title:') !== -1) { + const parts = q.split('"'); + if (parts.length === 3) { + q = parts[1]; + } + } + this.$input.val(q); } - } - this.$input.val(q); } - } - submitForm() { - if (this.facet.read() === 'title') { - const q = this.$input.val(); - this.$input.val(SearchBar.marshalBookSearchQuery(q)); + submitForm() { + if (this.facet.read() === 'title') { + const q = this.$input.val(); + this.$input.val(SearchBar.marshalBookSearchQuery(q)); + } + this.$form.attr( + 'action', + SearchBar.composeSearchUrl(this.facetEndpoint, this.$input.val()), + ); + SearchUtils.addModeInputsToForm(this.$form, SearchUtils.mode.read()); } - this.$form.attr( - 'action', - SearchBar.composeSearchUrl(this.facetEndpoint, this.$input.val()), - ); - SearchUtils.addModeInputsToForm(this.$form, SearchUtils.mode.read()); - } - - /** Initialize event handlers that allow the form to collapse for small screens */ - initCollapsibleMode() { - this.toggleCollapsibleModeForSmallScreens($(window).width()); - $(window).on( - 'resize', - debounce(() => { + + /** Initialize event handlers that allow the form to collapse for small screens */ + initCollapsibleMode() { this.toggleCollapsibleModeForSmallScreens($(window).width()); - }, 50), - ); - - const expandAndFocusSearch = (event) => { - if (this.inCollapsibleMode && this.collapsed) { - event.preventDefault(); - this.toggleCollapse(); - this.$input.trigger('focus'); - } - }; - const expandSelectors = ['.search-component', 'a[href="/search"]']; - - // When clicking on the search bar or a link to /search, expand search if it isn't already. - // If clicking elsewhere, collapse search. - $(document).on('submit', '.in-collapsible-mode', (event) => - expandAndFocusSearch(event), - ); - $(document).on('click', (event) => { - const shouldExpand = (item) => $(event.target).closest(item).length === 1; - if (expandSelectors.some(shouldExpand)) { - expandAndFocusSearch(event); - } else { - if (!this.collapsed) this.toggleCollapse(); - } - }); - } - - /** + $(window).on( + 'resize', + debounce(() => { + this.toggleCollapsibleModeForSmallScreens($(window).width()); + }, 50), + ); + + const expandAndFocusSearch = (event) => { + if (this.inCollapsibleMode && this.collapsed) { + event.preventDefault(); + this.toggleCollapse(); + this.$input.trigger('focus'); + } + }; + const expandSelectors = ['.search-component', 'a[href="/search"]']; + + // When clicking on the search bar or a link to /search, expand search if it isn't already. + // If clicking elsewhere, collapse search. + $(document).on('submit', '.in-collapsible-mode', (event) => + expandAndFocusSearch(event), + ); + $(document).on('click', (event) => { + const shouldExpand = (item) => $(event.target).closest(item).length === 1; + if (expandSelectors.some(shouldExpand)) { + expandAndFocusSearch(event); + } else { + if (!this.collapsed) this.toggleCollapse(); + } + }); + } + + /** * Enables/disables CollapsibleMode depending on screen size * @param {Number} windowWidth */ - toggleCollapsibleModeForSmallScreens(windowWidth) { - if (windowWidth < 568) { - if (!this.inCollapsibleMode) { - this.enableCollapsibleMode(); - this.collapse(); - } - this.clearAutocompletionResults(); - } else { - if (this.inCollapsibleMode) { - this.disableCollapsibleMode(); - } + toggleCollapsibleModeForSmallScreens(windowWidth) { + if (windowWidth < 568) { + if (!this.inCollapsibleMode) { + this.enableCollapsibleMode(); + this.collapse(); + } + this.clearAutocompletionResults(); + } else { + if (this.inCollapsibleMode) { + this.disableCollapsibleMode(); + } + } + } + + /** Collapses or expands the searchbar */ + toggleCollapse() { + if (this.collapsed) { + this.expand(); + } else { + this.collapse(); + } } - } - - /** Collapses or expands the searchbar */ - toggleCollapse() { - if (this.collapsed) { - this.expand(); - } else { - this.collapse(); + + collapse() { + $('header#header-bar .logo-component').removeClass('hidden'); + this.$component.removeClass('expanded'); + this.collapsed = true; + } + + expand() { + $('header#header-bar .logo-component').addClass('hidden'); + this.$component.addClass('expanded'); + this.collapsed = false; + } + + enableCollapsibleMode() { + this.$form.addClass('in-collapsible-mode'); + this.inCollapsibleMode = true; + } + + disableCollapsibleMode() { + this.collapse(); + this.$form.removeClass('in-collapsible-mode'); + this.inCollapsibleMode = false; } - } - - collapse() { - $('header#header-bar .logo-component').removeClass('hidden'); - this.$component.removeClass('expanded'); - this.collapsed = true; - } - - expand() { - $('header#header-bar .logo-component').addClass('hidden'); - this.$component.addClass('expanded'); - this.collapsed = false; - } - - enableCollapsibleMode() { - this.$form.addClass('in-collapsible-mode'); - this.inCollapsibleMode = true; - } - - disableCollapsibleMode() { - this.collapse(); - this.$form.removeClass('in-collapsible-mode'); - this.inCollapsibleMode = false; - } - - /** + + /** * Converts an already processed query into a search url * @param {String} facetEndpoint * @param {String} q query that's ready to get passed to the search endpoint @@ -302,176 +302,176 @@ export class SearchBar { * @param {Number} [limit] how many items to get * @param {String[]} [fields] the Solr fields to fetch (if using JSON) */ - static composeSearchUrl( - facetEndpoint, - q, - json = false, - limit = null, - fields = null, - ) { - let url = facetEndpoint; - if (json) { - url += `.json?q=${q}&_spellcheck_count=0`; - } else { - url += `?q=${q}`; - } + static composeSearchUrl( + facetEndpoint, + q, + json = false, + limit = null, + fields = null, + ) { + let url = facetEndpoint; + if (json) { + url += `.json?q=${q}&_spellcheck_count=0`; + } else { + url += `?q=${q}`; + } - if (limit) url += `&limit=${limit}`; - if (fields) url += `&fields=${fields.map(encodeURIComponent).join(',')}`; - url += `&mode=${SearchUtils.mode.read()}`; - return url; - } + if (limit) url += `&limit=${limit}`; + if (fields) url += `&fields=${fields.map(encodeURIComponent).join(',')}`; + url += `&mode=${SearchUtils.mode.read()}`; + return url; + } - /** + /** * Prepare an unprocessed query for book searching * @param {String} q * @return {String} */ - static marshalBookSearchQuery(q) { - if (q && q.indexOf(':') === -1 && q.indexOf('"') === -1) { - q = `title: "${q}"`; + static marshalBookSearchQuery(q) { + if (q && q.indexOf(':') === -1 && q.indexOf('"') === -1) { + q = `title: "${q}"`; + } + return q; } - return q; - } - /** Setup event listeners for autocompletion */ - initAutocompletionLogic() { + /** Setup event listeners for autocompletion */ + initAutocompletionLogic() { // searches should be cancelled if you click anywhere in the page - $(document.body).on('click', this.clearAutocompletionResults.bind(this)); - // but clicking search input should not empty search results. - this.$input.on('click', false); - - this.$input.on( - 'keyup', - debounce( - (event) => { - // ignore directional keys, enter, escape, and shift for callback - if (![13, 16, 27, 37, 38, 39, 40].includes(event.keyCode)) { - this.renderAutocompletionResults(); - } - }, - 500, - false, - ), - ); - - this.$input.on( - 'focus', - debounce( - (event) => { - event.stopPropagation(); - // don't render on focus if there are already results showing, avoid flashing - const resultsAreRendered = this.$results.children().length > 0; - if (this.escapeInput || resultsAreRendered) { - return; - } - this.renderAutocompletionResults(); - }, - 300, - false, - ), - ); - } - - /** + $(document.body).on('click', this.clearAutocompletionResults.bind(this)); + // but clicking search input should not empty search results. + this.$input.on('click', false); + + this.$input.on( + 'keyup', + debounce( + (event) => { + // ignore directional keys, enter, escape, and shift for callback + if (![13, 16, 27, 37, 38, 39, 40].includes(event.keyCode)) { + this.renderAutocompletionResults(); + } + }, + 500, + false, + ), + ); + + this.$input.on( + 'focus', + debounce( + (event) => { + event.stopPropagation(); + // don't render on focus if there are already results showing, avoid flashing + const resultsAreRendered = this.$results.children().length > 0; + if (this.escapeInput || resultsAreRendered) { + return; + } + this.renderAutocompletionResults(); + }, + 300, + false, + ), + ); + } + + /** * @async * Awkwardly fetches the the results as well as renders them :/ * Cleans up and performs the query, then update the autocomplete results * @returns {JQuery.jqXHR} **/ - renderAutocompletionResults() { - let q = this.$input.val().trim(); - if ( - q.length < 3 || + renderAutocompletionResults() { + let q = this.$input.val().trim(); + if ( + q.length < 3 || q.toLowerCase() === 'the' || !(this.facetEndpoint in RENDER_AUTOCOMPLETE_RESULT) - ) { - return; - } - if (this.facet.read() === 'title') { - q = SearchBar.marshalBookSearchQuery(q); - } - - this.$results.css('opacity', 0.5); - return $.getJSON( - SearchBar.composeSearchUrl( - this.facetEndpoint, - q, - true, - 10, - DEFAULT_JSON_FIELDS, - ), - (data) => { - const renderer = RENDER_AUTOCOMPLETE_RESULT[this.facetEndpoint]; - this.$results.css('opacity', 1); - this.clearAutocompletionResults(); - for (const d in data.docs) { - this.$results.append(renderer(data.docs[d])); + ) { + return; } - }, - ); - } + if (this.facet.read() === 'title') { + q = SearchBar.marshalBookSearchQuery(q); + } + + this.$results.css('opacity', 0.5); + return $.getJSON( + SearchBar.composeSearchUrl( + this.facetEndpoint, + q, + true, + 10, + DEFAULT_JSON_FIELDS, + ), + (data) => { + const renderer = RENDER_AUTOCOMPLETE_RESULT[this.facetEndpoint]; + this.$results.css('opacity', 1); + this.clearAutocompletionResults(); + for (const d in data.docs) { + this.$results.append(renderer(data.docs[d])); + } + }, + ); + } - clearAutocompletionResults() { - this.$results.empty(); - } + clearAutocompletionResults() { + this.$results.empty(); + } - /** + /** * Updates the UI to match after the facet is changed * @param {String} newFacet */ - handleFacetValueChange(newFacet) { + handleFacetValueChange(newFacet) { // update the UI - this.$facetSelect.val(newFacet); - const text = this.$facetSelect.find('option:selected').text(); - $('header#header-bar .search-facet-value').html(text); + this.$facetSelect.val(newFacet); + const text = this.$facetSelect.find('option:selected').text(); + $('header#header-bar .search-facet-value').html(text); - // Add immediate refresh when input has value and focus is on the facet selector - if (this.$input.val() && this.$facetSelect.is(':focus')) { - this.renderAutocompletionResults(); + // Add immediate refresh when input has value and focus is on the facet selector + if (this.$input.val() && this.$facetSelect.is(':focus')) { + this.renderAutocompletionResults(); + } } - } - /** + /** * Handles changes to the facet from the UI * @param {JQuery.Event} event */ - handleFacetSelectChange(event) { - const newFacet = event.target.value; - // We don't want to persist advanced becaues it behaves like a button - if (newFacet === 'advanced') { - event.preventDefault(); - this.navigateTo('/advancedsearch'); - } else { - this.facet.write(newFacet); + handleFacetSelectChange(event) { + const newFacet = event.target.value; + // We don't want to persist advanced becaues it behaves like a button + if (newFacet === 'advanced') { + event.preventDefault(); + this.navigateTo('/advancedsearch'); + } else { + this.facet.write(newFacet); + } } - } - /** + /** * For testing purposes, wraps window.location * @returns {URL} The current URL */ - getCurUrl() { - return window.location; - } + getCurUrl() { + return window.location; + } - /** + /** * Just so we can stub/test this * @param {String} path */ - navigateTo(path) { - window.location.assign(path); - } + navigateTo(path) { + window.location.assign(path); + } - /** + /** * Makes changes to the UI after a change occurs to the mode * Parts of this might be dead code; I don't really understand why * this is necessary, so opting to leave it alone for now. * @param {String} newMode */ - handleSearchModeChange(newMode) { - $('.instantsearch-mode').val(newMode); - $(`input[name=mode][value=${newMode}]`).prop('checked', true); - SearchUtils.addModeInputsToForm(this.$form, newMode); - } + handleSearchModeChange(newMode) { + $('.instantsearch-mode').val(newMode); + $(`input[name=mode][value=${newMode}]`).prop('checked', true); + SearchUtils.addModeInputsToForm(this.$form, newMode); + } } diff --git a/openlibrary/plugins/openlibrary/js/SearchPage.js b/openlibrary/plugins/openlibrary/js/SearchPage.js index b0b869f03e9..4bc5355268f 100644 --- a/openlibrary/plugins/openlibrary/js/SearchPage.js +++ b/openlibrary/plugins/openlibrary/js/SearchPage.js @@ -5,19 +5,19 @@ import { addModeInputsToForm, mode as searchMode } from './SearchUtils'; /** Manages some (PROBABLY VERY FEW) of the interactions on the search page */ export class SearchPage { - /** + /** * @param {HTMLFormElement|JQuery} form the .olform search form * @param {SearchModeSelector} searchModeSelector */ - constructor(form, searchModeSelector) { - this.$form = $(form); - searchMode.sync(this.updateModeInputs.bind(this)); - this.$form.on('submit', this.updateModeInputs.bind(this)); - searchModeSelector.change(() => this.$form.trigger('submit')); - } + constructor(form, searchModeSelector) { + this.$form = $(form); + searchMode.sync(this.updateModeInputs.bind(this)); + this.$form.on('submit', this.updateModeInputs.bind(this)); + searchModeSelector.change(() => this.$form.trigger('submit')); + } - /** Convenience wrapper of {@link addModeInputsToForm} */ - updateModeInputs() { - addModeInputsToForm(this.$form, searchMode.read()); - } + /** Convenience wrapper of {@link addModeInputsToForm} */ + updateModeInputs() { + addModeInputsToForm(this.$form, searchMode.read()); + } } diff --git a/openlibrary/plugins/openlibrary/js/SearchUtils.js b/openlibrary/plugins/openlibrary/js/SearchUtils.js index 7481b8f3e9e..7606b887541 100644 --- a/openlibrary/plugins/openlibrary/js/SearchUtils.js +++ b/openlibrary/plugins/openlibrary/js/SearchUtils.js @@ -8,21 +8,21 @@ import { removeURLParameter } from './Browser'; * @param {String} searchMode */ export function addModeInputsToForm($form, searchMode) { - $("input[name='has_fulltext']").remove(); + $('input[name=\'has_fulltext\']').remove(); - let url = $form.attr('action'); - if (url) { - url = removeURLParameter(url, 'm'); - url = removeURLParameter(url, 'has_fulltext'); - url = removeURLParameter(url, 'subject_facet'); + let url = $form.attr('action'); + if (url) { + url = removeURLParameter(url, 'm'); + url = removeURLParameter(url, 'has_fulltext'); + url = removeURLParameter(url, 'subject_facet'); - if (searchMode !== 'everything') { - $form.append('<input type="hidden" name="has_fulltext" value="true"/>'); - url = `${url + (url.indexOf('?') > -1 ? '&' : '?')}has_fulltext=true`; - } + if (searchMode !== 'everything') { + $form.append('<input type="hidden" name="has_fulltext" value="true"/>'); + url = `${url + (url.indexOf('?') > -1 ? '&' : '?')}has_fulltext=true`; + } - $form.attr('action', url); - } + $form.attr('action', url); + } } /** @@ -35,108 +35,108 @@ export function addModeInputsToForm($form, searchMode) { /** String value that's persisted to localstorage */ export class PersistentValue { - /** + /** * @param {String} key * @param {PersistentValue.Options} options */ - constructor(key, options = {}) { - this.key = key; - this.options = Object.assign({}, PersistentValue.DEFAULT_OPTIONS, options); - this._listeners = []; - - const noValue = this.read() === null; - const isValid = () => - !this.options.initValidation || this.options.initValidation(this.read()); - if (noValue || !isValid()) { - this.write(this.options.default); + constructor(key, options = {}) { + this.key = key; + this.options = Object.assign({}, PersistentValue.DEFAULT_OPTIONS, options); + this._listeners = []; + + const noValue = this.read() === null; + const isValid = () => + !this.options.initValidation || this.options.initValidation(this.read()); + if (noValue || !isValid()) { + this.write(this.options.default); + } } - } - /** + /** * Read the stored value * @return {String} */ - read() { - return localStorage.getItem(this.key); - } + read() { + return localStorage.getItem(this.key); + } - /** + /** * Update the stored value * @param {String} newValue */ - write(newValue) { - const oldValue = this.read(); - let toWrite = newValue; - if (this.options.writeTransformation) { - toWrite = this.options.writeTransformation(newValue, oldValue); + write(newValue) { + const oldValue = this.read(); + let toWrite = newValue; + if (this.options.writeTransformation) { + toWrite = this.options.writeTransformation(newValue, oldValue); + } + + if (toWrite === null) { + localStorage.removeItem(this.key); + } else { + localStorage.setItem(this.key, toWrite); + } + + if (oldValue !== toWrite) { + this._emit(toWrite); + } } - if (toWrite === null) { - localStorage.removeItem(this.key); - } else { - localStorage.setItem(this.key, toWrite); - } - - if (oldValue !== toWrite) { - this._emit(toWrite); - } - } - - /** + /** * Listen to updates to this value * @param {Function} listener * @param {Boolean} callAtStart whether to call the listener right now with the current value */ - sync(listener, callAtStart = true) { - this._listeners.push(listener); - if (callAtStart) listener(this.read()); - } + sync(listener, callAtStart = true) { + this._listeners.push(listener); + if (callAtStart) listener(this.read()); + } - /** + /** * @private * Notify listeners of an update * @param {String} newValue */ - _emit(newValue) { - this._listeners.forEach((listener) => listener(newValue)); - } + _emit(newValue) { + this._listeners.forEach((listener) => listener(newValue)); + } } /** @type {PersistentValue.Options} */ PersistentValue.DEFAULT_OPTIONS = { - default: null, - initValidation: null, - writeTransformation: null, + default: null, + initValidation: null, + writeTransformation: null, }; const MODES = ['everything', 'ebooks']; const DEFAULT_MODE = 'everything'; /** Search mode; {@see MODES} */ export const mode = new PersistentValue('mode', { - default: DEFAULT_MODE, - initValidation: (mode) => MODES.indexOf(mode) !== -1, - writeTransformation(newValue, oldValue) { - const mode = (newValue && newValue.toLowerCase()) || oldValue; - const isValidMode = MODES.indexOf(mode) !== -1; - return isValidMode ? mode : DEFAULT_MODE; - }, + default: DEFAULT_MODE, + initValidation: (mode) => MODES.indexOf(mode) !== -1, + writeTransformation(newValue, oldValue) { + const mode = (newValue && newValue.toLowerCase()) || oldValue; + const isValidMode = MODES.indexOf(mode) !== -1; + return isValidMode ? mode : DEFAULT_MODE; + }, }); /** Manages interactions of the search mode radio buttons */ export class SearchModeSelector { - /** + /** * @param {JQuery} radioButtons */ - constructor(radioButtons) { - this.$radioButtons = radioButtons; - this.change((newMode) => mode.write(newMode)); - } + constructor(radioButtons) { + this.$radioButtons = radioButtons; + this.change((newMode) => mode.write(newMode)); + } - /** + /** * Listen for changes * @param {Function} handler */ - change(handler) { - this.$radioButtons.on('change', (event) => handler($(event.target).val())); - } + change(handler) { + this.$radioButtons.on('change', (event) => handler($(event.target).val())); + } } diff --git a/openlibrary/plugins/openlibrary/js/Toast.js b/openlibrary/plugins/openlibrary/js/Toast.js index a32f2fc53e6..41f4431f484 100644 --- a/openlibrary/plugins/openlibrary/js/Toast.js +++ b/openlibrary/plugins/openlibrary/js/Toast.js @@ -8,40 +8,40 @@ import '../../../../static/css/components/toast.css'; const DEFAULT_TIMEOUT = 2500; export class Toast { - /** + /** * @param {JQuery} $toast The element containing the appropriate parts * @param {JQuery|HTMLElement} containerParent where to add the toast bar */ - constructor($toast, containerParent = document.body) { - const $parent = $(containerParent); - if (!$parent.has('.toast-container').length) { - $parent.prepend('<div class="toast-container"></div>'); + constructor($toast, containerParent = document.body) { + const $parent = $(containerParent); + if (!$parent.has('.toast-container').length) { + $parent.prepend('<div class="toast-container"></div>'); + } + if ($toast.data('toast-trigger')) { + $($toast.data('toast-trigger')).on('click', () => this.show()); + } + /** The toast bar that the toast will be added to. */ + this.$container = $parent.children('.toast-container').first(); + this.$toast = $toast; } - if ($toast.data('toast-trigger')) { - $($toast.data('toast-trigger')).on('click', () => this.show()); - } - /** The toast bar that the toast will be added to. */ - this.$container = $parent.children('.toast-container').first(); - this.$toast = $toast; - } - /** Displays the toast component on the page. */ - show() { - this.$toast.appendTo(this.$container).fadeIn(); - this.$toast.find('.toast__close').one('click', () => this.close()); - } + /** Displays the toast component on the page. */ + show() { + this.$toast.appendTo(this.$container).fadeIn(); + this.$toast.find('.toast__close').one('click', () => this.close()); + } - /** Hides the toast component and removes it from the DOM. */ - close() { - this.$toast.fadeOut('slow', () => this.$toast.remove()); - } + /** Hides the toast component and removes it from the DOM. */ + close() { + this.$toast.fadeOut('slow', () => this.$toast.remove()); + } } /** * Creates a small pop-up message that closes after some amount of time. */ export class FadingToast extends Toast { - /** + /** * Creates a new toast component, adds a close listener to the component, and adds the component * as the first child of the given parent element. * @@ -49,45 +49,45 @@ export class FadingToast extends Toast { * @param {JQuery} [$parent] Designates where the toast component will be attached * @param {number} [timeout] Amount of time, in milliseconds, that the component will be visible */ - constructor(message, $parent = null, timeout = DEFAULT_TIMEOUT) { + constructor(message, $parent = null, timeout = DEFAULT_TIMEOUT) { // TODO(i18n-js) - const $toast = $(`<div class="toast"> + const $toast = $(`<div class="toast"> <span class="toast__body">${message}</span> <a class="toast__close">×<span class="shift">Close</span></a> </div>`); - // Prevent sending null parent: - if ($parent) { - super($toast, $parent); - } else { - super($toast); + // Prevent sending null parent: + if ($parent) { + super($toast, $parent); + } else { + super($toast); + } + this.timeout = timeout; } - this.timeout = timeout; - } - /** @override */ - show() { - super.show(); + /** @override */ + show() { + super.show(); - setTimeout(() => { - this.close(); - }, this.timeout); - } + setTimeout(() => { + this.close(); + }, this.timeout); + } } /** * Creates a small pop-up message that must be closed by the viewer. */ export class PersistentToast extends Toast { - /** + /** * @param {string} message String that will be displayed within the toast component * @param {string} classes Additional classes to add to the toast component */ - constructor(message, classes = '') { - const $toast = $(`<div class="toast ${classes}"> + constructor(message, classes = '') { + const $toast = $(`<div class="toast ${classes}"> <span class="toast__body">${message}</span> <a class="toast__close">×<span class="shift">Close</span> </div>`); - super($toast); - } + super($toast); + } } diff --git a/openlibrary/plugins/openlibrary/js/add-book.js b/openlibrary/plugins/openlibrary/js/add-book.js index c42702d2e0c..18d36453d60 100644 --- a/openlibrary/plugins/openlibrary/js/add-book.js +++ b/openlibrary/plugins/openlibrary/js/add-book.js @@ -1,13 +1,13 @@ import { - isChecksumValidIsbn10, - isChecksumValidIsbn13, - isFormatValidIsbn10, - isFormatValidIsbn13, - isValidLccn, - isValidOclc, - parseIsbn, - parseLccn, - parseOclc, + isChecksumValidIsbn10, + isChecksumValidIsbn13, + isFormatValidIsbn10, + isFormatValidIsbn13, + isValidLccn, + isValidOclc, + parseIsbn, + parseLccn, + parseOclc, } from './idValidation.js'; import { trimInputValues } from './utils.js'; @@ -19,184 +19,184 @@ let invalidOclc; let emptyId; const i18nStrings = JSON.parse( - document.querySelector('form[name=edit]').dataset.i18n, + document.querySelector('form[name=edit]').dataset.i18n, ); const addBookForm = $('form#addbook'); export function initAddBookImport() { - $('.list-books a').on('click', function () { - var li = $(this).parents('li').first(); - $('input#work').val(`/works/${li.attr('id')}`); - addBookForm.trigger('submit'); - }); - $('#bookAddCont').on('click', () => { - $('input#work').val('none-of-these'); - addBookForm.trigger('submit'); - }); - - invalidChecksum = i18nStrings.invalid_checksum; - invalidIsbn10 = i18nStrings.invalid_isbn10; - invalidIsbn13 = i18nStrings.invalid_isbn13; - invalidLccn = i18nStrings.invalid_lccn; - invalidOclc = i18nStrings.invalid_oclc; - emptyId = i18nStrings.empty_id; - - $('#id_value').on('change', autoCompleteIdName); - $('#addbook').on('submit', parseAndValidateId); - $('#id_value').on('input', clearErrors); - $('#id_name').on('change', clearErrors); - - $('#publish_date').on('blur', validatePublishDate); - - trimInputValues('input'); - - // Prevents submission if the publish date is > 1 year in the future - addBookForm.on('submit', () => { - if ($('#publish-date-errors').hasClass('hidden')) { - return true; - } else return false; - }); + $('.list-books a').on('click', function () { + var li = $(this).parents('li').first(); + $('input#work').val(`/works/${li.attr('id')}`); + addBookForm.trigger('submit'); + }); + $('#bookAddCont').on('click', () => { + $('input#work').val('none-of-these'); + addBookForm.trigger('submit'); + }); + + invalidChecksum = i18nStrings.invalid_checksum; + invalidIsbn10 = i18nStrings.invalid_isbn10; + invalidIsbn13 = i18nStrings.invalid_isbn13; + invalidLccn = i18nStrings.invalid_lccn; + invalidOclc = i18nStrings.invalid_oclc; + emptyId = i18nStrings.empty_id; + + $('#id_value').on('change', autoCompleteIdName); + $('#addbook').on('submit', parseAndValidateId); + $('#id_value').on('input', clearErrors); + $('#id_name').on('change', clearErrors); + + $('#publish_date').on('blur', validatePublishDate); + + trimInputValues('input'); + + // Prevents submission if the publish date is > 1 year in the future + addBookForm.on('submit', () => { + if ($('#publish-date-errors').hasClass('hidden')) { + return true; + } else return false; + }); } // a flag to make raiseIsbnError perform differently upon subsequent calls let addBookWithIsbnErrors = false; function displayIsbnError(event, errorMessage) { - if (!addBookWithIsbnErrors) { - addBookWithIsbnErrors = true; + if (!addBookWithIsbnErrors) { + addBookWithIsbnErrors = true; + const errorDiv = document.getElementById('id-errors'); + errorDiv.classList.remove('hidden'); + errorDiv.textContent = errorMessage; + const confirm = document.getElementById('confirm-add'); + confirm.classList.remove('hidden'); + const isbnInput = document.getElementById('id_value'); + isbnInput.focus({ focusVisible: true }); + event.preventDefault(); + return; + } + // parsing potentially invalid ISBN + document.getElementById('id_value').value = parseIsbn( + document.getElementById('id_value').value, + ); +} + +function displayIdentifierError(event, errorMessage) { const errorDiv = document.getElementById('id-errors'); errorDiv.classList.remove('hidden'); errorDiv.textContent = errorMessage; - const confirm = document.getElementById('confirm-add'); - confirm.classList.remove('hidden'); - const isbnInput = document.getElementById('id_value'); - isbnInput.focus({ focusVisible: true }); event.preventDefault(); return; - } - // parsing potentially invalid ISBN - document.getElementById('id_value').value = parseIsbn( - document.getElementById('id_value').value, - ); -} - -function displayIdentifierError(event, errorMessage) { - const errorDiv = document.getElementById('id-errors'); - errorDiv.classList.remove('hidden'); - errorDiv.textContent = errorMessage; - event.preventDefault(); - return; } function clearErrors() { - addBookWithIsbnErrors = false; - const errorDiv = document.getElementById('id-errors'); - errorDiv.classList.add('hidden'); - const confirm = document.getElementById('confirm-add'); - confirm.classList.add('hidden'); + addBookWithIsbnErrors = false; + const errorDiv = document.getElementById('id-errors'); + errorDiv.classList.add('hidden'); + const confirm = document.getElementById('confirm-add'); + confirm.classList.add('hidden'); } function parseAndValidateId(event) { - const fieldName = document.getElementById('id_name').value; - const idValue = document.getElementById('id_value').value; - - if (fieldName === 'isbn_10') { - parseAndValidateIsbn10(event, idValue); - } else if (fieldName === 'isbn_13') { - parseAndValidateIsbn13(event, idValue); - } else if (fieldName === 'lccn') { - parseAndValidateLccn(event, idValue); - } else if (fieldName === 'oclc_numbers') { - parseAndValidateOclc(event, idValue); - } else if (!fieldName || !isEmptyId(event, idValue)) { - document.getElementById('id_value').value = idValue.trim(); - } + const fieldName = document.getElementById('id_name').value; + const idValue = document.getElementById('id_value').value; + + if (fieldName === 'isbn_10') { + parseAndValidateIsbn10(event, idValue); + } else if (fieldName === 'isbn_13') { + parseAndValidateIsbn13(event, idValue); + } else if (fieldName === 'lccn') { + parseAndValidateLccn(event, idValue); + } else if (fieldName === 'oclc_numbers') { + parseAndValidateOclc(event, idValue); + } else if (!fieldName || !isEmptyId(event, idValue)) { + document.getElementById('id_value').value = idValue.trim(); + } } function isEmptyId(event, idValue) { - if (!idValue.trim()) { - const errorDiv = document.getElementById('id-errors'); - errorDiv.classList.remove('hidden'); - errorDiv.textContent = emptyId; - event.preventDefault(); - return true; - } - return false; + if (!idValue.trim()) { + const errorDiv = document.getElementById('id-errors'); + errorDiv.classList.remove('hidden'); + errorDiv.textContent = emptyId; + event.preventDefault(); + return true; + } + return false; } function parseAndValidateIsbn10(event, idValue) { - // parsing valid ISBN that passes checks - idValue = parseIsbn(idValue); - if (!isFormatValidIsbn10(idValue)) { - return displayIsbnError(event, invalidIsbn10); - } - if (!isChecksumValidIsbn10(idValue)) { - return displayIsbnError(event, invalidChecksum); - } - document.getElementById('id_value').value = idValue; + // parsing valid ISBN that passes checks + idValue = parseIsbn(idValue); + if (!isFormatValidIsbn10(idValue)) { + return displayIsbnError(event, invalidIsbn10); + } + if (!isChecksumValidIsbn10(idValue)) { + return displayIsbnError(event, invalidChecksum); + } + document.getElementById('id_value').value = idValue; } function parseAndValidateIsbn13(event, idValue) { - idValue = parseIsbn(idValue); - if (!isFormatValidIsbn13(idValue)) { - return displayIsbnError(event, invalidIsbn13); - } - if (!isChecksumValidIsbn13(idValue)) { - return displayIsbnError(event, invalidChecksum); - } - document.getElementById('id_value').value = idValue; + idValue = parseIsbn(idValue); + if (!isFormatValidIsbn13(idValue)) { + return displayIsbnError(event, invalidIsbn13); + } + if (!isChecksumValidIsbn13(idValue)) { + return displayIsbnError(event, invalidChecksum); + } + document.getElementById('id_value').value = idValue; } function parseAndValidateLccn(event, idValue) { - idValue = parseLccn(idValue); - if (!isValidLccn(idValue)) { - return displayIdentifierError(event, invalidLccn); - } - document.getElementById('id_value').value = idValue; + idValue = parseLccn(idValue); + if (!isValidLccn(idValue)) { + return displayIdentifierError(event, invalidLccn); + } + document.getElementById('id_value').value = idValue; } function parseAndValidateOclc(event, idValue) { - idValue = parseOclc(idValue); - if (!isValidOclc(idValue)) { - return displayIdentifierError(event, invalidOclc); - } - document.getElementById('id_value').value = idValue; + idValue = parseOclc(idValue); + if (!isValidOclc(idValue)) { + return displayIdentifierError(event, invalidOclc); + } + document.getElementById('id_value').value = idValue; } function autoCompleteIdName() { - const idValue = document.querySelector('input#id_value').value.trim(); - const idValueIsbn = parseIsbn(idValue); - const currentSelection = document.getElementById('id_name').value; - - if (isFormatValidIsbn10(idValueIsbn) && isChecksumValidIsbn10(idValueIsbn)) { - document.getElementById('id_name').value = 'isbn_10'; - } else if ( - isFormatValidIsbn13(idValueIsbn) && + const idValue = document.querySelector('input#id_value').value.trim(); + const idValueIsbn = parseIsbn(idValue); + const currentSelection = document.getElementById('id_name').value; + + if (isFormatValidIsbn10(idValueIsbn) && isChecksumValidIsbn10(idValueIsbn)) { + document.getElementById('id_name').value = 'isbn_10'; + } else if ( + isFormatValidIsbn13(idValueIsbn) && isChecksumValidIsbn13(idValueIsbn) - ) { - document.getElementById('id_name').value = 'isbn_13'; - } else if (isValidLccn(parseLccn(idValue))) { - document.getElementById('id_name').value = 'lccn'; - } else { - document.getElementById('id_name').value = currentSelection || ''; - } + ) { + document.getElementById('id_name').value = 'isbn_13'; + } else if (isValidLccn(parseLccn(idValue))) { + document.getElementById('id_name').value = 'lccn'; + } else { + document.getElementById('id_name').value = currentSelection || ''; + } } function validatePublishDate() { - // validate publish-date to make sure the date is not in future - // used in templates/books/add.html - const publish_date = this.value; - // if it doesn't have even three digits then it can't be a future date - const tokens = /(\d{3,})/.exec(publish_date); - const year = new Date().getFullYear(); - const isValidDate = tokens && tokens[1] && parseInt(tokens[1]) <= year + 1; // allow one year in future. - - const errorDiv = document.getElementById('publish-date-errors'); - - if (!isValidDate) { - errorDiv.classList.remove('hidden'); - errorDiv.textContent = i18nStrings['invalid_publish_date']; - } else { - errorDiv.classList.add('hidden'); - } + // validate publish-date to make sure the date is not in future + // used in templates/books/add.html + const publish_date = this.value; + // if it doesn't have even three digits then it can't be a future date + const tokens = /(\d{3,})/.exec(publish_date); + const year = new Date().getFullYear(); + const isValidDate = tokens && tokens[1] && parseInt(tokens[1]) <= year + 1; // allow one year in future. + + const errorDiv = document.getElementById('publish-date-errors'); + + if (!isValidDate) { + errorDiv.classList.remove('hidden'); + errorDiv.textContent = i18nStrings['invalid_publish_date']; + } else { + errorDiv.classList.add('hidden'); + } } diff --git a/openlibrary/plugins/openlibrary/js/add_provider.js b/openlibrary/plugins/openlibrary/js/add_provider.js index b6a9601a5c3..3bbbee6393c 100644 --- a/openlibrary/plugins/openlibrary/js/add_provider.js +++ b/openlibrary/plugins/openlibrary/js/add_provider.js @@ -1,35 +1,35 @@ export function initAddProviderRowLink(elem) { - elem.addEventListener('click', function () { - let index = Number(elem.dataset.index); - const tbody = document.querySelector('#provider-table-body'); - tbody.appendChild(createProviderRow(index)); - if (index === 0) { - document.querySelector('#provider-table').classList.remove('hidden'); - } - this.dataset.index = ++index; - }); + elem.addEventListener('click', function () { + let index = Number(elem.dataset.index); + const tbody = document.querySelector('#provider-table-body'); + tbody.appendChild(createProviderRow(index)); + if (index === 0) { + document.querySelector('#provider-table').classList.remove('hidden'); + } + this.dataset.index = ++index; + }); } function createProviderRow(index) { - const tr = document.createElement('tr'); + const tr = document.createElement('tr'); - const innerHtml = `${createTextInputDataCell(index, 'url')} + const innerHtml = `${createTextInputDataCell(index, 'url')} ${createSelectDataCell(index, 'access', accessTypeValues)} ${createSelectDataCell(index, 'format', formatValues)} ${createTextInputDataCell(index, 'provider_name')}`; - tr.innerHTML = innerHtml; - return tr; + tr.innerHTML = innerHtml; + return tr; } function createTextInputDataCell(index, type) { - const id = `edition--providers--${index}--${type}`; - return `<td><input name="${id}" id="${id}" ${type === 'url' ? 'type="url"' : ''}></td>`; + const id = `edition--providers--${index}--${type}`; + return `<td><input name="${id}" id="${id}" ${type === 'url' ? 'type="url"' : ''}></td>`; } function createSelectDataCell(index, type, values) { - const id = `edition--providers--${index}--${type}`; - return `<td> + const id = `edition--providers--${index}--${type}`; + return `<td> <select name="${id}" id="${id}"> ${createSelectOptions(values)} </select> @@ -37,24 +37,24 @@ function createSelectDataCell(index, type, values) { } const accessTypeValues = [ - { value: '', text: '' }, - { value: 'read', text: 'Read' }, - { value: 'listen', text: 'Listen' }, - { value: 'buy', text: 'Buy' }, - { value: 'borrow', text: 'Borrow' }, - { value: 'preview', text: 'Preview' }, + { value: '', text: '' }, + { value: 'read', text: 'Read' }, + { value: 'listen', text: 'Listen' }, + { value: 'buy', text: 'Buy' }, + { value: 'borrow', text: 'Borrow' }, + { value: 'preview', text: 'Preview' }, ]; const formatValues = [ - { value: '', text: '' }, - { value: 'web', text: 'Web' }, - { value: 'epub', text: 'ePub' }, - { value: 'pdf', text: 'PDF' }, + { value: '', text: '' }, + { value: 'web', text: 'Web' }, + { value: 'epub', text: 'ePub' }, + { value: 'pdf', text: 'PDF' }, ]; function createSelectOptions(values) { - let html = ''; - for (const value of values) { - html += `<option value="${value.value}">${value.text}</option>\n`; - } - return html; + let html = ''; + for (const value of values) { + html += `<option value="${value.value}">${value.text}</option>\n`; + } + return html; } diff --git a/openlibrary/plugins/openlibrary/js/admin.js b/openlibrary/plugins/openlibrary/js/admin.js index e5c2b8e8aea..e62857e8f51 100644 --- a/openlibrary/plugins/openlibrary/js/admin.js +++ b/openlibrary/plugins/openlibrary/js/admin.js @@ -3,34 +3,34 @@ */ export function initAdmin() { - // admin/people/view - $('a.tag').on('click', function () { - var action; - var tag; + // admin/people/view + $('a.tag').on('click', function () { + var action; + var tag; - $(this).toggleClass('active'); - action = $(this).hasClass('active') ? 'add_tag' : 'remove_tag'; - tag = $(this).text(); - $.post(window.location.href, { - action: action, - tag: tag, + $(this).toggleClass('active'); + action = $(this).hasClass('active') ? 'add_tag' : 'remove_tag'; + tag = $(this).text(); + $.post(window.location.href, { + action: action, + tag: tag, + }); }); - }); - // admin/people/edits - $('#checkall').on('click', function () { - $('form.olform').find(':checkbox').prop('checked', this.checked); - }); + // admin/people/edits + $('#checkall').on('click', function () { + $('form.olform').find(':checkbox').prop('checked', this.checked); + }); } export function initAnonymizationButton(button) { - const displayName = button.dataset.displayName; - const confirmMessage = `Really anonymize ${displayName}'s account? This will delete ${displayName}'s profile page and booknotes, and anonymize ${displayName}'s reading log, reviews, star ratings, and merge request submissions.`; - button.addEventListener('click', (event) => { - if (!confirm(confirmMessage)) { - event.preventDefault(); - } - }); + const displayName = button.dataset.displayName; + const confirmMessage = `Really anonymize ${displayName}'s account? This will delete ${displayName}'s profile page and booknotes, and anonymize ${displayName}'s reading log, reviews, star ratings, and merge request submissions.`; + button.addEventListener('click', (event) => { + if (!confirm(confirmMessage)) { + event.preventDefault(); + } + }); } /** @@ -40,12 +40,12 @@ export function initAnonymizationButton(button) { * @param {NodeList<HTMLButtonElement>} buttons */ export function initConfirmationButtons(buttons) { - const confirmMessage = 'Are you sure?'; - for (const button of buttons) { - button.addEventListener('click', (event) => { - if (!confirm(confirmMessage)) { - event.preventDefault(); - } - }); - } + const confirmMessage = 'Are you sure?'; + for (const button of buttons) { + button.addEventListener('click', (event) => { + if (!confirm(confirmMessage)) { + event.preventDefault(); + } + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/affiliate-links.js b/openlibrary/plugins/openlibrary/js/affiliate-links.js index f30fcce14b2..267341c4910 100644 --- a/openlibrary/plugins/openlibrary/js/affiliate-links.js +++ b/openlibrary/plugins/openlibrary/js/affiliate-links.js @@ -10,17 +10,17 @@ import { buildPartialsUrl } from './utils'; * @param {NodeList<HTMLElement>} affiliateLinksSections Collection of each affiliate links section that is on the page */ export function initAffiliateLinks(affiliateLinksSections) { - const isLoading = showLoadingIndicators(affiliateLinksSections); - if (isLoading) { + const isLoading = showLoadingIndicators(affiliateLinksSections); + if (isLoading) { // Replace loading indicators with fetched partials - const title = affiliateLinksSections[0].dataset.title; - const opts = JSON.parse(affiliateLinksSections[0].dataset.opts); - const args = [title, opts]; - const d = { args: args }; + const title = affiliateLinksSections[0].dataset.title; + const opts = JSON.parse(affiliateLinksSections[0].dataset.opts); + const args = [title, opts]; + const d = { args: args }; - getPartials(d, affiliateLinksSections); - } + getPartials(d, affiliateLinksSections); + } } /** @@ -31,15 +31,15 @@ export function initAffiliateLinks(affiliateLinksSections) { * @returns {boolean} `true` if a loading indicator is displayed on the screen */ function showLoadingIndicators(linkSections) { - let isLoading = false; - for (const section of linkSections) { - const loadingIndicator = section.querySelector('.loadingIndicator'); - if (loadingIndicator) { - isLoading = true; - loadingIndicator.classList.remove('hidden'); + let isLoading = false; + for (const section of linkSections) { + const loadingIndicator = section.querySelector('.loadingIndicator'); + if (loadingIndicator) { + isLoading = true; + loadingIndicator.classList.remove('hidden'); + } } - } - return isLoading; + return isLoading; } /** @@ -50,50 +50,50 @@ function showLoadingIndicators(linkSections) { * @returns {Promise} */ async function getPartials(data, affiliateLinksSections) { - const dataString = JSON.stringify(data); + const dataString = JSON.stringify(data); - return fetch(buildPartialsUrl('AffiliateLinks', { data: dataString })) - .then((resp) => { - if (resp.status !== 200) { - throw new Error( - `Failed to fetch partials. Status code: ${resp.status}`, - ); - } - return resp.json(); - }) - .then((data) => { - const span = document.createElement('span'); - span.innerHTML = data['partials']; - const links = span.firstElementChild; - for (const section of affiliateLinksSections) { - section.replaceWith(links.cloneNode(true)); - } - }) - .catch(() => { - // XXX : Handle errors sensibly - for (const section of affiliateLinksSections) { - const loadingIndicator = section.querySelector('.loadingIndicator'); - if (loadingIndicator) { - loadingIndicator.classList.add('hidden'); - } + return fetch(buildPartialsUrl('AffiliateLinks', { data: dataString })) + .then((resp) => { + if (resp.status !== 200) { + throw new Error( + `Failed to fetch partials. Status code: ${resp.status}`, + ); + } + return resp.json(); + }) + .then((data) => { + const span = document.createElement('span'); + span.innerHTML = data['partials']; + const links = span.firstElementChild; + for (const section of affiliateLinksSections) { + section.replaceWith(links.cloneNode(true)); + } + }) + .catch(() => { + // XXX : Handle errors sensibly + for (const section of affiliateLinksSections) { + const loadingIndicator = section.querySelector('.loadingIndicator'); + if (loadingIndicator) { + loadingIndicator.classList.add('hidden'); + } - const existingRetryAffordance = section.querySelector( - '.affiliate-links-section__retry', - ); - if (existingRetryAffordance) { - existingRetryAffordance.classList.remove('hidden'); - } else { - section.insertAdjacentHTML('afterbegin', renderRetryLink()); - const retryAffordance = section.querySelector( - '.affiliate-links-section__retry', - ); - retryAffordance.addEventListener('click', () => { - retryAffordance.classList.add('hidden'); - getPartials(data, affiliateLinksSections); - }); - } - } - }); + const existingRetryAffordance = section.querySelector( + '.affiliate-links-section__retry', + ); + if (existingRetryAffordance) { + existingRetryAffordance.classList.remove('hidden'); + } else { + section.insertAdjacentHTML('afterbegin', renderRetryLink()); + const retryAffordance = section.querySelector( + '.affiliate-links-section__retry', + ); + retryAffordance.addEventListener('click', () => { + retryAffordance.classList.add('hidden'); + getPartials(data, affiliateLinksSections); + }); + } + } + }); } /** @@ -102,5 +102,5 @@ async function getPartials(data, affiliateLinksSections) { * @returns {string} HTML for a retry link. */ function renderRetryLink() { - return '<span class="affiliate-links-section__retry">Failed to fetch affiliate links. <button type="button" style="border:none;background:none;color:blue;text-decoration:underline;cursor:pointer;">Retry?</button></span>'; + return '<span class="affiliate-links-section__retry">Failed to fetch affiliate links. <button type="button" style="border:none;background:none;color:blue;text-decoration:underline;cursor:pointer;">Retry?</button></span>'; } diff --git a/openlibrary/plugins/openlibrary/js/autocomplete.js b/openlibrary/plugins/openlibrary/js/autocomplete.js index 6cbc376592b..9464a6544a6 100644 --- a/openlibrary/plugins/openlibrary/js/autocomplete.js +++ b/openlibrary/plugins/openlibrary/js/autocomplete.js @@ -12,13 +12,13 @@ import 'jquery-ui-touch-punch'; // this makes drag-to-reorder work on touch devi * @return {string} */ export function highlight(value, term) { - return value.replace( - new RegExp( - `(?![^&;]+;)(?!<[^<>]*)(${term.replace(/([\^$()[]\{\}\*\.\+\?\|\\])/gi, '$1')})(?![^<>]*>)(?![^&;]+;)`, - 'gi', - ), - '<strong>$1</strong>', - ); + return value.replace( + new RegExp( + `(?![^&;]+;)(?!<[^<>]*)(${term.replace(/([\^$()[]\{\}\*\.\+\?\|\\])/gi, '$1')})(?![^<>]*>)(?![^&;]+;)`, + 'gi', + ), + '<strong>$1</strong>', + ); } /** @@ -31,29 +31,29 @@ export function highlight(value, term) { * @return {array} of modified results that are compatible with the jquery autocomplete search suggestions */ export const mapApiResultsToAutocompleteSuggestions = ( - results, - labelFormatter, - addNewFieldTerm, + results, + labelFormatter, + addNewFieldTerm, ) => { - const mapAPIResultToSuggestedItem = (r) => ({ - key: r.key, - label: labelFormatter(r), - value: r.name, - }); - - // When no results if callback is defined, append a create new entry - if (addNewFieldTerm) { - results.push({ - name: addNewFieldTerm, - key: '__new__', - value: addNewFieldTerm, + const mapAPIResultToSuggestedItem = (r) => ({ + key: r.key, + label: labelFormatter(r), + value: r.name, }); - } - return results.map(mapAPIResultToSuggestedItem); + + // When no results if callback is defined, append a create new entry + if (addNewFieldTerm) { + results.push({ + name: addNewFieldTerm, + key: '__new__', + value: addNewFieldTerm, + }); + } + return results.map(mapAPIResultToSuggestedItem); }; export function init() { - /** + /** * Some extra options for when creating an autocomplete input field * @typedef {Object} OpenLibraryAutocompleteOptions * @property {string} endpoint - url to hit for autocomplete results @@ -65,7 +65,7 @@ export function init() { * @property {boolean} [sortable=false] */ - /** + /** * @private * @param{HTMLInputElement} _this - input element that will become autocompleting. * @param{OpenLibraryAutocompleteOptions} ol_ac_opts @@ -73,96 +73,96 @@ export function init() { * @param {Function} ac_opts.formatItem - optional item formatter. Returns a string of HTML for rendering as an item. * @param {Function} ac_opts.termPreprocessor - optional hook for processing the search term before doing the search */ - function setup_autocomplete(_this, ol_ac_opts, ac_opts) { - var default_ac_opts = { - minChars: 2, - autoFill: true, - formatItem: (item) => item.name, - /** + function setup_autocomplete(_this, ol_ac_opts, ac_opts) { + var default_ac_opts = { + minChars: 2, + autoFill: true, + formatItem: (item) => item.name, + /** * Adds the ac_over class to the selected autocomplete item * * @param {Event} _event (unused) * @param {Object} ui containing item key */ - focus: (_event, ui) => { - const $list = $(_this).data('list'); - if ($list) { - $list - .find('li') - .removeClass('ac_over') - .filter( - (_, el) => $(el).data('ui-autocomplete-item').key === ui.item.key, - ) - .addClass('ac_over'); - } - return ac_opts.autoFill; - }, - select: function (_event, ui) { - var item = ui.item; - var $this = $(this); - $this.closest('.ac-input').find('.ac-input__value').val(item.key); - const $preview = $this.closest('.ac-input').find('.ac-input__preview'); - if ($preview.length) { - $preview.html(item.label); - } - setTimeout(() => { - $this.addClass('accept'); - }, 0); - }, - mustMatch: true, - formatMatch: (item) => item.name, - termPreprocessor: (term) => term, - }; + focus: (_event, ui) => { + const $list = $(_this).data('list'); + if ($list) { + $list + .find('li') + .removeClass('ac_over') + .filter( + (_, el) => $(el).data('ui-autocomplete-item').key === ui.item.key, + ) + .addClass('ac_over'); + } + return ac_opts.autoFill; + }, + select: function (_event, ui) { + var item = ui.item; + var $this = $(this); + $this.closest('.ac-input').find('.ac-input__value').val(item.key); + const $preview = $this.closest('.ac-input').find('.ac-input__preview'); + if ($preview.length) { + $preview.html(item.label); + } + setTimeout(() => { + $this.addClass('accept'); + }, 0); + }, + mustMatch: true, + formatMatch: (item) => item.name, + termPreprocessor: (term) => term, + }; - $.widget('custom.autocompleteHTML', $.ui.autocomplete, { - _renderMenu($ul, items) { - $ul.addClass('ac_results').attr('id', this.ulRef); - items.forEach((item) => { - $('<li>') - .data('ui-autocomplete-item', item) - .attr('aria-label', item.value) - .html(item.label) - .appendTo($ul); + $.widget('custom.autocompleteHTML', $.ui.autocomplete, { + _renderMenu($ul, items) { + $ul.addClass('ac_results').attr('id', this.ulRef); + items.forEach((item) => { + $('<li>') + .data('ui-autocomplete-item', item) + .attr('aria-label', item.value) + .html(item.label) + .appendTo($ul); + }); + // store list so we can add ac_over hover effect in `focus` event + $(_this).data('list', $ul); + }, }); - // store list so we can add ac_over hover effect in `focus` event - $(_this).data('list', $ul); - }, - }); - const options = $.extend(default_ac_opts, ac_opts); - options.source = (q, response) => { - const term = options.termPreprocessor(q.term); - const params = { - q: term, - limit: options.max, - }; - if (location.search.indexOf('lang=') !== -1) { - params.lang = new URLSearchParams(location.search).get('lang'); - } - if (params.q.length < options.minChars) return; - return $.ajax({ - url: ol_ac_opts.endpoint, - data: params, - }).then((results) => { - response( - mapApiResultsToAutocompleteSuggestions( - results, - (r) => highlight(options.formatItem(r), term), - ol_ac_opts.addnew === true || + const options = $.extend(default_ac_opts, ac_opts); + options.source = (q, response) => { + const term = options.termPreprocessor(q.term); + const params = { + q: term, + limit: options.max, + }; + if (location.search.indexOf('lang=') !== -1) { + params.lang = new URLSearchParams(location.search).get('lang'); + } + if (params.q.length < options.minChars) return; + return $.ajax({ + url: ol_ac_opts.endpoint, + data: params, + }).then((results) => { + response( + mapApiResultsToAutocompleteSuggestions( + results, + (r) => highlight(options.formatItem(r), term), + ol_ac_opts.addnew === true || (ol_ac_opts.addnew && ol_ac_opts.addnew(term)) - ? ol_ac_opts.new_name || term - : null, - ), - ); - }); - }; - $(_this) - .autocompleteHTML(options) - .on('keypress', function () { - $(this).removeClass('accept').removeClass('reject'); - }); - } + ? ol_ac_opts.new_name || term + : null, + ), + ); + }); + }; + $(_this) + .autocompleteHTML(options) + .on('keypress', function () { + $(this).removeClass('accept').removeClass('reject'); + }); + } - /** + /** * @this HTMLElement - the element that contains the different inputs. * Expects an html structure like: * <div class="multi-input-autocomplete"> @@ -178,150 +178,150 @@ export function init() { * @param {OpenLibraryAutocompleteOptions} ol_ac_opts * @param {Object} ac_opts - options given to override defaults of $.autocomplete; see that. */ - $.fn.setup_multi_input_autocomplete = function ( - input_renderer, - ol_ac_opts, - ac_opts, - ) { + $.fn.setup_multi_input_autocomplete = function ( + input_renderer, + ol_ac_opts, + ac_opts, + ) { /** @type {JQuery<HTMLElement>} */ - var container = $(this); - - // first let's init any pre-existing inputs - container.find('.ac-input__visible').each(function () { - setup_autocomplete(this, ol_ac_opts, ac_opts); - }); - const allow_empty = ol_ac_opts.allow_empty; + var container = $(this); - function update_visible() { - if (allow_empty || container.find('.mia__input').length > 1) { - container.find('.mia__remove').show(); - } else { - container.find('.mia__remove').hide(); - } - } + // first let's init any pre-existing inputs + container.find('.ac-input__visible').each(function () { + setup_autocomplete(this, ol_ac_opts, ac_opts); + }); + const allow_empty = ol_ac_opts.allow_empty; - function update_indices() { - container.find('.mia__input').each(function (index) { - $(this) - .find('.mia__index') - .each(function () { - $(this).text( - $(this) - .text() - .replace(/\d+/, index + 1), - ); - }); - $(this) - .find('[name]') - .each(function () { - // this won't behave nicely with nested numeric things, if that ever happens - if ($(this).attr('name').match(/\d+/)?.length > 1) { - throw new Error('nested numeric names not supported'); + function update_visible() { + if (allow_empty || container.find('.mia__input').length > 1) { + container.find('.mia__remove').show(); + } else { + container.find('.mia__remove').hide(); } - $(this).attr('name', $(this).attr('name').replace(/\d+/, index)); - if ($(this).attr('id')) { - $(this).attr('id', $(this).attr('id').replace(/\d+/, index)); - } - }); - }); - } - - update_visible(); + } - if (ol_ac_opts.sortable) { - container.sortable({ - handle: '.mia__reorder', - items: '.mia__input', - update: update_indices, - cancel: '.mia__move, .mia__remove', - }); - } + function update_indices() { + container.find('.mia__input').each(function (index) { + $(this) + .find('.mia__index') + .each(function () { + $(this).text( + $(this) + .text() + .replace(/\d+/, index + 1), + ); + }); + $(this) + .find('[name]') + .each(function () { + // this won't behave nicely with nested numeric things, if that ever happens + if ($(this).attr('name').match(/\d+/)?.length > 1) { + throw new Error('nested numeric names not supported'); + } + $(this).attr('name', $(this).attr('name').replace(/\d+/, index)); + if ($(this).attr('id')) { + $(this).attr('id', $(this).attr('id').replace(/\d+/, index)); + } + }); + }); + } - container.on('click', '.mia__remove', function () { - if (allow_empty || container.find('.mia__input').length > 1) { - $(this).closest('.mia__input').remove(); update_visible(); - update_indices(); - } - }); - // Add move button functionality - container.on('click', '.mia__move', function (event) { - event.preventDefault(); - const $currentItem = $(this).closest('.mia__input'); - const $allItems = container.find('.mia__input'); - const currentIndex = $allItems.index($currentItem); - const currentPosition = currentIndex + 1; // 1-based position for user display - const totalItems = $allItems.length; + if (ol_ac_opts.sortable) { + container.sortable({ + handle: '.mia__reorder', + items: '.mia__input', + update: update_indices, + cancel: '.mia__move, .mia__remove', + }); + } - // Create a clear message showing current position - const message = `Enter the new position (1-${totalItems}):`; + container.on('click', '.mia__remove', function () { + if (allow_empty || container.find('.mia__input').length > 1) { + $(this).closest('.mia__input').remove(); + update_visible(); + update_indices(); + } + }); - const userInput = prompt(message); + // Add move button functionality + container.on('click', '.mia__move', function (event) { + event.preventDefault(); + const $currentItem = $(this).closest('.mia__input'); + const $allItems = container.find('.mia__input'); + const currentIndex = $allItems.index($currentItem); + const currentPosition = currentIndex + 1; // 1-based position for user display + const totalItems = $allItems.length; - // Handle cancellation - if (userInput === null) { - return; - } + // Create a clear message showing current position + const message = `Enter the new position (1-${totalItems}):`; - const newPosition = parseFloat(userInput.trim()); + const userInput = prompt(message); - // Validate the input - if (isNaN(newPosition) || newPosition < 1 || newPosition > totalItems) { - alert(`Please enter a valid number between 1 and ${totalItems}.`); - return; - } + // Handle cancellation + if (userInput === null) { + return; + } - // Check if it's the same position - if (newPosition === currentPosition) { - alert('Item is already at that position.'); - return; - } + const newPosition = parseFloat(userInput.trim()); - // Perform the move - const newIndex = newPosition - 1; // Convert to 0-based index + // Validate the input + if (isNaN(newPosition) || newPosition < 1 || newPosition > totalItems) { + alert(`Please enter a valid number between 1 and ${totalItems}.`); + return; + } - if (newIndex < currentIndex) { - $currentItem.insertBefore($allItems.eq(newIndex)); - } else { - $currentItem.insertAfter($allItems.eq(newIndex)); - } + // Check if it's the same position + if (newPosition === currentPosition) { + alert('Item is already at that position.'); + return; + } - // Update indices after move - update_indices(); - }); + // Perform the move + const newIndex = newPosition - 1; // Convert to 0-based index - container.on('click', '.mia__add', (event) => { - var next_index, new_input; - event.preventDefault(); + if (newIndex < currentIndex) { + $currentItem.insertBefore($allItems.eq(newIndex)); + } else { + $currentItem.insertAfter($allItems.eq(newIndex)); + } - next_index = container.find('.mia__input').length; - new_input = $(input_renderer(next_index, { key: '', name: '' })); - new_input.insertBefore(container.find('.mia__add')); - setup_autocomplete( - new_input.find('.ac-input__visible')[0], - ol_ac_opts, - ac_opts, - ); - update_visible(); - }); - }; + // Update indices after move + update_indices(); + }); + + container.on('click', '.mia__add', (event) => { + var next_index, new_input; + event.preventDefault(); + + next_index = container.find('.mia__input').length; + new_input = $(input_renderer(next_index, { key: '', name: '' })); + new_input.insertBefore(container.find('.mia__add')); + setup_autocomplete( + new_input.find('.ac-input__visible')[0], + ol_ac_opts, + ac_opts, + ); + update_visible(); + }); + }; - /** + /** * @this HTMLElement - the element that contains the input. * @param {string} autocomplete_selector - selector to find the input element use for autocomplete. * @param {OpenLibraryAutocompleteOptions} ol_ac_opts * @param {Object} ac_opts - options given to override defaults of $.autocomplete; see that. */ - $.fn.setup_csv_autocomplete = function ( - autocomplete_selector, - ol_ac_opts, - ac_opts, - ) { - const container = $(this); - const dataConfig = JSON.parse(container[0].dataset.config); + $.fn.setup_csv_autocomplete = function ( + autocomplete_selector, + ol_ac_opts, + ac_opts, + ) { + const container = $(this); + const dataConfig = JSON.parse(container[0].dataset.config); - /** + /** * Converts a csv string to an array of strings * * Eg @@ -330,58 +330,58 @@ export function init() { * @param {string} val * @returns {string[]} */ - function splitField(val) { - const m = val.match(/("[^"]+"|[^,"]+)/g); - if (!m) { - throw new Error('Invalid CSV'); - } - - return m.map((s) => s.trim().replace(/^"(.*)"$/, '$1')).filter((s) => s); - } + function splitField(val) { + const m = val.match(/("[^"]+"|[^,"]+)/g); + if (!m) { + throw new Error('Invalid CSV'); + } - function joinField(vals) { - const escaped = vals.map((val) => (val.includes(',') ? `"${val}"` : val)); - return escaped.join(', '); - } + return m.map((s) => s.trim().replace(/^"(.*)"$/, '$1')).filter((s) => s); + } - const default_ac_opts = { - minChars: 2, - max: 25, - matchSubset: false, - autoFill: false, - position: { my: 'right top', at: 'right bottom' }, - termPreprocessor: (subject_string) => { - const terms = splitField(subject_string); - if (terms.length !== dataConfig.data.length) { - return terms.pop(); - } else { - $('ul.ui-autocomplete').hide(); - return ''; + function joinField(vals) { + const escaped = vals.map((val) => (val.includes(',') ? `"${val}"` : val)); + return escaped.join(', '); } - }, - select: function (event, ui) { - const terms = splitField(this.value); - terms.splice(terms.length - 1, 1, ui.item.value); - this.value = `${joinField(terms)}, `; - dataConfig.data.push(ui.item.value); - container[0].dataset.config = JSON.stringify(dataConfig); - $(this).trigger('input'); - return false; - }, - response: function (event, ui) { - /* Remove any entries already on the list */ - const terms = splitField(this.value); - ui.content.splice( - 0, - ui.content.length, - ...ui.content.filter((record) => !terms.includes(record.value)), - ); - }, - }; - container.find(autocomplete_selector).each(function () { - const options = $.extend(default_ac_opts, ac_opts); - setup_autocomplete(this, ol_ac_opts, options); - }); - }; + const default_ac_opts = { + minChars: 2, + max: 25, + matchSubset: false, + autoFill: false, + position: { my: 'right top', at: 'right bottom' }, + termPreprocessor: (subject_string) => { + const terms = splitField(subject_string); + if (terms.length !== dataConfig.data.length) { + return terms.pop(); + } else { + $('ul.ui-autocomplete').hide(); + return ''; + } + }, + select: function (event, ui) { + const terms = splitField(this.value); + terms.splice(terms.length - 1, 1, ui.item.value); + this.value = `${joinField(terms)}, `; + dataConfig.data.push(ui.item.value); + container[0].dataset.config = JSON.stringify(dataConfig); + $(this).trigger('input'); + return false; + }, + response: function (event, ui) { + /* Remove any entries already on the list */ + const terms = splitField(this.value); + ui.content.splice( + 0, + ui.content.length, + ...ui.content.filter((record) => !terms.includes(record.value)), + ); + }, + }; + + container.find(autocomplete_selector).each(function () { + const options = $.extend(default_ac_opts, ac_opts); + setup_autocomplete(this, ol_ac_opts, options); + }); + }; } diff --git a/openlibrary/plugins/openlibrary/js/banner/index.js b/openlibrary/plugins/openlibrary/js/banner/index.js index 3dad5e16486..67301490ee7 100644 --- a/openlibrary/plugins/openlibrary/js/banner/index.js +++ b/openlibrary/plugins/openlibrary/js/banner/index.js @@ -8,22 +8,22 @@ * @param {Function} successCallback */ function setBannerCookie(cookieName, cookieDurationDays, successCallback) { - $.ajax({ - type: 'POST', - url: '/hide_banner', - data: JSON.stringify({ - 'cookie-name': cookieName, - 'cookie-duration-days': cookieDurationDays, - }), - contentType: 'application/json', - dataType: 'json', + $.ajax({ + type: 'POST', + url: '/hide_banner', + data: JSON.stringify({ + 'cookie-name': cookieName, + 'cookie-duration-days': cookieDurationDays, + }), + contentType: 'application/json', + dataType: 'json', - beforeSend: (xhr) => { - xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.setRequestHeader('Accept', 'application/json'); - }, - success: successCallback, - }); + beforeSend: (xhr) => { + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('Accept', 'application/json'); + }, + success: successCallback, + }); } /** @@ -32,18 +32,18 @@ function setBannerCookie(cookieName, cookieDurationDays, successCallback) { * @param {NodeList<HTMLElement>} banners */ export function initDismissibleBanners(banners) { - for (const banner of banners) { - const cookieName = banner.dataset.cookieName; - const cookieDurationDays = banner.dataset.cookieDurationDays; + for (const banner of banners) { + const cookieName = banner.dataset.cookieName; + const cookieDurationDays = banner.dataset.cookieDurationDays; - const dismissButton = banner.querySelector( - '.page-banner--dismissable-close', - ); - dismissButton.addEventListener('click', () => { - const successCallback = () => { - banner.remove(); - }; - setBannerCookie(cookieName, cookieDurationDays, successCallback); - }); - } + const dismissButton = banner.querySelector( + '.page-banner--dismissable-close', + ); + dismissButton.addEventListener('click', () => { + const successCallback = () => { + banner.remove(); + }; + setBannerCookie(cookieName, cookieDurationDays, successCallback); + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/book-page-lists.js b/openlibrary/plugins/openlibrary/js/book-page-lists.js index 05eeb459d77..9eae0945579 100644 --- a/openlibrary/plugins/openlibrary/js/book-page-lists.js +++ b/openlibrary/plugins/openlibrary/js/book-page-lists.js @@ -7,60 +7,60 @@ import { buildPartialsUrl } from './utils'; * @param elem {HTMLElement} Container for book page lists section */ export function initListsSection(elem) { - // Show loading indicator - const loadingIndicator = elem.querySelector('.loadingIndicator'); - loadingIndicator.classList.remove('hidden'); + // Show loading indicator + const loadingIndicator = elem.querySelector('.loadingIndicator'); + loadingIndicator.classList.remove('hidden'); - const ids = JSON.parse(elem.dataset.ids); + const ids = JSON.parse(elem.dataset.ids); - const intersectionObserver = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - // Unregister intersection listener - intersectionObserver.unobserve(entries[0].target); - fetchPartials(ids.work, ids.edition) - .then((resp) => { - // Check response code, continue if not 4XX or 5XX - return resp.json(); - }) - .then((data) => { - // Replace loading indicator with partials - const listSection = loadingIndicator.parentElement; - const fragment = document.createDocumentFragment(); + const intersectionObserver = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + // Unregister intersection listener + intersectionObserver.unobserve(entries[0].target); + fetchPartials(ids.work, ids.edition) + .then((resp) => { + // Check response code, continue if not 4XX or 5XX + return resp.json(); + }) + .then((data) => { + // Replace loading indicator with partials + const listSection = loadingIndicator.parentElement; + const fragment = document.createDocumentFragment(); - for (const htmlString of data.partials) { - const template = document.createElement('template'); - template.innerHTML = htmlString; - fragment.append(...template.content.childNodes); - } + for (const htmlString of data.partials) { + const template = document.createElement('template'); + template.innerHTML = htmlString; + fragment.append(...template.content.childNodes); + } - listSection.replaceChildren(fragment); + listSection.replaceChildren(fragment); - // Show "See All" link - if (data.hasLists) { - const showAllLink = elem.querySelector('.lists-heading a'); - if (showAllLink) { - showAllLink.classList.remove('hidden'); - } - } - // Initialize private buttons after content is loaded - initPrivateButtonsAfterLoad(listSection); + // Show "See All" link + if (data.hasLists) { + const showAllLink = elem.querySelector('.lists-heading a'); + if (showAllLink) { + showAllLink.classList.remove('hidden'); + } + } + // Initialize private buttons after content is loaded + initPrivateButtonsAfterLoad(listSection); - const followForms = listSection.querySelectorAll('.follow-form'); - initAsyncFollowing(followForms); + const followForms = listSection.querySelectorAll('.follow-form'); + initAsyncFollowing(followForms); + }); + } }); - } - }); - }, - { - root: null, - rootMargin: '200px', - threshold: 0, - }, - ); + }, + { + root: null, + rootMargin: '200px', + threshold: 0, + }, + ); - intersectionObserver.observe(elem); + intersectionObserver.observe(elem); } /** @@ -68,26 +68,26 @@ export function initListsSection(elem) { * @param {HTMLElement} container - The container that now has the loaded content */ function initPrivateButtonsAfterLoad(container) { - const privateButtons = container.querySelectorAll( - '.list-follow-card__private-button', - ); - if (privateButtons.length > 0) { - import(/* webpackChunkName: "private-buttons" */ './private-button').then( - (module) => { - module.initPrivateButtons(privateButtons); - }, + const privateButtons = container.querySelectorAll( + '.list-follow-card__private-button', ); - } + if (privateButtons.length > 0) { + import(/* webpackChunkName: "private-buttons" */ './private-button').then( + (module) => { + module.initPrivateButtons(privateButtons); + }, + ); + } } async function fetchPartials(workId, editionId) { - const params = {}; - if (workId) { - params.workId = workId; - } - if (editionId) { - params.editionId = editionId; - } + const params = {}; + if (workId) { + params.workId = workId; + } + if (editionId) { + params.editionId = editionId; + } - return fetch(buildPartialsUrl('BPListsSection', params)); + return fetch(buildPartialsUrl('BPListsSection', params)); } diff --git a/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js b/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js index 57cdd231ef1..35333ed4378 100644 --- a/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js +++ b/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js @@ -4,26 +4,26 @@ * @param {NodeList<HTMLElement>} crumbs - NodeList of breadcrumb select elements. */ export function initBreadcrumbSelect(crumbs) { - const allowedKeys = new Set(['Tab', 'Enter', ' ']); - const preventedKeys = new Set(['ArrowUp', 'ArrowDown']); - // watch crumbs for changes, - // ensures it's a full value change, not a user exploring options via keyboard - function handleNavEvents(nav) { - let ignoreChange = false; + const allowedKeys = new Set(['Tab', 'Enter', ' ']); + const preventedKeys = new Set(['ArrowUp', 'ArrowDown']); + // watch crumbs for changes, + // ensures it's a full value change, not a user exploring options via keyboard + function handleNavEvents(nav) { + let ignoreChange = false; - nav.addEventListener('change', () => { - if (ignoreChange) return; - window.location = nav.value; - }); + nav.addEventListener('change', () => { + if (ignoreChange) return; + window.location = nav.value; + }); - nav.addEventListener('keydown', ({ key }) => { - if (preventedKeys.has(key)) { - ignoreChange = true; - } else if (allowedKeys.has(key)) { - ignoreChange = false; - } - }); - } + nav.addEventListener('keydown', ({ key }) => { + if (preventedKeys.has(key)) { + ignoreChange = true; + } else if (allowedKeys.has(key)) { + ignoreChange = false; + } + }); + } - crumbs.forEach(handleNavEvents); + crumbs.forEach(handleNavEvents); } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger.js index f25968137bf..ca51c155af5 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger.js @@ -29,181 +29,181 @@ const COLLECTION_PREFIX = 'collection:'; * @class */ export class BulkTagger { - /** + /** * Sets references to key Bulk Tagger affordances. * * @param {HTMLElement} bulkTagger Reference to root element of the Bulk Tagger */ - constructor(bulkTagger) { + constructor(bulkTagger) { /** * Reference to root Bulk Tagger element. * @member {HTMLFormElement} */ - this.rootElement = bulkTagger; + this.rootElement = bulkTagger; - /** + /** * Reference to the Bulk Tagger's subject search box. * @member {HTMLInputElement} */ - this.searchInput = bulkTagger.querySelector('.subjects-search-input'); + this.searchInput = bulkTagger.querySelector('.subjects-search-input'); - /** + /** * Menu option container that holds options for staged tags and tags * that already exist on one or more selected works. * * @member {SortedMenuOptionContainer} */ - this.selectedOptionsContainer; + this.selectedOptionsContainer; - /** + /** * Menu option container that holds options representing search results. * * @member {SortedMenuOptionContainer} */ - this.searchResultsOptionsContainer; + this.searchResultsOptionsContainer; - /** + /** * Reference to the element which contains the affordance that creates new subjects. * @member {HTMLElement} */ - this.createSubjectElem = bulkTagger.querySelector( - '.search-subject-row-name', - ); + this.createSubjectElem = bulkTagger.querySelector( + '.search-subject-row-name', + ); - /** + /** * Element which displays the subject name within the "create new tag" affordance. * @member {HTMLElement} */ - this.subjectNameElem = + this.subjectNameElem = this.createSubjectElem.querySelector('.subject-name'); - /** + /** * Reference to input which holds the subjects to be batch added. * @member {HTMLInputElement} */ - this.addSubjectsInput = bulkTagger.querySelector('input[name=tags_to_add]'); + this.addSubjectsInput = bulkTagger.querySelector('input[name=tags_to_add]'); - /** + /** * Input which contains the subjects to be batch removed. * @member {HTMLInputElement} */ - this.removeSubjectsInput = bulkTagger.querySelector( - 'input[name=tags_to_remove]', - ); + this.removeSubjectsInput = bulkTagger.querySelector( + 'input[name=tags_to_remove]', + ); - /** + /** * Reference to hidden input which holds a comma-separated list of work OLIDs * @member {HTMLInputElement} */ - this.selectedWorksInput = bulkTagger.querySelector('input[name=work_ids]'); + this.selectedWorksInput = bulkTagger.querySelector('input[name=work_ids]'); - /** + /** * Reference to the bulk tagger form's submit button. * * @member {HTMLButtonElement} */ - this.submitButton = this.rootElement.querySelector('.bulk-tagging-submit'); + this.submitButton = this.rootElement.querySelector('.bulk-tagging-submit'); - /** + /** * Stores works' subjects that have been fetched from the server. * * Keys to the map are work IDs. * @member {Map<String, Array<Tag>>} */ - this.existingSubjects = new Map(); + this.existingSubjects = new Map(); - /** + /** * Array containing OLIDs of each selected work. * * @member {Array<String>} */ - this.selectedWorks = []; + this.selectedWorks = []; - /** + /** * Tags staged for adding to all selected works. * * @member {Array<Tag>} */ - this.tagsToAdd = []; + this.tagsToAdd = []; - /** + /** * Tags staged for removal from all selected works. * * @member {Array<Tag>} */ - this.tagsToRemove = []; + this.tagsToRemove = []; - /** + /** * `true` if the bulk tagger appears on a book page. * * @type {boolean} */ - this.isBookPageEdit = false; - } + this.isBookPageEdit = false; + } - /** + /** * Initialized the menu option containers, and adds event listeners to the Bulk Tagger. */ - initialize() { + initialize() { // Create sorted menu option containers: - this.selectedOptionsContainer = new SortedMenuOptionContainer( - this.rootElement.querySelector('.selected-tag-subjects'), - ); - this.searchResultsOptionsContainer = new SortedMenuOptionContainer( - this.rootElement.querySelector('.subjects-search-results'), - ); - - // Add "hide menu" functionality: - const closeFormButton = this.rootElement.querySelector( - '.close-bulk-tagging-form', - ); - closeFormButton.addEventListener('click', () => { - this.hideTaggingMenu(); - }); - - // Add input listener to subject search box: - const debouncedInputChangeHandler = debounce( - this.onSearchInputChange.bind(this), - 500, - ); - this.searchInput.addEventListener('input', () => { - const searchTerm = this.searchInput.value.trim(); - debouncedInputChangeHandler(searchTerm); - }); - - // Prevent redirect on batch subject submission: - this.submitButton.addEventListener('click', (event) => { - event.preventDefault(); - this.submitBatch(); - }); - - // Add click listeners to "create subject" options: - const createSubjectButtons = this.rootElement.querySelectorAll( - '.subject-type-option', - ); - for (const elem of createSubjectButtons) { - elem.addEventListener('click', () => - this.onCreateTag(new Tag(this.searchInput.value, elem.dataset.tagType)), - ); + this.selectedOptionsContainer = new SortedMenuOptionContainer( + this.rootElement.querySelector('.selected-tag-subjects'), + ); + this.searchResultsOptionsContainer = new SortedMenuOptionContainer( + this.rootElement.querySelector('.subjects-search-results'), + ); + + // Add "hide menu" functionality: + const closeFormButton = this.rootElement.querySelector( + '.close-bulk-tagging-form', + ); + closeFormButton.addEventListener('click', () => { + this.hideTaggingMenu(); + }); + + // Add input listener to subject search box: + const debouncedInputChangeHandler = debounce( + this.onSearchInputChange.bind(this), + 500, + ); + this.searchInput.addEventListener('input', () => { + const searchTerm = this.searchInput.value.trim(); + debouncedInputChangeHandler(searchTerm); + }); + + // Prevent redirect on batch subject submission: + this.submitButton.addEventListener('click', (event) => { + event.preventDefault(); + this.submitBatch(); + }); + + // Add click listeners to "create subject" options: + const createSubjectButtons = this.rootElement.querySelectorAll( + '.subject-type-option', + ); + for (const elem of createSubjectButtons) { + elem.addEventListener('click', () => + this.onCreateTag(new Tag(this.searchInput.value, elem.dataset.tagType)), + ); + } } - } - /** + /** * Hides the Bulk Tagger. */ - hideTaggingMenu() { - this.rootElement.classList.add('hidden'); - this.rootElement.dispatchEvent(new CustomEvent('option-hidden')); - } + hideTaggingMenu() { + this.rootElement.classList.add('hidden'); + this.rootElement.dispatchEvent(new CustomEvent('option-hidden')); + } - /** + /** * Displays the Bulk Tagger. */ - showTaggingMenu() { - this.rootElement.classList.remove('hidden'); - } + showTaggingMenu() { + this.rootElement.classList.remove('hidden'); + } - /** + /** * Updates the BulkTagger when works are selected. * * Stores given array in `selectedWorks`, fetches the @@ -212,160 +212,160 @@ export class BulkTagger { * * @param {Array<String>} workIds */ - async updateWorks(workIds) { - this.showLoadingIndicator(); + async updateWorks(workIds) { + this.showLoadingIndicator(); - this.selectedWorks = workIds; + this.selectedWorks = workIds; - await this.fetchSubjectsForWorks(workIds); - this.updateMenuOptions(); + await this.fetchSubjectsForWorks(workIds); + this.updateMenuOptions(); - this.hideLoadingIndicator(); - } + this.hideLoadingIndicator(); + } - /** + /** * Hides all menu options and shows a loading indicator. */ - showLoadingIndicator() { - const menuOptionContainer = this.rootElement.querySelector( - '.selection-container', - ); - menuOptionContainer.classList.add('hidden'); - const loadingIndicator = + showLoadingIndicator() { + const menuOptionContainer = this.rootElement.querySelector( + '.selection-container', + ); + menuOptionContainer.classList.add('hidden'); + const loadingIndicator = this.rootElement.querySelector('.loading-indicator'); - loadingIndicator.classList.remove('hidden'); - } + loadingIndicator.classList.remove('hidden'); + } - /** + /** * Hides the loading indicator and shows all menu options. */ - hideLoadingIndicator() { - const loadingIndicator = + hideLoadingIndicator() { + const loadingIndicator = this.rootElement.querySelector('.loading-indicator'); - loadingIndicator.classList.add('hidden'); - const menuOptionContainer = this.rootElement.querySelector( - '.selection-container', - ); - menuOptionContainer.classList.remove('hidden'); - } - - /** + loadingIndicator.classList.add('hidden'); + const menuOptionContainer = this.rootElement.querySelector( + '.selection-container', + ); + menuOptionContainer.classList.remove('hidden'); + } + + /** * Fetches and stores subject information for the given work OLIDs. * * If we already have fetched the data for a work ID, we do not fetch it * again. * @param {Array<String>} workIds */ - async fetchSubjectsForWorks(workIds) { - const worksWithMissingSubjects = workIds.filter( - (id) => !this.existingSubjects.has(id), - ); - - await Promise.all( - worksWithMissingSubjects.map(async (id) => { - // XXX : Too many network requests --- use bulk search if/when it is available - await this.fetchWork(id) - // XXX : Handle failures - .then((response) => response.json()) - .then((data) => { - const entry = { - subjects: data.subjects || [], - subject_people: data.subject_people || [], - subject_places: data.subject_places || [], - subject_times: data.subject_times || [], - }; - // Move collection labels from `subjects` to `collections` - entry.collections = entry.subjects.filter((label) => - label.startsWith(COLLECTION_PREFIX), - ); - entry.subjects = entry.subjects.filter( - (label) => !entry.collections.includes(label), - ); - for (let i = 0; i < entry.collections.length; ++i) { - // Remove collection prefix from label - entry.collections[i] = entry.collections[i].substring( - COLLECTION_PREFIX.length, - ); - } - if (!this.existingSubjects.has(id)) { - this.existingSubjects.set(id, []); - } - // `key` is the type, `value` is the array of tag names - for (const [key, value] of Object.entries(entry)) { - for (const tagName of value) { - this.existingSubjects.get(id).push(new Tag(tagName, key)); - } - } - }); - }), - ); - } + async fetchSubjectsForWorks(workIds) { + const worksWithMissingSubjects = workIds.filter( + (id) => !this.existingSubjects.has(id), + ); + + await Promise.all( + worksWithMissingSubjects.map(async (id) => { + // XXX : Too many network requests --- use bulk search if/when it is available + await this.fetchWork(id) + // XXX : Handle failures + .then((response) => response.json()) + .then((data) => { + const entry = { + subjects: data.subjects || [], + subject_people: data.subject_people || [], + subject_places: data.subject_places || [], + subject_times: data.subject_times || [], + }; + // Move collection labels from `subjects` to `collections` + entry.collections = entry.subjects.filter((label) => + label.startsWith(COLLECTION_PREFIX), + ); + entry.subjects = entry.subjects.filter( + (label) => !entry.collections.includes(label), + ); + for (let i = 0; i < entry.collections.length; ++i) { + // Remove collection prefix from label + entry.collections[i] = entry.collections[i].substring( + COLLECTION_PREFIX.length, + ); + } + if (!this.existingSubjects.has(id)) { + this.existingSubjects.set(id, []); + } + // `key` is the type, `value` is the array of tag names + for (const [key, value] of Object.entries(entry)) { + for (const tagName of value) { + this.existingSubjects.get(id).push(new Tag(tagName, key)); + } + } + }); + }), + ); + } - /** + /** * Creates `MenuOption` affordances for all staged tags, and each existing tag that * was fetched from the server. */ - updateMenuOptions() { - this.selectedOptionsContainer.clear(); - - // Add staged tags first, then add all other missing subjects. - // This order prevents unnecessary state mangement steps. - - // Create menu options for each staged tag: - this.tagsToAdd.forEach((tag) => { - const menuOption = new MenuOption( - tag, - MenuOptionState.ALL_TAGGED, - this.selectedWorks.length, - ); - menuOption.initialize(); - this.selectedOptionsContainer.add(menuOption); - }); - - this.tagsToRemove.forEach((tag) => { - const menuOption = new MenuOption(tag, MenuOptionState.NONE_TAGGED, 0); - menuOption.initialize(); - this.selectedOptionsContainer.add(menuOption); - }); - - // Create menu options for each existing tag: - const stagedMenuOptions = []; - for (const workOlid of this.selectedWorks) { - const existingTagsForWork = this.existingSubjects.get(workOlid); - for (const tag of existingTagsForWork) { - // Does an option for this tag already exist in the container? - if (!this.selectedOptionsContainer.containsOptionWithTag(tag)) { - // Have we already created and staged a menu option for this tag? - const stagedOption = stagedMenuOptions.find((option) => - option.tag.equals(tag), - ); - if (stagedOption) { - stagedOption.taggedWorksCount++; - if (stagedOption.taggedWorksCount === this.selectedWorks.length) { - stagedOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED); - } - } else { - const state = + updateMenuOptions() { + this.selectedOptionsContainer.clear(); + + // Add staged tags first, then add all other missing subjects. + // This order prevents unnecessary state mangement steps. + + // Create menu options for each staged tag: + this.tagsToAdd.forEach((tag) => { + const menuOption = new MenuOption( + tag, + MenuOptionState.ALL_TAGGED, + this.selectedWorks.length, + ); + menuOption.initialize(); + this.selectedOptionsContainer.add(menuOption); + }); + + this.tagsToRemove.forEach((tag) => { + const menuOption = new MenuOption(tag, MenuOptionState.NONE_TAGGED, 0); + menuOption.initialize(); + this.selectedOptionsContainer.add(menuOption); + }); + + // Create menu options for each existing tag: + const stagedMenuOptions = []; + for (const workOlid of this.selectedWorks) { + const existingTagsForWork = this.existingSubjects.get(workOlid); + for (const tag of existingTagsForWork) { + // Does an option for this tag already exist in the container? + if (!this.selectedOptionsContainer.containsOptionWithTag(tag)) { + // Have we already created and staged a menu option for this tag? + const stagedOption = stagedMenuOptions.find((option) => + option.tag.equals(tag), + ); + if (stagedOption) { + stagedOption.taggedWorksCount++; + if (stagedOption.taggedWorksCount === this.selectedWorks.length) { + stagedOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED); + } + } else { + const state = this.selectedWorks.length === 1 - ? MenuOptionState.ALL_TAGGED - : MenuOptionState.SOME_TAGGED; - const newOption = new MenuOption(tag, state, 1); - newOption.initialize(); - stagedMenuOptions.push(newOption); - } + ? MenuOptionState.ALL_TAGGED + : MenuOptionState.SOME_TAGGED; + const newOption = new MenuOption(tag, state, 1); + newOption.initialize(); + stagedMenuOptions.push(newOption); + } + } + } } - } - } - stagedMenuOptions.forEach((option) => - option.rootElement.addEventListener('click', () => - this.onMenuOptionClick(option), - ), - ); - this.selectedOptionsContainer.add(...stagedMenuOptions); - } + stagedMenuOptions.forEach((option) => + option.rootElement.addEventListener('click', () => + this.onMenuOptionClick(option), + ), + ); + this.selectedOptionsContainer.add(...stagedMenuOptions); + } - /** + /** * Click handler for menu options. * * Changes the menu option's state, and stages the option's tag @@ -373,69 +373,69 @@ export class BulkTagger { * * @param {MenuOption} menuOption The clicked menu option */ - onMenuOptionClick(menuOption) { - let stagedTagIndex; - switch (menuOption.optionState) { - case MenuOptionState.NONE_TAGGED: - stagedTagIndex = this.tagsToRemove.findIndex( - (tag) => - tag.tagName === menuOption.tag.tagName && + onMenuOptionClick(menuOption) { + let stagedTagIndex; + switch (menuOption.optionState) { + case MenuOptionState.NONE_TAGGED: + stagedTagIndex = this.tagsToRemove.findIndex( + (tag) => + tag.tagName === menuOption.tag.tagName && tag.tagType === menuOption.tag.tagType, - ); - if (stagedTagIndex > -1) { - this.tagsToRemove.splice(stagedTagIndex, 1); - } - this.tagsToAdd.push(menuOption.tag); - menuOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED); - break; - case MenuOptionState.SOME_TAGGED: - this.tagsToAdd.push(menuOption.tag); - menuOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED); - break; - case MenuOptionState.ALL_TAGGED: - stagedTagIndex = this.tagsToAdd.findIndex( - (tag) => - tag.tagName === menuOption.tag.tagName && + ); + if (stagedTagIndex > -1) { + this.tagsToRemove.splice(stagedTagIndex, 1); + } + this.tagsToAdd.push(menuOption.tag); + menuOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED); + break; + case MenuOptionState.SOME_TAGGED: + this.tagsToAdd.push(menuOption.tag); + menuOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED); + break; + case MenuOptionState.ALL_TAGGED: + stagedTagIndex = this.tagsToAdd.findIndex( + (tag) => + tag.tagName === menuOption.tag.tagName && tag.tagType === menuOption.tag.tagType, - ); - if (stagedTagIndex > -1) { - this.tagsToAdd.splice(stagedTagIndex, 1); + ); + if (stagedTagIndex > -1) { + this.tagsToAdd.splice(stagedTagIndex, 1); + } + this.tagsToRemove.push(menuOption.tag); + menuOption.updateMenuOptionState(MenuOptionState.NONE_TAGGED); + break; } - this.tagsToRemove.push(menuOption.tag); - menuOption.updateMenuOptionState(MenuOptionState.NONE_TAGGED); - break; - } - menuOption.stage(); - this.updateSubmitButtonState(); - } + menuOption.stage(); + this.updateSubmitButtonState(); + } - /** + /** * Disables or enables form submission button. * * Button is enabled if there are any tags staged for submission. * Otherwise, the button will be disabled. */ - updateSubmitButtonState() { - const stagedTagCount = this.tagsToAdd.length + this.tagsToRemove.length; + updateSubmitButtonState() { + const stagedTagCount = this.tagsToAdd.length + this.tagsToRemove.length; - if (stagedTagCount > 0) { - this.submitButton.removeAttribute('disabled'); - } else { - this.submitButton.setAttribute('disabled', 'true'); + if (stagedTagCount > 0) { + this.submitButton.removeAttribute('disabled'); + } else { + this.submitButton.setAttribute('disabled', 'true'); + } } - } - /** + /** * Fetches a work from OL. * * @param {String} workOlid */ - async fetchWork(workOlid) { - return fetch(`/works/${workOlid}.json`); - } + async fetchWork(workOlid) { + return fetch(`/works/${workOlid}.json`); + } - /** + /** * Performs a subject search for the given search term, and updates * the Bulk Tagger with the results. * @@ -444,72 +444,72 @@ export class BulkTagger { * * @param {String} searchTerm */ - onSearchInputChange(searchTerm) { + onSearchInputChange(searchTerm) { // Remove search results that are not selected: - const resultsToRemove = + const resultsToRemove = this.searchResultsOptionsContainer.sortedMenuOptions.filter( - (option) => option.optionState !== MenuOptionState.ALL_TAGGED, + (option) => option.optionState !== MenuOptionState.ALL_TAGGED, ); - this.searchResultsOptionsContainer.remove(...resultsToRemove); - - // Hide menu options that do not begin with the search term (case-insensitive) - const trimmedSearchTerm = searchTerm.trim(); - - const allOptions = this.selectedOptionsContainer.sortedMenuOptions.concat( - this.searchResultsOptionsContainer.sortedMenuOptions, - ); - allOptions.forEach((option) => { - if ( - option.tag.tagName - .toLowerCase() - .startsWith(trimmedSearchTerm.toLowerCase()) - ) { - option.show(); - } else { - option.hide(); - } - }); - - if (trimmedSearchTerm !== '') { - // Perform search: - fetch(`/search/subjects.json?q=${searchTerm}&limit=${maxDisplayResults}`) - .then((response) => response.json()) - .then((data) => { - if (data['docs'].length !== 0) { - for (const obj of data['docs']) { - const tag = new Tag(obj.name, null, obj['subject_type']); - - if ( - !this.selectedOptionsContainer.containsOptionWithTag(tag) && - !this.searchResultsOptionsContainer.containsOptionWithTag(tag) - ) { - const menuOption = this.createSearchMenuOption(tag); - this.searchResultsOptionsContainer.add(menuOption); - } - } - } + this.searchResultsOptionsContainer.remove(...resultsToRemove); + + // Hide menu options that do not begin with the search term (case-insensitive) + const trimmedSearchTerm = searchTerm.trim(); - // Update and show create subject affordance - this.updateAndShowNewSubjectAffordance(trimmedSearchTerm); + const allOptions = this.selectedOptionsContainer.sortedMenuOptions.concat( + this.searchResultsOptionsContainer.sortedMenuOptions, + ); + allOptions.forEach((option) => { + if ( + option.tag.tagName + .toLowerCase() + .startsWith(trimmedSearchTerm.toLowerCase()) + ) { + option.show(); + } else { + option.hide(); + } }); - } else { - // Hide create subject affordance - this.createSubjectElem.classList.add('hidden'); + + if (trimmedSearchTerm !== '') { + // Perform search: + fetch(`/search/subjects.json?q=${searchTerm}&limit=${maxDisplayResults}`) + .then((response) => response.json()) + .then((data) => { + if (data['docs'].length !== 0) { + for (const obj of data['docs']) { + const tag = new Tag(obj.name, null, obj['subject_type']); + + if ( + !this.selectedOptionsContainer.containsOptionWithTag(tag) && + !this.searchResultsOptionsContainer.containsOptionWithTag(tag) + ) { + const menuOption = this.createSearchMenuOption(tag); + this.searchResultsOptionsContainer.add(menuOption); + } + } + } + + // Update and show create subject affordance + this.updateAndShowNewSubjectAffordance(trimmedSearchTerm); + }); + } else { + // Hide create subject affordance + this.createSubjectElem.classList.add('hidden'); + } } - } - /** + /** * Updates the "create subject" affordance with the given subject name, * and shows the affordance if it is hidden. * * @param {String} subjectName The name of the subject */ - updateAndShowNewSubjectAffordance(subjectName) { - this.subjectNameElem.innerText = subjectName; - this.createSubjectElem.classList.remove('hidden'); - } + updateAndShowNewSubjectAffordance(subjectName) { + this.subjectNameElem.innerText = subjectName; + this.createSubjectElem.classList.remove('hidden'); + } - /** + /** * Creates, hydrates, and returns a new menu option based on a search result. * * In addition to the usual click listener, the newly created element will have an @@ -522,29 +522,29 @@ export class BulkTagger { * @param {Tag} tag * @returns {MenuOption} A menu option representing the given tag */ - createSearchMenuOption(tag) { - const menuOption = new MenuOption(tag, MenuOptionState.NONE_TAGGED, 0); - menuOption.initialize(); - menuOption.rootElement.addEventListener('click', () => - this.onMenuOptionClick(menuOption), - ); - menuOption.rootElement.addEventListener('option-hidden', () => { - // Move to selected menu options container if selected and hidden - if (menuOption.optionState === MenuOptionState.ALL_TAGGED) { - if ( - menuOption.rootElement.parentElement === + createSearchMenuOption(tag) { + const menuOption = new MenuOption(tag, MenuOptionState.NONE_TAGGED, 0); + menuOption.initialize(); + menuOption.rootElement.addEventListener('click', () => + this.onMenuOptionClick(menuOption), + ); + menuOption.rootElement.addEventListener('option-hidden', () => { + // Move to selected menu options container if selected and hidden + if (menuOption.optionState === MenuOptionState.ALL_TAGGED) { + if ( + menuOption.rootElement.parentElement === this.searchResultsOptionsContainer.rootElement - ) { - this.searchResultsOptionsContainer.remove(menuOption); - this.selectedOptionsContainer.add(menuOption); - } - } - }); + ) { + this.searchResultsOptionsContainer.remove(menuOption); + this.selectedOptionsContainer.add(menuOption); + } + } + }); - return menuOption; - } + return menuOption; + } - /** + /** * Adds a menu option representing the given tag to the selected options container. * * If the container already has a menu option for the given tag, this method returns @@ -556,116 +556,116 @@ export class BulkTagger { * * @param {Tag} tag */ - onCreateTag(tag) { + onCreateTag(tag) { // Return if menu option already exists in selected options: - if (this.selectedOptionsContainer.containsOptionWithTag(tag)) { - return; - } + if (this.selectedOptionsContainer.containsOptionWithTag(tag)) { + return; + } - // Stage tag for addition: - this.tagsToAdd.push(tag); - - // If tag is represented by a search result object, update existing object - // instead of creating a new one: - const existingOption = this.searchResultsOptionsContainer.findByTag(tag); - if (existingOption) { - this.searchResultsOptionsContainer.remove(existingOption); - this.selectedOptionsContainer.add(existingOption); - existingOption.taggedWorksCount = this.selectedWorks.length; - existingOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED); - } else { - const menuOption = new MenuOption( - tag, - MenuOptionState.ALL_TAGGED, - this.selectedWorks.length, - ); - menuOption.initialize(); - menuOption.rootElement.addEventListener('click', () => - this.onMenuOptionClick(menuOption), - ); - this.selectedOptionsContainer.add(menuOption); - } + // Stage tag for addition: + this.tagsToAdd.push(tag); + + // If tag is represented by a search result object, update existing object + // instead of creating a new one: + const existingOption = this.searchResultsOptionsContainer.findByTag(tag); + if (existingOption) { + this.searchResultsOptionsContainer.remove(existingOption); + this.selectedOptionsContainer.add(existingOption); + existingOption.taggedWorksCount = this.selectedWorks.length; + existingOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED); + } else { + const menuOption = new MenuOption( + tag, + MenuOptionState.ALL_TAGGED, + this.selectedWorks.length, + ); + menuOption.initialize(); + menuOption.rootElement.addEventListener('click', () => + this.onMenuOptionClick(menuOption), + ); + this.selectedOptionsContainer.add(menuOption); + } - this.updateSubmitButtonState(); - } + this.updateSubmitButtonState(); + } - /** + /** * Submits the bulk tagging form and updates the view. */ - submitBatch() { + submitBatch() { // Disable button - this.submitButton.disabled = true; - - this.submitButton.textContent = 'Submitting...'; + this.submitButton.disabled = true; - const url = this.rootElement.action; - this.prepareFormForSubmission(); - const formData = new FormData(this.rootElement); - if (this.isBookPageEdit) { - formData.append('book_page_edit', true); - } + this.submitButton.textContent = 'Submitting...'; - fetch(url, { - method: 'post', - body: formData, - }).then((response) => { - if (!response.ok) { - this.submitButton.disabled = false; - this.submitButton.textContent = 'Submit'; - new FadingToast( - 'Batch subject update failed. Please try again in a few minutes.', - ).show(); - } else { - this.hideTaggingMenu(); - new FadingToast('Subjects successfully updated.').show(); - this.submitButton.textContent = 'Submit'; - this.updateFetchedSubjects(); - this.resetTaggingMenu(); + const url = this.rootElement.action; + this.prepareFormForSubmission(); + const formData = new FormData(this.rootElement); if (this.isBookPageEdit) { - window.ILE.clearAndReset(); - window.location.reload(); + formData.append('book_page_edit', true); } - } - }); - } - /** + fetch(url, { + method: 'post', + body: formData, + }).then((response) => { + if (!response.ok) { + this.submitButton.disabled = false; + this.submitButton.textContent = 'Submit'; + new FadingToast( + 'Batch subject update failed. Please try again in a few minutes.', + ).show(); + } else { + this.hideTaggingMenu(); + new FadingToast('Subjects successfully updated.').show(); + this.submitButton.textContent = 'Submit'; + this.updateFetchedSubjects(); + this.resetTaggingMenu(); + if (this.isBookPageEdit) { + window.ILE.clearAndReset(); + window.location.reload(); + } + } + }); + } + + /** * Populates the form's hidden inputs. * * Expected to be called just before the form is submitted. */ - prepareFormForSubmission() { - this.selectedWorksInput.value = this.selectedWorks.join(','); - - const addSubjectsValue = { - subjects: this.findMatches(this.tagsToAdd, 'subjects'), - subject_people: this.findMatches(this.tagsToAdd, 'subject_people'), - subject_places: this.findMatches(this.tagsToAdd, 'subject_places'), - subject_times: this.findMatches(this.tagsToAdd, 'subject_times'), - }; - const collectionsToAdd = this.findMatches(this.tagsToAdd, 'collections'); - collectionsToAdd.forEach((label) => - addSubjectsValue.subjects.push(`${COLLECTION_PREFIX}${label}`), - ); - this.addSubjectsInput.value = JSON.stringify(addSubjectsValue); - - const removeSubjectsValue = { - subjects: this.findMatches(this.tagsToRemove, 'subjects'), - subject_people: this.findMatches(this.tagsToRemove, 'subject_people'), - subject_places: this.findMatches(this.tagsToRemove, 'subject_places'), - subject_times: this.findMatches(this.tagsToRemove, 'subject_times'), - }; - const collectionsToRemove = this.findMatches( - this.tagsToRemove, - 'collections', - ); - collectionsToRemove.forEach((label) => - removeSubjectsValue.subjects.push(`${COLLECTION_PREFIX}${label}`), - ); - this.removeSubjectsInput.value = JSON.stringify(removeSubjectsValue); - } - - /** + prepareFormForSubmission() { + this.selectedWorksInput.value = this.selectedWorks.join(','); + + const addSubjectsValue = { + subjects: this.findMatches(this.tagsToAdd, 'subjects'), + subject_people: this.findMatches(this.tagsToAdd, 'subject_people'), + subject_places: this.findMatches(this.tagsToAdd, 'subject_places'), + subject_times: this.findMatches(this.tagsToAdd, 'subject_times'), + }; + const collectionsToAdd = this.findMatches(this.tagsToAdd, 'collections'); + collectionsToAdd.forEach((label) => + addSubjectsValue.subjects.push(`${COLLECTION_PREFIX}${label}`), + ); + this.addSubjectsInput.value = JSON.stringify(addSubjectsValue); + + const removeSubjectsValue = { + subjects: this.findMatches(this.tagsToRemove, 'subjects'), + subject_people: this.findMatches(this.tagsToRemove, 'subject_people'), + subject_places: this.findMatches(this.tagsToRemove, 'subject_places'), + subject_times: this.findMatches(this.tagsToRemove, 'subject_times'), + }; + const collectionsToRemove = this.findMatches( + this.tagsToRemove, + 'collections', + ); + collectionsToRemove.forEach((label) => + removeSubjectsValue.subjects.push(`${COLLECTION_PREFIX}${label}`), + ); + this.removeSubjectsInput.value = JSON.stringify(removeSubjectsValue); + } + + /** * Filters tags that match the given type, and returns the names of * each filtered tag. * @@ -673,61 +673,61 @@ export class BulkTagger { * @param {String} type Snake-cased tag type * @returns {Array<String>} The names of the filtered tags */ - findMatches(tags, type) { - const results = []; - tags.reduce((_acc, tag) => { - if (tag.tagType === type) { - results.push(tag.tagName); - } - }, []); - return results; - } - - /** + findMatches(tags, type) { + const results = []; + tags.reduce((_acc, tag) => { + if (tag.tagType === type) { + results.push(tag.tagName); + } + }, []); + return results; + } + + /** * Updates the data structure which contains the fetched works' subjects. * * Meant to be called after the form has been submitted, but before the * `resetTaggingMenu` call is made. */ - updateFetchedSubjects() { - for (const tag of this.tagsToAdd) { - this.existingSubjects.forEach((tags) => { - const tagExists = + updateFetchedSubjects() { + for (const tag of this.tagsToAdd) { + this.existingSubjects.forEach((tags) => { + const tagExists = tags.findIndex( - (t) => t.tagName === tag.tagName && t.tagType === tag.tagType, + (t) => t.tagName === tag.tagName && t.tagType === tag.tagType, ) > -1; - if (!tagExists) { - tags.push(tag); + if (!tagExists) { + tags.push(tag); + } + }); } - }); - } - for (const tag of this.tagsToRemove) { - this.existingSubjects.forEach((tags) => { - const tagIndex = tags.findIndex( - (t) => t.tagName === tag.tagName && t.tagType === tag.tagType, - ); - const tagExists = tagIndex > -1; - if (tagExists) { - tags.splice(tagIndex, 1); + for (const tag of this.tagsToRemove) { + this.existingSubjects.forEach((tags) => { + const tagIndex = tags.findIndex( + (t) => t.tagName === tag.tagName && t.tagType === tag.tagType, + ); + const tagExists = tagIndex > -1; + if (tagExists) { + tags.splice(tagIndex, 1); + } + }); } - }); } - } - /** + /** * Clears the bulk tagger form. */ - resetTaggingMenu() { - this.searchInput.value = ''; - this.addSubjectsInput.value = ''; - this.removeSubjectsInput.value = ''; - this.selectedOptionsContainer.clear(); - this.searchResultsOptionsContainer.clear(); - - this.createSubjectElem.classList.add('hidden'); - - this.tagsToAdd = []; - this.tagsToRemove = []; - } + resetTaggingMenu() { + this.searchInput.value = ''; + this.addSubjectsInput.value = ''; + this.removeSubjectsInput.value = ''; + this.selectedOptionsContainer.clear(); + this.searchResultsOptionsContainer.clear(); + + this.createSubjectElem.classList.add('hidden'); + + this.tagsToAdd = []; + this.tagsToRemove = []; + } } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js index 4a90c41c1dd..48932b2da62 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js @@ -2,11 +2,11 @@ * Maps tag display types to BEM suffixes. */ const classTypeSuffixes = { - subjects: '--subject', - subject_people: '--person', - subject_places: '--place', - subject_times: '--time', - collections: '--collection', + subjects: '--subject', + subject_people: '--person', + subject_places: '--place', + subject_times: '--time', + collections: '--collection', }; /** @@ -22,13 +22,13 @@ const classTypeSuffixes = { * @enum {OptionState} */ export const MenuOptionState = { - NONE_TAGGED: 0, - SOME_TAGGED: 1, - ALL_TAGGED: 2, + NONE_TAGGED: 0, + SOME_TAGGED: 1, + ALL_TAGGED: 2, }; export class MenuOption { - /** + /** * Creates a new MenuOption that represents the given tag. * * `rootElement` of this object is not set until `initialize` is called. @@ -37,7 +37,7 @@ export class MenuOption { * @param {OptionState} optionState * @param {Number} taggedWorksCount Number of selected works which have the given tag */ - constructor(tag, optionState, taggedWorksCount) { + constructor(tag, optionState, taggedWorksCount) { /** * Reference to the root element of this MenuOption. * @@ -45,17 +45,17 @@ export class MenuOption { * @member {HTMLElement} * @see {initialize} */ - this.rootElement; + this.rootElement; - /** + /** * Copy of the tag which is represented by this menu option. * * @member {Tag} * @readonly */ - this.tag = tag; + this.tag = tag; - /** + /** * Represents the amount of selected works that share this tag. * * Not meant to be updated directly. Use `updateMenuOptionState()`, @@ -63,67 +63,67 @@ export class MenuOption { * * @member {OptionState} */ - this.optionState = optionState; + this.optionState = optionState; - /** + /** * Tracks number of selected works which have this tag. * * @member {Number} */ - this.taggedWorksCount = taggedWorksCount; - } + this.taggedWorksCount = taggedWorksCount; + } - /** + /** * Creates a new menu option. * * Must be called before an event handler can be attached to * this menu option */ - initialize() { - this.createMenuOption(); - } + initialize() { + this.createMenuOption(); + } - /** + /** * Creates a new menu option affordance based on the current menu option state. * * Stores newly created element as `rootElement`. The new element is not * attached to the DOM, and does not yet have any attached event handlers. */ - createMenuOption() { - const parentElem = document.createElement('div'); - parentElem.classList.add('selected-tag'); + createMenuOption() { + const parentElem = document.createElement('div'); + parentElem.classList.add('selected-tag'); - let bemSuffix = ''; - switch (this.optionState) { - case MenuOptionState.NONE_TAGGED: - bemSuffix = 'none-tagged'; - break; - case MenuOptionState.SOME_TAGGED: - bemSuffix = 'some-tagged'; - break; - case MenuOptionState.ALL_TAGGED: - bemSuffix = 'all-tagged'; - break; - } + let bemSuffix = ''; + switch (this.optionState) { + case MenuOptionState.NONE_TAGGED: + bemSuffix = 'none-tagged'; + break; + case MenuOptionState.SOME_TAGGED: + bemSuffix = 'some-tagged'; + break; + case MenuOptionState.ALL_TAGGED: + bemSuffix = 'all-tagged'; + break; + } - const markup = `<span class="selected-tag__status selected-tag__status--${bemSuffix}"></span> + const markup = `<span class="selected-tag__status selected-tag__status--${bemSuffix}"></span> <span class="selected-tag__name">${this.tag.tagName}</span> <span class="selected-tag__type-container"> <span class="selected-tag__type selected-tag__type${classTypeSuffixes[this.tag.tagType]}">${this.tag.displayType}</span> </span>`; - parentElem.innerHTML = markup; - this.rootElement = parentElem; - } + parentElem.innerHTML = markup; + this.rootElement = parentElem; + } - /** + /** * Removes this MenuOption from the DOM. */ - remove() { - this.rootElement.remove(); - } + remove() { + this.rootElement.remove(); + } - /** + /** * Sets the value of `optionState` and updates the view. * * @param {OptionState} menuOptionState @@ -133,67 +133,67 @@ export class MenuOption { * @see {@link MenuOptionState} * @see {initialize} */ - updateMenuOptionState(menuOptionState) { - if (this.rootElement) { - // `rootElement` not set until `initialize` is called - this.optionState = menuOptionState; - const statusIndicator = this.rootElement.querySelector( - '.selected-tag__status', - ); - switch (menuOptionState) { - case MenuOptionState.NONE_TAGGED: - statusIndicator.classList.remove( - 'selected-tag__status--all-tagged', - 'selected-tag__status--some-tagged', - ); - statusIndicator.classList.add('selected-tag__status--none-tagged'); - break; - case MenuOptionState.SOME_TAGGED: - statusIndicator.classList.remove( - 'selected-tag__status--all-tagged', - 'selected-tag__status--none-tagged', - ); - statusIndicator.classList.add('selected-tag__status--some-tagged'); - break; - case MenuOptionState.ALL_TAGGED: - statusIndicator.classList.remove( - 'selected-tag__status--none-tagged', - 'selected-tag__status--some-tagged', - ); - statusIndicator.classList.add('selected-tag__status--all-tagged'); - break; - default: - // XXX : `optionState` is now incorrect - throw new Error('Unexpected value passed for menu option state.'); - } - } else { - throw new Error( - 'MenuOption must be initialized before state can be updated.', - ); + updateMenuOptionState(menuOptionState) { + if (this.rootElement) { + // `rootElement` not set until `initialize` is called + this.optionState = menuOptionState; + const statusIndicator = this.rootElement.querySelector( + '.selected-tag__status', + ); + switch (menuOptionState) { + case MenuOptionState.NONE_TAGGED: + statusIndicator.classList.remove( + 'selected-tag__status--all-tagged', + 'selected-tag__status--some-tagged', + ); + statusIndicator.classList.add('selected-tag__status--none-tagged'); + break; + case MenuOptionState.SOME_TAGGED: + statusIndicator.classList.remove( + 'selected-tag__status--all-tagged', + 'selected-tag__status--none-tagged', + ); + statusIndicator.classList.add('selected-tag__status--some-tagged'); + break; + case MenuOptionState.ALL_TAGGED: + statusIndicator.classList.remove( + 'selected-tag__status--none-tagged', + 'selected-tag__status--some-tagged', + ); + statusIndicator.classList.add('selected-tag__status--all-tagged'); + break; + default: + // XXX : `optionState` is now incorrect + throw new Error('Unexpected value passed for menu option state.'); + } + } else { + throw new Error( + 'MenuOption must be initialized before state can be updated.', + ); + } } - } - /** + /** * Hides this menu option. * * Fires an `option-hidden` event when this is called. */ - hide() { - this.rootElement.classList.add('hidden'); - this.rootElement.dispatchEvent(new CustomEvent('option-hidden')); - } + hide() { + this.rootElement.classList.add('hidden'); + this.rootElement.dispatchEvent(new CustomEvent('option-hidden')); + } - /** + /** * Shows this menu option. */ - show() { - this.rootElement.classList.remove('hidden'); - } + show() { + this.rootElement.classList.remove('hidden'); + } - /** + /** * Stages the selected menu option. */ - stage() { - this.rootElement.classList.add('selected-tag--staged'); - } + stage() { + this.rootElement.classList.add('selected-tag--staged'); + } } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js index 01e76d9f39c..1755625e819 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js @@ -6,7 +6,7 @@ * to the DOM in the correct order, based on tag name and type. */ export class SortedMenuOptionContainer { - /** + /** * Creates a new sorted menu options container, with the given * element as the root element. * @@ -17,128 +17,128 @@ export class SortedMenuOptionContainer { * * @param {HTMLElement} element The container */ - constructor(element) { - this.rootElement = element; - this.sortedMenuOptions = []; - } + constructor(element) { + this.rootElement = element; + this.sortedMenuOptions = []; + } - /** + /** * Attaches the given menu options to this container, in order. * * @param {...MenuOption} menuOptions Menu options to be added to the container. */ - add(...menuOptions) { - for (const option of menuOptions) { - const index = this.findIndex(option); - this.sortedMenuOptions.splice(index, 0, option); - this.updateViewOnAdd(option, index); + add(...menuOptions) { + for (const option of menuOptions) { + const index = this.findIndex(option); + this.sortedMenuOptions.splice(index, 0, option); + this.updateViewOnAdd(option, index); + } } - } - /** + /** * Adds the given menu option to this container at the given index. * * @param {MenuOption} menuOption The option being attached to the DOM. * @param {Number} index The index where the given option will be inserted. */ - updateViewOnAdd(menuOption, index) { - if (index === 0) { - this.rootElement.prepend(menuOption.rootElement); - } else { - const sibling = this.rootElement.children[index - 1]; - sibling.insertAdjacentElement('afterend', menuOption.rootElement); + updateViewOnAdd(menuOption, index) { + if (index === 0) { + this.rootElement.prepend(menuOption.rootElement); + } else { + const sibling = this.rootElement.children[index - 1]; + sibling.insertAdjacentElement('afterend', menuOption.rootElement); + } } - } - /** + /** * Removes the given menu options from this container. * * @param {...MenuOption} menuOptions Options that are to be removed from this container */ - remove(...menuOptions) { - for (const option of menuOptions) { - const index = this.findIndex(option); - const removed = this.sortedMenuOptions.splice(index, 1); - removed.forEach((option) => option.remove()); + remove(...menuOptions) { + for (const option of menuOptions) { + const index = this.findIndex(option); + const removed = this.sortedMenuOptions.splice(index, 1); + removed.forEach((option) => option.remove()); + } } - } - /** + /** * Finds the correct index to insert the given menu option, such that * the array is alphabetically ordered (case-insensitive). * * @param {MenuOption} menuOption * @returns {Number} Index where the given menu option should be inserted. */ - findIndex(menuOption) { - let index = 0; + findIndex(menuOption) { + let index = 0; - // XXX : Binary search? - while (index < this.sortedMenuOptions.length) { - const currentMenuOption = this.sortedMenuOptions[index]; + // XXX : Binary search? + while (index < this.sortedMenuOptions.length) { + const currentMenuOption = this.sortedMenuOptions[index]; - if ( - currentMenuOption.tag.tagName.toLowerCase() === + if ( + currentMenuOption.tag.tagName.toLowerCase() === menuOption.tag.tagName.toLowerCase() - ) { - // Compare types - if ( - currentMenuOption.tag.tagType.toLowerCase() >= + ) { + // Compare types + if ( + currentMenuOption.tag.tagType.toLowerCase() >= menuOption.tag.tagType.toLowerCase() - ) { - return index; - } - } else if ( - currentMenuOption.tag.tagName.toLowerCase() > + ) { + return index; + } + } else if ( + currentMenuOption.tag.tagName.toLowerCase() > menuOption.tag.tagName.toLowerCase() - ) { + ) { + return index; + } + ++index; + } + return index; - } - ++index; } - return index; - } - - /** + /** * Checks if the given menu option is in this container. * * @param {MenuOption} menuOption The object that we are searching for * @returns {boolean} `true` if a matching menu option exists in this container */ - contains(menuOption) { - return this.sortedMenuOptions.some((option) => - menuOption.tag.equals(option.tag), - ); - } + contains(menuOption) { + return this.sortedMenuOptions.some((option) => + menuOption.tag.equals(option.tag), + ); + } - /** + /** * Checks if a menu option which represents the given tag is in this container. * * @param {Tag} tag * @returns {boolean} `true` if a menu option which represents the given tag is in this container. */ - containsOptionWithTag(tag) { - return this.sortedMenuOptions.some((option) => tag.equals(option.tag)); - } + containsOptionWithTag(tag) { + return this.sortedMenuOptions.some((option) => tag.equals(option.tag)); + } - /** + /** * Returns the first menu option found which represents the given tag, or `undefined` if none were found. * * @param {Tag} tag * @returns {MenuOption|undefined} The first matching menu option, or `undefined` if none were found. */ - findByTag(tag) { - return this.sortedMenuOptions.find((option) => tag.equals(option.tag)); - } + findByTag(tag) { + return this.sortedMenuOptions.find((option) => tag.equals(option.tag)); + } - /** + /** * Removes all menu options from this container. */ - clear() { - while (this.sortedMenuOptions.length > 0) { - this.sortedMenuOptions.pop(); + clear() { + while (this.sortedMenuOptions.length > 0) { + this.sortedMenuOptions.pop(); + } + this.rootElement.innerHTML = ''; } - this.rootElement.innerHTML = ''; - } } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js index e036129c848..4c7ef444a73 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js @@ -4,7 +4,7 @@ * @returns HTML for the bulk tagging form */ export function renderBulkTagger() { - return `<form action="/tags/bulk_tag_works" method="post" class="bulk-tagging-form hidden"> + return `<form action="/tags/bulk_tag_works" method="post" class="bulk-tagging-form hidden"> <div class="form-header"> <p>Manage Subjects</p> <div class="close-bulk-tagging-form">x</div> diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js index 5eb561450cf..1dadf9d2bc3 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js @@ -3,11 +3,11 @@ * can be displayed in the UI. */ const displayTypeMapping = { - subjects: 'subject', - subject_people: 'person', - subject_places: 'place', - subject_times: 'time', - collections: 'collection', + subjects: 'subject', + subject_people: 'person', + subject_places: 'place', + subject_times: 'time', + collections: 'collection', }; /** @@ -15,11 +15,11 @@ const displayTypeMapping = { * technical types. */ export const subjectTypeMapping = { - subject: 'subjects', - person: 'subject_people', - place: 'subject_places', - time: 'subject_times', - collection: 'collections', + subject: 'subjects', + person: 'subject_people', + place: 'subject_places', + time: 'subject_times', + collection: 'collections', }; /** @@ -34,22 +34,22 @@ export const subjectTypeMapping = { * @see {Array.sort} */ export function compare(tagA, tagB) { - const lowerA = createComparableTag(tagA); - const lowerB = createComparableTag(tagB); + const lowerA = createComparableTag(tagA); + const lowerB = createComparableTag(tagB); - if (lowerA.tagName < lowerB.tagName) { - return -1; - } else if (lowerA.tagName > lowerB.tagName) { - return 1; - } else { - if (lowerA.tagType < lowerB.tagType) { - return -1; - } else if (lowerA.tagType > lowerB.tagType) { - return 1; + if (lowerA.tagName < lowerB.tagName) { + return -1; + } else if (lowerA.tagName > lowerB.tagName) { + return 1; + } else { + if (lowerA.tagType < lowerB.tagType) { + return -1; + } else if (lowerA.tagType > lowerB.tagType) { + return 1; + } } - } - return 0; + return 0; } /** @@ -63,10 +63,10 @@ export function compare(tagA, tagB) { * @see {compare} */ function createComparableTag(tag) { - return { - tagName: tag.tagName.toLowerCase(), - tagType: tag.tagType.toLowerCase(), - }; + return { + tagName: tag.tagName.toLowerCase(), + tagType: tag.tagType.toLowerCase(), + }; } /** @@ -76,7 +76,7 @@ function createComparableTag(tag) { * type string that is suitable for displaying in the UI. */ export class Tag { - /** + /** * Creates a new Tag object. * * If only one tag type is passed to the constructor, the missing @@ -88,16 +88,16 @@ export class Tag { * * @throws Will throw an error if both `tagType` and `displayType` are falsey */ - constructor(tagName, tagType = null, displayType = null) { - if (!(tagType || displayType)) { - throw new Error('Tag must have at least one type'); + constructor(tagName, tagType = null, displayType = null) { + if (!(tagType || displayType)) { + throw new Error('Tag must have at least one type'); + } + this.tagName = tagName; + this.tagType = tagType || this.convertToType(displayType); + this.displayType = displayType || this.convertToDisplayType(tagType); } - this.tagName = tagName; - this.tagType = tagType || this.convertToType(displayType); - this.displayType = displayType || this.convertToDisplayType(tagType); - } - /** + /** * Returns the technical tag type corresponding to the given * UI-ready type string. * @@ -105,15 +105,15 @@ export class Tag { * @returns {String} The corresponding technical tag type * @throws Will throw an error if the given type is unrecognized. */ - convertToType(displayType) { - const result = subjectTypeMapping[displayType]; - if (!result) { - throw new Error('Unrecognized `displayType` value'); + convertToType(displayType) { + const result = subjectTypeMapping[displayType]; + if (!result) { + throw new Error('Unrecognized `displayType` value'); + } + return result; } - return result; - } - /** + /** * Given a technical tag type, returns a type string that can be * displayed in the UI. * @@ -121,15 +121,15 @@ export class Tag { * @returns {String} A type string that can be displayed in the UI * @throws Will throw an error if the given type is unrecognized */ - convertToDisplayType(tagType) { - const result = displayTypeMapping[tagType]; - if (!result) { - throw new Error('Unrecognized `tagType` value'); + convertToDisplayType(tagType) { + const result = displayTypeMapping[tagType]; + if (!result) { + throw new Error('Unrecognized `tagType` value'); + } + return result; } - return result; - } - /** + /** * Determins if the given tag is equal to this tag. * * Two tags are considered equal if case-insensitive comparisons of @@ -138,13 +138,13 @@ export class Tag { * @param {Tag} tag * @returns `true` if the given tag is considered equivalent to this tag. */ - equals(tag) { - const lowerSelf = createComparableTag(this); - const lowerTag = createComparableTag(tag); + equals(tag) { + const lowerSelf = createComparableTag(this); + const lowerTag = createComparableTag(tag); - return ( - lowerSelf.tagName === lowerTag.tagName && + return ( + lowerSelf.tagName === lowerTag.tagName && lowerSelf.tagType === lowerTag.tagType - ); - } + ); + } } diff --git a/openlibrary/plugins/openlibrary/js/carousel/Carousel.js b/openlibrary/plugins/openlibrary/js/carousel/Carousel.js index bb64bf484f1..965017da55b 100644 --- a/openlibrary/plugins/openlibrary/js/carousel/Carousel.js +++ b/openlibrary/plugins/openlibrary/js/carousel/Carousel.js @@ -22,170 +22,170 @@ import { buildPartialsUrl } from '../utils.js'; // used in templates/covers/add.html export class Carousel { - /** + /** * @param {jQuery} $container */ - constructor($container) { + constructor($container) { /** @type {CarouselConfig} */ - this.config = Object.assign( - { - booksPerBreakpoint: [6, 5, 4, 3, 2, 1], - analyticsCategory: 'Carousel', - carouselKey: '', - }, - JSON.parse($container.attr('data-config')), - ); - - /** @type {CarouselConfig['loadMore']} */ - this.loadMore = Object.assign( - { - limit: 18, // 3 pages of 6 books - pageMode: 'page', - locked: false, - allDone: false, - page: 1, - }, - this.config.loadMore || {}, - ); - - /** @type {jquery} */ - this.$container = $container; - - //This loads in i18n strings from a hidden input element, generated in the books/custom_carousel.html template. - const i18nInput = document.querySelector( - 'input[name="carousel-i18n-strings"]', - ); - if (i18nInput) { - this.i18n = JSON.parse(i18nInput.value); + this.config = Object.assign( + { + booksPerBreakpoint: [6, 5, 4, 3, 2, 1], + analyticsCategory: 'Carousel', + carouselKey: '', + }, + JSON.parse($container.attr('data-config')), + ); + + /** @type {CarouselConfig['loadMore']} */ + this.loadMore = Object.assign( + { + limit: 18, // 3 pages of 6 books + pageMode: 'page', + locked: false, + allDone: false, + page: 1, + }, + this.config.loadMore || {}, + ); + + /** @type {jquery} */ + this.$container = $container; + + //This loads in i18n strings from a hidden input element, generated in the books/custom_carousel.html template. + const i18nInput = document.querySelector( + 'input[name="carousel-i18n-strings"]', + ); + if (i18nInput) { + this.i18n = JSON.parse(i18nInput.value); + } + } + + get slick() { + return this.$container.slick('getSlick'); } - } - - get slick() { - return this.$container.slick('getSlick'); - } - - init() { - this.$container.slick({ - infinite: false, - speed: 300, - slidesToShow: this.config.booksPerBreakpoint[0], - slidesToScroll: this.config.booksPerBreakpoint[0], - responsive: [1200, 1024, 600, 480, 360].map((breakpoint, i) => ({ - breakpoint: breakpoint, - settings: { - slidesToShow: this.config.booksPerBreakpoint[i + 1], - slidesToScroll: this.config.booksPerBreakpoint[i + 1], - infinite: false, - }, - })), - }); - - // Slick internally changes the click handlers on the next/prev buttons, - // so we listen via the container instead - this.$container.on('click', '.slick-next', (ev) => { - // Note: This will actually fail on the last 'next', but that's okay - if ($(ev.target).hasClass('slick-disabled')) return; - - window.archive_analytics.ol_send_event_ping({ - category: this.config.analyticsCategory, - action: 'Next', - label: this.config.carouselKey, - }); - }); - - this.$container.on('swipe', (ev, _slick, direction) => { - if (direction === 'left') { - window.archive_analytics.ol_send_event_ping({ - category: this.config.analyticsCategory, - action: 'Next', - label: this.config.carouselKey, + + init() { + this.$container.slick({ + infinite: false, + speed: 300, + slidesToShow: this.config.booksPerBreakpoint[0], + slidesToScroll: this.config.booksPerBreakpoint[0], + responsive: [1200, 1024, 600, 480, 360].map((breakpoint, i) => ({ + breakpoint: breakpoint, + settings: { + slidesToShow: this.config.booksPerBreakpoint[i + 1], + slidesToScroll: this.config.booksPerBreakpoint[i + 1], + infinite: false, + }, + })), }); - } - }); - - // if a loadMore config is provided and it has a (required) url - const loadMore = this.loadMore; - if (loadMore && loadMore.queryType) { - // Bind an action listener to this carousel on resize or advance - this.$container.on('afterChange', (_ev, _slick, curSlide) => { - const totalSlides = this.slick.$slides.length; - const numActiveSlides = - this.slick.$slides.filter('.slick-active').length; - // this allows us to pre-load before hitting last page - const needsMoreCards = totalSlides - curSlide <= numActiveSlides * 2; - if (!loadMore.locked && !loadMore.allDone && needsMoreCards) { - loadMore.locked = true; // lock for critical section + // Slick internally changes the click handlers on the next/prev buttons, + // so we listen via the container instead + this.$container.on('click', '.slick-next', (ev) => { + // Note: This will actually fail on the last 'next', but that's okay + if ($(ev.target).hasClass('slick-disabled')) return; + + window.archive_analytics.ol_send_event_ping({ + category: this.config.analyticsCategory, + action: 'Next', + label: this.config.carouselKey, + }); + }); - if (loadMore.pageMode === 'page') { - loadMore.page++; - } else { - // i.e. offset, start from last slide - loadMore.page = totalSlides; - } + this.$container.on('swipe', (ev, _slick, direction) => { + if (direction === 'left') { + window.archive_analytics.ol_send_event_ping({ + category: this.config.analyticsCategory, + action: 'Next', + label: this.config.carouselKey, + }); + } + }); - this.fetchPartials(); - } - }); - - document.addEventListener('filter', (ev) => { - loadMore.extraParams = { - published_in: `${ev.detail.yearFrom}-${ev.detail.yearTo}`, - }; - - // Reset the page count - the result set is now 'new' - if (loadMore.pageMode === 'page') { - loadMore.page = 1; - } else { - loadMore.page = 0; + // if a loadMore config is provided and it has a (required) url + const loadMore = this.loadMore; + if (loadMore && loadMore.queryType) { + // Bind an action listener to this carousel on resize or advance + this.$container.on('afterChange', (_ev, _slick, curSlide) => { + const totalSlides = this.slick.$slides.length; + const numActiveSlides = + this.slick.$slides.filter('.slick-active').length; + // this allows us to pre-load before hitting last page + const needsMoreCards = totalSlides - curSlide <= numActiveSlides * 2; + + if (!loadMore.locked && !loadMore.allDone && needsMoreCards) { + loadMore.locked = true; // lock for critical section + + if (loadMore.pageMode === 'page') { + loadMore.page++; + } else { + // i.e. offset, start from last slide + loadMore.page = totalSlides; + } + + this.fetchPartials(); + } + }); + + document.addEventListener('filter', (ev) => { + loadMore.extraParams = { + published_in: `${ev.detail.yearFrom}-${ev.detail.yearTo}`, + }; + + // Reset the page count - the result set is now 'new' + if (loadMore.pageMode === 'page') { + loadMore.page = 1; + } else { + loadMore.page = 0; + } + loadMore.allDone = false; + + this.clearCarousel(); + this.fetchPartials(); + }); } - loadMore.allDone = false; + } + + fetchPartials() { + const loadMore = this.loadMore; + const url = buildPartialsUrl('CarouselLoadMore', { + queryType: loadMore.queryType, + q: loadMore.q, + limit: loadMore.limit, + page: loadMore.page, + sorts: loadMore.sorts, + subject: loadMore.subject, + pageMode: loadMore.pageMode, + hasFulltextOnly: loadMore.hasFulltextOnly, + secondaryAction: loadMore.secondaryAction, + key: loadMore.key, + ...loadMore.extraParams, + }); + this.appendLoadingSlide(); + $.ajax({ url: url, type: 'GET' }).then((results) => { + this.removeLoadingSlide(); + const cards = results.partials || []; + cards.forEach((card) => this.slick.addSlide(card)); + + if (!cards.length) { + loadMore.allDone = true; + } + loadMore.locked = false; + }); + } + + clearCarousel() { + this.slick.removeSlide(this.slick.$slides.length, true, true); + } + + appendLoadingSlide() { + this.slick.addSlide( + `<div class="carousel__item carousel__loading-end">${this.i18n['loading']}</div>`, + ); + } - this.clearCarousel(); - this.fetchPartials(); - }); + removeLoadingSlide() { + this.slick.removeSlide(this.slick.$slides.length - 1); } - } - - fetchPartials() { - const loadMore = this.loadMore; - const url = buildPartialsUrl('CarouselLoadMore', { - queryType: loadMore.queryType, - q: loadMore.q, - limit: loadMore.limit, - page: loadMore.page, - sorts: loadMore.sorts, - subject: loadMore.subject, - pageMode: loadMore.pageMode, - hasFulltextOnly: loadMore.hasFulltextOnly, - secondaryAction: loadMore.secondaryAction, - key: loadMore.key, - ...loadMore.extraParams, - }); - this.appendLoadingSlide(); - $.ajax({ url: url, type: 'GET' }).then((results) => { - this.removeLoadingSlide(); - const cards = results.partials || []; - cards.forEach((card) => this.slick.addSlide(card)); - - if (!cards.length) { - loadMore.allDone = true; - } - loadMore.locked = false; - }); - } - - clearCarousel() { - this.slick.removeSlide(this.slick.$slides.length, true, true); - } - - appendLoadingSlide() { - this.slick.addSlide( - `<div class="carousel__item carousel__loading-end">${this.i18n['loading']}</div>`, - ); - } - - removeLoadingSlide() { - this.slick.removeSlide(this.slick.$slides.length - 1); - } } diff --git a/openlibrary/plugins/openlibrary/js/carousel/index.js b/openlibrary/plugins/openlibrary/js/carousel/index.js index f3e6a25b975..16ccfbbfb7d 100644 --- a/openlibrary/plugins/openlibrary/js/carousel/index.js +++ b/openlibrary/plugins/openlibrary/js/carousel/index.js @@ -1,14 +1,14 @@ import { Carousel } from './Carousel'; export function initialzeCarousels(elems) { - elems.forEach((elem) => { - new Carousel($(elem)).init(); - const elemSlides = elem.querySelectorAll('.slick-slide'); - elemSlides.forEach((slide) => { - const $slide = $(slide); - if ($slide.attr('aria-describedby') !== undefined) { - $slide.attr('id', $slide.attr('aria-describedby')); - } + elems.forEach((elem) => { + new Carousel($(elem)).init(); + const elemSlides = elem.querySelectorAll('.slick-slide'); + elemSlides.forEach((slide) => { + const $slide = $(slide); + if ($slide.attr('aria-describedby') !== undefined) { + $slide.attr('id', $slide.attr('aria-describedby')); + } + }); }); - }); } diff --git a/openlibrary/plugins/openlibrary/js/clampers.js b/openlibrary/plugins/openlibrary/js/clampers.js index 18e4b558942..b47e68f7210 100644 --- a/openlibrary/plugins/openlibrary/js/clampers.js +++ b/openlibrary/plugins/openlibrary/js/clampers.js @@ -3,32 +3,32 @@ * */ export function initClampers(clampers) { - for (const clamper of clampers) { - if (clamper.clientHeight === clamper.scrollHeight) { - clamper.classList.remove('clamp'); - } else { - /* + for (const clamper of clampers) { + if (clamper.clientHeight === clamper.scrollHeight) { + clamper.classList.remove('clamp'); + } else { + /* Clamper used to collapse category list by toggling `hidden` style on parent element */ - clamper.addEventListener('click', (event) => { - if (event.target instanceof HTMLAnchorElement) { - return; - } + clamper.addEventListener('click', (event) => { + if (event.target instanceof HTMLAnchorElement) { + return; + } - clamper.style.display = + clamper.style.display = clamper.style.display === '-webkit-box' || clamper.style.display === '' - ? 'unset' - : '-webkit-box'; + ? 'unset' + : '-webkit-box'; - if (clamper.getAttribute('data-before') === '\u25BE ') { - clamper.setAttribute('data-before', '\u25B8 '); - } else { - clamper.setAttribute('data-before', '\u25BE '); + if (clamper.getAttribute('data-before') === '\u25BE ') { + clamper.setAttribute('data-before', '\u25B8 '); + } else { + clamper.setAttribute('data-before', '\u25BE '); + } + }); } - }); } - } } diff --git a/openlibrary/plugins/openlibrary/js/compact-title/index.js b/openlibrary/plugins/openlibrary/js/compact-title/index.js index 9c90b3a21eb..eb94b86201f 100644 --- a/openlibrary/plugins/openlibrary/js/compact-title/index.js +++ b/openlibrary/plugins/openlibrary/js/compact-title/index.js @@ -26,15 +26,15 @@ let mainTitleElem; * @param {HTMLElement} title The compact title component */ export function initCompactTitle(navbar, title) { - mainTitleElem = document.querySelector( - '.work-title-and-author.desktop .work-title', - ); - // Show compact title on page reload: - onScroll(navbar, title); - // And update on scroll - window.addEventListener('scroll', () => { + mainTitleElem = document.querySelector( + '.work-title-and-author.desktop .work-title', + ); + // Show compact title on page reload: onScroll(navbar, title); - }); + // And update on scroll + window.addEventListener('scroll', () => { + onScroll(navbar, title); + }); } /** @@ -47,42 +47,42 @@ export function initCompactTitle(navbar, title) { * @param {HTMLElement} title The compact title component */ function onScroll(navbar, title) { - const compactTitleBounds = title.getBoundingClientRect(); - const navbarBounds = navbar.getBoundingClientRect(); - const mainTitleBounds = mainTitleElem.getBoundingClientRect(); - if (mainTitleBounds.bottom < navbarBounds.bottom) { + const compactTitleBounds = title.getBoundingClientRect(); + const navbarBounds = navbar.getBoundingClientRect(); + const mainTitleBounds = mainTitleElem.getBoundingClientRect(); + if (mainTitleBounds.bottom < navbarBounds.bottom) { // The main title is off-screen - if (!navbar.classList.contains('sticky--lowest')) { - // Compact title not displayed - // Display compact title - title.classList.remove('hidden'); - // Animate navbar - $(navbar) - .addClass('nav-bar-wrapper--slidedown') - .one('animationend', () => { - $(navbar).addClass('sticky--lowest'); - $(navbar).removeClass('nav-bar-wrapper--slidedown'); - // Ensure correct nav item is selected after compact title slides in: - updateSelectedNavItem(); - }); + if (!navbar.classList.contains('sticky--lowest')) { + // Compact title not displayed + // Display compact title + title.classList.remove('hidden'); + // Animate navbar + $(navbar) + .addClass('nav-bar-wrapper--slidedown') + .one('animationend', () => { + $(navbar).addClass('sticky--lowest'); + $(navbar).removeClass('nav-bar-wrapper--slidedown'); + // Ensure correct nav item is selected after compact title slides in: + updateSelectedNavItem(); + }); + } else { + if (navbarBounds.top < compactTitleBounds.bottom) { + // We've scrolled to the bottom of the container, and the navbar is unstuck + title.classList.add('hidden'); + } else { + title.classList.remove('hidden'); + } + } } else { - if (navbarBounds.top < compactTitleBounds.bottom) { - // We've scrolled to the bottom of the container, and the navbar is unstuck - title.classList.add('hidden'); - } else { - title.classList.remove('hidden'); - } - } - } else { // At least some of the main title is below the navbar - if (!title.classList.contains('hidden')) { - title.classList.add('hidden'); - $(navbar) - .addClass('nav-bar-wrapper--slideup') - .one('animationend', () => { - $(navbar).removeClass('sticky--lowest'); - $(navbar).removeClass('nav-bar-wrapper--slideup'); - }); + if (!title.classList.contains('hidden')) { + title.classList.add('hidden'); + $(navbar) + .addClass('nav-bar-wrapper--slideup') + .one('animationend', () => { + $(navbar).removeClass('sticky--lowest'); + $(navbar).removeClass('nav-bar-wrapper--slideup'); + }); + } } - } } diff --git a/openlibrary/plugins/openlibrary/js/dialog.js b/openlibrary/plugins/openlibrary/js/dialog.js index dfb25f59a44..9aca4fb7763 100644 --- a/openlibrary/plugins/openlibrary/js/dialog.js +++ b/openlibrary/plugins/openlibrary/js/dialog.js @@ -8,76 +8,76 @@ import 'jquery-colorbox'; * @return {Function} for creating a confirm dialog */ function initConfirmationDialogs() { - const CONFIRMATION_PROMPT_DEFAULTS = { autoOpen: false, modal: true }; - $('#noMaster').dialog(CONFIRMATION_PROMPT_DEFAULTS); + const CONFIRMATION_PROMPT_DEFAULTS = { autoOpen: false, modal: true }; + $('#noMaster').dialog(CONFIRMATION_PROMPT_DEFAULTS); - const $confirmMerge = $('#confirmMerge'); - if ($confirmMerge.length) { - $confirmMerge.dialog( - $.extend({}, CONFIRMATION_PROMPT_DEFAULTS, { - buttons: { - 'Yes, Merge': function () { - const commentInput = document.querySelector( - '#author-merge-comment', - ); - if (commentInput.value) { - document.querySelector('#hidden-comment-input').value = + const $confirmMerge = $('#confirmMerge'); + if ($confirmMerge.length) { + $confirmMerge.dialog( + $.extend({}, CONFIRMATION_PROMPT_DEFAULTS, { + buttons: { + 'Yes, Merge': function () { + const commentInput = document.querySelector( + '#author-merge-comment', + ); + if (commentInput.value) { + document.querySelector('#hidden-comment-input').value = commentInput.value; - } - $('#mergeForm').trigger('submit'); - $(this).parents().find('button').attr('disabled', 'disabled'); - }, - 'No, Cancel': function () { - $(this).dialog('close'); - }, - }, - }), + } + $('#mergeForm').trigger('submit'); + $(this).parents().find('button').attr('disabled', 'disabled'); + }, + 'No, Cancel': function () { + $(this).dialog('close'); + }, + }, + }), + ); + } + $('#leave-waitinglist-dialog').dialog( + $.extend({}, CONFIRMATION_PROMPT_DEFAULTS, { + width: 450, + resizable: false, + buttons: { + 'Yes, I\'m sure': function () { + $(this).dialog('close'); + $(this).data('origin').closest('td').find('form').trigger('submit'); + }, + 'No, cancel': function () { + $(this).dialog('close'); + }, + }, + }), ); - } - $('#leave-waitinglist-dialog').dialog( - $.extend({}, CONFIRMATION_PROMPT_DEFAULTS, { - width: 450, - resizable: false, - buttons: { - "Yes, I'm sure": function () { - $(this).dialog('close'); - $(this).data('origin').closest('td').find('form').trigger('submit'); - }, - 'No, cancel': function () { - $(this).dialog('close'); - }, - }, - }), - ); } export function initPreviewDialogs() { - // Delegated click handler for Book Preview buttons. - // Uses event delegation so dynamically-added buttons (e.g. from - // lazy-loaded carousels) work without re-initialization. - $(document) - .off('click.bookPreview') - .on('click.bookPreview', '[data-book-preview]', function (e) { - e.preventDefault(); - const $button = $(this); - $.colorbox({ - width: '100%', - maxWidth: '640px', - inline: true, - opacity: '0.5', - href: '#bookPreview', - onOpen() { - const $iframe = $('#bookPreview iframe'); - $iframe.prop('src', $button.data('iframe-src')); + // Delegated click handler for Book Preview buttons. + // Uses event delegation so dynamically-added buttons (e.g. from + // lazy-loaded carousels) work without re-initialization. + $(document) + .off('click.bookPreview') + .on('click.bookPreview', '[data-book-preview]', function (e) { + e.preventDefault(); + const $button = $(this); + $.colorbox({ + width: '100%', + maxWidth: '640px', + inline: true, + opacity: '0.5', + href: '#bookPreview', + onOpen() { + const $iframe = $('#bookPreview iframe'); + $iframe.prop('src', $button.data('iframe-src')); - const $link = $('#bookPreview .learn-more a'); - $link[0].href = $button.data('iframe-link'); - }, - onCleanup() { - $('#bookPreview iframe').prop('src', ''); - }, - }); - }); + const $link = $('#bookPreview .learn-more a'); + $link[0].href = $button.data('iframe-link'); + }, + onCleanup() { + $('#bookPreview iframe').prop('src', ''); + }, + }); + }); } /** @@ -87,31 +87,31 @@ export function initPreviewDialogs() { * communicates where the HTML of that dialog lives. */ export function initDialogs() { - $('.dialog--open').on('click', function () { - const $link = $(this), - href = `#${$link.attr('aria-controls')}`; + $('.dialog--open').on('click', function () { + const $link = $(this), + href = `#${$link.attr('aria-controls')}`; - $link.colorbox({ - inline: true, - opacity: '0.5', - href, - maxWidth: '640px', - width: '100%', + $link.colorbox({ + inline: true, + opacity: '0.5', + href, + maxWidth: '640px', + width: '100%', + }); }); - }); - initConfirmationDialogs(); - initPreviewDialogs(); + initConfirmationDialogs(); + initPreviewDialogs(); - // This will close the dialog in the current page. - $('.dialog--close') - .attr('href', '#') - .on('click', (e) => { - e.preventDefault(); - $.fn.colorbox.close(); - }); - // This will close the colorbox from the parent. - $('.dialog--close-parent').on('click', () => parent.$.fn.colorbox.close()); + // This will close the dialog in the current page. + $('.dialog--close') + .attr('href', '#') + .on('click', (e) => { + e.preventDefault(); + $.fn.colorbox.close(); + }); + // This will close the colorbox from the parent. + $('.dialog--close-parent').on('click', () => parent.$.fn.colorbox.close()); } /** @@ -120,7 +120,7 @@ export function initDialogs() { * @param {NodeList<Element>} closers */ export function initDialogClosers(closers) { - closers.forEach((closer) => { - $(closer).on('click', () => $.fn.colorbox.close()); - }); + closers.forEach((closer) => { + $(closer).on('click', () => $.fn.colorbox.close()); + }); } diff --git a/openlibrary/plugins/openlibrary/js/dropper/Dropper.js b/openlibrary/plugins/openlibrary/js/dropper/Dropper.js index 8a6cc37a16a..02388936c2e 100644 --- a/openlibrary/plugins/openlibrary/js/dropper/Dropper.js +++ b/openlibrary/plugins/openlibrary/js/dropper/Dropper.js @@ -21,7 +21,7 @@ * @class */ export class Dropper { - /** + /** * Creates a new dropper. * * Sets the initial state of the dropper, and sets references to key @@ -29,15 +29,15 @@ export class Dropper { * * @param {HTMLElement} dropper Reference to the dropper's root element */ - constructor(dropper) { + constructor(dropper) { /** * References the root element of the dropper. * * @member {HTMLElement} */ - this.dropper = dropper; + this.dropper = dropper; - /** + /** * jQuery object containing the root element of the dropper. * * **Note:** jQuery is only used here for its slide animations. @@ -46,71 +46,71 @@ export class Dropper { * * @member {JQuery<HTMLElement>} */ - this.$dropper = $(dropper); + this.$dropper = $(dropper); - /** + /** * Reference to the affordance that, when clicked, toggles * the "Open" state of this dropper. * * @member {HTMLElement} */ - this.dropClick = dropper.querySelector('.generic-dropper__dropclick'); + this.dropClick = dropper.querySelector('.generic-dropper__dropclick'); - /** + /** * Tracks the current "Open" state of this dropper. * * @member {boolean} */ - this.isDropperOpen = dropper.classList.contains( - 'generic-dropper-wrapper--active', - ); + this.isDropperOpen = dropper.classList.contains( + 'generic-dropper-wrapper--active', + ); - /** + /** * Tracks whether this dropper is disabled. * * A disabled dropper cannot be toggled. * * @member {boolean} */ - this.isDropperDisabled = dropper.classList.contains( - 'generic-dropper--disabled', - ); - } + this.isDropperDisabled = dropper.classList.contains( + 'generic-dropper--disabled', + ); + } - /** + /** * Adds click listener to dropper's toggle arrow. */ - initialize() { - this.dropClick.addEventListener('click', () => { - this.toggleDropper(); - }); - } + initialize() { + this.dropClick.addEventListener('click', () => { + this.toggleDropper(); + }); + } - /** + /** * Function that is called after a dropper has opened. * * Subclasses of `Dropper` may override this to add * functionality that should occur on dropper open. */ - onOpen() {} + onOpen() {} - /** + /** * Function that is called after a dropper has closed. * * Subclasses of `Dropper` may override this to add * functionality that should occur on dropper close. */ - onClose() {} + onClose() {} - /** + /** * Function that is called when the drop-click affordance of * a disabled dropper is clicked. * * Subclasses of `Dropper` may override this as needed. */ - onDisabledClick() {} + onDisabledClick() {} - /** + /** * Closes dropper if opened; opens dropper if closed. * * Toggles value of `isDropperOpen`. @@ -119,24 +119,24 @@ export class Dropper { * Calls either `onOpen()` or `onClose()` after the dropper * has been toggled. */ - toggleDropper() { - if (this.isDropperDisabled) { - this.onDisabledClick(); - } else { - this.$dropper.find('.generic-dropper__dropdown').slideToggle(25); - this.$dropper.find('.arrow').toggleClass('up'); - this.$dropper.toggleClass('generic-dropper-wrapper--active'); - this.isDropperOpen = !this.isDropperOpen; + toggleDropper() { + if (this.isDropperDisabled) { + this.onDisabledClick(); + } else { + this.$dropper.find('.generic-dropper__dropdown').slideToggle(25); + this.$dropper.find('.arrow').toggleClass('up'); + this.$dropper.toggleClass('generic-dropper-wrapper--active'); + this.isDropperOpen = !this.isDropperOpen; - if (this.isDropperOpen) { - this.onOpen(); - } else { - this.onClose(); - } + if (this.isDropperOpen) { + this.onOpen(); + } else { + this.onClose(); + } + } } - } - /** + /** * Closes this dropper. * * Sets `isDropperOpen` to `false`. @@ -144,16 +144,16 @@ export class Dropper { * Calls `onDisabledClick()` if this dropper is disabled. * Otherwise, closes dropper and calls `onClose()`. */ - closeDropper() { - if (this.isDropperDisabled) { - this.onDisabledClick(); - } else { - this.$dropper.find('.generic-dropper__dropdown').slideUp(25); - this.$dropper.find('.arrow').removeClass('up'); - this.$dropper.removeClass('generic-dropper-wrapper--active'); - this.isDropperOpen = false; + closeDropper() { + if (this.isDropperDisabled) { + this.onDisabledClick(); + } else { + this.$dropper.find('.generic-dropper__dropdown').slideUp(25); + this.$dropper.find('.arrow').removeClass('up'); + this.$dropper.removeClass('generic-dropper-wrapper--active'); + this.isDropperOpen = false; - this.onClose(); + this.onClose(); + } } - } } diff --git a/openlibrary/plugins/openlibrary/js/dropper/index.js b/openlibrary/plugins/openlibrary/js/dropper/index.js index b0db00103d5..41421cc5968 100644 --- a/openlibrary/plugins/openlibrary/js/dropper/index.js +++ b/openlibrary/plugins/openlibrary/js/dropper/index.js @@ -12,44 +12,44 @@ const droppers = []; * @param {HTMLCollection<HTMLElement>} dropperElements */ export function initDroppers(dropperElements) { - for (const dropper of dropperElements) { - droppers.push(dropper); + for (const dropper of dropperElements) { + droppers.push(dropper); - $(dropper).on( - 'click', - '.dropclick', - debounce( - function () { - $(this).next('.dropdown').slideToggle(25); - $(this).parent().next('.dropdown').slideToggle(25); - $(this).parent().find('.arrow').toggleClass('up'); - }, - 300, - false, - ), - ); + $(dropper).on( + 'click', + '.dropclick', + debounce( + function () { + $(this).next('.dropdown').slideToggle(25); + $(this).parent().next('.dropdown').slideToggle(25); + $(this).parent().find('.arrow').toggleClass('up'); + }, + 300, + false, + ), + ); - $(dropper).on( - 'click', - '.dropper__close', - debounce( - () => { - closeDropper($(dropper)); - }, - 300, - false, - ), - ); - } - - // Close any open dropdown list if the user clicks outside of component: - $(document).on('click', (event) => { - for (const dropper of droppers) { - if (!dropper.contains(event.target)) { - closeDropper($(dropper)); - } + $(dropper).on( + 'click', + '.dropper__close', + debounce( + () => { + closeDropper($(dropper)); + }, + 300, + false, + ), + ); } - }); + + // Close any open dropdown list if the user clicks outside of component: + $(document).on('click', (event) => { + for (const dropper of droppers) { + if (!dropper.contains(event.target)) { + closeDropper($(dropper)); + } + } + }); } /** @@ -57,10 +57,10 @@ export function initDroppers(dropperElements) { * @param {jQuery.Object} $container */ function closeDropper($container) { - $container.find('.dropdown').slideUp(25); // Legacy droppers - $container.find('.generic-dropper__dropdown').slideUp(25); // New generic droppers - $container.find('.arrow').removeClass('up'); - $container.removeClass('generic-dropper-wrapper--active'); + $container.find('.dropdown').slideUp(25); // Legacy droppers + $container.find('.generic-dropper__dropdown').slideUp(25); // New generic droppers + $container.find('.arrow').removeClass('up'); + $container.removeClass('generic-dropper-wrapper--active'); } /** @@ -73,14 +73,14 @@ function closeDropper($container) { * @param {NodeList<HTMLElement>} dropperElements */ export function initGenericDroppers(dropperElements) { - const genericDroppers = Array.from(dropperElements); + const genericDroppers = Array.from(dropperElements); - // Close any open dropdown if the user clicks outside of component: - $(document).on('click', (event) => { - for (const dropper of genericDroppers) { - if (!dropper.contains(event.target)) { - closeDropper($(dropper)); - } - } - }); + // Close any open dropdown if the user clicks outside of component: + $(document).on('click', (event) => { + for (const dropper of genericDroppers) { + if (!dropper.contains(event.target)) { + closeDropper($(dropper)); + } + } + }); } diff --git a/openlibrary/plugins/openlibrary/js/edit.js b/openlibrary/plugins/openlibrary/js/edit.js index 5e8854a53bb..fdd52bde891 100644 --- a/openlibrary/plugins/openlibrary/js/edit.js +++ b/openlibrary/plugins/openlibrary/js/edit.js @@ -1,13 +1,13 @@ import { init as initAutocomplete } from './autocomplete'; import { - isChecksumValidIsbn10, - isChecksumValidIsbn13, - isFormatValidIsbn10, - isFormatValidIsbn13, - isIdDupe, - isValidLccn, - parseIsbn, - parseLccn, + isChecksumValidIsbn10, + isChecksumValidIsbn13, + isFormatValidIsbn10, + isFormatValidIsbn13, + isIdDupe, + isValidLccn, + parseIsbn, + parseLccn, } from './idValidation'; import { isbnOverride } from './isbnOverride'; import { init as initJqueryRepeat } from './jquery.repeat'; @@ -24,22 +24,22 @@ import { trimInputValues } from './utils.js'; /* Globals are provided by the edit about template */ function error(errordiv, input, message) { - $(errordiv).show().html(message); - $(input).trigger('focus'); - return false; + $(errordiv).show().html(message); + $(input).trigger('focus'); + return false; } function update_len() { - var len = $('#excerpts-excerpt').val().length; - var color; - if (len > 2000) { - color = '#e44028'; - } else { - color = 'gray'; - } - $('#excerpts-excerpt-len') - .html(2000 - len) - .css('color', color); + var len = $('#excerpts-excerpt').val().length; + var color; + if (len > 2000) { + color = '#e44028'; + } else { + color = 'gray'; + } + $('#excerpts-excerpt-len') + .html(2000 - len) + .css('color', color); } /** @@ -50,14 +50,14 @@ function update_len() { * @return {boolean} is character number below or equal to limit */ function limitChars(textid, limit) { - var text = $(`#${textid}`).val(); - var textlength = text.length; - if (textlength > limit) { - $(`#${textid}`).val(text.substr(0, limit)); - return false; - } else { - return true; - } + var text = $(`#${textid}`).val(); + var textlength = text.length; + if (textlength > limit) { + $(`#${textid}`).val(text.substr(0, limit)); + return false; + } else { + return true; + } } /** @@ -66,44 +66,44 @@ function limitChars(textid, limit) { * @returns {*[]} - array of jQuery elements */ function getJqueryElements(selector) { - const queryResult = $(selector); - const jQueryElementArray = []; - for (let i = 0; i < queryResult.length; i++) { - jQueryElementArray.push(queryResult.eq(i)); - } - return jQueryElementArray; + const queryResult = $(selector); + const jQueryElementArray = []; + for (let i = 0; i < queryResult.length; i++) { + jQueryElementArray.push(queryResult.eq(i)); + } + return jQueryElementArray; } export function initRoleValidation() { - initJqueryRepeat(); - const dataConfig = JSON.parse( - document.querySelector('#roles').dataset.config, - ); - $('#roles').repeat({ - vars: { prefix: 'edition--' }, - validate: (data) => { - if (data.role === '' || data.role === '---') { - return error( - '#role-errors', - '#select-role', - dataConfig['Please select a role.'], - ); - } - if (data.name === '') { - return error( - '#role-errors', - '#role-name', - dataConfig['You need to give this ROLE a name.'].replace( - /ROLE/, - data.role, - ), - ); - } - $('#role-errors').hide(); - $('#select-role, #role-name').val(''); - return true; - }, - }); + initJqueryRepeat(); + const dataConfig = JSON.parse( + document.querySelector('#roles').dataset.config, + ); + $('#roles').repeat({ + vars: { prefix: 'edition--' }, + validate: (data) => { + if (data.role === '' || data.role === '---') { + return error( + '#role-errors', + '#select-role', + dataConfig['Please select a role.'], + ); + } + if (data.name === '') { + return error( + '#role-errors', + '#role-name', + dataConfig['You need to give this ROLE a name.'].replace( + /ROLE/, + data.role, + ), + ); + } + $('#role-errors').hide(); + $('#select-role, #role-name').val(''); + return true; + }, + }); } /** @@ -113,26 +113,26 @@ export function initRoleValidation() { * @param {String} isbnConfirmString a const with the HTML to create the confirmation message/buttons */ export function isbnConfirmAdd(data) { - const isbnConfirmString = `ISBN ${data.value} may be invalid. Add it anyway? <button class="repeat-add" id="yes-add-isbn" type="button">Yes</button> <button id="do-not-add-isbn" type="button">No</button>`; - // Display the error and option to add the ISBN anyway. - $('#id-errors').show().html(isbnConfirmString); - - const yesButtonSelector = '#yes-add-isbn'; - const noButtonSelector = '#do-not-add-isbn'; - const onYes = () => { - $('#id-errors').hide(); - }; - const onNo = () => { - $('#id-errors').hide(); - isbnOverride.clear(); - }; - $(document).on('click', yesButtonSelector, onYes); - $(document).on('click', noButtonSelector, onNo); - - // Save the data to isbnOverride so it can be picked up via onAdd in - // js/jquery.repeat.js when the user confirms adding the invalid ISBN. - isbnOverride.set(data); - return false; + const isbnConfirmString = `ISBN ${data.value} may be invalid. Add it anyway? <button class="repeat-add" id="yes-add-isbn" type="button">Yes</button> <button id="do-not-add-isbn" type="button">No</button>`; + // Display the error and option to add the ISBN anyway. + $('#id-errors').show().html(isbnConfirmString); + + const yesButtonSelector = '#yes-add-isbn'; + const noButtonSelector = '#do-not-add-isbn'; + const onYes = () => { + $('#id-errors').hide(); + }; + const onNo = () => { + $('#id-errors').hide(); + isbnOverride.clear(); + }; + $(document).on('click', yesButtonSelector, onYes); + $(document).on('click', noButtonSelector, onNo); + + // Save the data to isbnOverride so it can be picked up via onAdd in + // js/jquery.repeat.js when the user confirms adding the invalid ISBN. + isbnOverride.set(data); + return false; } /** @@ -144,29 +144,29 @@ export function isbnConfirmAdd(data) { * @returns {boolean} true if ISBN passes validation, else returns false and displays appropriate error */ function validateIsbn10(data, dataConfig, label) { - data.value = parseIsbn(data.value); - - if (!isFormatValidIsbn10(data.value)) { - return error( - '#id-errors', - '#id-value', - dataConfig['ID must be exactly 10 characters [0-9] or X.'].replace( - /ID/, - label, - ), - ); - } - // Here the ISBN has a valid format, but also has an invalid checksum. Give the user a chance to verify - // the ISBN, as books sometimes issue with invalid ISBNs and we want to be able to add them. - // See https://en-academic.com/dic.nsf/enwiki/8948#cite_ref-18 for more. - else if ( - isFormatValidIsbn10(data.value) === true && + data.value = parseIsbn(data.value); + + if (!isFormatValidIsbn10(data.value)) { + return error( + '#id-errors', + '#id-value', + dataConfig['ID must be exactly 10 characters [0-9] or X.'].replace( + /ID/, + label, + ), + ); + } + // Here the ISBN has a valid format, but also has an invalid checksum. Give the user a chance to verify + // the ISBN, as books sometimes issue with invalid ISBNs and we want to be able to add them. + // See https://en-academic.com/dic.nsf/enwiki/8948#cite_ref-18 for more. + else if ( + isFormatValidIsbn10(data.value) === true && isChecksumValidIsbn10(data.value) === false - ) { - isbnConfirmAdd(data); - return false; - } - return true; + ) { + isbnConfirmAdd(data); + return false; + } + return true; } /** @@ -178,28 +178,28 @@ function validateIsbn10(data, dataConfig, label) { * @returns {boolean} true if ISBN passes validation, else returns false and displays appropriate error */ function validateIsbn13(data, dataConfig, label) { - data.value = parseIsbn(data.value); - - if (isFormatValidIsbn13(data.value) === false) { - return error( - '#id-errors', - '#id-value', - dataConfig[ - 'ID must be exactly 13 digits [0-9]. For example: 978-1-56619-909-4' - ].replace(/ID/, label), - ); - } - // Here the ISBN has a valid format, but also has an invalid checksum. Give the user a chance to verify - // the ISBN, as books sometimes issue with invalid ISBNs and we want to be able to add them. - // See https://en-academic.com/dic.nsf/enwiki/8948#cite_ref-18 for more. - else if ( - isFormatValidIsbn13(data.value) === true && + data.value = parseIsbn(data.value); + + if (isFormatValidIsbn13(data.value) === false) { + return error( + '#id-errors', + '#id-value', + dataConfig[ + 'ID must be exactly 13 digits [0-9]. For example: 978-1-56619-909-4' + ].replace(/ID/, label), + ); + } + // Here the ISBN has a valid format, but also has an invalid checksum. Give the user a chance to verify + // the ISBN, as books sometimes issue with invalid ISBNs and we want to be able to add them. + // See https://en-academic.com/dic.nsf/enwiki/8948#cite_ref-18 for more. + else if ( + isFormatValidIsbn13(data.value) === true && isChecksumValidIsbn13(data.value) === false - ) { - isbnConfirmAdd(data); - return false; - } - return true; + ) { + isbnConfirmAdd(data); + return false; + } + return true; } /** @@ -211,17 +211,17 @@ function validateIsbn13(data, dataConfig, label) { * @returns {boolean} true if LCCN passes validation, else returns false and displays appropriate error */ function validateLccn(data, dataConfig, label) { - data.value = parseLccn(data.value); - - if (isValidLccn(data.value) === false) { - $('#id-value').val(data.value); - return error( - '#id-errors', - '#id-value', - dataConfig['Invalid ID format'].replace(/ID/, label), - ); - } - return true; + data.value = parseLccn(data.value); + + if (isValidLccn(data.value) === false) { + $('#id-value').val(data.value); + return error( + '#id-errors', + '#id-value', + dataConfig['Invalid ID format'].replace(/ID/, label), + ); + } + return true; } /** @@ -232,281 +232,281 @@ function validateLccn(data, dataConfig, label) { * @returns {boolean} true if identifier passes validation */ export function validateIdentifiers(data) { - const dataConfig = JSON.parse( - document.querySelector('#identifiers').dataset.config, - ); - - if (data.name === '' || data.name === '---') { - $('#id-value').val(data.value); - return error( - '#id-errors', - '#select-id', - dataConfig['Please select an identifier.'], - ); - } - const label = $('#select-id').find(`option[value='${data.name}']`).html(); - if (data.value === '') { - return error( - '#id-errors', - '#id-value', - dataConfig['You need to give a value to ID.'].replace(/ID/, label), - ); - } - if (['ocaid'].includes(data.name) && /\s/g.test(data.value)) { - return error( - '#id-errors', - '#id-value', - dataConfig['ID ids cannot contain whitespace.'].replace(/ID/, label), + const dataConfig = JSON.parse( + document.querySelector('#identifiers').dataset.config, ); - } - - let validId = true; - if (data.name === 'isbn_10') { - validId = validateIsbn10(data, dataConfig, label); - } else if (data.name === 'isbn_13') { - validId = validateIsbn13(data, dataConfig, label); - } else if (data.name === 'lccn') { - validId = validateLccn(data, dataConfig, label); - } - - // checking for duplicate identifier entry on all identifier types - // expects parsed ids so placed after validate - const entries = document.querySelectorAll(`.${data.name}`); - if (isIdDupe(entries, data.value) === true) { + + if (data.name === '' || data.name === '---') { + $('#id-value').val(data.value); + return error( + '#id-errors', + '#select-id', + dataConfig['Please select an identifier.'], + ); + } + const label = $('#select-id').find(`option[value='${data.name}']`).html(); + if (data.value === '') { + return error( + '#id-errors', + '#id-value', + dataConfig['You need to give a value to ID.'].replace(/ID/, label), + ); + } + if (['ocaid'].includes(data.name) && /\s/g.test(data.value)) { + return error( + '#id-errors', + '#id-value', + dataConfig['ID ids cannot contain whitespace.'].replace(/ID/, label), + ); + } + + let validId = true; + if (data.name === 'isbn_10') { + validId = validateIsbn10(data, dataConfig, label); + } else if (data.name === 'isbn_13') { + validId = validateIsbn13(data, dataConfig, label); + } else if (data.name === 'lccn') { + validId = validateLccn(data, dataConfig, label); + } + + // checking for duplicate identifier entry on all identifier types + // expects parsed ids so placed after validate + const entries = document.querySelectorAll(`.${data.name}`); + if (isIdDupe(entries, data.value) === true) { // isbnOverride being set will override the dupe checker, so clear isbnOverride if there's a dupe. - if (isbnOverride.get()) { - isbnOverride.clear(); + if (isbnOverride.get()) { + isbnOverride.clear(); + } + return error( + '#id-errors', + '#id-value', + dataConfig['That ID already exists for this edition.'].replace( + /ID/, + label, + ), + ); } - return error( - '#id-errors', - '#id-value', - dataConfig['That ID already exists for this edition.'].replace( - /ID/, - label, - ), - ); - } - if (validId === false) return false; - $('#id-errors').hide(); - return true; + if (validId === false) return false; + $('#id-errors').hide(); + return true; } export function initClassificationValidation() { - initJqueryRepeat(); - const dataConfig = JSON.parse( - document.querySelector('#classifications').dataset.config, - ); - - // Prevent form submission on Enter for classification fields - $('#classification-value').on('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - $('#classifications .repeat-add').trigger('click'); - return false; - } - }); + initJqueryRepeat(); + const dataConfig = JSON.parse( + document.querySelector('#classifications').dataset.config, + ); - $('#classifications').repeat({ - vars: { prefix: 'edition--' }, - validate: (data) => { - if (data.name === '' || data.name === '---') { - return error( - '#classification-errors', - '#select-classification', - dataConfig['Please select a classification.'], - ); - } - if (data.value === '') { - const label = $('#select-classification') - .find(`option[value='${data.name}']`) - .html(); - return error( - '#classification-errors', - '#classification-value', - dataConfig['You need to give a value to CLASS.'].replace( - /CLASS/, - label, - ), - ); - } - $('#classification-errors').hide(); - $('#select-classification, #classification-value').val(''); - return true; - }, - }); + // Prevent form submission on Enter for classification fields + $('#classification-value').on('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + $('#classifications .repeat-add').trigger('click'); + return false; + } + }); + + $('#classifications').repeat({ + vars: { prefix: 'edition--' }, + validate: (data) => { + if (data.name === '' || data.name === '---') { + return error( + '#classification-errors', + '#select-classification', + dataConfig['Please select a classification.'], + ); + } + if (data.value === '') { + const label = $('#select-classification') + .find(`option[value='${data.name}']`) + .html(); + return error( + '#classification-errors', + '#classification-value', + dataConfig['You need to give a value to CLASS.'].replace( + /CLASS/, + label, + ), + ); + } + $('#classification-errors').hide(); + $('#select-classification, #classification-value').val(''); + return true; + }, + }); } export function initLanguageMultiInputAutocomplete() { - initAutocomplete(); - $(() => { - getJqueryElements('.multi-input-autocomplete--language').forEach( - (jqueryElement) => { - jqueryElement.setup_multi_input_autocomplete( - render_language_field, - { - endpoint: '/languages/_autocomplete', - sortable: true, - }, - { - max: 6, - formatItem: render_language_autocomplete_item, - }, + initAutocomplete(); + $(() => { + getJqueryElements('.multi-input-autocomplete--language').forEach( + (jqueryElement) => { + jqueryElement.setup_multi_input_autocomplete( + render_language_field, + { + endpoint: '/languages/_autocomplete', + sortable: true, + }, + { + max: 6, + formatItem: render_language_autocomplete_item, + }, + ); + }, ); - }, - ); - }); + }); } export function initWorksMultiInputAutocomplete() { - initAutocomplete(); - $(() => { - getJqueryElements('.multi-input-autocomplete--works').forEach( - (jqueryElement) => { - /* Values in the html passed from Python code */ - const dataConfig = JSON.parse(jqueryElement[0].dataset.config || '{}'); - jqueryElement.setup_multi_input_autocomplete( - render_work_field, - { - endpoint: '/works/_autocomplete', - addnew: dataConfig['addnew'] || false, - new_name: dataConfig['new_name'] || '', - allow_empty: dataConfig['allow_empty'] || false, - }, - { - minChars: 2, - max: 11, - matchSubset: false, - autoFill: true, - formatItem: render_work_autocomplete_item, - }, + initAutocomplete(); + $(() => { + getJqueryElements('.multi-input-autocomplete--works').forEach( + (jqueryElement) => { + /* Values in the html passed from Python code */ + const dataConfig = JSON.parse(jqueryElement[0].dataset.config || '{}'); + jqueryElement.setup_multi_input_autocomplete( + render_work_field, + { + endpoint: '/works/_autocomplete', + addnew: dataConfig['addnew'] || false, + new_name: dataConfig['new_name'] || '', + allow_empty: dataConfig['allow_empty'] || false, + }, + { + minChars: 2, + max: 11, + matchSubset: false, + autoFill: true, + formatItem: render_work_autocomplete_item, + }, + ); + }, ); - }, - ); - }); + }); - // Show the new work options checkboxes only if "New work" selected - $('input[name="works--0"]').on('autocompleteselect', (_event, ui) => { - $('.new-work-options').toggle(ui.item.key === '__new__'); - }); + // Show the new work options checkboxes only if "New work" selected + $('input[name="works--0"]').on('autocompleteselect', (_event, ui) => { + $('.new-work-options').toggle(ui.item.key === '__new__'); + }); } export function initSeedsMultiInputAutocomplete() { - initAutocomplete(); - $(() => { - getJqueryElements('.multi-input-autocomplete--seeds').forEach( - (jqueryElement) => { - /* Values in the html passed from Python code */ - jqueryElement.setup_multi_input_autocomplete( - render_seed_field, - { - endpoint: '/works/_autocomplete', - addnew: false, - allow_empty: true, - sortable: true, - }, - { - minChars: 2, - max: 11, - matchSubset: false, - autoFill: true, - formatItem: render_lazy_work_preview, - }, + initAutocomplete(); + $(() => { + getJqueryElements('.multi-input-autocomplete--seeds').forEach( + (jqueryElement) => { + /* Values in the html passed from Python code */ + jqueryElement.setup_multi_input_autocomplete( + render_seed_field, + { + endpoint: '/works/_autocomplete', + addnew: false, + allow_empty: true, + sortable: true, + }, + { + minChars: 2, + max: 11, + matchSubset: false, + autoFill: true, + formatItem: render_lazy_work_preview, + }, + ); + }, ); - }, - ); - }); + }); } export function initAuthorMultiInputAutocomplete() { - initAutocomplete(); - getJqueryElements('.multi-input-autocomplete--author').forEach( - (jqueryElement) => { - /* Values in the html passed from Python code */ - const dataConfig = JSON.parse(jqueryElement[0].dataset.config); - jqueryElement.setup_multi_input_autocomplete( - render_author.bind( - null, - dataConfig.name_path, - dataConfig.dict_path, - false, - ), - { - endpoint: '/authors/_autocomplete', - // Don't render "Create new author" if searching by key - addnew: (query) => !/OL\d+A/i.test(query), - sortable: true, - }, - { - minChars: 2, - max: 11, - matchSubset: false, - autoFill: true, - formatItem: render_author_autocomplete_item, + initAutocomplete(); + getJqueryElements('.multi-input-autocomplete--author').forEach( + (jqueryElement) => { + /* Values in the html passed from Python code */ + const dataConfig = JSON.parse(jqueryElement[0].dataset.config); + jqueryElement.setup_multi_input_autocomplete( + render_author.bind( + null, + dataConfig.name_path, + dataConfig.dict_path, + false, + ), + { + endpoint: '/authors/_autocomplete', + // Don't render "Create new author" if searching by key + addnew: (query) => !/OL\d+A/i.test(query), + sortable: true, + }, + { + minChars: 2, + max: 11, + matchSubset: false, + autoFill: true, + formatItem: render_author_autocomplete_item, + }, + ); }, - ); - }, - ); + ); } export function initSeriesMultiInputAutocomplete() { - initAutocomplete(); - getJqueryElements('.multi-input-autocomplete--series').forEach( - (jqueryElement) => { - /* Values in the html passed from Python code */ - const dataConfig = JSON.parse(jqueryElement[0].dataset.config); - jqueryElement.setup_multi_input_autocomplete( - render_series.bind( - null, - dataConfig.name_path, - dataConfig.dict_path, - false, - ), - { - endpoint: '/series/_autocomplete', - // Don't render "Create new series" if searching by key - addnew: (query) => !/OL\d+L/i.test(query), - sortable: true, + initAutocomplete(); + getJqueryElements('.multi-input-autocomplete--series').forEach( + (jqueryElement) => { + /* Values in the html passed from Python code */ + const dataConfig = JSON.parse(jqueryElement[0].dataset.config); + jqueryElement.setup_multi_input_autocomplete( + render_series.bind( + null, + dataConfig.name_path, + dataConfig.dict_path, + false, + ), + { + endpoint: '/series/_autocomplete', + // Don't render "Create new series" if searching by key + addnew: (query) => !/OL\d+L/i.test(query), + sortable: true, + }, + { + minChars: 2, + max: 11, + matchSubset: false, + autoFill: true, + formatItem: render_series_autocomplete_item, + }, + ); }, - { - minChars: 2, - max: 11, - matchSubset: false, - autoFill: true, - formatItem: render_series_autocomplete_item, - }, - ); - }, - ); + ); } export function initSubjectsAutocomplete() { - initAutocomplete(); - getJqueryElements('.csv-autocomplete--subjects').forEach((jqueryElement) => { - const dataConfig = JSON.parse(jqueryElement[0].dataset.config); - jqueryElement.setup_csv_autocomplete( - 'textarea', - { - endpoint: `/subjects_autocomplete?type=${dataConfig.facet}`, - addnew: false, - }, - { - formatItem: render_subject_autocomplete_item, - }, - ); - }); + initAutocomplete(); + getJqueryElements('.csv-autocomplete--subjects').forEach((jqueryElement) => { + const dataConfig = JSON.parse(jqueryElement[0].dataset.config); + jqueryElement.setup_csv_autocomplete( + 'textarea', + { + endpoint: `/subjects_autocomplete?type=${dataConfig.facet}`, + addnew: false, + }, + { + formatItem: render_subject_autocomplete_item, + }, + ); + }); - /* Resize textarea to fit on input */ - $('.csv-autocomplete--subjects textarea').on('input', function () { - this.style.height = 'auto'; - this.style.height = `${this.scrollHeight + 5}px`; - }); + /* Resize textarea to fit on input */ + $('.csv-autocomplete--subjects textarea').on('input', function () { + this.style.height = 'auto'; + this.style.height = `${this.scrollHeight + 5}px`; + }); } export function initEditRow() { - document - .querySelector('#add_row_button') - .addEventListener('click', () => add_row('website')); + document + .querySelector('#add_row_button') + .addEventListener('click', () => add_row('website')); } /** @@ -514,67 +514,67 @@ export function initEditRow() { * @param string name - when prefixed with clone_ should match an element identifier in the page. e.g. if name would refer to clone_website */ function add_row(name) { - const inputBoxes = document.querySelectorAll(`#clone_${name} input`); - const inputBox = document.createElement('input'); - inputBox.name = `${name}#${inputBoxes.length}`; - inputBox.type = 'text'; - inputBoxes[inputBoxes.length - 1].after(inputBox); + const inputBoxes = document.querySelectorAll(`#clone_${name} input`); + const inputBox = document.createElement('input'); + inputBox.name = `${name}#${inputBoxes.length}`; + inputBox.type = 'text'; + inputBoxes[inputBoxes.length - 1].after(inputBox); } function show_hide_title() { - if ($('#excerpts-display .repeat-item').length > 1) { - $('#excerpts-so-far').show(); - } else { - $('#excerpts-so-far').hide(); - } + if ($('#excerpts-display .repeat-item').length > 1) { + $('#excerpts-so-far').show(); + } else { + $('#excerpts-so-far').hide(); + } } export function initEditExcerpts() { - initJqueryRepeat(); - $('#excerpts').repeat({ - vars: { - prefix: 'work--excerpts', - }, - validate: (data) => { - const i18nStrings = JSON.parse( - document.querySelector('#excerpts-errors').dataset.i18n, - ); - - if (!data.excerpt) { - return error( - '#excerpts-errors', - '#excerpts-excerpt', - i18nStrings['empty_excerpt'], - ); - } - if (data.excerpt.length > 2000) { - return error( - '#excerpts-errors', - '#excerpts-excerpt', - i18nStrings['over_wordcount'], - ); - } - $('#excerpts-errors').hide(); - $('#excerpts-excerpt').val(''); - return true; - }, - }); - - // update length on every keystroke - $('#excerpts-excerpt').on('keyup', () => { - limitChars('excerpts-excerpt', 2000); - update_len(); - }); + initJqueryRepeat(); + $('#excerpts').repeat({ + vars: { + prefix: 'work--excerpts', + }, + validate: (data) => { + const i18nStrings = JSON.parse( + document.querySelector('#excerpts-errors').dataset.i18n, + ); + + if (!data.excerpt) { + return error( + '#excerpts-errors', + '#excerpts-excerpt', + i18nStrings['empty_excerpt'], + ); + } + if (data.excerpt.length > 2000) { + return error( + '#excerpts-errors', + '#excerpts-excerpt', + i18nStrings['over_wordcount'], + ); + } + $('#excerpts-errors').hide(); + $('#excerpts-excerpt').val(''); + return true; + }, + }); - // update length on add. - $('#excerpts') - .on('repeat-add', update_len) - .on('repeat-add', show_hide_title) - .on('repeat-remove', show_hide_title); + // update length on every keystroke + $('#excerpts-excerpt').on('keyup', () => { + limitChars('excerpts-excerpt', 2000); + update_len(); + }); - // update length on load - update_len(); - show_hide_title(); + // update length on add. + $('#excerpts') + .on('repeat-add', update_len) + .on('repeat-add', show_hide_title) + .on('repeat-remove', show_hide_title); + + // update length on load + update_len(); + show_hide_title(); } /** @@ -587,40 +587,40 @@ export function initEditExcerpts() { * - '#link-errors' */ export function initEditLinks() { - initJqueryRepeat(); - $('#links').repeat({ - vars: { - prefix: $('#links').data('prefix'), - }, - validate: (data) => { - const i18nStrings = JSON.parse( - document.querySelector('#link-errors').dataset.i18n, - ); - const url = data.url.trim(); - - if (data.title.trim() === '') { - $('#link-errors').html(i18nStrings['empty_label']); - $('#link-errors').removeClass('hidden'); - $('#link-label').trigger('focus'); - return false; - } - if (url === '') { - $('#link-errors').html(i18nStrings['empty_url']); - $('#link-errors').removeClass('hidden'); - $('#link-url').trigger('focus'); - return false; - } - if (!isValidURL(url)) { - $('#link-errors').html(i18nStrings['invalid_url']); - $('#link-errors').removeClass('hidden'); - $('#link-url').trigger('focus'); - return false; - } - $('#link-errors').addClass('hidden'); - $('#link-label, #link-url').val(''); - return true; - }, - }); + initJqueryRepeat(); + $('#links').repeat({ + vars: { + prefix: $('#links').data('prefix'), + }, + validate: (data) => { + const i18nStrings = JSON.parse( + document.querySelector('#link-errors').dataset.i18n, + ); + const url = data.url.trim(); + + if (data.title.trim() === '') { + $('#link-errors').html(i18nStrings['empty_label']); + $('#link-errors').removeClass('hidden'); + $('#link-label').trigger('focus'); + return false; + } + if (url === '') { + $('#link-errors').html(i18nStrings['empty_url']); + $('#link-errors').removeClass('hidden'); + $('#link-url').trigger('focus'); + return false; + } + if (!isValidURL(url)) { + $('#link-errors').html(i18nStrings['invalid_url']); + $('#link-errors').removeClass('hidden'); + $('#link-url').trigger('focus'); + return false; + } + $('#link-errors').addClass('hidden'); + $('#link-label, #link-url').val(''); + return true; + }, + }); } /** @@ -632,24 +632,24 @@ export function initEditLinks() { * - '#contentHead' */ export function initEdit() { - var hash = document.location.hash || '#edition'; - var tab = hash.split('/')[0]; - var link = `#link_${tab.substring(1)}`; - var fieldname = `:input${hash.replace('/', '-')}`; - - trimInputValues('.olform input'); - - $(link).trigger('click'); - - // input field is enabled only after the tab is selected and that takes some time after clicking the link. - // wait for 1 sec after clicking the link and focus the input field - if ($(fieldname).length !== 0) { - setTimeout(() => { - // scroll such that top of the content is visible - $(fieldname).trigger('focus'); - $(window).scrollTop($('#contentHead').offset().top); - }, 1000); - } + var hash = document.location.hash || '#edition'; + var tab = hash.split('/')[0]; + var link = `#link_${tab.substring(1)}`; + var fieldname = `:input${hash.replace('/', '-')}`; + + trimInputValues('.olform input'); + + $(link).trigger('click'); + + // input field is enabled only after the tab is selected and that takes some time after clicking the link. + // wait for 1 sec after clicking the link and focus the input field + if ($(fieldname).length !== 0) { + setTimeout(() => { + // scroll such that top of the content is visible + $(fieldname).trigger('focus'); + $(window).scrollTop($('#contentHead').offset().top); + }, 1000); + } } /** @@ -657,10 +657,10 @@ export function initEdit() { * @param string url */ function isValidURL(url) { - try { - new URL(url); - return true; - } catch (e) { - return false; - } + try { + new URL(url); + return true; + } catch (e) { + return false; + } } diff --git a/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js b/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js index df0f1e4530b..1342027e6d2 100644 --- a/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js +++ b/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js @@ -12,10 +12,10 @@ const navbars = []; * @param {HTMLCollection<HTMLElement>} navbarWrappers Each navbar found on the book page */ export function initNavbars(navbarWrappers) { - for (const wrapper of navbarWrappers) { - const navbar = new EdtionNavBar(wrapper); - navbars.push(navbar); - } + for (const wrapper of navbarWrappers) { + const navbar = new EdtionNavBar(wrapper); + navbars.push(navbar); + } } /** @@ -27,7 +27,7 @@ export function initNavbars(navbarWrappers) { * stickied to a new position). */ export function updateSelectedNavItem() { - for (const navbar of navbars) { - navbar.updateSelected(); - } + for (const navbar of navbars) { + navbar.updateSelected(); + } } diff --git a/openlibrary/plugins/openlibrary/js/editions-table/index.js b/openlibrary/plugins/openlibrary/js/editions-table/index.js index 13cfd006b28..422d7d524e7 100755 --- a/openlibrary/plugins/openlibrary/js/editions-table/index.js +++ b/openlibrary/plugins/openlibrary/js/editions-table/index.js @@ -5,111 +5,111 @@ const DEFAULT_LENGTH = 3; const LS_RESULTS_LENGTH_KEY = 'editions-table.resultsLength'; export function initEditionsTable() { - var rowCount; - let currentLength; - // Prevent reinitialization of the editions datatable - if ($.fn.DataTable.isDataTable($('#editions'))) { - return; - } - $('#editions th.title').on('mouseover', function () { - if ($(this).hasClass('sorting_asc')) { - $(this).attr('title', 'Sort latest to earliest'); - } else if ($(this).hasClass('sorting_desc')) { - $(this).attr('title', 'Sort earliest to latest'); - } else { - $(this).attr('title', 'Sort by publish date'); - } - }); - $('#editions th.read').on('mouseover', function () { - if ($(this).hasClass('sorting_asc')) { - $(this).attr('title', 'Push readable versions to the bottom'); - } else if ($(this).hasClass('sorting_desc')) { - $(this).attr('title', 'Sort by editions to read'); - } else { - $(this).attr('title', 'Available to read'); + var rowCount; + let currentLength; + // Prevent reinitialization of the editions datatable + if ($.fn.DataTable.isDataTable($('#editions'))) { + return; } - }); + $('#editions th.title').on('mouseover', function () { + if ($(this).hasClass('sorting_asc')) { + $(this).attr('title', 'Sort latest to earliest'); + } else if ($(this).hasClass('sorting_desc')) { + $(this).attr('title', 'Sort earliest to latest'); + } else { + $(this).attr('title', 'Sort by publish date'); + } + }); + $('#editions th.read').on('mouseover', function () { + if ($(this).hasClass('sorting_asc')) { + $(this).attr('title', 'Push readable versions to the bottom'); + } else if ($(this).hasClass('sorting_desc')) { + $(this).attr('title', 'Sort by editions to read'); + } else { + $(this).attr('title', 'Available to read'); + } + }); - function toggleSorting(e) { - $('#editions th span').html(''); - $(e).find('span').html(' ↑'); - if ($(e).hasClass('sorting_asc')) { - $(e).find('span').html(' ↓'); - } else if ($(e).hasClass('sorting_desc')) { - $(e).find('span').html(' ↑'); + function toggleSorting(e) { + $('#editions th span').html(''); + $(e).find('span').html(' ↑'); + if ($(e).hasClass('sorting_asc')) { + $(e).find('span').html(' ↓'); + } else if ($(e).hasClass('sorting_desc')) { + $(e).find('span').html(' ↑'); + } } - } - $('#editions th.read span').html(' ↑'); - $('#editions th').on('mouseup', function () { - toggleSorting(this); - }); - - $('#editions').on('length.dt', (e, settings, length) => { - localStorage.setItem(LS_RESULTS_LENGTH_KEY, length); - }); - - $('#editions th').on('keydown', function (e) { - if (e.key === 'Enter') { - toggleSorting(this); - } - }); + $('#editions th.read span').html(' ↑'); + $('#editions th').on('mouseup', function () { + toggleSorting(this); + }); - rowCount = $('#editions tbody tr').length; - if (rowCount < 4) { - $('#editions').DataTable({ - aoColumns: [{ sType: 'html' }, null], - order: [[1, 'asc']], - bPaginate: false, - bInfo: false, - bFilter: false, - bStateSave: false, - bAutoWidth: false, + $('#editions').on('length.dt', (e, settings, length) => { + localStorage.setItem(LS_RESULTS_LENGTH_KEY, length); }); - } else { - currentLength = Number(localStorage.getItem(LS_RESULTS_LENGTH_KEY)); - $('#editions').DataTable({ - aoColumns: [{ sType: 'html' }, null], - order: [[1, 'asc']], - lengthMenu: [ - [3, 10, 25, 50, 100, -1], - [3, 10, 25, 50, 100, 'All'], - ], - bPaginate: true, - bInfo: true, - sPaginationType: 'full_numbers', - bFilter: true, - bStateSave: false, - bAutoWidth: false, - pageLength: currentLength ? currentLength : DEFAULT_LENGTH, - drawCallback: () => { - if ($('#ile-toolbar')) { - const editionStorage = JSON.parse( - sessionStorage.getItem('ile-items'), - )['edition']; - const matchEdition = (string) => { - return string.match(/OL[0-9]+[a-zA-Z]/); - }; - for (const el of $('.ile-selected')) { - const anchor = el.getElementsByTagName('a'); - if (anchor.length) { - const edIdentifier = matchEdition(anchor[0].getAttribute('href')); - if (!editionStorage.includes(edIdentifier[0])) { - el.classList.remove('ile-selected'); - } - } - } - for (const el of $('.ile-selectable')) { - const anchor = el.getElementsByTagName('a'); - if (anchor.length) { - const edIdentifier = matchEdition(anchor[0].getAttribute('href')); - if (editionStorage.includes(edIdentifier[0])) { - el.classList.add('ile-selected'); - } - } - } + + $('#editions th').on('keydown', function (e) { + if (e.key === 'Enter') { + toggleSorting(this); } - }, }); - } + + rowCount = $('#editions tbody tr').length; + if (rowCount < 4) { + $('#editions').DataTable({ + aoColumns: [{ sType: 'html' }, null], + order: [[1, 'asc']], + bPaginate: false, + bInfo: false, + bFilter: false, + bStateSave: false, + bAutoWidth: false, + }); + } else { + currentLength = Number(localStorage.getItem(LS_RESULTS_LENGTH_KEY)); + $('#editions').DataTable({ + aoColumns: [{ sType: 'html' }, null], + order: [[1, 'asc']], + lengthMenu: [ + [3, 10, 25, 50, 100, -1], + [3, 10, 25, 50, 100, 'All'], + ], + bPaginate: true, + bInfo: true, + sPaginationType: 'full_numbers', + bFilter: true, + bStateSave: false, + bAutoWidth: false, + pageLength: currentLength ? currentLength : DEFAULT_LENGTH, + drawCallback: () => { + if ($('#ile-toolbar')) { + const editionStorage = JSON.parse( + sessionStorage.getItem('ile-items'), + )['edition']; + const matchEdition = (string) => { + return string.match(/OL[0-9]+[a-zA-Z]/); + }; + for (const el of $('.ile-selected')) { + const anchor = el.getElementsByTagName('a'); + if (anchor.length) { + const edIdentifier = matchEdition(anchor[0].getAttribute('href')); + if (!editionStorage.includes(edIdentifier[0])) { + el.classList.remove('ile-selected'); + } + } + } + for (const el of $('.ile-selectable')) { + const anchor = el.getElementsByTagName('a'); + if (anchor.length) { + const edIdentifier = matchEdition(anchor[0].getAttribute('href')); + if (editionStorage.includes(edIdentifier[0])) { + el.classList.add('ile-selected'); + } + } + } + } + }, + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/following.js b/openlibrary/plugins/openlibrary/js/following.js index 87d75163a74..2cfe1b24aaf 100644 --- a/openlibrary/plugins/openlibrary/js/following.js +++ b/openlibrary/plugins/openlibrary/js/following.js @@ -1,42 +1,42 @@ import { PersistentToast } from './Toast'; export async function initAsyncFollowing(followForms) { - followForms.forEach((form) => { - form.addEventListener('submit', async (e) => { - e.preventDefault(); - const url = form.action; - const formData = new FormData(form); - const submitButton = form.querySelector('button[type=submit]'); - const stateInput = form.querySelector('input[name=state]'); + followForms.forEach((form) => { + form.addEventListener('submit', async (e) => { + e.preventDefault(); + const url = form.action; + const formData = new FormData(form); + const submitButton = form.querySelector('button[type=submit]'); + const stateInput = form.querySelector('input[name=state]'); - const isFollowRequest = stateInput.value === '0'; - const i18nStrings = JSON.parse(submitButton.dataset.i18n); - submitButton.disabled = true; + const isFollowRequest = stateInput.value === '0'; + const i18nStrings = JSON.parse(submitButton.dataset.i18n); + submitButton.disabled = true; - await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams(formData), - }) - .then((resp) => { - if (!resp.ok) { - throw new Error('Network response was not ok'); - } - submitButton.classList.toggle('cta-btn--primary'); - submitButton.classList.toggle('cta-btn--delete'); - submitButton.textContent = isFollowRequest - ? i18nStrings.unfollow - : i18nStrings.follow; - stateInput.value = isFollowRequest ? '1' : '0'; - }) - .catch(() => { - new PersistentToast(i18nStrings.errorMsg).show(); - }) - .finally(() => { - submitButton.disabled = false; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams(formData), + }) + .then((resp) => { + if (!resp.ok) { + throw new Error('Network response was not ok'); + } + submitButton.classList.toggle('cta-btn--primary'); + submitButton.classList.toggle('cta-btn--delete'); + submitButton.textContent = isFollowRequest + ? i18nStrings.unfollow + : i18nStrings.follow; + stateInput.value = isFollowRequest ? '1' : '0'; + }) + .catch(() => { + new PersistentToast(i18nStrings.errorMsg).show(); + }) + .finally(() => { + submitButton.disabled = false; + }); }); }); - }); } diff --git a/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js b/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js index 2342abafb01..e3315ea3a45 100644 --- a/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js +++ b/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js @@ -1,66 +1,66 @@ import { buildPartialsUrl } from './utils'; export function initFulltextSearchSuggestion(fulltextSearchSuggestion) { - const isLoading = showLoadingIndicators(fulltextSearchSuggestion); - if (isLoading) { - const query = fulltextSearchSuggestion.dataset.query; - getPartials(fulltextSearchSuggestion, query); - } + const isLoading = showLoadingIndicators(fulltextSearchSuggestion); + if (isLoading) { + const query = fulltextSearchSuggestion.dataset.query; + getPartials(fulltextSearchSuggestion, query); + } } function showLoadingIndicators(fulltextSearchSuggestion) { - let isLoading = false; - const loadingIndicator = + let isLoading = false; + const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator'); - if (loadingIndicator) { - isLoading = true; - loadingIndicator.classList.remove('hidden'); - } - return isLoading; + if (loadingIndicator) { + isLoading = true; + loadingIndicator.classList.remove('hidden'); + } + return isLoading; } async function getPartials(fulltextSearchSuggestion, query) { - return fetch(buildPartialsUrl('FulltextSearchSuggestion', { data: query })) - .then((resp) => { - if (resp.status !== 200) { - throw new Error( - `Failed to fetch partials. Status code: ${resp.status}`, - ); - } - return resp.json(); - }) - .then((data) => { - fulltextSearchSuggestion.innerHTML += data['partials']; - const loadingIndicator = + return fetch(buildPartialsUrl('FulltextSearchSuggestion', { data: query })) + .then((resp) => { + if (resp.status !== 200) { + throw new Error( + `Failed to fetch partials. Status code: ${resp.status}`, + ); + } + return resp.json(); + }) + .then((data) => { + fulltextSearchSuggestion.innerHTML += data['partials']; + const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator'); - if (loadingIndicator) { - loadingIndicator.classList.add('hidden'); - } - }) - .catch(() => { - const loadingIndicator = + if (loadingIndicator) { + loadingIndicator.classList.add('hidden'); + } + }) + .catch(() => { + const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator'); - if (loadingIndicator) { - loadingIndicator.classList.add('hidden'); - } - const existingRetryAffordance = fulltextSearchSuggestion.querySelector( - '.fulltext-suggestions__retry', - ); - if (existingRetryAffordance) { - existingRetryAffordance.classList.remove('hidden'); - } else { - fulltextSearchSuggestion.insertAdjacentHTML( - 'afterbegin', - renderRetryLink(), - ); - const retryAffordance = fulltextSearchSuggestion.querySelector( - '.fulltext-suggestions__retry', - ); - retryAffordance.addEventListener('click', () => { - retryAffordance.classList.add('hidden'); - getPartials(fulltextSearchSuggestion, query); + if (loadingIndicator) { + loadingIndicator.classList.add('hidden'); + } + const existingRetryAffordance = fulltextSearchSuggestion.querySelector( + '.fulltext-suggestions__retry', + ); + if (existingRetryAffordance) { + existingRetryAffordance.classList.remove('hidden'); + } else { + fulltextSearchSuggestion.insertAdjacentHTML( + 'afterbegin', + renderRetryLink(), + ); + const retryAffordance = fulltextSearchSuggestion.querySelector( + '.fulltext-suggestions__retry', + ); + retryAffordance.addEventListener('click', () => { + retryAffordance.classList.add('hidden'); + getPartials(fulltextSearchSuggestion, query); + }); + } }); - } - }); } /** @@ -69,5 +69,5 @@ async function getPartials(fulltextSearchSuggestion, query) { * @returns {string} HTML for a retry link. */ function renderRetryLink() { - return '<span class="fulltext-suggestions__retry">Failed to fetch fulltext search suggestions. <button type="button" style="border:none;background:none;color:blue;text-decoration:underline;cursor:pointer;">Retry?</button></span>'; + return '<span class="fulltext-suggestions__retry">Failed to fetch fulltext search suggestions. <button type="button" style="border:none;background:none;color:blue;text-decoration:underline;cursor:pointer;">Retry?</button></span>'; } diff --git a/openlibrary/plugins/openlibrary/js/go-back-links.js b/openlibrary/plugins/openlibrary/js/go-back-links.js index 47d74ba23bd..8028ae7fc3a 100644 --- a/openlibrary/plugins/openlibrary/js/go-back-links.js +++ b/openlibrary/plugins/openlibrary/js/go-back-links.js @@ -5,13 +5,13 @@ * @param {NodeList<HTMLElement>} goBackLinks */ export function initGoBackLinks(goBackLinks) { - for (const link of goBackLinks) { - link.addEventListener('click', () => { - if (history.length > 2) { - history.go(-1); - } else { - window.location.href = '/'; - } - }); - } + for (const link of goBackLinks) { + link.addEventListener('click', () => { + if (history.length > 2) { + history.go(-1); + } else { + window.location.href = '/'; + } + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/goodreads_import.js b/openlibrary/plugins/openlibrary/js/goodreads_import.js index a15c63a015d..60b3049e217 100644 --- a/openlibrary/plugins/openlibrary/js/goodreads_import.js +++ b/openlibrary/plugins/openlibrary/js/goodreads_import.js @@ -1,191 +1,191 @@ import Promise from 'promise-polyfill'; export function initGoodreadsImport() { - var count, prevPromise; + var count, prevPromise; - $(document).on('click', 'th.toggle-all input', function () { - var checked = $(this).prop('checked'); - $('input.add-book').each(function () { - $(this).prop('checked', checked); - if (checked) { - $(this).attr('checked', 'checked'); - } else { - $(this).removeAttr('checked'); - } + $(document).on('click', 'th.toggle-all input', function () { + var checked = $(this).prop('checked'); + $('input.add-book').each(function () { + $(this).prop('checked', checked); + if (checked) { + $(this).attr('checked', 'checked'); + } else { + $(this).removeAttr('checked'); + } + }); + const l = $('.add-book[checked*="checked"]').length; + $('.import-submit').attr('value', `Import ${l} Books`); }); - const l = $('.add-book[checked*="checked"]').length; - $('.import-submit').attr('value', `Import ${l} Books`); - }); - $(document).on('click', 'input.add-book', function () { - if ($(this).prop('checked')) { - $(this).attr('checked', 'checked'); - } else { - $(this).removeAttr('checked'); - } - const l = $('.add-book[checked*="checked"]').length; - $('.import-submit').attr('value', `Import ${l} Books`); - }); + $(document).on('click', 'input.add-book', function () { + if ($(this).prop('checked')) { + $(this).attr('checked', 'checked'); + } else { + $(this).removeAttr('checked'); + } + const l = $('.add-book[checked*="checked"]').length; + $('.import-submit').attr('value', `Import ${l} Books`); + }); - //updates the progress bar based on the book count - function func1(value) { - const l = $('.add-book[checked*="checked"]').length; - const elem = document.getElementById('myBar'); - elem.style.width = `${value * (100 / l)}%`; - elem.innerHTML = `${value} Books`; - if (value * (100 / l) >= 100) { - elem.innerHTML = ''; - $('#myBar').append( - '<a href="/account/books" style="color:white"> Go to your Reading Log </a>', - ); - $('.cancel-button').addClass('hidden'); + //updates the progress bar based on the book count + function func1(value) { + const l = $('.add-book[checked*="checked"]').length; + const elem = document.getElementById('myBar'); + elem.style.width = `${value * (100 / l)}%`; + elem.innerHTML = `${value} Books`; + if (value * (100 / l) >= 100) { + elem.innerHTML = ''; + $('#myBar').append( + '<a href="/account/books" style="color:white"> Go to your Reading Log </a>', + ); + $('.cancel-button').addClass('hidden'); + } } - } - $('.import-submit').on('click', () => { - $('#myProgress').removeClass('hidden'); - $('.cancel-button').removeClass('hidden'); - $('input.import-submit').addClass('hidden'); - $('th.import-status').removeClass('hidden'); - $('th.status-reason').removeClass('hidden'); - const shelves = { read: 3, 'currently-reading': 2, 'to-read': 1 }; - count = 0; - prevPromise = Promise.resolve(); - $('input.add-book').each(function () { - var input = $(this), - checked = input.prop('checked'); - var value = JSON.parse(input.val().replace(/'/g, '"')); - var shelf = value['Exclusive Shelf']; - var shelf_id = 0; - const hasFailure = () => - $(`[isbn=${value['ISBN']}]`).hasClass('import-failure'); - const fail = (reason) => { - if (!hasFailure()) { - const element = $(`[isbn=${value['ISBN']}]`); - element.append( - `<td class="error-imported">Error</td><td class="error-imported">${reason}</td>'`, - ); - element.removeClass('selected'); - element.addClass('import-failure'); - } - }; + $('.import-submit').on('click', () => { + $('#myProgress').removeClass('hidden'); + $('.cancel-button').removeClass('hidden'); + $('input.import-submit').addClass('hidden'); + $('th.import-status').removeClass('hidden'); + $('th.status-reason').removeClass('hidden'); + const shelves = { read: 3, 'currently-reading': 2, 'to-read': 1 }; + count = 0; + prevPromise = Promise.resolve(); + $('input.add-book').each(function () { + var input = $(this), + checked = input.prop('checked'); + var value = JSON.parse(input.val().replace(/'/g, '"')); + var shelf = value['Exclusive Shelf']; + var shelf_id = 0; + const hasFailure = () => + $(`[isbn=${value['ISBN']}]`).hasClass('import-failure'); + const fail = (reason) => { + if (!hasFailure()) { + const element = $(`[isbn=${value['ISBN']}]`); + element.append( + `<td class="error-imported">Error</td><td class="error-imported">${reason}</td>'`, + ); + element.removeClass('selected'); + element.addClass('import-failure'); + } + }; - if (!checked) { - func1(++count); - return; - } + if (!checked) { + func1(++count); + return; + } - if (shelves[shelf]) { - shelf_id = shelves[shelf]; - } + if (shelves[shelf]) { + shelf_id = shelves[shelf]; + } - //used 'return' instead of 'return false' because the loop was being exited entirely - if (shelf_id === 0) { - fail('Custom shelves are not supported'); - func1(++count); - return; - } + //used 'return' instead of 'return false' because the loop was being exited entirely + if (shelf_id === 0) { + fail('Custom shelves are not supported'); + func1(++count); + return; + } - prevPromise = prevPromise - .then(() => { - // prevPromise changes in each iteration - $(`[isbn=${value['ISBN']}]`).addClass('selected'); - return getWork(value['ISBN']); // return a new Promise - }) - .then((data) => { - var obj = JSON.parse(data); - $.ajax({ - url: `${obj['works'][0].key}/bookshelves.json`, - type: 'POST', - data: { - dont_remove: true, - edition_id: obj['key'], - bookshelf_id: shelf_id, - }, - dataType: 'json', - }) - .fail(() => { - fail('Failed to add book to reading log'); - }) - .done(() => { - if (value['My Rating'] !== '0') { - return $.ajax({ - url: `${obj['works'][0].key}/ratings.json`, - type: 'POST', - data: { - rating: parseInt(value['My Rating']), - edition_id: obj['key'], - bookshelf_id: shelf_id, - }, - dataType: 'json', - fail: () => { - fail('Failed to add rating'); - }, - }); - } - }) - .then(() => { - if (value['Date Read'] !== '') { - const date_read = value['Date Read'].split('/'); // Format: "YYYY/MM/DD" - return $.ajax({ - url: `${obj['works'][0].key}/check-ins`, - type: 'POST', - data: JSON.stringify({ - edition_key: obj['key'], - event_type: 3, // BookshelfEvent.FINISH - year: parseInt(date_read[0]), - month: parseInt(date_read[1]), - day: parseInt(date_read[2]), - }), - dataType: 'json', - contentType: 'application/json', - beforeSend: (xhr) => { - xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.setRequestHeader('Accept', 'application/json'); - }, - fail: () => { - fail('Failed to set the read date'); - }, + prevPromise = prevPromise + .then(() => { + // prevPromise changes in each iteration + $(`[isbn=${value['ISBN']}]`).addClass('selected'); + return getWork(value['ISBN']); // return a new Promise + }) + .then((data) => { + var obj = JSON.parse(data); + $.ajax({ + url: `${obj['works'][0].key}/bookshelves.json`, + type: 'POST', + data: { + dont_remove: true, + edition_id: obj['key'], + bookshelf_id: shelf_id, + }, + dataType: 'json', + }) + .fail(() => { + fail('Failed to add book to reading log'); + }) + .done(() => { + if (value['My Rating'] !== '0') { + return $.ajax({ + url: `${obj['works'][0].key}/ratings.json`, + type: 'POST', + data: { + rating: parseInt(value['My Rating']), + edition_id: obj['key'], + bookshelf_id: shelf_id, + }, + dataType: 'json', + fail: () => { + fail('Failed to add rating'); + }, + }); + } + }) + .then(() => { + if (value['Date Read'] !== '') { + const date_read = value['Date Read'].split('/'); // Format: "YYYY/MM/DD" + return $.ajax({ + url: `${obj['works'][0].key}/check-ins`, + type: 'POST', + data: JSON.stringify({ + edition_key: obj['key'], + event_type: 3, // BookshelfEvent.FINISH + year: parseInt(date_read[0]), + month: parseInt(date_read[1]), + day: parseInt(date_read[2]), + }), + dataType: 'json', + contentType: 'application/json', + beforeSend: (xhr) => { + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('Accept', 'application/json'); + }, + fail: () => { + fail('Failed to set the read date'); + }, + }); + } + }); + if (!hasFailure()) { + $(`[isbn=${value['ISBN']}]`).append( + '<td class="success-imported">Imported</td>', + ); + $(`[isbn=${value['ISBN']}]`).removeClass('selected'); + } + func1(++count); + }) + .catch(() => { + fail('Book not in collection'); + func1(++count); }); - } - }); - if (!hasFailure()) { - $(`[isbn=${value['ISBN']}]`).append( - '<td class="success-imported">Imported</td>', - ); - $(`[isbn=${value['ISBN']}]`).removeClass('selected'); - } - func1(++count); - }) - .catch(() => { - fail('Book not in collection'); - func1(++count); }); - }); - $('td.books-wo-isbn').each(function () { - $(this).removeClass('hidden'); + $('td.books-wo-isbn').each(function () { + $(this).removeClass('hidden'); + }); }); - }); - function getWork(isbn) { - return new Promise((resolve, reject) => { - var request = new XMLHttpRequest(); + function getWork(isbn) { + return new Promise((resolve, reject) => { + var request = new XMLHttpRequest(); - request.open('GET', `/isbn/${isbn}.json`); - request.onload = () => { - if (request.status === 200) { - resolve(request.response); // we get the data here, so resolve the Promise - } else { - reject(Error(request.statusText)); // if status is not 200 OK, reject. - } - }; + request.open('GET', `/isbn/${isbn}.json`); + request.onload = () => { + if (request.status === 200) { + resolve(request.response); // we get the data here, so resolve the Promise + } else { + reject(Error(request.statusText)); // if status is not 200 OK, reject. + } + }; - request.onerror = () => { - reject(Error('Error fetching data.')); // error occurred, so reject the Promise - }; + request.onerror = () => { + reject(Error('Error fetching data.')); // error occurred, so reject the Promise + }; - request.send(); // send the request - }); - } + request.send(); // send the request + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/graphs/index.js b/openlibrary/plugins/openlibrary/js/graphs/index.js index 67564fb1d8e..2245763067f 100644 --- a/openlibrary/plugins/openlibrary/js/graphs/index.js +++ b/openlibrary/plugins/openlibrary/js/graphs/index.js @@ -2,33 +2,33 @@ import options from './options.js'; import { loadEditionsGraph, loadGraphIfExists } from './plot'; export function plotAdminGraphs() { - loadGraphIfExists('editgraph', {}, 'edit(s) on'); - loadGraphIfExists('membergraph', {}, 'new members(s) on'); - loadGraphIfExists('works_minigraph', {}, ' works on '); - loadGraphIfExists('editions_minigraph', {}, ' editions on '); - loadGraphIfExists('covers_minigraph', {}, ' covers on '); - loadGraphIfExists('authors_minigraph', {}, ' authors on '); - loadGraphIfExists('lists_minigraph', {}, ' lists on '); - loadGraphIfExists('members_minigraph', {}, ' members on '); - loadGraphIfExists('books-added-per-day', options.booksAdded); + loadGraphIfExists('editgraph', {}, 'edit(s) on'); + loadGraphIfExists('membergraph', {}, 'new members(s) on'); + loadGraphIfExists('works_minigraph', {}, ' works on '); + loadGraphIfExists('editions_minigraph', {}, ' editions on '); + loadGraphIfExists('covers_minigraph', {}, ' covers on '); + loadGraphIfExists('authors_minigraph', {}, ' authors on '); + loadGraphIfExists('lists_minigraph', {}, ' lists on '); + loadGraphIfExists('members_minigraph', {}, ' members on '); + loadGraphIfExists('books-added-per-day', options.booksAdded); } export function initHomepageGraphs() { - loadGraphIfExists('visitors-graph', {}, 'unique visitors on', '#e44028'); - loadGraphIfExists('members-graph', {}, 'new members on', '#748d36'); - loadGraphIfExists('edits-graph', {}, 'catalog edits on', '#00636a'); - loadGraphIfExists('lists-graph', {}, 'lists created on', '#ffa337'); - loadGraphIfExists('ebooks-graph', {}, 'ebooks borrowed on', '#35672e'); + loadGraphIfExists('visitors-graph', {}, 'unique visitors on', '#e44028'); + loadGraphIfExists('members-graph', {}, 'new members on', '#748d36'); + loadGraphIfExists('edits-graph', {}, 'catalog edits on', '#00636a'); + loadGraphIfExists('lists-graph', {}, 'lists created on', '#ffa337'); + loadGraphIfExists('ebooks-graph', {}, 'ebooks borrowed on', '#35672e'); } export function initPublishersGraph() { - if (document.getElementById('chartPubHistory')) { - loadEditionsGraph('chartPubHistory', {}, 'editions in'); - } + if (document.getElementById('chartPubHistory')) { + loadEditionsGraph('chartPubHistory', {}, 'editions in'); + } } export function init() { - plotAdminGraphs(); - initHomepageGraphs(); - initPublishersGraph(); + plotAdminGraphs(); + initHomepageGraphs(); + initPublishersGraph(); } diff --git a/openlibrary/plugins/openlibrary/js/graphs/options.js b/openlibrary/plugins/openlibrary/js/graphs/options.js index 62612f501bd..9ce2b858644 100644 --- a/openlibrary/plugins/openlibrary/js/graphs/options.js +++ b/openlibrary/plugins/openlibrary/js/graphs/options.js @@ -1,55 +1,55 @@ const booksAdded = { - series: { - stack: 0, - bars: { - show: true, - align: 'left', - barWidth: 20 * 60 * 60 * 1000, - }, - }, - grid: { - hoverable: true, - show: true, - borderWidth: 1, - borderColor: '#d9d9d9', - }, - xaxis: { - mode: 'time', - }, - legend: { - show: true, - position: 'nw', - }, + series: { + stack: 0, + bars: { + show: true, + align: 'left', + barWidth: 20 * 60 * 60 * 1000, + }, + }, + grid: { + hoverable: true, + show: true, + borderWidth: 1, + borderColor: '#d9d9d9', + }, + xaxis: { + mode: 'time', + }, + legend: { + show: true, + position: 'nw', + }, }; const loans = { - series: { - stack: 0, - bars: { - show: true, - align: 'left', - barWidth: 20 * 60 * 60 * 1000, - }, - }, - grid: { - hoverable: true, - show: true, - borderWidth: 1, - borderColor: '#d9d9d9', - }, - xaxis: { - mode: 'time', - }, - yaxis: { - position: 'right', - }, - legend: { - show: true, - position: 'nw', - }, + series: { + stack: 0, + bars: { + show: true, + align: 'left', + barWidth: 20 * 60 * 60 * 1000, + }, + }, + grid: { + hoverable: true, + show: true, + borderWidth: 1, + borderColor: '#d9d9d9', + }, + xaxis: { + mode: 'time', + }, + yaxis: { + position: 'right', + }, + legend: { + show: true, + position: 'nw', + }, }; export default { - booksAdded, - loans, + booksAdded, + loans, }; diff --git a/openlibrary/plugins/openlibrary/js/graphs/plot.js b/openlibrary/plugins/openlibrary/js/graphs/plot.js index a51633a7b63..b9e302f7869 100644 --- a/openlibrary/plugins/openlibrary/js/graphs/plot.js +++ b/openlibrary/plugins/openlibrary/js/graphs/plot.js @@ -16,248 +16,248 @@ import 'flot/jquery.flot.time.js'; * - http://localhost:8080/publishers/Barnes_&_Noble */ export function loadEditionsGraph() { - var data, options, placeholder, plot, dateFrom, dateTo, previousPoint; - data = [ - { - data: JSON.parse( - document.getElementById('graph-json-chartPubHistory').textContent, - ), - }, - ]; - options = { - series: { - bars: { - show: true, - fill: 0.6, - color: '#615132', - align: 'center', - }, - points: { - show: true, - }, - color: '#615132', - }, - grid: { - hoverable: true, - clickable: true, - autoHighlight: true, - tickColor: '#d9d9d9', - borderWidth: 1, - borderColor: '#d9d9d9', - backgroundColor: '#fff', - }, - xaxis: { tickDecimals: 0 }, - yaxis: { tickDecimals: 0 }, - selection: { mode: 'xy', color: '#00636a' }, - crosshair: { - mode: 'xy', - color: 'rgba(000, 099, 106, 0.4)', - lineWidth: 1, - }, - }; + var data, options, placeholder, plot, dateFrom, dateTo, previousPoint; + data = [ + { + data: JSON.parse( + document.getElementById('graph-json-chartPubHistory').textContent, + ), + }, + ]; + options = { + series: { + bars: { + show: true, + fill: 0.6, + color: '#615132', + align: 'center', + }, + points: { + show: true, + }, + color: '#615132', + }, + grid: { + hoverable: true, + clickable: true, + autoHighlight: true, + tickColor: '#d9d9d9', + borderWidth: 1, + borderColor: '#d9d9d9', + backgroundColor: '#fff', + }, + xaxis: { tickDecimals: 0 }, + yaxis: { tickDecimals: 0 }, + selection: { mode: 'xy', color: '#00636a' }, + crosshair: { + mode: 'xy', + color: 'rgba(000, 099, 106, 0.4)', + lineWidth: 1, + }, + }; - placeholder = $('#chartPubHistory'); - function showTooltip(x, y, contents) { - $(`<div id="chartLabel">${contents}</div>`) - .css({ - position: 'absolute', - display: 'none', - top: y + 12, - left: x + 12, - border: '1px solid #615132', - padding: '2px', - 'background-color': '#fffdcd', - color: '#615132', - 'font-size': '11px', - opacity: 0.9, - 'z-index': 100, - }) - .appendTo('body') - .fadeIn(200); - } - previousPoint = null; - placeholder.bind('plothover', (event, pos, item) => { - var x, y; - $('#x').text(pos.x.toFixed(0)); - $('#y').text(pos.y.toFixed(0)); - if (item) { - if (previousPoint !== item.datapoint) { - previousPoint = item.datapoint; - $('#chartLabel').remove(); - x = item.datapoint[0].toFixed(0); - y = item.datapoint[1].toFixed(0); - if (y === 1) { - showTooltip(item.pageX, item.pageY, `${y} edition in ${x}`); + placeholder = $('#chartPubHistory'); + function showTooltip(x, y, contents) { + $(`<div id="chartLabel">${contents}</div>`) + .css({ + position: 'absolute', + display: 'none', + top: y + 12, + left: x + 12, + border: '1px solid #615132', + padding: '2px', + 'background-color': '#fffdcd', + color: '#615132', + 'font-size': '11px', + opacity: 0.9, + 'z-index': 100, + }) + .appendTo('body') + .fadeIn(200); + } + previousPoint = null; + placeholder.bind('plothover', (event, pos, item) => { + var x, y; + $('#x').text(pos.x.toFixed(0)); + $('#y').text(pos.y.toFixed(0)); + if (item) { + if (previousPoint !== item.datapoint) { + previousPoint = item.datapoint; + $('#chartLabel').remove(); + x = item.datapoint[0].toFixed(0); + y = item.datapoint[1].toFixed(0); + if (y === 1) { + showTooltip(item.pageX, item.pageY, `${y} edition in ${x}`); + } else { + showTooltip(item.pageX, item.pageY, `${y} editions in ${x}`); + } + } } else { - showTooltip(item.pageX, item.pageY, `${y} editions in ${x}`); + $('#chartLabel').remove(); + previousPoint = null; } - } - } else { - $('#chartLabel').remove(); - previousPoint = null; - } - }); + }); - placeholder.bind('plotclick', (event, pos, item) => { - if (item) { - plot.unhighlight(); - const yearFrom = item.datapoint[0].toFixed(0); - applyDateFilter(yearFrom, yearFrom); - - plot.highlight(item.series, item.datapoint); - } else { - plot.unhighlight(); - } - }); + placeholder.bind('plotclick', (event, pos, item) => { + if (item) { + plot.unhighlight(); + const yearFrom = item.datapoint[0].toFixed(0); + applyDateFilter(yearFrom, yearFrom); - placeholder.bind('plotselected', (event, ranges) => { - plot = $.plot( - placeholder, - data, - $.extend(true, {}, options, { - xaxis: { min: ranges.xaxis.from, max: ranges.xaxis.to }, - yaxis: { min: ranges.yaxis.from, max: ranges.yaxis.to }, - }), - ); + plot.highlight(item.series, item.datapoint); + } else { + plot.unhighlight(); + } + }); - const yearFrom = ranges.xaxis.from.toFixed(0); - const yearTo = ranges.xaxis.to.toFixed(0); - applyDateFilter(yearFrom, yearTo); - }); + placeholder.bind('plotselected', (event, ranges) => { + plot = $.plot( + placeholder, + data, + $.extend(true, {}, options, { + xaxis: { min: ranges.xaxis.from, max: ranges.xaxis.to }, + yaxis: { min: ranges.yaxis.from, max: ranges.yaxis.to }, + }), + ); - function applyDateFilter( - yearFrom, - yearTo, - hideSelector = '.chartUnzoom', - showSelector = '.chartZoom', - ) { - document.dispatchEvent( - new CustomEvent('filter', { - detail: { yearFrom: yearFrom, yearTo: yearTo }, - }), - ); - $(hideSelector).hide(); - $(showSelector).removeClass('hidden').show(); - } + const yearFrom = ranges.xaxis.from.toFixed(0); + const yearTo = ranges.xaxis.to.toFixed(0); + applyDateFilter(yearFrom, yearTo); + }); - plot = $.plot(placeholder, data, options); - dateFrom = plot.getAxes().xaxis.min.toFixed(0); - dateTo = plot.getAxes().xaxis.max.toFixed(0); + function applyDateFilter( + yearFrom, + yearTo, + hideSelector = '.chartUnzoom', + showSelector = '.chartZoom', + ) { + document.dispatchEvent( + new CustomEvent('filter', { + detail: { yearFrom: yearFrom, yearTo: yearTo }, + }), + ); + $(hideSelector).hide(); + $(showSelector).removeClass('hidden').show(); + } - $('.resetSelection').on('click', () => { plot = $.plot(placeholder, data, options); + dateFrom = plot.getAxes().xaxis.min.toFixed(0); + dateTo = plot.getAxes().xaxis.max.toFixed(0); - const yearFrom = plot.getAxes().xaxis.min.toFixed(0); - const yearTo = plot.getAxes().xaxis.max.toFixed(0); - applyDateFilter(yearFrom, yearTo, '.chartZoom', '.chartUnzoom'); - }); + $('.resetSelection').on('click', () => { + plot = $.plot(placeholder, data, options); - $('.chartYaxis').css({ top: '60px', left: '-60px' }); + const yearFrom = plot.getAxes().xaxis.min.toFixed(0); + const yearTo = plot.getAxes().xaxis.max.toFixed(0); + applyDateFilter(yearFrom, yearTo, '.chartZoom', '.chartUnzoom'); + }); - if (dateFrom === dateTo - 1) { - $('.clickdata').text(`Published in ${dateFrom}`); - } else { - $('.clickdata').text(`Published between ${dateFrom} & ${dateTo - 1}.`); - } + $('.chartYaxis').css({ top: '60px', left: '-60px' }); + + if (dateFrom === dateTo - 1) { + $('.clickdata').text(`Published in ${dateFrom}`); + } else { + $('.clickdata').text(`Published between ${dateFrom} & ${dateTo - 1}.`); + } } export function plot_minigraph(node, data) { - var options = { - series: { - lines: { - show: true, - fill: 0, - color: '#748d36', - }, - points: { - show: false, - }, - color: '#748d36', - }, - grid: { - hoverable: false, - show: false, - }, - }; - $.plot(node, [data], options); + var options = { + series: { + lines: { + show: true, + fill: 0, + color: '#748d36', + }, + points: { + show: false, + }, + color: '#748d36', + }, + grid: { + hoverable: false, + show: false, + }, + }; + $.plot(node, [data], options); } export function plot_tooltip_graph( - node, - data, - tooltip_message, - color = '#748d36', + node, + data, + tooltip_message, + color = '#748d36', ) { - var i, options, graph; - // empty set of rows. Escape early. - if (!data.length) { - return; - } - for (i = 0; i < data.length; ++i) { - data[i][0] += 60 * 60 * 1000; - } + var i, options, graph; + // empty set of rows. Escape early. + if (!data.length) { + return; + } + for (i = 0; i < data.length; ++i) { + data[i][0] += 60 * 60 * 1000; + } - options = { - series: { - bars: { - show: true, - fill: 1, - fillColor: color, - color, - align: 'left', - barWidth: 24 * 60 * 60 * 1000, - }, - points: { - show: false, - }, - color, - }, - grid: { - hoverable: true, - show: false, - }, - xaxis: { - mode: 'time', - }, - }; + options = { + series: { + bars: { + show: true, + fill: 1, + fillColor: color, + color, + align: 'left', + barWidth: 24 * 60 * 60 * 1000, + }, + points: { + show: false, + }, + color, + }, + grid: { + hoverable: true, + show: false, + }, + xaxis: { + mode: 'time', + }, + }; - graph = $.plot(node, [data], options); + graph = $.plot(node, [data], options); - function showTooltip(x, y, contents) { - $(`<div id="chartLabelA">${contents}</div>`) - .css({ - position: 'absolute', - display: 'none', - top: y + 12, - left: x + 12, - border: '1px solid #ccc', - padding: '2px', - backgroundColor: '#efefef', - color: '#454545', - fontSize: '11px', - webkitBoxShadow: '1px 1px 3px #333', - mozBoxShadow: '1px 1px 1px #000', - boxShadow: '1px 1px 1px #000', - }) - .appendTo('body') - .fadeIn(200); - } - node.bind('plothover', (event, pos, item) => { - var date, milli, x, y; - $('#x').text(pos.x); - $('#y').text(pos.y.toFixed(0)); - if (item) { - $('#chartLabelA').remove(); - milli = item.datapoint[0]; - date = new Date(milli); - x = date.toDateString(); - y = item.datapoint[1].toFixed(0); - showTooltip(item.pageX, item.pageY, `${y} ${tooltip_message} ${x}`); - } else { - $('#chartLabelA').remove(); + function showTooltip(x, y, contents) { + $(`<div id="chartLabelA">${contents}</div>`) + .css({ + position: 'absolute', + display: 'none', + top: y + 12, + left: x + 12, + border: '1px solid #ccc', + padding: '2px', + backgroundColor: '#efefef', + color: '#454545', + fontSize: '11px', + webkitBoxShadow: '1px 1px 3px #333', + mozBoxShadow: '1px 1px 1px #000', + boxShadow: '1px 1px 1px #000', + }) + .appendTo('body') + .fadeIn(200); } - }); - return graph; + node.bind('plothover', (event, pos, item) => { + var date, milli, x, y; + $('#x').text(pos.x); + $('#y').text(pos.y.toFixed(0)); + if (item) { + $('#chartLabelA').remove(); + milli = item.datapoint[0]; + date = new Date(milli); + x = date.toDateString(); + y = item.datapoint[1].toFixed(0); + showTooltip(item.pageX, item.pageY, `${y} ${tooltip_message} ${x}`); + } else { + $('#chartLabelA').remove(); + } + }); + return graph; } /** @@ -269,34 +269,34 @@ export function plot_tooltip_graph( * Ignored if options and no tooltip_message is passed. */ export function loadGraph( - id, - options = {}, - tooltip_message = '', - color = null, + id, + options = {}, + tooltip_message = '', + color = null, ) { - let data; - const node = document.getElementById(id); - const graphSelector = `graph-json-${id}`; - const dataSource = document.getElementById(graphSelector); - if (!node) { - throw new Error(`No graph associated with ${id} on the page.`); - } - if (!dataSource) { - throw new Error( - `No data associated with ${id} - make sure a script tag with type text/json and id "${graphSelector}" is present on the page.`, - ); - } else { - try { - data = JSON.parse(dataSource.textContent); - } catch (e) { - throw new Error(`Unable to parse JSON in ${graphSelector}`); + let data; + const node = document.getElementById(id); + const graphSelector = `graph-json-${id}`; + const dataSource = document.getElementById(graphSelector); + if (!node) { + throw new Error(`No graph associated with ${id} on the page.`); } - if (tooltip_message) { - return plot_tooltip_graph($(node), data, tooltip_message, color); + if (!dataSource) { + throw new Error( + `No data associated with ${id} - make sure a script tag with type text/json and id "${graphSelector}" is present on the page.`, + ); } else { - return $.plot($(node), [{ data: data }], options); + try { + data = JSON.parse(dataSource.textContent); + } catch (e) { + throw new Error(`Unable to parse JSON in ${graphSelector}`); + } + if (tooltip_message) { + return plot_tooltip_graph($(node), data, tooltip_message, color); + } else { + return $.plot($(node), [{ data: data }], options); + } } - } } /** @@ -308,7 +308,7 @@ export function loadGraph( * Ignored if options and no tooltip_message is passed. */ export function loadGraphIfExists(id, options, tooltip_message, color) { - if ($(`#${id}`).length) { - loadGraph(id, options, tooltip_message, color); - } + if ($(`#${id}`).length) { + loadGraph(id, options, tooltip_message, color); + } } diff --git a/openlibrary/plugins/openlibrary/js/i18n.js b/openlibrary/plugins/openlibrary/js/i18n.js index 97f4c4a7e34..d84f1b35c33 100644 --- a/openlibrary/plugins/openlibrary/js/i18n.js +++ b/openlibrary/plugins/openlibrary/js/i18n.js @@ -1,21 +1,21 @@ // used in templates/lists/preview.html export function sprintf(s) { - var args = arguments; - var i = 1; - return s.replace(/%[%s]/g, (match) => { - if (match === '%%') return '%'; - else return args[i++]; - }); + var args = arguments; + var i = 1; + return s.replace(/%[%s]/g, (match) => { + if (match === '%%') return '%'; + else return args[i++]; + }); } // dummy i18n functions // used in plugins/upstream/code.py export function ugettext(s) { - return s; + return s; } // used in templates/borrow/read.html export function ungettext(s1, s2, n) { - return n === 1 ? s1 : s2; + return n === 1 ? s1 : s2; } diff --git a/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js b/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js index 8a8af1d4825..258df735694 100644 --- a/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js +++ b/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js @@ -4,28 +4,28 @@ * @param {*} element - The element to be modified by the handleMessageEvent function. */ export function initMessageEventListener(element) { - /** + /** * Handles messages from archive.org and performs actions based on the message type. * * @param {MessageEvent} e - The message event. */ - function handleMessageEvent(e) { - if (!/[./]archive\.org$$/.test(e.origin)) return; + function handleMessageEvent(e) { + if (!/[./]archive\.org$$/.test(e.origin)) return; - if (e.data.type === 'resize') { - element.setAttribute('scrolling', 'no'); - if (e.data.height) element.style.height = `${e.data.height}px`; - } else if (e.data.type === 's3-keys') { - const s3AccessInput = document.querySelector('#access'); - const s3SecretInput = document.querySelector('#secret'); - s3AccessInput.value = e.data.s3.access; - s3SecretInput.value = e.data.s3.secret; + if (e.data.type === 'resize') { + element.setAttribute('scrolling', 'no'); + if (e.data.height) element.style.height = `${e.data.height}px`; + } else if (e.data.type === 's3-keys') { + const s3AccessInput = document.querySelector('#access'); + const s3SecretInput = document.querySelector('#secret'); + s3AccessInput.value = e.data.s3.access; + s3SecretInput.value = e.data.s3.secret; - const loginForm = document.querySelector('#register'); - loginForm.action = '/account/login'; - loginForm.submit(); + const loginForm = document.querySelector('#register'); + loginForm.action = '/account/login'; + loginForm.submit(); + } } - } - window.addEventListener('message', handleMessageEvent, false); + window.addEventListener('message', handleMessageEvent, false); } diff --git a/openlibrary/plugins/openlibrary/js/idValidation.js b/openlibrary/plugins/openlibrary/js/idValidation.js index d70adfeadcd..090d58b3495 100644 --- a/openlibrary/plugins/openlibrary/js/idValidation.js +++ b/openlibrary/plugins/openlibrary/js/idValidation.js @@ -4,7 +4,7 @@ * @returns {String} parsed isbn string */ export function parseIsbn(isbn) { - return isbn.replace(/[ -]/g, ''); + return isbn.replace(/[ -]/g, ''); } /** @@ -14,8 +14,8 @@ export function parseIsbn(isbn) { * @returns {boolean} true if the isbn has a valid format */ export function isFormatValidIsbn10(isbn) { - const regex = /^[0-9]{9}[0-9X]$/; - return regex.test(isbn); + const regex = /^[0-9]{9}[0-9X]$/; + return regex.test(isbn); } /** @@ -25,15 +25,15 @@ export function isFormatValidIsbn10(isbn) { * @returns {boolean} true if ISBN string is a valid ISBN 10 */ export function isChecksumValidIsbn10(isbn) { - const chars = isbn.replace('X', 'A').split(''); + const chars = isbn.replace('X', 'A').split(''); - chars.reverse(); - const sum = chars - .map((char, idx) => (idx + 1) * parseInt(char, 16)) - .reduce((acc, sum) => acc + sum, 0); + chars.reverse(); + const sum = chars + .map((char, idx) => (idx + 1) * parseInt(char, 16)) + .reduce((acc, sum) => acc + sum, 0); - // The ISBN 10 is valid if the checksum mod 11 is 0. - return sum % 11 === 0; + // The ISBN 10 is valid if the checksum mod 11 is 0. + return sum % 11 === 0; } /** @@ -43,8 +43,8 @@ export function isChecksumValidIsbn10(isbn) { * @returns {boolean} true if the isbn has a valid format */ export function isFormatValidIsbn13(isbn) { - const regex = /^[0-9]{13}$/; - return regex.test(isbn); + const regex = /^[0-9]{13}$/; + return regex.test(isbn); } /** @@ -54,13 +54,13 @@ export function isFormatValidIsbn13(isbn) { * @returns {Boolean} true if ISBN string is a valid ISBN 13 */ export function isChecksumValidIsbn13(isbn) { - const chars = isbn.split(''); - const sum = chars - .map((char, idx) => ((idx % 2) * 2 + 1) * parseInt(char, 10)) - .reduce((sum, num) => sum + num, 0); + const chars = isbn.split(''); + const sum = chars + .map((char, idx) => ((idx % 2) * 2 + 1) * parseInt(char, 10)) + .reduce((sum, num) => sum + num, 0); - // The ISBN 13 is valid if the checksum mod 10 is 0. - return sum % 10 === 0; + // The ISBN 13 is valid if the checksum mod 10 is 0. + return sum % 10 === 0; } /** @@ -70,26 +70,26 @@ export function isChecksumValidIsbn13(isbn) { * @returns {String} parsed LCCN string */ export function parseLccn(lccn) { - // cleaning initial lccn entry - const parsed = lccn + // cleaning initial lccn entry + const parsed = lccn // any alpha characters need to be lowercase - .toLowerCase() + .toLowerCase() // remove any whitespace - .replace(/\s/g, '') + .replace(/\s/g, '') // remove leading and trailing dashes - .replace(/^[-]+/, '') - .replace(/[-]+$/, '') + .replace(/^[-]+/, '') + .replace(/[-]+$/, '') // remove any revised text - .replace(/rev.*/g, '') + .replace(/rev.*/g, '') // remove first forward slash and everything to its right - .replace(/[/]+.*$/, ''); + .replace(/[/]+.*$/, ''); - // splitting at hyphen and padding the right hand value with zeros up to 6 characters - const groups = parsed.match(/(.+)-+([0-9]+)/); - if (groups && groups.length === 3) { - return groups[1] + groups[2].padStart(6, '0'); - } - return parsed; + // splitting at hyphen and padding the right hand value with zeros up to 6 characters + const groups = parsed.match(/(.+)-+([0-9]+)/); + if (groups && groups.length === 3) { + return groups[1] + groups[2].padStart(6, '0'); + } + return parsed; } /** @@ -99,10 +99,10 @@ export function parseLccn(lccn) { * @returns {boolean} true if given LCCN is valid syntax, false otherwise */ export function isValidLccn(lccn) { - // matching parsed entry to regex representing valid lccn - // regex taken from /openlibrary/utils/lccn.py - const regex = /^([a-z]|[a-z]?([a-z]{2}|[0-9]{2})|[a-z]{2}[0-9]{2})?[0-9]{8}$/; - return regex.test(lccn); + // matching parsed entry to regex representing valid lccn + // regex taken from /openlibrary/utils/lccn.py + const regex = /^([a-z]|[a-z]?([a-z]{2}|[0-9]{2})|[a-z]{2}[0-9]{2})?[0-9]{8}$/; + return regex.test(lccn); } /** @@ -111,14 +111,14 @@ export function isValidLccn(lccn) { * @returns {String} parsed OCLC string */ export function parseOclc(oclc) { - // cleaning initial oclc entry - return ( - oclc - // remove any whitespace - .replace(/\s/g, '') - // remove leading/padding zeroes - .replace(/^0+/, '') - ); + // cleaning initial oclc entry + return ( + oclc + // remove any whitespace + .replace(/\s/g, '') + // remove leading/padding zeroes + .replace(/^0+/, '') + ); } /** @@ -131,9 +131,9 @@ export function parseOclc(oclc) { * @returns {boolean} true if given OCLC is valid syntax, false otherwise */ export function isValidOclc(oclc) { - // matching parsed entry to regex representing valid oclc - const regex = /^[1-9][0-9]*$/; - return regex.test(oclc); + // matching parsed entry to regex representing valid oclc + const regex = /^[1-9][0-9]*$/; + return regex.test(oclc); } /** @@ -146,6 +146,6 @@ export function isValidOclc(oclc) { * @returns {boolean} true if the new identifier has already been entered */ export function isIdDupe(idEntries, newId) { - // check each current entry value against new identifier - return Array.from(idEntries).some((entry) => entry['value'] === newId); + // check each current entry value against new identifier + return Array.from(idEntries).some((entry) => entry['value'] === newId); } diff --git a/openlibrary/plugins/openlibrary/js/ile/index.js b/openlibrary/plugins/openlibrary/js/ile/index.js index a06f36b848b..097a52f8f7c 100644 --- a/openlibrary/plugins/openlibrary/js/ile/index.js +++ b/openlibrary/plugins/openlibrary/js/ile/index.js @@ -5,18 +5,18 @@ import { renderBulkTagger } from '../bulk-tagger/index.js'; import SelectionManager from './utils/SelectionManager/SelectionManager.js'; export function init() { - const ile = new IntegratedLibrarianEnvironment(); - // @ts-expect-error - window.ILE = ile; - ile.init(); + const ile = new IntegratedLibrarianEnvironment(); + // @ts-expect-error + window.ILE = ile; + ile.init(); } export class IntegratedLibrarianEnvironment { - constructor() { - this.selectionManager = new SelectionManager(this); - /** This is the main ILE toolbar. Should be moved to a Vue component. */ - this.$toolbar = $( - ` + constructor() { + this.selectionManager = new SelectionManager(this); + /** This is the main ILE toolbar. Should be moved to a Vue component. */ + this.$toolbar = $( + ` <div id="ile-toolbar"> <div id="ile-selections"> <div id="ile-drag-status"> @@ -28,78 +28,78 @@ export class IntegratedLibrarianEnvironment { <div id="ile-drag-actions"></div> <div id="ile-hidden-forms"></div> </div>`.trim(), - ); - this.$selectionActions = this.$toolbar.find('#ile-selection-actions'); - this.$statusText = this.$toolbar.find('.text'); - this.$statusImages = this.$toolbar.find('.images ul'); - this.$actions = this.$toolbar.find('#ile-drag-actions'); - this.$hiddenForms = this.$toolbar.find('#ile-hidden-forms'); - this.bulkTagger = null; - } + ); + this.$selectionActions = this.$toolbar.find('#ile-selection-actions'); + this.$statusText = this.$toolbar.find('.text'); + this.$statusImages = this.$toolbar.find('.images ul'); + this.$actions = this.$toolbar.find('#ile-drag-actions'); + this.$hiddenForms = this.$toolbar.find('#ile-hidden-forms'); + this.bulkTagger = null; + } - init() { + init() { // Add the ILE toolbar to bottom of screen - $(document.body).append(this.$toolbar.hide()); + $(document.body).append(this.$toolbar.hide()); - // Ready bulk tagger: - this.createBulkTagger(); + // Ready bulk tagger: + this.createBulkTagger(); - this.selectionManager.init(); - } + this.selectionManager.init(); + } - /** @param {string} text */ - setStatusText(text) { - this.$statusText.text(text); - this.$toolbar.toggle(text.length > 0); - } + /** @param {string} text */ + setStatusText(text) { + this.$statusText.text(text); + this.$toolbar.toggle(text.length > 0); + } - /** + /** * Resets the status bar. */ - reset() { - for (const elem of $('.ile-selected')) { - elem.classList.remove('ile-selected'); + reset() { + for (const elem of $('.ile-selected')) { + elem.classList.remove('ile-selected'); + } + this.setStatusText(''); + this.$selectionActions.empty(); + this.$statusImages.empty(); + this.$actions.empty(); } - this.setStatusText(''); - this.$selectionActions.empty(); - this.$statusImages.empty(); - this.$actions.empty(); - } - /** + /** * Clears all items selected in SelectionManager. * * This indirectly calls `IntegratedLibrarianEnvironment.reset()`. */ - clearAndReset() { - this.selectionManager.clearSelectedItems(); - } + clearAndReset() { + this.selectionManager.clearSelectedItems(); + } - /** + /** * Creates a new Bulk Tagger component and attaches it to the DOM. * * Sets the value of `IntegratedLibrarianEnvironment.bulkTagger` */ - createBulkTagger() { - const target = this.$hiddenForms[0]; - target.innerHTML += renderBulkTagger(); - const bulkTaggerElem = document.querySelector('.bulk-tagging-form'); - // @ts-expect-error - this.bulkTagger = new BulkTagger(bulkTaggerElem); - this.bulkTagger.initialize(); - } + createBulkTagger() { + const target = this.$hiddenForms[0]; + target.innerHTML += renderBulkTagger(); + const bulkTaggerElem = document.querySelector('.bulk-tagging-form'); + // @ts-expect-error + this.bulkTagger = new BulkTagger(bulkTaggerElem); + this.bulkTagger.initialize(); + } - /** + /** * Updates the Bulk Tagger with the selected works, then displays the tagger. * * @param {Array<String>} workIds * @param {boolean} isBookPageEdit `true` if the bulk tagger is opened on a /books or /works page */ - updateAndShowBulkTagger(workIds, isBookPageEdit = false) { - if (this.bulkTagger) { - this.bulkTagger.isBookPageEdit = isBookPageEdit; - this.bulkTagger.updateWorks(workIds); - this.bulkTagger.showTaggingMenu(); + updateAndShowBulkTagger(workIds, isBookPageEdit = false) { + if (this.bulkTagger) { + this.bulkTagger.isBookPageEdit = isBookPageEdit; + this.bulkTagger.updateWorks(workIds); + this.bulkTagger.showTaggingMenu(); + } } - } } diff --git a/openlibrary/plugins/openlibrary/js/ile/utils/ol.js b/openlibrary/plugins/openlibrary/js/ile/utils/ol.js index db58d00a249..872e593723d 100644 --- a/openlibrary/plugins/openlibrary/js/ile/utils/ol.js +++ b/openlibrary/plugins/openlibrary/js/ile/utils/ol.js @@ -12,16 +12,16 @@ import uniqBy from 'lodash/uniqBy'; * @param {WorkOLID} new_work */ export async function move_to_work(edition_ids, old_work, new_work) { - for (const olid of edition_ids) { - const url = `/books/${olid}.json`; - const record = await fetch(url).then((r) => r.json()); + for (const olid of edition_ids) { + const url = `/books/${olid}.json`; + const record = await fetch(url).then((r) => r.json()); - record.works = [{ key: `/works/${new_work}` }]; - record._comment = 'move to correct work'; - const r = await fetch(url, { method: 'PUT', body: JSON.stringify(record) }); - // eslint-disable-next-line no-console - console.log(`moved ${olid}; ${r.status}`); - } + record.works = [{ key: `/works/${new_work}` }]; + record._comment = 'move to correct work'; + const r = await fetch(url, { method: 'PUT', body: JSON.stringify(record) }); + // eslint-disable-next-line no-console + console.log(`moved ${olid}; ${r.status}`); + } } /** @@ -31,30 +31,30 @@ export async function move_to_work(edition_ids, old_work, new_work) { * @param {AuthorOLID} new_author */ export async function move_to_author(work_ids, old_author, new_author) { - for (const olid of work_ids) { - const url = `/works/${olid}.json`; - const record = await fetch(url).then((r) => r.json()); - if (record.authors.find((a) => a.author.key.includes(old_author))) { - record.authors = uniqBy( - record.authors.map((a) => { - if (!a.author.key.includes(old_author)) return a; + for (const olid of work_ids) { + const url = `/works/${olid}.json`; + const record = await fetch(url).then((r) => r.json()); + if (record.authors.find((a) => a.author.key.includes(old_author))) { + record.authors = uniqBy( + record.authors.map((a) => { + if (!a.author.key.includes(old_author)) return a; - const copy = JSON.parse(JSON.stringify(a)); - copy.author.key = `/authors/${new_author}`; - return copy; - }), - (a) => a.author.key, - ); - record._comment = 'move to correct author'; - const r = await fetch(url, { - method: 'PUT', - body: JSON.stringify(record), - }); - // eslint-disable-next-line no-console - console.log(`moved ${olid}; ${r.status}`); - } else { - // eslint-disable-next-line no-console - console.warn(`${old_author} not in ${url}!`); + const copy = JSON.parse(JSON.stringify(a)); + copy.author.key = `/authors/${new_author}`; + return copy; + }), + (a) => a.author.key, + ); + record._comment = 'move to correct author'; + const r = await fetch(url, { + method: 'PUT', + body: JSON.stringify(record), + }); + // eslint-disable-next-line no-console + console.log(`moved ${olid}; ${r.status}`); + } else { + // eslint-disable-next-line no-console + console.warn(`${old_author} not in ${url}!`); + } } - } } diff --git a/openlibrary/plugins/openlibrary/js/index.js b/openlibrary/plugins/openlibrary/js/index.js index cef525ec4dd..7640fca2368 100644 --- a/openlibrary/plugins/openlibrary/js/index.js +++ b/openlibrary/plugins/openlibrary/js/index.js @@ -25,91 +25,91 @@ initAnalytics(); // Initialise some things jQuery(() => { - // conditionally load polyfill for <details> tags (IE11) - // See http://diveintohtml5.info/everything.html#details - if (!('open' in document.createElement('details'))) { - import(/* webpackChunkName: "details-polyfill" */ 'details-polyfill'); - } - - // Polyfill for .matches() - if (!Element.prototype.matches) { - Element.prototype.matches = + // conditionally load polyfill for <details> tags (IE11) + // See http://diveintohtml5.info/everything.html#details + if (!('open' in document.createElement('details'))) { + import(/* webpackChunkName: "details-polyfill" */ 'details-polyfill'); + } + + // Polyfill for .matches() + if (!Element.prototype.matches) { + Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; - } - - // Polyfill for .closest() - if (!Element.prototype.closest) { - Element.prototype.closest = function (s) { - let el = this; - do { - if (Element.prototype.matches.call(el, s)) return el; - el = el.parentElement || el.parentNode; - } while (el !== null && el.nodeType === 1); - return null; - }; - } - - const $tabs = $('.ol-tabs'); - if ($tabs.length) { - import(/* webpackChunkName: "tabs" */ './tabs').then((module) => - module.initTabs($tabs), - ); - } + } - const $autocomplete = $('.multi-input-autocomplete'); - if ($autocomplete.length) { - import(/* webpackChunkName: "autocomplete" */ './autocomplete').then( - (module) => module.init($), - ); - } + // Polyfill for .closest() + if (!Element.prototype.closest) { + Element.prototype.closest = function (s) { + let el = this; + do { + if (Element.prototype.matches.call(el, s)) return el; + el = el.parentElement || el.parentNode; + } while (el !== null && el.nodeType === 1); + return null; + }; + } - // hide all images in .no-img - $('.no-img img').hide(); + const $tabs = $('.ol-tabs'); + if ($tabs.length) { + import(/* webpackChunkName: "tabs" */ './tabs').then((module) => + module.initTabs($tabs), + ); + } - // disable save button after click - $("button[name='_save']").on('submit', function () { - $(this).attr('disabled', true); - }); + const $autocomplete = $('.multi-input-autocomplete'); + if ($autocomplete.length) { + import(/* webpackChunkName: "autocomplete" */ './autocomplete').then( + (module) => module.init($), + ); + } - // wmd editor - const $markdownTextAreas = $('textarea.markdown'); - if ($markdownTextAreas.length) { - import(/* webpackChunkName: "markdown-editor" */ './markdown-editor').then( - (module) => module.initMarkdownEditor($markdownTextAreas), + // hide all images in .no-img + $('.no-img img').hide(); + + // disable save button after click + $('button[name=\'_save\']').on('submit', function () { + $(this).attr('disabled', true); + }); + + // wmd editor + const $markdownTextAreas = $('textarea.markdown'); + if ($markdownTextAreas.length) { + import(/* webpackChunkName: "markdown-editor" */ './markdown-editor').then( + (module) => module.initMarkdownEditor($markdownTextAreas), + ); + } + + init($); + + const edition = document.getElementById('addWork'); + const autocompleteAuthor = document.querySelector( + '.multi-input-autocomplete--author', + ); + const autocompleteSeries = document.querySelector( + '.multi-input-autocomplete--series', + ); + const autocompleteLanguage = document.querySelector( + '.multi-input-autocomplete--language', + ); + const autocompleteWorks = document.querySelector( + '.multi-input-autocomplete--works', + ); + const autocompleteSeeds = document.querySelector( + '.multi-input-autocomplete--seeds', + ); + const autocompleteSubjects = document.querySelector( + '.csv-autocomplete--subjects', ); - } - - init($); - - const edition = document.getElementById('addWork'); - const autocompleteAuthor = document.querySelector( - '.multi-input-autocomplete--author', - ); - const autocompleteSeries = document.querySelector( - '.multi-input-autocomplete--series', - ); - const autocompleteLanguage = document.querySelector( - '.multi-input-autocomplete--language', - ); - const autocompleteWorks = document.querySelector( - '.multi-input-autocomplete--works', - ); - const autocompleteSeeds = document.querySelector( - '.multi-input-autocomplete--seeds', - ); - const autocompleteSubjects = document.querySelector( - '.csv-autocomplete--subjects', - ); - const addRowButton = document.getElementById('add_row_button'); - const roles = document.querySelector('#roles'); - const classifications = document.querySelector('#classifications'); - const excerpts = document.getElementById('excerpts'); - const links = document.getElementById('links'); - - // conditionally load for user edit page - if ( - edition || + const addRowButton = document.getElementById('add_row_button'); + const roles = document.querySelector('#roles'); + const classifications = document.querySelector('#classifications'); + const excerpts = document.getElementById('excerpts'); + const links = document.getElementById('links'); + + // conditionally load for user edit page + if ( + edition || autocompleteAuthor || autocompleteSeries || autocompleteLanguage || @@ -121,599 +121,599 @@ jQuery(() => { classifications || excerpts || links - ) { - import(/* webpackChunkName: "user-website" */ './edit').then((module) => { - if (edition) { - module.initEdit(); - } - if (addRowButton) { - module.initEditRow(); - } - if (excerpts) { - module.initEditExcerpts(); - } - if (links) { - module.initEditLinks(); - } - if (autocompleteAuthor) { - module.initAuthorMultiInputAutocomplete(); - } - if (autocompleteSeries) { - module.initSeriesMultiInputAutocomplete(); - } - if (roles) { - module.initRoleValidation(); - } - if (classifications) { - module.initClassificationValidation(); - } - if (autocompleteLanguage) { - module.initLanguageMultiInputAutocomplete(); - } - if (autocompleteWorks) { - module.initWorksMultiInputAutocomplete(); - } - if (autocompleteSubjects) { - module.initSubjectsAutocomplete(); - } - if (autocompleteSeeds) { - module.initSeedsMultiInputAutocomplete(); - } - }); - } - - // conditionally load for author merge page - const mergePageElement = document.querySelector('#author-merge-page'); - const preMergePageElement = document.getElementById('preMerge'); - if (mergePageElement || preMergePageElement) { - import(/* webpackChunkName: "merge" */ './merge').then((module) => { - if (mergePageElement) { - module.initAuthorMergePage(); - } - if (preMergePageElement) { - module.initAuthorView(); - } - }); - } + ) { + import(/* webpackChunkName: "user-website" */ './edit').then((module) => { + if (edition) { + module.initEdit(); + } + if (addRowButton) { + module.initEditRow(); + } + if (excerpts) { + module.initEditExcerpts(); + } + if (links) { + module.initEditLinks(); + } + if (autocompleteAuthor) { + module.initAuthorMultiInputAutocomplete(); + } + if (autocompleteSeries) { + module.initSeriesMultiInputAutocomplete(); + } + if (roles) { + module.initRoleValidation(); + } + if (classifications) { + module.initClassificationValidation(); + } + if (autocompleteLanguage) { + module.initLanguageMultiInputAutocomplete(); + } + if (autocompleteWorks) { + module.initWorksMultiInputAutocomplete(); + } + if (autocompleteSubjects) { + module.initSubjectsAutocomplete(); + } + if (autocompleteSeeds) { + module.initSeedsMultiInputAutocomplete(); + } + }); + } - // conditionally load for type changing input - const typeChanger = document.getElementById('type.key'); - if (typeChanger) { - import(/* webpackChunkName: "type-changer" */ './type_changer.js').then( - (module) => module.initTypeChanger(typeChanger), - ); - } + // conditionally load for author merge page + const mergePageElement = document.querySelector('#author-merge-page'); + const preMergePageElement = document.getElementById('preMerge'); + if (mergePageElement || preMergePageElement) { + import(/* webpackChunkName: "merge" */ './merge').then((module) => { + if (mergePageElement) { + module.initAuthorMergePage(); + } + if (preMergePageElement) { + module.initAuthorView(); + } + }); + } - // conditionally load validation and submission js for registration form - if (document.querySelector('form[name=signup]')) { - import(/* webpackChunkName: "signup" */ './signup.js').then((module) => - module.initSignupForm(), - ); - } + // conditionally load for type changing input + const typeChanger = document.getElementById('type.key'); + if (typeChanger) { + import(/* webpackChunkName: "type-changer" */ './type_changer.js').then( + (module) => module.initTypeChanger(typeChanger), + ); + } - // conditionally load submission js for login form - if (document.querySelector('form[name=login]')) { - import(/* webpackChunkName: "signup" */ './signup.js').then((module) => - module.initLoginForm(), - ); - } - - // conditionally load clamping components - const clampers = document.querySelectorAll('.clamp'); - if (clampers.length) { - import(/* webpackChunkName: "clampers" */ './clampers.js').then( - (module) => { - if (clampers.length) { - module.initClampers(clampers); - } - }, - ); - } - - // conditionally loads Goodreads import based on class in the page - if (document.getElementsByClassName('import-table').length) { - import( - /* webpackChunkName: "goodreads-import" */ './goodreads_import.js' - ).then((module) => module.initGoodreadsImport()); - } - // conditionally load list seed item deletion dialog functionality based on id on lists pages - if (document.getElementById('listResults')) { - import(/* webpackChunkName: "ListViewBody" */ './lists/ListViewBody.js'); - } - - // Enable any carousels in the page - const carouselElements = document.querySelectorAll( - '.carousel--progressively-enhanced', - ); - if (carouselElements.length) { - import(/* webpackChunkName: "carousel" */ './carousel').then((module) => { - module.initialzeCarousels(carouselElements); - }); - } - if ($('script[type="text/json+graph"]').length > 0) { - import(/* webpackChunkName: "graphs" */ './graphs').then((module) => - module.init(), - ); - } - - const readingLogCharts = document.querySelector('.readinglog-charts'); - if (readingLogCharts) { - const readingLogConfig = JSON.parse(readingLogCharts.dataset.config); - import( - /* webpackChunkName: "readinglog-stats" */ './readinglog_stats' - ).then((module) => module.init(readingLogConfig)); - } - - if (document.getElementsByClassName('toast').length) { - import(/* webpackChunkName: "Toast" */ './Toast').then((module) => { - Array.from(document.getElementsByClassName('toast')).forEach( - (el) => new module.Toast($(el)), - ); - }); - } - - if ($('.lazy-thing-preview').length) { - import( - /* webpackChunkName: "lazy-thing-preview" */ './lazy-thing-preview' - ).then((module) => new module.LazyThingPreview().init()); - } - - // Disable data export buttons on form submit - const patronImportForms = document.querySelectorAll('.patron-export-form'); - if (patronImportForms.length) { - import(/* webpackChunkName: "patron-exports" */ './patron_exports').then( - (module) => module.initPatronExportForms(patronImportForms), + // conditionally load validation and submission js for registration form + if (document.querySelector('form[name=signup]')) { + import(/* webpackChunkName: "signup" */ './signup.js').then((module) => + module.initSignupForm(), + ); + } + + // conditionally load submission js for login form + if (document.querySelector('form[name=login]')) { + import(/* webpackChunkName: "signup" */ './signup.js').then((module) => + module.initLoginForm(), + ); + } + + // conditionally load clamping components + const clampers = document.querySelectorAll('.clamp'); + if (clampers.length) { + import(/* webpackChunkName: "clampers" */ './clampers.js').then( + (module) => { + if (clampers.length) { + module.initClampers(clampers); + } + }, + ); + } + + // conditionally loads Goodreads import based on class in the page + if (document.getElementsByClassName('import-table').length) { + import( + /* webpackChunkName: "goodreads-import" */ './goodreads_import.js' + ).then((module) => module.initGoodreadsImport()); + } + // conditionally load list seed item deletion dialog functionality based on id on lists pages + if (document.getElementById('listResults')) { + import(/* webpackChunkName: "ListViewBody" */ './lists/ListViewBody.js'); + } + + // Enable any carousels in the page + const carouselElements = document.querySelectorAll( + '.carousel--progressively-enhanced', ); - } - - const $observationModalLinks = $('.observations-modal-link'); - const $notesModalLinks = $('.notes-modal-link'); - const $notesPageButtons = $('.note-page-buttons'); - const $shareModalLinks = $('.share-modal-link'); - if ( - $observationModalLinks.length || + if (carouselElements.length) { + import(/* webpackChunkName: "carousel" */ './carousel').then((module) => { + module.initialzeCarousels(carouselElements); + }); + } + if ($('script[type="text/json+graph"]').length > 0) { + import(/* webpackChunkName: "graphs" */ './graphs').then((module) => + module.init(), + ); + } + + const readingLogCharts = document.querySelector('.readinglog-charts'); + if (readingLogCharts) { + const readingLogConfig = JSON.parse(readingLogCharts.dataset.config); + import( + /* webpackChunkName: "readinglog-stats" */ './readinglog_stats' + ).then((module) => module.init(readingLogConfig)); + } + + if (document.getElementsByClassName('toast').length) { + import(/* webpackChunkName: "Toast" */ './Toast').then((module) => { + Array.from(document.getElementsByClassName('toast')).forEach( + (el) => new module.Toast($(el)), + ); + }); + } + + if ($('.lazy-thing-preview').length) { + import( + /* webpackChunkName: "lazy-thing-preview" */ './lazy-thing-preview' + ).then((module) => new module.LazyThingPreview().init()); + } + + // Disable data export buttons on form submit + const patronImportForms = document.querySelectorAll('.patron-export-form'); + if (patronImportForms.length) { + import(/* webpackChunkName: "patron-exports" */ './patron_exports').then( + (module) => module.initPatronExportForms(patronImportForms), + ); + } + + const $observationModalLinks = $('.observations-modal-link'); + const $notesModalLinks = $('.notes-modal-link'); + const $notesPageButtons = $('.note-page-buttons'); + const $shareModalLinks = $('.share-modal-link'); + if ( + $observationModalLinks.length || $notesModalLinks.length || $notesPageButtons.length || $shareModalLinks.length - ) { - import(/* webpackChunkName: "modal-links" */ './modals').then((module) => { - if ($observationModalLinks.length) { - module.initObservationsModal($observationModalLinks); - } - if ($notesModalLinks.length) { - module.initNotesModal($notesModalLinks); - } - if ($notesPageButtons.length) { - module.addNotesPageButtonListeners(); - } - if ($shareModalLinks.length) { - module.initShareModal($shareModalLinks); - } - }); - } + ) { + import(/* webpackChunkName: "modal-links" */ './modals').then((module) => { + if ($observationModalLinks.length) { + module.initObservationsModal($observationModalLinks); + } + if ($notesModalLinks.length) { + module.initNotesModal($notesModalLinks); + } + if ($notesPageButtons.length) { + module.addNotesPageButtonListeners(); + } + if ($shareModalLinks.length) { + module.initShareModal($shareModalLinks); + } + }); + } - const manageCoversElement = + const manageCoversElement = document.getElementsByClassName('manageCovers').length; - const addCoversElement = document.getElementsByClassName('imageIntro').length; - const saveCoversElement = + const addCoversElement = document.getElementsByClassName('imageIntro').length; + const saveCoversElement = document.getElementsByClassName('imageSaved').length; - const coverForm = document.querySelector('.ol-cover-form--clipboard'); + const coverForm = document.querySelector('.ol-cover-form--clipboard'); - if ( - addCoversElement || + if ( + addCoversElement || manageCoversElement || saveCoversElement || coverForm - ) { - import(/* webpackChunkName: "covers" */ './covers').then((module) => { - if (manageCoversElement) { - module.initCoversChange(); - } - if (addCoversElement) { - module.initCoversAddManage(); - } - if (saveCoversElement) { - module.initCoversSaved(); - } - if (coverForm) { - module.initPasteForm(coverForm); - } - }); - } - - if (document.getElementById('addbook')) { - import(/* webpackChunkName: "add-book" */ './add-book').then((module) => - module.initAddBookImport(), - ); - } - - if (document.getElementById('autofill-dev-credentials')) { - document.getElementById('username').value = 'openlibrary@example.com'; - document.getElementById('password').value = 'admin123'; - document.getElementById('remember').checked = true; - } - const anonymizationButton = document.querySelector( - '.account-anonymization-button', - ); - const adminLinks = document.getElementById('adminLinks'); - const confirmButtons = document.querySelectorAll('.do-confirm'); - if (adminLinks || anonymizationButton || confirmButtons.length) { - import(/* webpackChunkName: "admin" */ './admin').then((module) => { - if (adminLinks) { - module.initAdmin(); - } - if (anonymizationButton) { - module.initAnonymizationButton(anonymizationButton); - } - if (confirmButtons.length) { - module.initConfirmationButtons(confirmButtons); - } - }); - } + ) { + import(/* webpackChunkName: "covers" */ './covers').then((module) => { + if (manageCoversElement) { + module.initCoversChange(); + } + if (addCoversElement) { + module.initCoversAddManage(); + } + if (saveCoversElement) { + module.initCoversSaved(); + } + if (coverForm) { + module.initPasteForm(coverForm); + } + }); + } - if (window.matchMedia('(display-mode: standalone)').matches) { - import(/* webpackChunkName: "offline-banner" */ './offline-banner').then( - (module) => module.initOfflineBanner(), - ); - } + if (document.getElementById('addbook')) { + import(/* webpackChunkName: "add-book" */ './add-book').then((module) => + module.initAddBookImport(), + ); + } - const searchFacets = document.getElementById('searchFacets'); - if (searchFacets) { - import(/* webpackChunkName: "search" */ './search').then((module) => - module.initSearchFacets(searchFacets), - ); - } - - // Conditionally load Integrated Librarian Environment - if (document.getElementsByClassName('show-librarian-tools').length) { - import(/* webpackChunkName: "ile" */ './ile') - .then((module) => module.init()) - .then(() => { - // book page subject editing - // Handle pencil clicks - document.querySelectorAll('.edit-subject-btn').forEach((btn) => { - btn.addEventListener('click', (e) => { - e.preventDefault(); - const workOlid = btn.dataset.workOlid; - if ( - !window.ILE.selectionManager.selectedItems.work.includes(workOlid) - ) { - window.ILE.selectionManager.addSelectedItem(workOlid); - window.ILE.selectionManager.updateToolbar(); + if (document.getElementById('autofill-dev-credentials')) { + document.getElementById('username').value = 'openlibrary@example.com'; + document.getElementById('password').value = 'admin123'; + document.getElementById('remember').checked = true; + } + const anonymizationButton = document.querySelector( + '.account-anonymization-button', + ); + const adminLinks = document.getElementById('adminLinks'); + const confirmButtons = document.querySelectorAll('.do-confirm'); + if (adminLinks || anonymizationButton || confirmButtons.length) { + import(/* webpackChunkName: "admin" */ './admin').then((module) => { + if (adminLinks) { + module.initAdmin(); + } + if (anonymizationButton) { + module.initAnonymizationButton(anonymizationButton); + } + if (confirmButtons.length) { + module.initConfirmationButtons(confirmButtons); } - window.ILE.updateAndShowBulkTagger([workOlid], true); - }); }); - }); - // Import ile then the datatable to apply clickable classes to all listed editions + } + + if (window.matchMedia('(display-mode: standalone)').matches) { + import(/* webpackChunkName: "offline-banner" */ './offline-banner').then( + (module) => module.initOfflineBanner(), + ); + } + + const searchFacets = document.getElementById('searchFacets'); + if (searchFacets) { + import(/* webpackChunkName: "search" */ './search').then((module) => + module.initSearchFacets(searchFacets), + ); + } + + // Conditionally load Integrated Librarian Environment + if (document.getElementsByClassName('show-librarian-tools').length) { + import(/* webpackChunkName: "ile" */ './ile') + .then((module) => module.init()) + .then(() => { + // book page subject editing + // Handle pencil clicks + document.querySelectorAll('.edit-subject-btn').forEach((btn) => { + btn.addEventListener('click', (e) => { + e.preventDefault(); + const workOlid = btn.dataset.workOlid; + if ( + !window.ILE.selectionManager.selectedItems.work.includes(workOlid) + ) { + window.ILE.selectionManager.addSelectedItem(workOlid); + window.ILE.selectionManager.updateToolbar(); + } + window.ILE.updateAndShowBulkTagger([workOlid], true); + }); + }); + }); + // Import ile then the datatable to apply clickable classes to all listed editions + if ( + document.getElementsByClassName('editions-table--progressively-enhanced') + .length + ) { + import(/* webpackChunkName: "editions-table" */ './editions-table').then( + (module) => module.initEditionsTable(), + ); + } + } + // conditionally load functionality based on what's in the page if ( - document.getElementsByClassName('editions-table--progressively-enhanced') - .length + document.getElementsByClassName('editions-table--progressively-enhanced') + .length ) { - import(/* webpackChunkName: "editions-table" */ './editions-table').then( - (module) => module.initEditionsTable(), - ); - } - } - // conditionally load functionality based on what's in the page - if ( - document.getElementsByClassName('editions-table--progressively-enhanced') - .length - ) { - import(/* webpackChunkName: "editions-table" */ './editions-table').then( - (module) => module.initEditionsTable(), - ); - } - if ($('#cboxPrevious').length) { - $('#cboxPrevious').attr({ - 'aria-label': 'Previous button', - 'aria-hidden': 'true', - }); - } - if ($('#cboxNext').length) { - $('#cboxNext').attr({ 'aria-label': 'Next button', 'aria-hidden': 'true' }); - } - if ($('#cboxSlideshow').length) { - $('#cboxSlideshow').attr({ - 'aria-label': 'Slideshow button', - 'aria-hidden': 'true', - }); - } - - const droppers = document.querySelectorAll('.dropper'); - const genericDroppers = document.querySelectorAll('.generic-dropper-wrapper'); - if (droppers.length || genericDroppers.length) { - import(/* webpackChunkName: "droppers" */ './dropper').then((module) => { - module.initDroppers(droppers); - module.initGenericDroppers(genericDroppers); - }); - } + import(/* webpackChunkName: "editions-table" */ './editions-table').then( + (module) => module.initEditionsTable(), + ); + } + if ($('#cboxPrevious').length) { + $('#cboxPrevious').attr({ + 'aria-label': 'Previous button', + 'aria-hidden': 'true', + }); + } + if ($('#cboxNext').length) { + $('#cboxNext').attr({ 'aria-label': 'Next button', 'aria-hidden': 'true' }); + } + if ($('#cboxSlideshow').length) { + $('#cboxSlideshow').attr({ + 'aria-label': 'Slideshow button', + 'aria-hidden': 'true', + }); + } + + const droppers = document.querySelectorAll('.dropper'); + const genericDroppers = document.querySelectorAll('.generic-dropper-wrapper'); + if (droppers.length || genericDroppers.length) { + import(/* webpackChunkName: "droppers" */ './dropper').then((module) => { + module.initDroppers(droppers); + module.initGenericDroppers(genericDroppers); + }); + } - // My Books Droppers (includes New List Form and Reading Check-Ins): - const myBooksDroppers = document.querySelectorAll('.my-books-dropper'); - if (myBooksDroppers.length) { - const actionableListShowcases = + // My Books Droppers (includes New List Form and Reading Check-Ins): + const myBooksDroppers = document.querySelectorAll('.my-books-dropper'); + if (myBooksDroppers.length) { + const actionableListShowcases = document.querySelectorAll('.actionable-item'); - import(/* webpackChunkName: "my-books" */ './my-books').then((module) => { - module.initMyBooksAffordances(myBooksDroppers, actionableListShowcases); - }); - } - - // TODO: Make these selectors a consistent interface - const $dialogs = $( - '.dialog--open,.dialog--close,#noMaster,#confirmMerge,#leave-waitinglist-dialog,#bookPreview', - ); - if ($dialogs.length) { - import(/* webpackChunkName: "dialog" */ './dialog').then((module) => - module.initDialogs(), + import(/* webpackChunkName: "my-books" */ './my-books').then((module) => { + module.initMyBooksAffordances(myBooksDroppers, actionableListShowcases); + }); + } + + // TODO: Make these selectors a consistent interface + const $dialogs = $( + '.dialog--open,.dialog--close,#noMaster,#confirmMerge,#leave-waitinglist-dialog,#bookPreview', ); - } + if ($dialogs.length) { + import(/* webpackChunkName: "dialog" */ './dialog').then((module) => + module.initDialogs(), + ); + } - const nativeDialogs = document.querySelectorAll('.native-dialog'); - if (nativeDialogs.length) { - import(/* webpackChunkName: "native-dialog" */ './native-dialog').then( - (module) => module.initDialogs(nativeDialogs), + const nativeDialogs = document.querySelectorAll('.native-dialog'); + if (nativeDialogs.length) { + import(/* webpackChunkName: "native-dialog" */ './native-dialog').then( + (module) => module.initDialogs(nativeDialogs), + ); + } + + // Yearly reading goal functionality + const setGoalLinks = document.querySelectorAll('.set-reading-goal-link'); + const goalEditLinks = document.querySelectorAll('.edit-reading-goal-link'); + const goalSubmitButtons = document.querySelectorAll( + '.reading-goal-submit-button', ); - } - - // Yearly reading goal functionality - const setGoalLinks = document.querySelectorAll('.set-reading-goal-link'); - const goalEditLinks = document.querySelectorAll('.edit-reading-goal-link'); - const goalSubmitButtons = document.querySelectorAll( - '.reading-goal-submit-button', - ); - const yearElements = document.querySelectorAll('.use-local-year'); - if ( - setGoalLinks.length || + const yearElements = document.querySelectorAll('.use-local-year'); + if ( + setGoalLinks.length || goalEditLinks.length || goalSubmitButtons.length || yearElements.length - ) { - import(/* webpackChunkName: "reading-goals" */ './reading-goals').then( - (module) => { - if (setGoalLinks.length) { - module.initYearlyGoalPrompt(setGoalLinks); - } - if (goalEditLinks.length) { - module.initGoalEditLinks(goalEditLinks); - } - if (goalSubmitButtons.length) { - module.initGoalSubmitButtons(goalSubmitButtons); - } - if (yearElements.length) { - module.displayLocalYear(yearElements); - } - }, - ); - } - - $(document).on('click', '.slide-toggle', function () { - $(`#${$(this).attr('aria-controls')}`).slideToggle(); - }); - - $('#wikiselect').on('focus', function () { - $(this).trigger('select'); - }); - - $('.hamburger-component .mask-menu').on('click', function () { - $('details[open]').not(this).removeAttr('open'); - }); - - $('.header-dropdown').on('keydown', (event) => { - if (event.key === 'Escape') { - $('.header-dropdown > details[open]').removeAttr('open'); - } - }); - - $('.dropdown-menu').each(function () { - $(this) - .find('a') - .last() - .on('focusout', () => { - $('.header-dropdown > details[open]').removeAttr('open'); - }); - }); - - // Open one dropdown at a time. - $(document).on('click', (event) => { - const $openMenus = $('.header-dropdown details[open]').parents( - '.header-dropdown', - ); - $openMenus - .filter((_, menu) => !$(event.target).closest(menu).length) - .find('details') - .removeAttr('open'); - }); - - // Prevent default star rating behavior: - const ratingForms = document.querySelectorAll('.star-rating-form'); - if (ratingForms.length) { - import(/* webpackChunkName: "star-ratings" */ './star-ratings').then( - (module) => module.initRatingHandlers(ratingForms), - ); - } + ) { + import(/* webpackChunkName: "reading-goals" */ './reading-goals').then( + (module) => { + if (setGoalLinks.length) { + module.initYearlyGoalPrompt(setGoalLinks); + } + if (goalEditLinks.length) { + module.initGoalEditLinks(goalEditLinks); + } + if (goalSubmitButtons.length) { + module.initGoalSubmitButtons(goalSubmitButtons); + } + if (yearElements.length) { + module.displayLocalYear(yearElements); + } + }, + ); + } - // Book page navbar initialization: - const navbarWrappers = document.querySelectorAll('.nav-bar-wrapper'); - if (navbarWrappers.length) { - // Add JS for book page navbar: - import(/* webpackChunkName: "nav-bar" */ './edition-nav-bar').then( - (module) => { - module.initNavbars(navbarWrappers); - }, - ); - // Add sticky title component animations to desktop views: - import(/* webpackChunkName: "compact-title" */ './compact-title').then( - (module) => { - const compactTitle = document.querySelector('.compact-title'); - const desktopNavbar = [...navbarWrappers].find((elem) => - elem.classList.contains('desktop-only'), - ); - module.initCompactTitle(desktopNavbar, compactTitle); - }, - ); - } + $(document).on('click', '.slide-toggle', function () { + $(`#${$(this).attr('aria-controls')}`).slideToggle(); + }); - // Add functionality for librarian merge request table: - const librarianQueue = document.querySelector('.librarian-queue-wrapper'); + $('#wikiselect').on('focus', function () { + $(this).trigger('select'); + }); - if (librarianQueue) { - import( - /* webpackChunkName: "merge-request-table" */ './merge-request-table' - ).then((module) => { - module.initLibrarianQueue(librarianQueue); + $('.hamburger-component .mask-menu').on('click', function () { + $('details[open]').not(this).removeAttr('open'); }); - } - // Add functionality to the team page for filtering members: - const teamCards = document.querySelector('.teamCards_container'); - if (teamCards) { - import(/* webpackChunkName "team" */ './team').then((module) => { - module.initTeamFilter(); + $('.header-dropdown').on('keydown', (event) => { + if (event.key === 'Escape') { + $('.header-dropdown > details[open]').removeAttr('open'); + } }); - } - // Add new providers in edit edition view: - const addProviderRowLink = document.querySelector('#add-new-provider-row'); - if (addProviderRowLink) { - import(/* webpackChunkName "add-provider-link" */ './add_provider').then( - (module) => module.initAddProviderRowLink(addProviderRowLink), - ); - } + $('.dropdown-menu').each(function () { + $(this) + .find('a') + .last() + .on('focusout', () => { + $('.header-dropdown > details[open]').removeAttr('open'); + }); + }); - // Allow banner announcements to be dismissed by logged-in users: - const banners = document.querySelectorAll('.page-banner--dismissable'); - if (banners.length) { - import(/* webpackChunkName: "dismissible-banner" */ './banner').then( - (module) => module.initDismissibleBanners(banners), - ); - } + // Open one dropdown at a time. + $(document).on('click', (event) => { + const $openMenus = $('.header-dropdown details[open]').parents( + '.header-dropdown', + ); + $openMenus + .filter((_, menu) => !$(event.target).closest(menu).length) + .find('details') + .removeAttr('open'); + }); - const returnForms = document.querySelectorAll('.return-form'); - if (returnForms.length) { - import(/* webpackChunkName: "return-form" */ './return-form').then( - (module) => module.initReturnForms(returnForms), - ); - } - - const crumbs = document.querySelectorAll('.crumb select'); - if (crumbs.length) { - import( - /* webpackChunkName: "breadcrumb-select" */ './breadcrumb_select' - ).then((module) => module.initBreadcrumbSelect(crumbs)); - } - - const interstitial = document.querySelector('.interstitial'); - if (interstitial) { - import(/* webpackChunkName: "interstitial" */ './interstitial').then( - (module) => module.initInterstitial(interstitial), - ); - } + // Prevent default star rating behavior: + const ratingForms = document.querySelectorAll('.star-rating-form'); + if (ratingForms.length) { + import(/* webpackChunkName: "star-ratings" */ './star-ratings').then( + (module) => module.initRatingHandlers(ratingForms), + ); + } + + // Book page navbar initialization: + const navbarWrappers = document.querySelectorAll('.nav-bar-wrapper'); + if (navbarWrappers.length) { + // Add JS for book page navbar: + import(/* webpackChunkName: "nav-bar" */ './edition-nav-bar').then( + (module) => { + module.initNavbars(navbarWrappers); + }, + ); + // Add sticky title component animations to desktop views: + import(/* webpackChunkName: "compact-title" */ './compact-title').then( + (module) => { + const compactTitle = document.querySelector('.compact-title'); + const desktopNavbar = [...navbarWrappers].find((elem) => + elem.classList.contains('desktop-only'), + ); + module.initCompactTitle(desktopNavbar, compactTitle); + }, + ); + } + + // Add functionality for librarian merge request table: + const librarianQueue = document.querySelector('.librarian-queue-wrapper'); + + if (librarianQueue) { + import( + /* webpackChunkName: "merge-request-table" */ './merge-request-table' + ).then((module) => { + module.initLibrarianQueue(librarianQueue); + }); + } + + // Add functionality to the team page for filtering members: + const teamCards = document.querySelector('.teamCards_container'); + if (teamCards) { + import(/* webpackChunkName "team" */ './team').then((module) => { + module.initTeamFilter(); + }); + } + + // Add new providers in edit edition view: + const addProviderRowLink = document.querySelector('#add-new-provider-row'); + if (addProviderRowLink) { + import(/* webpackChunkName "add-provider-link" */ './add_provider').then( + (module) => module.initAddProviderRowLink(addProviderRowLink), + ); + } + + // Allow banner announcements to be dismissed by logged-in users: + const banners = document.querySelectorAll('.page-banner--dismissable'); + if (banners.length) { + import(/* webpackChunkName: "dismissible-banner" */ './banner').then( + (module) => module.initDismissibleBanners(banners), + ); + } - const leaveWaitlistLinks = document.querySelectorAll('a.leave'); - if ( - leaveWaitlistLinks.length && + const returnForms = document.querySelectorAll('.return-form'); + if (returnForms.length) { + import(/* webpackChunkName: "return-form" */ './return-form').then( + (module) => module.initReturnForms(returnForms), + ); + } + + const crumbs = document.querySelectorAll('.crumb select'); + if (crumbs.length) { + import( + /* webpackChunkName: "breadcrumb-select" */ './breadcrumb_select' + ).then((module) => module.initBreadcrumbSelect(crumbs)); + } + + const interstitial = document.querySelector('.interstitial'); + if (interstitial) { + import(/* webpackChunkName: "interstitial" */ './interstitial').then( + (module) => module.initInterstitial(interstitial), + ); + } + + const leaveWaitlistLinks = document.querySelectorAll('a.leave'); + if ( + leaveWaitlistLinks.length && document.getElementById('leave-waitinglist-dialog') - ) { - import(/* webpackChunkName: "waitlist" */ './waitlist').then((module) => - module.initLeaveWaitlist(leaveWaitlistLinks), - ); - } - - const thirdPartyLoginsIframe = document.getElementById( - 'ia-third-party-logins', - ); - if (thirdPartyLoginsIframe) { - import( - /* webpackChunkName: "ia_thirdparty_logins" */ './ia_thirdparty_logins' - ).then((module) => module.initMessageEventListener(thirdPartyLoginsIframe)); - } - - // Password visibility toggle: - const passwordVisibilityToggle = document.querySelector( - '.password-visibility-toggle', - ); - if (passwordVisibilityToggle) { - import( - /* webpackChunkName: "password-visibility-toggle" */ './password-toggle' - ).then((module) => module.initPasswordToggling(passwordVisibilityToggle)); - } - - // Affiliate links: - const affiliateLinksSection = document.querySelectorAll( - '.affiliate-links-section', - ); - if (affiliateLinksSection.length) { - import(/* webpackChunkName: "affiliate-links" */ './affiliate-links').then( - (module) => module.initAffiliateLinks(affiliateLinksSection), - ); - } - - // Fulltext search box: - const fulltextSearchSuggestion = document.querySelector( - '#fulltext-search-suggestion', - ); - if (fulltextSearchSuggestion) { - import( - /* webpackChunkName: "fulltext-search-suggestion" */ './fulltext-search-suggestion' - ).then((module) => - module.initFulltextSearchSuggestion(fulltextSearchSuggestion), - ); - } + ) { + import(/* webpackChunkName: "waitlist" */ './waitlist').then((module) => + module.initLeaveWaitlist(leaveWaitlistLinks), + ); + } - // Go back redirect: - const backLinks = document.querySelectorAll('.go-back-link'); - if (backLinks.length) { - import(/* webpackChunkName: "go-back-links" */ './go-back-links').then( - (module) => module.initGoBackLinks(backLinks), + const thirdPartyLoginsIframe = document.getElementById( + 'ia-third-party-logins', ); - } + if (thirdPartyLoginsIframe) { + import( + /* webpackChunkName: "ia_thirdparty_logins" */ './ia_thirdparty_logins' + ).then((module) => module.initMessageEventListener(thirdPartyLoginsIframe)); + } - // Lazy-load book page lists section - const listSection = document.querySelector('.lists-section'); - if (listSection) { - import(/* webpackChunkName: "book-page-lists" */ './book-page-lists').then( - (module) => module.initListsSection(listSection), + // Password visibility toggle: + const passwordVisibilityToggle = document.querySelector( + '.password-visibility-toggle', ); - } + if (passwordVisibilityToggle) { + import( + /* webpackChunkName: "password-visibility-toggle" */ './password-toggle' + ).then((module) => module.initPasswordToggling(passwordVisibilityToggle)); + } - // Initialize follow forms lazily - const followForms = document.querySelectorAll('.follow-form'); - if (followForms.length) { - import(/* webpackChunkName: "following" */ './following').then((module) => - module.initAsyncFollowing(followForms), + // Affiliate links: + const affiliateLinksSection = document.querySelectorAll( + '.affiliate-links-section', ); - } + if (affiliateLinksSection.length) { + import(/* webpackChunkName: "affiliate-links" */ './affiliate-links').then( + (module) => module.initAffiliateLinks(affiliateLinksSection), + ); + } - // Generalized carousel lazy-loading - const lazyCarousels = document.querySelectorAll('.lazy-carousel'); - if (lazyCarousels.length) { - import(/* webpackChunkName: "lazy-carousels" */ './lazy-carousel').then( - (module) => module.initLazyCarousel(lazyCarousels), - ); - } - - // Librarian Dashboard - const librarianDashboard = document.querySelector('.librarian-dashboard'); - if (librarianDashboard) { - import( - /* webpackChunkName: "librarian-dashboard" */ './librarian-dashboard' - ).then((module) => module.initLibrarianDashboard(librarianDashboard)); - } - - // List books - if (document.querySelector('.list-books')) { - import(/* webpackChunkName: "list-books" */ './list_books').then((module) => - module.ListBooks.init(), + // Fulltext search box: + const fulltextSearchSuggestion = document.querySelector( + '#fulltext-search-suggestion', ); - } + if (fulltextSearchSuggestion) { + import( + /* webpackChunkName: "fulltext-search-suggestion" */ './fulltext-search-suggestion' + ).then((module) => + module.initFulltextSearchSuggestion(fulltextSearchSuggestion), + ); + } - // Stats page login counts - const monthlyLoginStats = document.querySelector('.monthly-login-counts'); - if (monthlyLoginStats) { - import(/* webpackChunkName: "stats" */ './stats').then((module) => - module.initUniqueLoginCounts(monthlyLoginStats), - ); - } + // Go back redirect: + const backLinks = document.querySelectorAll('.go-back-link'); + if (backLinks.length) { + import(/* webpackChunkName: "go-back-links" */ './go-back-links').then( + (module) => module.initGoBackLinks(backLinks), + ); + } + + // Lazy-load book page lists section + const listSection = document.querySelector('.lists-section'); + if (listSection) { + import(/* webpackChunkName: "book-page-lists" */ './book-page-lists').then( + (module) => module.initListsSection(listSection), + ); + } + + // Initialize follow forms lazily + const followForms = document.querySelectorAll('.follow-form'); + if (followForms.length) { + import(/* webpackChunkName: "following" */ './following').then((module) => + module.initAsyncFollowing(followForms), + ); + } + + // Generalized carousel lazy-loading + const lazyCarousels = document.querySelectorAll('.lazy-carousel'); + if (lazyCarousels.length) { + import(/* webpackChunkName: "lazy-carousels" */ './lazy-carousel').then( + (module) => module.initLazyCarousel(lazyCarousels), + ); + } + + // Librarian Dashboard + const librarianDashboard = document.querySelector('.librarian-dashboard'); + if (librarianDashboard) { + import( + /* webpackChunkName: "librarian-dashboard" */ './librarian-dashboard' + ).then((module) => module.initLibrarianDashboard(librarianDashboard)); + } + + // List books + if (document.querySelector('.list-books')) { + import(/* webpackChunkName: "list-books" */ './list_books').then((module) => + module.ListBooks.init(), + ); + } + + // Stats page login counts + const monthlyLoginStats = document.querySelector('.monthly-login-counts'); + if (monthlyLoginStats) { + import(/* webpackChunkName: "stats" */ './stats').then((module) => + module.initUniqueLoginCounts(monthlyLoginStats), + ); + } }); diff --git a/openlibrary/plugins/openlibrary/js/interstitial.js b/openlibrary/plugins/openlibrary/js/interstitial.js index 29c212c0c1f..a7fbc577c9c 100644 --- a/openlibrary/plugins/openlibrary/js/interstitial.js +++ b/openlibrary/plugins/openlibrary/js/interstitial.js @@ -1,21 +1,21 @@ export function initInterstitial(elem) { - let seconds = elem.dataset.wait; - const url = elem.dataset.url; - const timerElement = elem.querySelector('#timer'); - const countdown = setInterval(() => { - seconds--; - timerElement.textContent = seconds; - if (seconds === 0) { - clearInterval(countdown); - window.location.href = url; - } - }, 1000); // 1 second interval + let seconds = elem.dataset.wait; + const url = elem.dataset.url; + const timerElement = elem.querySelector('#timer'); + const countdown = setInterval(() => { + seconds--; + timerElement.textContent = seconds; + if (seconds === 0) { + clearInterval(countdown); + window.location.href = url; + } + }, 1000); // 1 second interval - // Add cancel button handler - const cancelButton = elem.querySelector('.close-window'); - if (cancelButton) { - cancelButton.addEventListener('click', () => { - window.close(); - }); - } + // Add cancel button handler + const cancelButton = elem.querySelector('.close-window'); + if (cancelButton) { + cancelButton.addEventListener('click', () => { + window.close(); + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/isbnOverride.js b/openlibrary/plugins/openlibrary/js/isbnOverride.js index 7e2976c96b3..4cd039cb5d1 100644 --- a/openlibrary/plugins/openlibrary/js/isbnOverride.js +++ b/openlibrary/plugins/openlibrary/js/isbnOverride.js @@ -8,14 +8,14 @@ * @property {Function} clear - clears the ISBN object */ export const isbnOverride = { - data: null, - set(isbnData) { - this.data = isbnData; - }, - get() { - return this.data; - }, - clear() { - this.data = null; - }, + data: null, + set(isbnData) { + this.data = isbnData; + }, + get() { + return this.data; + }, + clear() { + this.data = null; + }, }; diff --git a/openlibrary/plugins/openlibrary/js/jquery.repeat.js b/openlibrary/plugins/openlibrary/js/jquery.repeat.js index 76e81fc46bd..36f9642bf31 100644 --- a/openlibrary/plugins/openlibrary/js/jquery.repeat.js +++ b/openlibrary/plugins/openlibrary/js/jquery.repeat.js @@ -7,108 +7,108 @@ import Template from './template'; * Used in addbook process. */ export function init() { - // used in books/edit/exercpt, books/edit/web and books/edit/edition - $.fn.repeat = function (options) { - var addSelector, removeSelector, id, elems, t, code, nextRowId; - options = options || {}; + // used in books/edit/exercpt, books/edit/web and books/edit/edition + $.fn.repeat = function (options) { + var addSelector, removeSelector, id, elems, t, code, nextRowId; + options = options || {}; - id = `#${this.attr('id')}`; - elems = { - _this: this, - add: $(`${id}-add`), - form: $(`${id}-form`), - display: $(`${id}-display`), - template: $(`${id}-template`), - }; + id = `#${this.attr('id')}`; + elems = { + _this: this, + add: $(`${id}-add`), + form: $(`${id}-form`), + display: $(`${id}-display`), + template: $(`${id}-template`), + }; - function createTemplate(selector) { - code = $(selector) - .html() - .replace(/%7B%7B/gi, '<%=') - .replace(/%7D%7D/gi, '%>') - .replace(/{{/g, '<%=') - .replace(/}}/g, '%>'); - return Template(code); - } + function createTemplate(selector) { + code = $(selector) + .html() + .replace(/%7B%7B/gi, '<%=') + .replace(/%7D%7D/gi, '%>') + .replace(/{{/g, '<%=') + .replace(/}}/g, '%>'); + return Template(code); + } - t = createTemplate(`${id}-template`); + t = createTemplate(`${id}-template`); - /** + /** * Search elems.form for input fields and create an * object representing. * @return {object} data mapping names to values */ - function formdata() { - var data = {}; - $(':input', elems.form).each(function () { - var $e = $(this), - name = $e.attr('name'), - type = $e.attr('type'), - _id = $e.attr('id'); + function formdata() { + var data = {}; + $(':input', elems.form).each(function () { + var $e = $(this), + name = $e.attr('name'), + type = $e.attr('type'), + _id = $e.attr('id'); - data[name] = $e.val().trim(); + data[name] = $e.val().trim(); - if (type === 'text' && _id === 'id-value') { - $e.val(''); + if (type === 'text' && _id === 'id-value') { + $e.val(''); + } + }); + return data; } - }); - return data; - } - /** + /** * triggered when "add link" button is clicked on author edit field. * Creates a removable `repeat-item`. * @param {jQuery.Event} event */ - function onAdd(event) { - var data, newid; - const isbnOverrideData = isbnOverride.get(); - event.preventDefault(); + function onAdd(event) { + var data, newid; + const isbnOverrideData = isbnOverride.get(); + event.preventDefault(); - // if no index, set it to the number of children - if (!nextRowId) { - nextRowId = elems.display.children().length; - } + // if no index, set it to the number of children + if (!nextRowId) { + nextRowId = elems.display.children().length; + } - // If a user confirms adding an ISBN with a failed checksum in - // js/edit.js, the {data} object is filled from the - // isbnOverrideData object rather than the input form. - if (isbnOverrideData) { - data = isbnOverrideData; - isbnOverride.clear(); - } else { - data = formdata(); - data.index = nextRowId; + // If a user confirms adding an ISBN with a failed checksum in + // js/edit.js, the {data} object is filled from the + // isbnOverrideData object rather than the input form. + if (isbnOverrideData) { + data = isbnOverrideData; + isbnOverride.clear(); + } else { + data = formdata(); + data.index = nextRowId; - if (options.validate && options.validate(data) === false) { - return; - } - } + if (options.validate && options.validate(data) === false) { + return; + } + } - $.extend(data, options.vars || {}); + $.extend(data, options.vars || {}); - newid = `${elems._this.attr('id')}--${nextRowId}`; - // increment the index to avoid situations where more than one element have same - nextRowId++; - // Create the HTML of a hidden input - elems.template - .clone() - .attr('id', newid) - .html(t(data)) - .show() - .appendTo(elems.display); + newid = `${elems._this.attr('id')}--${nextRowId}`; + // increment the index to avoid situations where more than one element have same + nextRowId++; + // Create the HTML of a hidden input + elems.template + .clone() + .attr('id', newid) + .html(t(data)) + .show() + .appendTo(elems.display); - elems._this.trigger('repeat-add'); - } - function onRemove(event) { - event.preventDefault(); - $(this).parents('.repeat-item').eq(0).remove(); - elems._this.trigger('repeat-remove'); - } - addSelector = `${id} .repeat-add`; - removeSelector = `${id} .repeat-remove`; - // Click handlers should apply to newly created add/remove selectors - $(document).on('click', addSelector, onAdd); - $(document).on('click', removeSelector, onRemove); - }; + elems._this.trigger('repeat-add'); + } + function onRemove(event) { + event.preventDefault(); + $(this).parents('.repeat-item').eq(0).remove(); + elems._this.trigger('repeat-remove'); + } + addSelector = `${id} .repeat-add`; + removeSelector = `${id} .repeat-remove`; + // Click handlers should apply to newly created add/remove selectors + $(document).on('click', addSelector, onAdd); + $(document).on('click', removeSelector, onRemove); + }; } diff --git a/openlibrary/plugins/openlibrary/js/jsdef.js b/openlibrary/plugins/openlibrary/js/jsdef.js index 65ca9d44711..e31b72bac4e 100644 --- a/openlibrary/plugins/openlibrary/js/jsdef.js +++ b/openlibrary/plugins/openlibrary/js/jsdef.js @@ -22,18 +22,18 @@ import { cond, truncate } from './utils'; //used in templates/lib/pagination.html export function range(begin, end, step) { - var r, i; - step = step || 1; - if (end === undefined) { - end = begin; - begin = 0; - } - - r = []; - for (i = begin; i < end; i += step) { - r[r.length] = i; - } - return r; + var r, i; + step = step || 1; + if (end === undefined) { + end = begin; + begin = 0; + } + + r = []; + for (i = begin; i < end; i += step) { + r[r.length] = i; + } + return r; } /** @@ -43,7 +43,7 @@ export function range(begin, end, step) { * a - b - c */ export function join(items) { - return items.join(this); + return items.join(this); } /** @@ -52,79 +52,79 @@ export function join(items) { // used in templates/admin/loans.html export function len(array) { - return array.length; + return array.length; } // used in templates/type/permission/edit.html export function enumerate(a) { - var b = new Array(a.length); - var i; - for (i in a) { - b[i] = [i, a[i]]; - } - return b; + var b = new Array(a.length); + var i; + for (i in a) { + b[i] = [i, a[i]]; + } + return b; } export function ForLoop(parent, seq) { - this.parent = parent; - this.seq = seq; + this.parent = parent; + this.seq = seq; - this.length = seq.length; - this.index0 = -1; + this.length = seq.length; + this.index0 = -1; } ForLoop.prototype.next = function () { - var i = this.index0 + 1; + var i = this.index0 + 1; - this.index0 = i; - this.index = i + 1; + this.index0 = i; + this.index = i + 1; - this.first = i === 0; - this.last = i === this.length - 1; + this.first = i === 0; + this.last = i === this.length - 1; - this.odd = this.index % 2 === 1; - this.even = this.index % 2 === 0; - this.parity = ['even', 'odd'][this.index % 2]; + this.odd = this.index % 2 === 1; + this.even = this.index % 2 === 0; + this.parity = ['even', 'odd'][this.index % 2]; - this.revindex0 = this.length - i; - this.revindex = this.length - i + 1; + this.revindex0 = this.length - i; + this.revindex = this.length - i + 1; }; // used in plugins/upstream/jsdef.py export function foreach(seq, parent_loop, callback) { - var loop = new ForLoop(parent_loop, seq); - var i, args, j; - - for (i = 0; i < seq.length; i++) { - loop.next(); - - args = [loop]; - - // case of "for a, b in ..." - if (callback.length > 2) { - for (j in seq[i]) { - args.push(seq[i][j]); - } - } else { - args[1] = seq[i]; + var loop = new ForLoop(parent_loop, seq); + var i, args, j; + + for (i = 0; i < seq.length; i++) { + loop.next(); + + args = [loop]; + + // case of "for a, b in ..." + if (callback.length > 2) { + for (j in seq[i]) { + args.push(seq[i][j]); + } + } else { + args[1] = seq[i]; + } + callback.apply(this, args); } - callback.apply(this, args); - } } // used in templates/lists/widget.html export function websafe(value) { - // Safari 6 is failing with weird javascript error in this function. - // Added try-catch to avoid it. - try { - if (value === null || value === undefined) { - return ''; - } else { - return htmlquote(value.toString()); + // Safari 6 is failing with weird javascript error in this function. + // Added try-catch to avoid it. + try { + if (value === null || value === undefined) { + return ''; + } else { + return htmlquote(value.toString()); + } + } catch (e) { + return ''; } - } catch (e) { - return ''; - } } /** @@ -133,18 +133,18 @@ export function websafe(value) { * @param {string|number} text to quote */ export function htmlquote(text) { - // This code exists for compatibility with template.js - text = String(text); - text = text.replace(/&/g, '&'); // Must be done first! - text = text.replace(/</g, '<'); - text = text.replace(/>/g, '>'); - text = text.replace(/'/g, '''); - text = text.replace(/"/g, '"'); - return text; + // This code exists for compatibility with template.js + text = String(text); + text = text.replace(/&/g, '&'); // Must be done first! + text = text.replace(/</g, '<'); + text = text.replace(/>/g, '>'); + text = text.replace(/'/g, '''); + text = text.replace(/"/g, '"'); + return text; } export function is_jsdef() { - return true; + return true; } /** @@ -157,27 +157,27 @@ export function is_jsdef() { * @param {any} def - the default value to return if the key isn't found */ export function jsdef_get(obj, key, def = null) { - return key in obj ? obj[key] : def; + return key in obj ? obj[key] : def; } export function exposeGlobally() { - // Extend existing prototypes - String.prototype.join = join; - - window.commify = commify; - window.cond = cond; - window.enumerate = enumerate; - window.foreach = foreach; - window.htmlquote = htmlquote; - window.jsdef_get = jsdef_get; - window.len = len; - window.range = range; - window.slice = slice; - window.sprintf = sprintf; - window.truncate = truncate; - window.urlencode = urlencode; - window.websafe = websafe; - window._ = ugettext; - window.ungettext = ungettext; - window.uggettext = ugettext; + // Extend existing prototypes + String.prototype.join = join; + + window.commify = commify; + window.cond = cond; + window.enumerate = enumerate; + window.foreach = foreach; + window.htmlquote = htmlquote; + window.jsdef_get = jsdef_get; + window.len = len; + window.range = range; + window.slice = slice; + window.sprintf = sprintf; + window.truncate = truncate; + window.urlencode = urlencode; + window.websafe = websafe; + window._ = ugettext; + window.ungettext = ungettext; + window.uggettext = ugettext; } diff --git a/openlibrary/plugins/openlibrary/js/lazy-carousel.js b/openlibrary/plugins/openlibrary/js/lazy-carousel.js index 66b9ad9d7cf..aba66337370 100644 --- a/openlibrary/plugins/openlibrary/js/lazy-carousel.js +++ b/openlibrary/plugins/openlibrary/js/lazy-carousel.js @@ -8,24 +8,24 @@ import { buildPartialsUrl } from './utils'; * @param elems {NodeList<HTMLElement>} Collection of placeholder carousel elements */ export function initLazyCarousel(elems) { - // Create intersection observer - const intersectionObserver = new IntersectionObserver(intersectionCallback, { - root: null, - rootMargin: '200px', - threshold: 0, - }); + // Create intersection observer + const intersectionObserver = new IntersectionObserver(intersectionCallback, { + root: null, + rootMargin: '200px', + threshold: 0, + }); - elems.forEach((elem) => { + elems.forEach((elem) => { // Observe element for intersections - intersectionObserver.observe(elem); + intersectionObserver.observe(elem); - // Add retry listener - const retryElem = elem.querySelector('.retry-btn'); - retryElem.addEventListener('click', (e) => { - e.preventDefault(); - handleRetry(elem); + // Add retry listener + const retryElem = elem.querySelector('.retry-btn'); + retryElem.addEventListener('click', (e) => { + e.preventDefault(); + handleRetry(elem); + }); }); - }); } /** @@ -35,7 +35,7 @@ export function initLazyCarousel(elems) { * @returns {Promise<Response>} */ async function fetchPartials(data) { - return fetch(buildPartialsUrl('LazyCarousel', { ...data })); + return fetch(buildPartialsUrl('LazyCarousel', { ...data })); } /** @@ -50,47 +50,47 @@ async function fetchPartials(data) { * @param target {HTMLElement} A placeholder element for a carousel */ function doFetchAndUpdate(target) { - const config = JSON.parse(target.dataset.config); - const loadingIndicator = target.querySelector('.loadingIndicator'); + const config = JSON.parse(target.dataset.config); + const loadingIndicator = target.querySelector('.loadingIndicator'); - fetchPartials(config) - .then((resp) => { - if (!resp.ok) { - throw new Error('Failed to fetch partials from server'); - } - return resp.json(); - }) - .then((data) => { - const newElem = document.createElement('div'); - newElem.innerHTML = data.partials.trim(); - const carouselElements = newElem.querySelectorAll( - '.carousel--progressively-enhanced', - ); - loadingIndicator.classList.add('hidden'); + fetchPartials(config) + .then((resp) => { + if (!resp.ok) { + throw new Error('Failed to fetch partials from server'); + } + return resp.json(); + }) + .then((data) => { + const newElem = document.createElement('div'); + newElem.innerHTML = data.partials.trim(); + const carouselElements = newElem.querySelectorAll( + '.carousel--progressively-enhanced', + ); + loadingIndicator.classList.add('hidden'); - if (carouselElements.length === 0 && config.fallback) { - // No results, disable filters - if (typeof config.fallback === 'string') { - config.query = config.fallback; - } - config.has_fulltext_only = false; - config.fallback = false; // Prevents infinite retries - target.dataset.config = JSON.stringify(config); + if (carouselElements.length === 0 && config.fallback) { + // No results, disable filters + if (typeof config.fallback === 'string') { + config.query = config.fallback; + } + config.has_fulltext_only = false; + config.fallback = false; // Prevents infinite retries + target.dataset.config = JSON.stringify(config); - target - .querySelector('.lazy-carousel-fallback') - .classList.remove('hidden'); - } else { - target.parentNode.insertBefore(newElem, target); - target.remove(); - initialzeCarousels(carouselElements); - } - }) - .catch(() => { - loadingIndicator.classList.add('hidden'); - const retryElem = target.querySelector('.lazy-carousel-retry'); - retryElem.classList.remove('hidden'); - }); + target + .querySelector('.lazy-carousel-fallback') + .classList.remove('hidden'); + } else { + target.parentNode.insertBefore(newElem, target); + target.remove(); + initialzeCarousels(carouselElements); + } + }) + .catch(() => { + loadingIndicator.classList.add('hidden'); + const retryElem = target.querySelector('.lazy-carousel-retry'); + retryElem.classList.remove('hidden'); + }); } /** @@ -100,13 +100,13 @@ function doFetchAndUpdate(target) { * @param target {Element} */ function handleRetry(target) { - target.querySelector('.loadingIndicator').classList.remove('hidden'); - target.querySelector('.lazy-carousel-retry').classList.add('hidden'); - const carouselFallbackElem = target.querySelector('.lazy-carousel-fallback'); - if (carouselFallbackElem) { - carouselFallbackElem.classList.add('hidden'); - } - doFetchAndUpdate(target); + target.querySelector('.loadingIndicator').classList.remove('hidden'); + target.querySelector('.lazy-carousel-retry').classList.add('hidden'); + const carouselFallbackElem = target.querySelector('.lazy-carousel-fallback'); + if (carouselFallbackElem) { + carouselFallbackElem.classList.add('hidden'); + } + doFetchAndUpdate(target); } /** @@ -118,11 +118,11 @@ function handleRetry(target) { * @param observer {IntersectionObserver} */ function intersectionCallback(entries, observer) { - entries.forEach((entry) => { - if (entry.isIntersecting) { - const target = entry.target; - observer.unobserve(target); - doFetchAndUpdate(target); - } - }); + entries.forEach((entry) => { + if (entry.isIntersecting) { + const target = entry.target; + observer.unobserve(target); + doFetchAndUpdate(target); + } + }); } diff --git a/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js b/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js index eebde3583c5..132306d0d10 100644 --- a/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js +++ b/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js @@ -19,132 +19,132 @@ import debounce from 'lodash/debounce'; * Currently only works with works, editions, and authors. */ export class LazyThingPreview { - constructor() { + constructor() { /** @type {Array<{key: string, render_fn: Function}>} */ - this.queue = []; - /** @type {Object<string, object>} */ - this.cache = {}; + this.queue = []; + /** @type {Object<string, object>} */ + this.cache = {}; - this.renderDebounced = debounce(this.render.bind(this), 100); - } + this.renderDebounced = debounce(this.render.bind(this), 100); + } - init() { - $('.lazy-thing-preview').each((i, el) => { - this.push({ - key: el.dataset.key, - render_fn_name: el.dataset.renderFn, - }); - }); - } + init() { + $('.lazy-thing-preview').each((i, el) => { + this.push({ + key: el.dataset.key, + render_fn_name: el.dataset.renderFn, + }); + }); + } - /** + /** * @param {{key: string, render_fn_name: string}} arg0 */ - push({ key, render_fn_name }) { - const render_fn = window[render_fn_name]; - if (this.cache[key]) { - this.renderKey(key, render_fn, this.cache[key]); - } else { - this.queue.push({ key, render_fn }); - this.renderDebounced(); + push({ key, render_fn_name }) { + const render_fn = window[render_fn_name]; + if (this.cache[key]) { + this.renderKey(key, render_fn, this.cache[key]); + } else { + this.queue.push({ key, render_fn }); + this.renderDebounced(); + } } - } - /** + /** * @param {string} key * @param {Function} render_fn * @param {object} book */ - renderKey(key, render_fn, book) { - const $el = $(`.lazy-thing-preview[data-key="${key}"]`); - $el.html(render_fn(book)); - } + renderKey(key, render_fn, book) { + const $el = $(`.lazy-thing-preview[data-key="${key}"]`); + $el.html(render_fn(book)); + } - /** + /** * @param {string[]} keys * @returns {AsyncGenerator<object[]>} */ - async *getThings(keys) { - const workKeys = keys.filter((key) => key.startsWith('/works/')); - const editionKeys = keys.filter((key) => key.startsWith('/books/')); - const authorKeys = keys.filter((key) => key.startsWith('/authors/')); - const fields = + async *getThings(keys) { + const workKeys = keys.filter((key) => key.startsWith('/works/')); + const editionKeys = keys.filter((key) => key.startsWith('/books/')); + const authorKeys = keys.filter((key) => key.startsWith('/authors/')); + const fields = 'key,type,cover_i,first_publish_year,author_name,title,subtitle,edition_count,editions'; - for (const keys of chunk(workKeys, 100)) { - const resp = await fetch( - `/search.json?${new URLSearchParams({ - q: `key:(${keys.join(' OR ')})`, - fields, - limit: '100', - })}`, - ).then((r) => r.json()); - yield resp.docs; - } - for (const keys of chunk(editionKeys, 100)) { - const resp = await fetch( - `/search.json?${new URLSearchParams({ - q: `edition_key:(${keys - .map((key) => key.split('/').pop()) - .join(' OR ')})`, - fields, - limit: '100', - })}`, - ).then((r) => r.json()); - yield resp.docs; - } - for (const keys of chunk(authorKeys, 100)) { - const resp = await fetch( - `/search/authors.json?${new URLSearchParams({ - q: `key:(${keys.join(' OR ')})`, - fields: 'key,type,name,top_work,top_subjects,birth_date,death_date', - limit: '100', - })}`, - ).then((r) => r.json()); - for (const doc of resp.docs) { - // This API returns keys without the /authors/ prefix 😭 - doc.key = `/authors/${doc.key}`; - } - yield resp.docs; + for (const keys of chunk(workKeys, 100)) { + const resp = await fetch( + `/search.json?${new URLSearchParams({ + q: `key:(${keys.join(' OR ')})`, + fields, + limit: '100', + })}`, + ).then((r) => r.json()); + yield resp.docs; + } + for (const keys of chunk(editionKeys, 100)) { + const resp = await fetch( + `/search.json?${new URLSearchParams({ + q: `edition_key:(${keys + .map((key) => key.split('/').pop()) + .join(' OR ')})`, + fields, + limit: '100', + })}`, + ).then((r) => r.json()); + yield resp.docs; + } + for (const keys of chunk(authorKeys, 100)) { + const resp = await fetch( + `/search/authors.json?${new URLSearchParams({ + q: `key:(${keys.join(' OR ')})`, + fields: 'key,type,name,top_work,top_subjects,birth_date,death_date', + limit: '100', + })}`, + ).then((r) => r.json()); + for (const doc of resp.docs) { + // This API returns keys without the /authors/ prefix 😭 + doc.key = `/authors/${doc.key}`; + } + yield resp.docs; + } } - } - async render() { - const keys = this.queue.map(({ key }) => key); - const render_fn_map = Object.fromEntries( - this.queue.map(({ key, render_fn }) => [key, render_fn]), - ); - for await (const thingBatch of this.getThings(keys)) { - for (const thing of thingBatch) { - this.cache[thing.key] = thing; - if (thing.type === 'work') { - const book = thing; - book.full_title = book.subtitle - ? `${book.title}: ${book.subtitle}` - : book.title; - if (book.editions.docs.length) { - const ed = book.editions.docs[0]; - ed.full_title = ed.subtitle - ? `${ed.title}: ${ed.subtitle}` - : ed.title; - ed.author_name = book.author_name; - ed.edition_count = book.edition_count; - this.cache[ed.key] = ed; + async render() { + const keys = this.queue.map(({ key }) => key); + const render_fn_map = Object.fromEntries( + this.queue.map(({ key, render_fn }) => [key, render_fn]), + ); + for await (const thingBatch of this.getThings(keys)) { + for (const thing of thingBatch) { + this.cache[thing.key] = thing; + if (thing.type === 'work') { + const book = thing; + book.full_title = book.subtitle + ? `${book.title}: ${book.subtitle}` + : book.title; + if (book.editions.docs.length) { + const ed = book.editions.docs[0]; + ed.full_title = ed.subtitle + ? `${ed.title}: ${ed.subtitle}` + : ed.title; + ed.author_name = book.author_name; + ed.edition_count = book.edition_count; + this.cache[ed.key] = ed; - if (ed.key in render_fn_map) { - this.renderKey(ed.key, render_fn_map[ed.key], ed); + if (ed.key in render_fn_map) { + this.renderKey(ed.key, render_fn_map[ed.key], ed); + } + } + } + + if (thing.key in render_fn_map) { + this.renderKey(thing.key, render_fn_map[thing.key], thing); + } } - } } - if (thing.key in render_fn_map) { - this.renderKey(thing.key, render_fn_map[thing.key], thing); - } - } + const missingKeys = keys.filter((key) => !this.cache[key]); + // eslint-disable-next-line no-console + console.warn('Books missing from cache', missingKeys); + this.queue = []; } - - const missingKeys = keys.filter((key) => !this.cache[key]); - // eslint-disable-next-line no-console - console.warn('Books missing from cache', missingKeys); - this.queue = []; - } } diff --git a/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js b/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js index 9d7a967720a..ffa0d6287ea 100644 --- a/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js +++ b/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js @@ -9,15 +9,15 @@ let i18nStrings; * @param {HTMLDetailsElement} rootElement */ export function initLibrarianDashboard(rootElement) { - i18nStrings = JSON.parse(rootElement.dataset.i18n); - const table = rootElement.querySelector('.dq-table'); - rootElement.addEventListener( - 'click', - () => { - populateTable(table); - }, - { once: true }, - ); + i18nStrings = JSON.parse(rootElement.dataset.i18n); + const table = rootElement.querySelector('.dq-table'); + rootElement.addEventListener( + 'click', + () => { + populateTable(table); + }, + { once: true }, + ); } /** @@ -27,10 +27,10 @@ export function initLibrarianDashboard(rootElement) { * @returns {Promise<void>} */ async function populateTable(table) { - const bookCount = Number(table.dataset.totalBooks); - const rows = table.querySelectorAll('.dq-table__row'); + const bookCount = Number(table.dataset.totalBooks); + const rows = table.querySelectorAll('.dq-table__row'); - await Promise.all([...rows].map((row) => updateRow(row, bookCount))); + await Promise.all([...rows].map((row) => updateRow(row, bookCount))); } /** @@ -41,44 +41,44 @@ async function populateTable(table) { * @returns {Promise<void>} */ async function updateRow(row, totalCount) { - const queryFragment = row.dataset.queryFragment; - const apiUrl = buildUrl(queryFragment, false); - const searchPageUrl = buildUrl(queryFragment); - - // Make query - const data = await fetch(apiUrl) - .then((resp) => { - if (!resp.ok) { - throw new Error(`Data quality response status : ${resp.status}`); - } - return resp.json(); - }) - .catch(() => { - return null; - }); - - // Render status cell markup - let newCellMarkup; - if (data === null) { - newCellMarkup = renderErrorCell(searchPageUrl); - } else { - newCellMarkup = renderResultsCells(data, totalCount, searchPageUrl); - } - - // Include retry affordance, regardless of result - newCellMarkup += renderRetryCell(); - - replaceStatusCells(row, newCellMarkup); - - // Add listener to retry affordance - const retryAffordance = row.querySelector('.dqs-run-again'); - retryAffordance.addEventListener('click', () => { + const queryFragment = row.dataset.queryFragment; + const apiUrl = buildUrl(queryFragment, false); + const searchPageUrl = buildUrl(queryFragment); + + // Make query + const data = await fetch(apiUrl) + .then((resp) => { + if (!resp.ok) { + throw new Error(`Data quality response status : ${resp.status}`); + } + return resp.json(); + }) + .catch(() => { + return null; + }); + + // Render status cell markup + let newCellMarkup; + if (data === null) { + newCellMarkup = renderErrorCell(searchPageUrl); + } else { + newCellMarkup = renderResultsCells(data, totalCount, searchPageUrl); + } + + // Include retry affordance, regardless of result + newCellMarkup += renderRetryCell(); + + replaceStatusCells(row, newCellMarkup); + + // Add listener to retry affordance + const retryAffordance = row.querySelector('.dqs-run-again'); + retryAffordance.addEventListener('click', () => { // Update view to "pending" - replaceStatusCells(row, renderPendingCell()); + replaceStatusCells(row, renderPendingCell()); - // Retry query - updateRow(row, totalCount); - }); + // Retry query + updateRow(row, totalCount); + }); } /** @@ -88,14 +88,14 @@ async function updateRow(row, totalCount) { * @param {boolean} forUi */ function buildUrl(queryFragment, forUi = true) { - const match = window.location.pathname.match(/authors\/(OL\d+A)/); - const queryParamString = match - ? `?q=author_key:${match[1]}` - : window.location.search; - - const params = new URLSearchParams(queryParamString); - params.set('q', `${params.get('q')} ${queryFragment}`); - return `/search${forUi ? '' : '.json'}?${params.toString()}`; + const match = window.location.pathname.match(/authors\/(OL\d+A)/); + const queryParamString = match + ? `?q=author_key:${match[1]}` + : window.location.search; + + const params = new URLSearchParams(queryParamString); + params.set('q', `${params.get('q')} ${queryFragment}`); + return `/search${forUi ? '' : '.json'}?${params.toString()}`; } /** @@ -105,14 +105,14 @@ function buildUrl(queryFragment, forUi = true) { * @param {string} newCellMarkup Markup for the new status cells */ function replaceStatusCells(row, newCellMarkup) { - const statusCells = row.querySelectorAll('td:not(.dq-table__criterion-cell)'); - for (const cell of statusCells) { - cell.remove(); - } - - const template = document.createElement('template'); - template.innerHTML = newCellMarkup; - row.append(...template.content.children); + const statusCells = row.querySelectorAll('td:not(.dq-table__criterion-cell)'); + for (const cell of statusCells) { + cell.remove(); + } + + const template = document.createElement('template'); + template.innerHTML = newCellMarkup; + row.append(...template.content.children); } /** @@ -125,10 +125,10 @@ function replaceStatusCells(row, newCellMarkup) { * @returns {string} HTML string */ function renderResultsCells(results, totalCount, failingHref) { - const numFound = results.numFound; - const percentage = Math.floor(((totalCount - numFound) / totalCount) * 100); + const numFound = results.numFound; + const percentage = Math.floor(((totalCount - numFound) / totalCount) * 100); - return `<td class="dq-table__results-cell"> + return `<td class="dq-table__results-cell"> <meter title="${numFound} of ${totalCount}" min="0" max="100" value="${percentage}"></meter> <span>${percentage}%</span> </td> @@ -143,7 +143,7 @@ function renderResultsCells(results, totalCount, failingHref) { * @returns {string} HTML string */ function renderRetryCell() { - return `<td> + return `<td> <button class="dqs-run-again"> ${i18nStrings['reload']} </button> @@ -157,7 +157,7 @@ function renderRetryCell() { * @returns {string} */ function renderErrorCell(href) { - return `<td colspan="2"> + return `<td colspan="2"> <a href="${href}">${i18nStrings['error']}</a> </td>`; } @@ -168,5 +168,5 @@ function renderErrorCell(href) { * @returns {string} */ function renderPendingCell() { - return `<td colspan="3">${i18nStrings['loading']}</td>`; + return `<td colspan="3">${i18nStrings['loading']}</td>`; } diff --git a/openlibrary/plugins/openlibrary/js/list_books.js b/openlibrary/plugins/openlibrary/js/list_books.js index 438b29f82f1..2455fec6099 100644 --- a/openlibrary/plugins/openlibrary/js/list_books.js +++ b/openlibrary/plugins/openlibrary/js/list_books.js @@ -1,37 +1,37 @@ export class ListBooks { - /** + /** * @param {HTMLElement} listBooks * @param {HTMLElement} layoutToolbar **/ - constructor(listBooks, layoutToolbar) { - this.listBooks = listBooks; - this.layoutToolbar = layoutToolbar; + constructor(listBooks, layoutToolbar) { + this.listBooks = listBooks; + this.layoutToolbar = layoutToolbar; - this.activeLayout = this.layoutToolbar.querySelector('a.active'); - } + this.activeLayout = this.layoutToolbar.querySelector('a.active'); + } - attach() { - $(this.layoutToolbar).on('click', 'a', this.updateLayout.bind(this)); - } + attach() { + $(this.layoutToolbar).on('click', 'a', this.updateLayout.bind(this)); + } - /** + /** * @param {MouseEvent} event */ - updateLayout(event) { - event.preventDefault(); - const layoutAnchor = event.target; - this.layoutToolbar.querySelector('a.active').classList.remove('active'); - layoutAnchor.classList.add('active'); - const layout = layoutAnchor.dataset.value; - this.listBooks.classList.toggle('list-books--grid', layout === 'grid'); - document.cookie = `LBL=${layout}; path=/; max-age=31536000`; - } + updateLayout(event) { + event.preventDefault(); + const layoutAnchor = event.target; + this.layoutToolbar.querySelector('a.active').classList.remove('active'); + layoutAnchor.classList.add('active'); + const layout = layoutAnchor.dataset.value; + this.listBooks.classList.toggle('list-books--grid', layout === 'grid'); + document.cookie = `LBL=${layout}; path=/; max-age=31536000`; + } - static init() { + static init() { // Assume only one list-books/layout per page - new ListBooks( - document.querySelector('.list-books'), - document.querySelector('.tools--layout'), - ).attach(); - } + new ListBooks( + document.querySelector('.list-books'), + document.querySelector('.tools--layout'), + ).attach(); + } } diff --git a/openlibrary/plugins/openlibrary/js/lists/ListService.js b/openlibrary/plugins/openlibrary/js/lists/ListService.js index 434cf4446ad..7f501321c2c 100644 --- a/openlibrary/plugins/openlibrary/js/lists/ListService.js +++ b/openlibrary/plugins/openlibrary/js/lists/ListService.js @@ -13,14 +13,14 @@ import { buildPartialsUrl } from '../utils'; * @returns {Promise<Response>} The results of the POST request */ export async function createList(userKey, data) { - return await fetch(`${userKey}/lists.json`, { - method: 'post', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: JSON.stringify(data), - }); + return await fetch(`${userKey}/lists.json`, { + method: 'post', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify(data), + }); } /** @@ -31,15 +31,15 @@ export async function createList(userKey, data) { * @returns {Promise<Response>} The result of the POST request */ export async function addItem(listKey, seed) { - const body = { add: [seed] }; - return await fetch(`${listKey}/seeds.json`, { - method: 'post', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: JSON.stringify(body), - }); + const body = { add: [seed] }; + return await fetch(`${listKey}/seeds.json`, { + method: 'post', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify(body), + }); } /** @@ -50,23 +50,23 @@ export async function addItem(listKey, seed) { * @returns {Promise<Response>} The POST response */ export async function removeItem(listKey, seed) { - const body = { remove: [seed] }; - return await fetch(`${listKey}/seeds.json`, { - method: 'post', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: JSON.stringify(body), - }); + const body = { remove: [seed] }; + return await fetch(`${listKey}/seeds.json`, { + method: 'post', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify(body), + }); } // XXX : jsdoc export async function getListPartials() { - return await fetch(buildPartialsUrl('MyBooksDropperLists'), { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - }); + return await fetch(buildPartialsUrl('MyBooksDropperLists'), { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }); } diff --git a/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js b/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js index 398d3a2dbb2..fea3670f917 100644 --- a/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js +++ b/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js @@ -9,13 +9,13 @@ import 'jquery-ui/ui/widgets/dialog'; */ const itemsWithDeleteList = $('.deleteList .resultTitle'); if (itemsWithDeleteList.length) { - const deleteListLink = $('.listDelete--myLists'); - itemsWithDeleteList.each(function () { - $(deleteListLink).clone().prependTo(this).removeClass('hidden'); - }); + const deleteListLink = $('.listDelete--myLists'); + itemsWithDeleteList.each(function () { + $(deleteListLink).clone().prependTo(this).removeClass('hidden'); + }); - // Clean up and remove placeholder element - $('.listDelete--myLists.hidden').remove(); + // Clean up and remove placeholder element + $('.listDelete--myLists.hidden').remove(); } /** @@ -23,13 +23,13 @@ if (itemsWithDeleteList.length) { */ const itemsWithDeleteSeed = $('.deleteSeed .resultTitle'); if (itemsWithDeleteSeed.length) { - const deleteSeedLink = $('.seedDelete--myLists'); - itemsWithDeleteSeed.each(function () { - $(deleteSeedLink).clone().prependTo(this).removeClass('hidden'); - }); + const deleteSeedLink = $('.seedDelete--myLists'); + itemsWithDeleteSeed.each(function () { + $(deleteSeedLink).clone().prependTo(this).removeClass('hidden'); + }); - // Clean up and remove placeholder element - $('.seedDelete--myLists.hidden').remove(); + // Clean up and remove placeholder element + $('.seedDelete--myLists.hidden').remove(); } /** @@ -39,32 +39,32 @@ if (itemsWithDeleteSeed.length) { * @param {function} success - click function */ function remove_seed(list_key, seed, success) { - if (seed[0] === '/') { - seed = { key: seed }; - } - - $.ajax({ - type: 'POST', - url: `${list_key}/seeds.json`, - contentType: 'application/json', - data: JSON.stringify({ - remove: [seed], - }), - dataType: 'json', - - beforeSend: (xhr) => { - xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.setRequestHeader('Accept', 'application/json'); - }, - success: success, - }); + if (seed[0] === '/') { + seed = { key: seed }; + } + + $.ajax({ + type: 'POST', + url: `${list_key}/seeds.json`, + contentType: 'application/json', + data: JSON.stringify({ + remove: [seed], + }), + dataType: 'json', + + beforeSend: (xhr) => { + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('Accept', 'application/json'); + }, + success: success, + }); } /** * @returns {number} count of number of seed books in a list */ function get_seed_count() { - return $('ul#listResults').children().length; + return $('ul#listResults').children().length; } /** @@ -72,7 +72,7 @@ function get_seed_count() { * @returns {string} i18n cancel text */ const getCancelButtonLabelText = () => { - return $('.listDelete a').data('cancel-text'); + return $('.listDelete a').data('cancel-text'); }; /** @@ -80,91 +80,91 @@ const getCancelButtonLabelText = () => { * @returns {string} i18n confirmation text */ const getConfirmButtonLabelText = () => { - return $('.listDelete a').data('confirm-text'); + return $('.listDelete a').data('confirm-text'); }; // Add listeners to each .listDelete link element // Sometimes .listDelete is dynamically added to the DOM, so we'll add the listener to a parent element $('#listResults').on('click', '.listDelete a', function () { - if ( - get_seed_count() > 1 && + if ( + get_seed_count() > 1 && !$(this).parent().hasClass('listDelete--myLists') - ) { - $('#remove-seed-dialog') - .data('seed-key', $(this).closest('[data-seed-key]').data('seed-key')) - .data('list-key', $(this).closest('[data-list-key]').data('list-key')) - .dialog('open'); - $('#remove-seed-dialog').removeClass('hidden'); - } else { - $('#delete-list-dialog') - .data('list-key', $(this).closest('[data-list-key]').data('list-key')) - .dialog('open'); - $('#delete-list-dialog').removeClass('hidden'); - } + ) { + $('#remove-seed-dialog') + .data('seed-key', $(this).closest('[data-seed-key]').data('seed-key')) + .data('list-key', $(this).closest('[data-list-key]').data('list-key')) + .dialog('open'); + $('#remove-seed-dialog').removeClass('hidden'); + } else { + $('#delete-list-dialog') + .data('list-key', $(this).closest('[data-list-key]').data('list-key')) + .dialog('open'); + $('#delete-list-dialog').removeClass('hidden'); + } }); // Set up 'Remove Seed' dialog; force user to confirm the destructive action of removing a seed $('#remove-seed-dialog').dialog({ - autoOpen: false, - width: 400, - modal: true, - resizable: false, - buttons: { - ConfirmRemoveSeed: { - text: getConfirmButtonLabelText(), - id: 'remove-seed-dialog--confirm', - click: function () { - var list_key = $(this).data('list-key'); - var seed_key = $(this).data('seed-key'); - - remove_seed(list_key, seed_key, () => { - $(`[data-seed-key='${seed_key}']`).remove(); - // update seed count - $('#list-items-count').load(`${location.href} #list-items-count`); - - // TODO: update edition count - - $(this).dialog('close'); - $('#remove-seed-dialog').addClass('hidden'); - }); - }, - }, - CancelRemoveSeed: { - text: getCancelButtonLabelText(), - id: 'remove-seed-dialog--cancel', - click: function () { - $(this).dialog('close'); - $('#remove-seed-dialog').addClass('hidden'); - }, + autoOpen: false, + width: 400, + modal: true, + resizable: false, + buttons: { + ConfirmRemoveSeed: { + text: getConfirmButtonLabelText(), + id: 'remove-seed-dialog--confirm', + click: function () { + var list_key = $(this).data('list-key'); + var seed_key = $(this).data('seed-key'); + + remove_seed(list_key, seed_key, () => { + $(`[data-seed-key='${seed_key}']`).remove(); + // update seed count + $('#list-items-count').load(`${location.href} #list-items-count`); + + // TODO: update edition count + + $(this).dialog('close'); + $('#remove-seed-dialog').addClass('hidden'); + }); + }, + }, + CancelRemoveSeed: { + text: getCancelButtonLabelText(), + id: 'remove-seed-dialog--cancel', + click: function () { + $(this).dialog('close'); + $('#remove-seed-dialog').addClass('hidden'); + }, + }, }, - }, }); // Set up 'Delete List' dialog; force user to confirm the destructive action of deleting a list $('#delete-list-dialog').dialog({ - autoOpen: false, - width: 400, - modal: true, - resizable: false, - buttons: { - ConfirmDeleteList: { - text: getConfirmButtonLabelText(), - id: 'delete-list-dialog--confirm', - click: function () { - var list_key = $(this).data('list-key'); - - $.post(`${list_key}/delete.json`, () => { - $(this).dialog('close'); - window.location.reload(); - }); - }, - }, - CancelDeleteList: { - text: getCancelButtonLabelText(), - id: 'delete-list-dialog--cancel', - click: function () { - $(this).dialog('close'); - }, + autoOpen: false, + width: 400, + modal: true, + resizable: false, + buttons: { + ConfirmDeleteList: { + text: getConfirmButtonLabelText(), + id: 'delete-list-dialog--confirm', + click: function () { + var list_key = $(this).data('list-key'); + + $.post(`${list_key}/delete.json`, () => { + $(this).dialog('close'); + window.location.reload(); + }); + }, + }, + CancelDeleteList: { + text: getCancelButtonLabelText(), + id: 'delete-list-dialog--cancel', + click: function () { + $(this).dialog('close'); + }, + }, }, - }, }); diff --git a/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js b/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js index 79ca3c587f8..09cbe3cdc7e 100644 --- a/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js +++ b/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js @@ -26,137 +26,137 @@ import { removeItem } from './ListService'; * @class */ export class ShowcaseItem { - /** + /** * Creates a new `ShowcaseItem` obect. * * Sets references needed for this ShowcaseItem's functionality. * * @param {HTMLElement} showcaseElem */ - constructor(showcaseElem) { + constructor(showcaseElem) { /** * Reference to the root element of this component. * @member {HTMLElement} */ - this.showcaseElem = showcaseElem; + this.showcaseElem = showcaseElem; - /** + /** * `true` if this object represents the active lists showcase. * @member {boolean} */ - this.isActiveShowcase = + this.isActiveShowcase = showcaseElem.parentElement.classList.contains('already-lists'); - /** + /** * Reference to the affordance which removes an item from this list. * @member {HTMLElement} */ - this.removeFromListAffordance = + this.removeFromListAffordance = showcaseElem.querySelector('.remove-from-list'); - /** + /** * Unique identifier for the showcased list. * @member {string} */ - this.listKey = this.removeFromListAffordance.dataset.listKey; + this.listKey = this.removeFromListAffordance.dataset.listKey; - /** + /** * Unique identifier for the showcased list member. * @member {string} */ - this.seedKey = showcaseElem.querySelector('input[name=seed-key]').value; + this.seedKey = showcaseElem.querySelector('input[name=seed-key]').value; - /** + /** * The list item's type. * @member {'subject'|'edition'|'work'|'author'} */ - this.type = showcaseElem.querySelector('input[name=seed-type]').value; + this.type = showcaseElem.querySelector('input[name=seed-type]').value; - /** + /** * `true` if this list item is a subject. * @member {boolean} */ - this.isSubject = this.type === 'subject'; + this.isSubject = this.type === 'subject'; - /** + /** * `true` if this list item is a work * @member {boolean} */ - this.isWork = !this.isSubject && this.seedKey.slice(-1) === 'W'; + this.isWork = !this.isSubject && this.seedKey.slice(-1) === 'W'; - /** + /** * `POST` request-ready representation of the list's seed key. * @member {string|object} */ - this.seed; - if (this.isSubject) { - this.seed = this.seedKey; - } else { - this.seed = { key: this.seedKey }; + this.seed; + if (this.isSubject) { + this.seed = this.seedKey; + } else { + this.seed = { key: this.seedKey }; + } } - } - /** + /** * Attaches click listeners to the showcase item's "Remove from list" * affordance. */ - initialize() { - this.removeFromListAffordance.addEventListener('click', (event) => { - event.preventDefault(); - this.removeShowcaseItem(); - }); - } + initialize() { + this.removeFromListAffordance.addEventListener('click', (event) => { + event.preventDefault(); + this.removeShowcaseItem(); + }); + } - /** + /** * Sends request to remove an item from a list, then updates the view. * * Removes any affiliated showcase items from the DOM, and updates all * dropper list affordances. */ - async removeShowcaseItem() { - await removeItem(this.listKey, this.seed) - .then((response) => response.json()) - .then(() => { - const showcases = myBooksStore.getShowcases(); + async removeShowcaseItem() { + await removeItem(this.listKey, this.seed) + .then((response) => response.json()) + .then(() => { + const showcases = myBooksStore.getShowcases(); - // Remove self: - this.removeSelf(); + // Remove self: + this.removeSelf(); - // Remove other showcase items that are associated with the list and seed key: - for (const showcase of showcases) { - if (showcase.isShowcaseForListAndSeed(this.listKey, this.seedKey)) { - showcase.removeSelf(); - } - } + // Remove other showcase items that are associated with the list and seed key: + for (const showcase of showcases) { + if (showcase.isShowcaseForListAndSeed(this.listKey, this.seedKey)) { + showcase.removeSelf(); + } + } - // Update droppers: - const droppers = myBooksStore.getDroppers(); - for (const dropper of droppers) { - dropper.readingLists.updateViewAfterModifyingList( - this.listKey, - this.isWork, - false, - ); - } - }); - } + // Update droppers: + const droppers = myBooksStore.getDroppers(); + for (const dropper of droppers) { + dropper.readingLists.updateViewAfterModifyingList( + this.listKey, + this.isWork, + false, + ); + } + }); + } - /** + /** * Removes associated showcase item from the DOM. * * Removes self from the myBooksStore's showcase array * upon success. */ - removeSelf() { - const showcases = myBooksStore.getShowcases(); - const thisIndex = showcases.indexOf(this); - if (thisIndex >= 0) { - this.showcaseElem.remove(); - showcases.splice(thisIndex, 1); + removeSelf() { + const showcases = myBooksStore.getShowcases(); + const thisIndex = showcases.indexOf(this); + if (thisIndex >= 0) { + this.showcaseElem.remove(); + showcases.splice(thisIndex, 1); + } } - } - /** + /** * Toggles the visiblity of active showcase items depending on their seed type. * * If `showWorks` is `true`, the only active showcase items that will be visible will @@ -167,34 +167,34 @@ export class ShowcaseItem { * * @param {boolean} showWorks `true` if only active showcase items related to works should be displayed */ - toggleVisibility(showWorks) { - if (this.isActiveShowcase) { - if (showWorks) { - if (this.isWork) { - this.showcaseElem.classList.remove('hidden'); - } else { - this.showcaseElem.classList.add('hidden'); - } - } else { - if (this.isWork) { - this.showcaseElem.classList.add('hidden'); - } else { - this.showcaseElem.classList.remove('hidden'); + toggleVisibility(showWorks) { + if (this.isActiveShowcase) { + if (showWorks) { + if (this.isWork) { + this.showcaseElem.classList.remove('hidden'); + } else { + this.showcaseElem.classList.add('hidden'); + } + } else { + if (this.isWork) { + this.showcaseElem.classList.add('hidden'); + } else { + this.showcaseElem.classList.remove('hidden'); + } + } } - } } - } - /** + /** * Determines if this showcase item is linked to the given keys. * * @param {string} listKey * @param {string} seedKey * @return {boolean} `true` if the given keys match this item's keys */ - isShowcaseForListAndSeed(listKey, seedKey) { - return this.listKey === listKey && this.seedKey === seedKey; - } + isShowcaseForListAndSeed(listKey, seedKey) { + return this.listKey === listKey && this.seedKey === seedKey; + } } /** @@ -213,19 +213,19 @@ const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; * @returns {string} Type of the given seed key. */ function getSeedType(seed) { - // XXX : validate input? - if (seed[0] !== '/') { - return 'subject'; - } - if (seed.endsWith('M')) { - return 'edition'; - } - if (seed.endsWith('W')) { - return 'work'; - } - if (seed.endsWith('A')) { - return 'author'; - } + // XXX : validate input? + if (seed[0] !== '/') { + return 'subject'; + } + if (seed.endsWith('M')) { + return 'edition'; + } + if (seed.endsWith('W')) { + return 'work'; + } + if (seed.endsWith('A')) { + return 'author'; + } } // XXX : remove this? @@ -241,21 +241,21 @@ function getSeedType(seed) { * @returns {HTMLLIElement} */ export function createActiveShowcaseItem( - listKey, - seedKey, - listTitle, - coverUrl = DEFAULT_COVER_URL, + listKey, + seedKey, + listTitle, + coverUrl = DEFAULT_COVER_URL, ) { - if (!i18nStrings) { - const i18nInput = document.querySelector('input[name=list-i18n-strings]'); - i18nStrings = JSON.parse(i18nInput.value); - } + if (!i18nStrings) { + const i18nInput = document.querySelector('input[name=list-i18n-strings]'); + i18nStrings = JSON.parse(i18nInput.value); + } - const splitKey = listKey.split('/'); - const userKey = `/${splitKey[1]}/${splitKey[2]}`; - const seedType = getSeedType(seedKey); + const splitKey = listKey.split('/'); + const userKey = `/${splitKey[1]}/${splitKey[2]}`; + const seedType = getSeedType(seedKey); - const itemMarkUp = `<span class="image"> + const itemMarkUp = `<span class="image"> <a href="${listKey}"><img src="${coverUrl}" alt="${i18nStrings['cover_of']}${listTitle}" title="${i18nStrings['cover_of']}${listTitle}"/></a> </span> <span class="data"> @@ -269,12 +269,12 @@ export function createActiveShowcaseItem( <span class="owner">${i18nStrings['from']} <a href="${userKey}">${i18nStrings['you']}</a></span> </span>`; - const li = document.createElement('li'); - li.classList.add('actionable-item'); - li.dataset.listKey = listKey; - li.innerHTML = itemMarkUp; + const li = document.createElement('li'); + li.classList.add('actionable-item'); + li.dataset.listKey = listKey; + li.innerHTML = itemMarkUp; - return li; + return li; } /** @@ -288,9 +288,9 @@ export function createActiveShowcaseItem( * @param {boolean} showWorksOnly */ export function toggleActiveShowcaseItems(showWorksOnly) { - for (const item of myBooksStore.getShowcases()) { - item.toggleVisibility(showWorksOnly); - } + for (const item of myBooksStore.getShowcases()) { + item.toggleVisibility(showWorksOnly); + } } /** @@ -309,20 +309,20 @@ export function toggleActiveShowcaseItems(showWorksOnly) { * @param {string} [coverUrl] */ export function attachNewActiveShowcaseItem( - listKey, - seedKey, - listTitle, - coverUrl = DEFAULT_COVER_URL, + listKey, + seedKey, + listTitle, + coverUrl = DEFAULT_COVER_URL, ) { - const activeListsShowcase = document.querySelector('.already-lists'); + const activeListsShowcase = document.querySelector('.already-lists'); - if (activeListsShowcase) { - const li = createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl); - activeListsShowcase.appendChild(li); + if (activeListsShowcase) { + const li = createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl); + activeListsShowcase.appendChild(li); - const showcase = new ShowcaseItem(li); - showcase.initialize(); + const showcase = new ShowcaseItem(li); + showcase.initialize(); - myBooksStore.getShowcases().push(showcase); - } + myBooksStore.getShowcases().push(showcase); + } } diff --git a/openlibrary/plugins/openlibrary/js/markdown-editor/index.js b/openlibrary/plugins/openlibrary/js/markdown-editor/index.js index 75aac27a135..e90490f0741 100644 --- a/openlibrary/plugins/openlibrary/js/markdown-editor/index.js +++ b/openlibrary/plugins/openlibrary/js/markdown-editor/index.js @@ -6,17 +6,17 @@ import '../../../../../vendor/js/wmd/jquery.wmd.js'; * @param {jQuery.Object} $textareas */ export function initMarkdownEditor($textareas) { - $textareas - .on('focus', () => { - // reveal the previous when the user focuses on the textarea for the first time - $('.wmd-preview').show(); - if ($('#prevHead').length === 0) { - $('.wmd-preview').before('<h3 id="prevHead">Preview</h3>'); - } - }) - .wmd({ - helpLink: '/help/markdown', - helpHoverTitle: 'Formatting Help', - helpTarget: '_new', - }); + $textareas + .on('focus', () => { + // reveal the previous when the user focuses on the textarea for the first time + $('.wmd-preview').show(); + if ($('#prevHead').length === 0) { + $('.wmd-preview').before('<h3 id="prevHead">Preview</h3>'); + } + }) + .wmd({ + helpLink: '/help/markdown', + helpHoverTitle: 'Formatting Help', + helpTarget: '_new', + }); } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestService.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestService.js index 61c1157b4b2..4aa12170252 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestService.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestService.js @@ -1,36 +1,36 @@ export const REQUEST_TYPES = { - WORK_MERGE: 1, - AUTHOR_MERGE: 2, + WORK_MERGE: 1, + AUTHOR_MERGE: 2, }; export async function createRequest( - olids, - action, - type, - comment = null, - primary = null, + olids, + action, + type, + comment = null, + primary = null, ) { - const data = { - rtype: 'create-request', - action: action, - mr_type: type, - olids: olids, - }; - if (comment) { - data['comment'] = comment; - } - if (primary) { - data['primary'] = primary; - } + const data = { + rtype: 'create-request', + action: action, + mr_type: type, + olids: olids, + }; + if (comment) { + data['comment'] = comment; + } + if (primary) { + data['primary'] = primary; + } - return fetch('/merges', { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }); + return fetch('/merges', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); } /** @@ -42,23 +42,23 @@ export async function createRequest( * @returns {Promise<Response>} */ async function updateRequest(action, mrid, comment = null) { - const data = { - rtype: 'update-request', - action: action, - mrid: mrid, - }; - if (comment) { - data['comment'] = comment; - } + const data = { + rtype: 'update-request', + action: action, + mrid: mrid, + }; + if (comment) { + data['comment'] = comment; + } - return fetch('/merges', { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }); + return fetch('/merges', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); } /** @@ -69,7 +69,7 @@ async function updateRequest(action, mrid, comment = null) { * @returns {Promise<Response>} The results of the update POST request */ export async function commentOnRequest(mrid, comment) { - return updateRequest('comment', mrid, comment); + return updateRequest('comment', mrid, comment); } /** @@ -78,17 +78,17 @@ export async function commentOnRequest(mrid, comment) { * @param {Number} mrid Unique identifier for the request being claimed */ export async function claimRequest(mrid) { - return updateRequest('claim', mrid); + return updateRequest('claim', mrid); } export async function unassignRequest(mrid) { - return updateRequest('unassign', mrid); + return updateRequest('unassign', mrid); } export async function declineRequest(mrid, comment) { - return updateRequest('decline', mrid, comment); + return updateRequest('decline', mrid, comment); } export async function approveRequest(mrid, comment) { - return updateRequest('approve', mrid, comment); + return updateRequest('approve', mrid, comment); } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js index 9e15ad21068..4142778d4d9 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js @@ -14,51 +14,51 @@ import { setI18nStrings, TableRow } from './MergeRequestTable/TableRow'; * @class */ export default class MergeRequestTable { - /** + /** * Creates references to the table and its header and hydrates each. * * @param {HTMLElement} mergeRequestTable */ - constructor(mergeRequestTable) { + constructor(mergeRequestTable) { /** * The `username` of the authenticated patron, or '' if logged out. * * @param {string} */ - this.username = mergeRequestTable.dataset.username; + this.username = mergeRequestTable.dataset.username; - const localizedStrings = JSON.parse(mergeRequestTable.dataset.i18n); - setI18nStrings(localizedStrings); + const localizedStrings = JSON.parse(mergeRequestTable.dataset.i18n); + setI18nStrings(localizedStrings); - /** + /** * Reference to this table's header. * * @param {HTMLElement} */ - this.tableHeader = new TableHeader( - mergeRequestTable.querySelector('.table-header'), - ); + this.tableHeader = new TableHeader( + mergeRequestTable.querySelector('.table-header'), + ); - /** + /** * References to each row in the table. * * @param {Array<TableRow>} */ - this.tableRows = []; - const rowElements = mergeRequestTable.querySelectorAll('.mr-table-row'); - for (const elem of rowElements) { - this.tableRows.push(new TableRow(elem, this.username)); + this.tableRows = []; + const rowElements = mergeRequestTable.querySelectorAll('.mr-table-row'); + for (const elem of rowElements) { + this.tableRows.push(new TableRow(elem, this.username)); + } } - } - /** + /** * Hydrates the librarian request table. */ - initialize() { - this.tableHeader.initialize(); - document.addEventListener('click', (event) => - this.tableHeader.closeMenusIfClickOutside(event), - ); - this.tableRows.forEach((elem) => elem.initialize()); - } + initialize() { + this.tableHeader.initialize(); + document.addEventListener('click', (event) => + this.tableHeader.closeMenusIfClickOutside(event), + ); + this.tableRows.forEach((elem) => elem.initialize()); + } } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js index 2dd83ecc543..081f1eccbe8 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js @@ -14,12 +14,12 @@ * @class */ export default class TableHeader { - /** + /** * Sets references to many table header affordances. * * @param {HTMLElement} tableHeader */ - constructor(tableHeader) { + constructor(tableHeader) { /** * References to each select menu. These are always visible * in the header bar, and, when clicked, display a drop-down @@ -27,118 +27,118 @@ export default class TableHeader { * * @param {NodeList<HTMLElement>} */ - this.dropMenuButtons = tableHeader.querySelectorAll('.mr-dropdown'); - /** + this.dropMenuButtons = tableHeader.querySelectorAll('.mr-dropdown'); + /** * References each drop-down filter option menu. * * @param {NodeList<HTMLElement>} */ - this.dropMenus = tableHeader.querySelectorAll('.mr-dropdown-menu'); - /** + this.dropMenus = tableHeader.querySelectorAll('.mr-dropdown-menu'); + /** * References each drop-down menu "X" affordance, which closes * the appropriate drop-down menu. * * @param{NodeList<HTMLElement>} */ - this.closeButtons = tableHeader.querySelectorAll('.dropdown-close'); - /** + this.closeButtons = tableHeader.querySelectorAll('.dropdown-close'); + /** * References each text input filter. * * @param{NodeList<HTMLElement>} */ - this.searchInputs = tableHeader.querySelectorAll('.filter'); - } + this.searchInputs = tableHeader.querySelectorAll('.filter'); + } - /** + /** * Hydrates the table header affordances. */ - initialize() { - this.initFilters(); - } + initialize() { + this.initFilters(); + } - /** + /** * Toggle a dropdown menu while closing other dropdown menus. * * @param {Event} event * @param {string} menuButtonId */ - toggleAMenuWhileClosingOthers(event, menuButtonId) { + toggleAMenuWhileClosingOthers(event, menuButtonId) { // prevent closing of menu on bubbling unless click menuButton itself - if (event.target.id === menuButtonId) { - // close other open menus, then toggle selected menu - this.closeOtherMenus(menuButtonId); - event.target.firstElementChild.classList.toggle('hidden'); + if (event.target.id === menuButtonId) { + // close other open menus, then toggle selected menu + this.closeOtherMenus(menuButtonId); + event.target.firstElementChild.classList.toggle('hidden'); + } } - } - /** + /** * Close dropdown menus whose menu button doesn't match a given id. * * @param {string} menuButtonId */ - closeOtherMenus(menuButtonId) { - this.dropMenuButtons.forEach((menuButton) => { - if (menuButton.id !== menuButtonId) { - menuButton.firstElementChild.classList.add('hidden'); - } - }); - } + closeOtherMenus(menuButtonId) { + this.dropMenuButtons.forEach((menuButton) => { + if (menuButton.id !== menuButtonId) { + menuButton.firstElementChild.classList.add('hidden'); + } + }); + } - /** + /** * Filters of dropdown menu items using case-insensitive string matching. * * @param {Event} event */ - filterMenuItems(event) { - const input = document.getElementById(event.target.id); - const filter = input.value.toUpperCase(); - const menu = input.closest('.mr-dropdown-menu'); - const items = menu.getElementsByClassName('dropdown-item'); - // skip first item in menu - for (let i = 1; i < items.length; i++) { - const text = items[i].textContent; - items[i].classList.toggle( - 'hidden', - text.toUpperCase().indexOf(filter) === -1, - ); + filterMenuItems(event) { + const input = document.getElementById(event.target.id); + const filter = input.value.toUpperCase(); + const menu = input.closest('.mr-dropdown-menu'); + const items = menu.getElementsByClassName('dropdown-item'); + // skip first item in menu + for (let i = 1; i < items.length; i++) { + const text = items[i].textContent; + items[i].classList.toggle( + 'hidden', + text.toUpperCase().indexOf(filter) === -1, + ); + } } - } - /** + /** * Close all dropdown menus when click anywhere on screen that's not part of * the dropdown menu; otherwise, keep dropdown menu open. * * @param {Event} event */ - closeMenusIfClickOutside(event) { - const menusClicked = Array.from(this.dropMenuButtons).filter( - (menuButton) => { - return menuButton.contains(event.target); - }, - ); - // want to preserve clicking in a menu, i.e. when filtering for users - if (!menusClicked.length) { - this.dropMenus.forEach((menu) => menu.classList.add('hidden')); + closeMenusIfClickOutside(event) { + const menusClicked = Array.from(this.dropMenuButtons).filter( + (menuButton) => { + return menuButton.contains(event.target); + }, + ); + // want to preserve clicking in a menu, i.e. when filtering for users + if (!menusClicked.length) { + this.dropMenus.forEach((menu) => menu.classList.add('hidden')); + } } - } - /** + /** * Initialize events for merge queue filter dropdown menu functionality. * */ - initFilters() { - this.dropMenuButtons.forEach((menuButton) => { - menuButton.addEventListener('click', (event) => { - this.toggleAMenuWhileClosingOthers(event, menuButton.id); - }); - }); - this.closeButtons.forEach((button) => { - button.addEventListener('click', (event) => { - event.target.closest('.mr-dropdown-menu').classList.toggle('hidden'); - }); - }); - this.searchInputs.forEach((input) => { - input.addEventListener('keyup', (event) => this.filterMenuItems(event)); - }); - } + initFilters() { + this.dropMenuButtons.forEach((menuButton) => { + menuButton.addEventListener('click', (event) => { + this.toggleAMenuWhileClosingOthers(event, menuButton.id); + }); + }); + this.closeButtons.forEach((button) => { + button.addEventListener('click', (event) => { + event.target.closest('.mr-dropdown-menu').classList.toggle('hidden'); + }); + }); + this.searchInputs.forEach((input) => { + input.addEventListener('keyup', (event) => this.filterMenuItems(event)); + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js index 360fc4a22ba..79b27901463 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js @@ -6,16 +6,16 @@ import { FadingToast } from '../../Toast'; import { - claimRequest, - commentOnRequest, - declineRequest, - unassignRequest, + claimRequest, + commentOnRequest, + declineRequest, + unassignRequest, } from '../MergeRequestService'; let i18nStrings; export function setI18nStrings(localizedStrings) { - i18nStrings = localizedStrings; + i18nStrings = localizedStrings; } /** @@ -31,7 +31,7 @@ export function setI18nStrings(localizedStrings) { * @class */ export class TableRow { - /** + /** * Creates a new librarian request table row. * * Stores reference to each interactive element in a row. @@ -39,208 +39,208 @@ export class TableRow { * @param {HTMLElement} row Root element of a table row * @param {string} username `username` of logged-in patron. Empty if unauthenticated. */ - constructor(row, username) { + constructor(row, username) { /** * Reference to this row. * * @param {HTMLElement} */ - this.row = row; - /** + this.row = row; + /** * `username` of authenticated patron, or '' if unauthenticated. * * @param {HTMLElement} */ - this.username = username; - /** + this.username = username; + /** * Unique identifier for this row. * * @param {Number} */ - this.mrid = row.dataset.mrid; - /** + this.mrid = row.dataset.mrid; + /** * Button used to toggle the full comments display's visibility. * * @param {HTMLElement} */ - this.toggleCommentButton = row.querySelector( - '.mr-comment-toggle__comment-expand', - ); - /** + this.toggleCommentButton = row.querySelector( + '.mr-comment-toggle__comment-expand', + ); + /** * Element which displays this row's comment count. * * @param {HTMLElement} */ - this.commentCountDisplay = row.querySelector( - '.mr-comment-toggle__comment-count', - ); - /** + this.commentCountDisplay = row.querySelector( + '.mr-comment-toggle__comment-count', + ); + /** * Element displaying the most recent comment on this request. * * @param {HTMLElement} */ - this.commentPreview = row.querySelector('.mr-details__comment-preview'); - /** + this.commentPreview = row.querySelector('.mr-details__comment-preview'); + /** * Hidden comments display. Also contains reply inputs, if rendered. * * @param {HTMLElement} */ - this.fullCommentsPanel = row.querySelector('.comment-panel'); - /** + this.fullCommentsPanel = row.querySelector('.comment-panel'); + /** * Element that displays all of the comments for this request. * * @param {HTMLElement} */ - this.commentsDisplay = this.fullCommentsPanel.querySelector( - '.comment-panel__comment-display', - ); - /** + this.commentsDisplay = this.fullCommentsPanel.querySelector( + '.comment-panel__comment-display', + ); + /** * The comment text input. * * @param {HTMLElement|null} */ - this.commentReplyInput = this.fullCommentsPanel.querySelector( - '.comment-panel__reply-input', - ); - /** + this.commentReplyInput = this.fullCommentsPanel.querySelector( + '.comment-panel__reply-input', + ); + /** * The comment reply button. * * @param {HTMLElement|null} */ - this.replyButton = this.fullCommentsPanel.querySelector( - '.comment-panel__reply-btn', - ); - /** + this.replyButton = this.fullCommentsPanel.querySelector( + '.comment-panel__reply-btn', + ); + /** * Affordance which allows one to close their own request. * * Only available on a patron's own open requests. * * @param {HTMLElement|null} */ - this.closeRequestButton = this.row.querySelector('.mr-close-link'); - /** + this.closeRequestButton = this.row.querySelector('.mr-close-link'); + /** * Button used by super-librarians to claim a request. * * @param {HTMLElement} */ - this.reviewButton = this.row.querySelector( - '.mr-review-actions__review-btn', - ); - /** + this.reviewButton = this.row.querySelector( + '.mr-review-actions__review-btn', + ); + /** * Reference to root element of the assignee display. * * @param {HTMLElement} */ - this.assigneeElement = this.row.querySelector( - '.mr-review-actions__assignee', - ); - /** + this.assigneeElement = this.row.querySelector( + '.mr-review-actions__assignee', + ); + /** * Assignee display element which displays the assignee's name. * * @param {HTMLElement} */ - this.assigneeLabel = this.row.querySelector( - '.mr-review-actions__assignee-name', - ); - /** + this.assigneeLabel = this.row.querySelector( + '.mr-review-actions__assignee-name', + ); + /** * Element that unassignees the current reviewer when clicked. * * @param {HTMLElement} */ - this.unassignReviewerButton = this.row.querySelector( - '.mr-review-actions__unassign', - ); - } + this.unassignReviewerButton = this.row.querySelector( + '.mr-review-actions__unassign', + ); + } - /** + /** * Hydrates interactive elements in this row. */ - initialize() { - this.toggleCommentButton.addEventListener('click', () => - this.toggleComments(), - ); - if (this.closeRequestButton) { - this.closeRequestButton.addEventListener('click', () => - this.closeRequest(), - ); - } - if (this.replyButton && this.commentReplyInput) { - this.replyButton.addEventListener('click', () => this.addComment()); - } - this.reviewButton.addEventListener('click', () => this.claimRequest()); - if (this.unassignReviewerButton) { - this.unassignReviewerButton.addEventListener('click', () => - this.unassignReviewer(), - ); + initialize() { + this.toggleCommentButton.addEventListener('click', () => + this.toggleComments(), + ); + if (this.closeRequestButton) { + this.closeRequestButton.addEventListener('click', () => + this.closeRequest(), + ); + } + if (this.replyButton && this.commentReplyInput) { + this.replyButton.addEventListener('click', () => this.addComment()); + } + this.reviewButton.addEventListener('click', () => this.claimRequest()); + if (this.unassignReviewerButton) { + this.unassignReviewerButton.addEventListener('click', () => + this.unassignReviewer(), + ); + } } - } - /** + /** * Toggles which comment display is currently visible. * * On page load the comment preview display is visible, while * the full comments panel is hidden. This function toggles * each element's visibility. */ - toggleComments() { - this.commentPreview.classList.toggle('hidden'); - this.fullCommentsPanel.classList.toggle('hidden'); + toggleComments() { + this.commentPreview.classList.toggle('hidden'); + this.fullCommentsPanel.classList.toggle('hidden'); - // Add depressed effect to toggle button: - this.toggleCommentButton.classList.toggle( - 'mr-comment-toggle__comment-expand--active', - ); - } + // Add depressed effect to toggle button: + this.toggleCommentButton.classList.toggle( + 'mr-comment-toggle__comment-expand--active', + ); + } - /** + /** * Closes the request linked to this row, and removes this * row from the DOM. */ - async closeRequest() { - const comment = prompt(i18nStrings['close_request_comment_prompt']); - if (comment !== null) { - // Comment will be `null` if "Cancel" button pressed - await declineRequest(this.mrid, comment) - .then((result) => result.json()) - .then((data) => { - if (data.status === 'ok') { - this.row.parentElement.removeChild(this.row); - } - }) - .catch((e) => { - // XXX : toast? - throw e; - }); + async closeRequest() { + const comment = prompt(i18nStrings['close_request_comment_prompt']); + if (comment !== null) { + // Comment will be `null` if "Cancel" button pressed + await declineRequest(this.mrid, comment) + .then((result) => result.json()) + .then((data) => { + if (data.status === 'ok') { + this.row.parentElement.removeChild(this.row); + } + }) + .catch((e) => { + // XXX : toast? + throw e; + }); + } } - } - /** + /** * `POST`s a new comment to the server. * * Updates the view on success. */ - async addComment() { - const comment = this.commentReplyInput.value.trim(); - if (comment) { - await commentOnRequest(this.mrid, comment) - .then((result) => result.json()) - .then((data) => { - if (data.status === 'ok') { - this.updateCommentViews(comment); - this.commentReplyInput.value = ''; - } else { - new FadingToast( - i18nStrings['comment_submission_failure_message'], - ).show(); - } - }) - .catch((e) => { - throw e; - }); + async addComment() { + const comment = this.commentReplyInput.value.trim(); + if (comment) { + await commentOnRequest(this.mrid, comment) + .then((result) => result.json()) + .then((data) => { + if (data.status === 'ok') { + this.updateCommentViews(comment); + this.commentReplyInput.value = ''; + } else { + new FadingToast( + i18nStrings['comment_submission_failure_message'], + ).show(); + } + }) + .catch((e) => { + throw e; + }); + } } - } - /** + /** * Updates row, setting given comment as most recent. * * First, escapes given comment. Replaces text of comment @@ -249,56 +249,56 @@ export class TableRow { * * @param {string} comment The newly added comment. */ - updateCommentViews(comment) { - const escapedComment = document.createTextNode(comment); + updateCommentViews(comment) { + const escapedComment = document.createTextNode(comment); - // Update preview: - this.commentPreview.innerText = escapedComment.textContent; + // Update preview: + this.commentPreview.innerText = escapedComment.textContent; - // Update full display: - const newComment = document.createElement('div'); - newComment.classList.add('comment-panel__comment'); - newComment.innerHTML = `<span class="commenter">@${this.username}</span> `; - newComment.appendChild(escapedComment); + // Update full display: + const newComment = document.createElement('div'); + newComment.classList.add('comment-panel__comment'); + newComment.innerHTML = `<span class="commenter">@${this.username}</span> `; + newComment.appendChild(escapedComment); - this.commentsDisplay.appendChild(newComment); - this.commentsDisplay.scrollTop = this.commentsDisplay.scrollHeight; + this.commentsDisplay.appendChild(newComment); + this.commentsDisplay.scrollTop = this.commentsDisplay.scrollHeight; - // Update comment count: - const count = Number(this.commentCountDisplay.innerText) + 1; - this.commentCountDisplay.innerText = count; - } + // Update comment count: + const count = Number(this.commentCountDisplay.innerText) + 1; + this.commentCountDisplay.innerText = count; + } - /** + /** * `POST`s claim to review this request, then updates the view. * * Hides the review button, and shows the assignee display. */ - async claimRequest() { - await claimRequest(this.mrid) - .then((result) => result.json()) - .then((data) => { - if (data.status === 'ok') { - this.assigneeLabel.innerText = `@${this.username}`; - this.assigneeElement.classList.remove('hidden'); - this.reviewButton.classList.add('hidden'); - } - }); - } + async claimRequest() { + await claimRequest(this.mrid) + .then((result) => result.json()) + .then((data) => { + if (data.status === 'ok') { + this.assigneeLabel.innerText = `@${this.username}`; + this.assigneeElement.classList.remove('hidden'); + this.reviewButton.classList.add('hidden'); + } + }); + } - /** + /** * `POST`s request to remove current assignee, then updates the view. * * Hides the assignee display and shows the review button on success. */ - async unassignReviewer() { - await unassignRequest(this.mrid) - .then((result) => result.json()) - .then((data) => { - if (data.status === 'ok') { - this.assigneeElement.classList.add('hidden'); - this.reviewButton.classList.remove('hidden'); - } - }); - } + async unassignReviewer() { + await unassignRequest(this.mrid) + .then((result) => result.json()) + .then((data) => { + if (data.status === 'ok') { + this.assigneeElement.classList.add('hidden'); + this.reviewButton.classList.remove('hidden'); + } + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/index.js b/openlibrary/plugins/openlibrary/js/merge-request-table/index.js index 70a0a5b9b4c..0b710fb9fc4 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/index.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/index.js @@ -6,6 +6,6 @@ import MergeRequestTable from './MergeRequestTable'; * @param {HTMLElement} elem Reference to the queue's root element. */ export function initLibrarianQueue(elem) { - const librarianQueue = new MergeRequestTable(elem); - librarianQueue.initialize(); + const librarianQueue = new MergeRequestTable(elem); + librarianQueue.initialize(); } diff --git a/openlibrary/plugins/openlibrary/js/merge.js b/openlibrary/plugins/openlibrary/js/merge.js index 4f5ccc22e35..86018ebf552 100644 --- a/openlibrary/plugins/openlibrary/js/merge.js +++ b/openlibrary/plugins/openlibrary/js/merge.js @@ -2,66 +2,66 @@ import 'jquery-ui/ui/widgets/dialog'; import { declineRequest } from './merge-request-table/MergeRequestService'; export function initAuthorMergePage() { - $('#save').on('click', () => { - const n = $('#mergeForm input[type=radio]:checked').length; - const confirmMergeButton = document.querySelector('#confirmMerge'); - if (n === 0) { - $('#noMaster').dialog('open'); - } else if (confirmMergeButton) { - $('#confirmMerge').dialog('open'); - } else { - $('#mergeForm').trigger('submit'); - } - return false; - }); - $('div.radio').first().find('input[type=radio]').prop('checked', true); - $('div.checkbox').first().find('input[type=checkbox]').prop('checked', true); - $('div.author').first().addClass('master'); - $('#include input[type=radio]').on('mouseover', function () { - $(this).parent().parent().addClass('mouseoverHighlight', 300); - }); - $('#include input[type=radio]').on('mouseout', function () { - $(this).parent().parent().removeClass('mouseoverHighlight', 100); - }); - $('#include input[type=radio]').on('click', function () { - const previousMaster = $('.merge').find('div.master'); - previousMaster.removeClass('master mergeSelection'); - previousMaster.find('input[type=checkbox]').prop('checked', false); - $(this).parent().parent().addClass('master'); - $(this) - .parent() - .parent() - .find('input[type=checkbox]') - .prop('checked', true); - }); - $('#include input[type=checkbox]').on('change', function () { - if (!$(this).parent().parent().hasClass('master')) { - if ($(this).is(':checked')) { - $(this).parent().parent().addClass('mergeSelection'); - } else { - $(this).parent().parent().removeClass('mergeSelection'); - } - } - }); - initRejectButton(); + $('#save').on('click', () => { + const n = $('#mergeForm input[type=radio]:checked').length; + const confirmMergeButton = document.querySelector('#confirmMerge'); + if (n === 0) { + $('#noMaster').dialog('open'); + } else if (confirmMergeButton) { + $('#confirmMerge').dialog('open'); + } else { + $('#mergeForm').trigger('submit'); + } + return false; + }); + $('div.radio').first().find('input[type=radio]').prop('checked', true); + $('div.checkbox').first().find('input[type=checkbox]').prop('checked', true); + $('div.author').first().addClass('master'); + $('#include input[type=radio]').on('mouseover', function () { + $(this).parent().parent().addClass('mouseoverHighlight', 300); + }); + $('#include input[type=radio]').on('mouseout', function () { + $(this).parent().parent().removeClass('mouseoverHighlight', 100); + }); + $('#include input[type=radio]').on('click', function () { + const previousMaster = $('.merge').find('div.master'); + previousMaster.removeClass('master mergeSelection'); + previousMaster.find('input[type=checkbox]').prop('checked', false); + $(this).parent().parent().addClass('master'); + $(this) + .parent() + .parent() + .find('input[type=checkbox]') + .prop('checked', true); + }); + $('#include input[type=checkbox]').on('change', function () { + if (!$(this).parent().parent().hasClass('master')) { + if ($(this).is(':checked')) { + $(this).parent().parent().addClass('mergeSelection'); + } else { + $(this).parent().parent().removeClass('mergeSelection'); + } + } + }); + initRejectButton(); } function initRejectButton() { - const rejectButton = document.querySelector('#reject-author-merge-btn'); - if (rejectButton) { - rejectButton.addEventListener('click', () => { - rejectMerge(); - rejectButton.disabled = true; - const approveButton = document.querySelector('#save'); - approveButton.disabled = true; - }); - } + const rejectButton = document.querySelector('#reject-author-merge-btn'); + if (rejectButton) { + rejectButton.addEventListener('click', () => { + rejectMerge(); + rejectButton.disabled = true; + const approveButton = document.querySelector('#save'); + approveButton.disabled = true; + }); + } } function rejectMerge() { - const commentInput = document.querySelector('#author-merge-comment'); - const mridInput = document.querySelector('#mrid-input'); - declineRequest(Number(mridInput.value), commentInput.value); + const commentInput = document.querySelector('#author-merge-comment'); + const mridInput = document.querySelector('#mrid-input'); + declineRequest(Number(mridInput.value), commentInput.value); } /** @@ -71,38 +71,38 @@ function rejectMerge() { * Assumes presence of element with '#preMerge' id and 'data-keys' attribute. */ export function initAuthorView() { - const dataKeysJSON = $('#preMerge').data('keys'); + const dataKeysJSON = $('#preMerge').data('keys'); - $('#preMerge').show(); - $('#preMerge').parent().show(); + $('#preMerge').show(); + $('#preMerge').parent().show(); - const data = { - master: dataKeysJSON['master'], - duplicates: dataKeysJSON['duplicates'], - olids: dataKeysJSON['olids'], - }; + const data = { + master: dataKeysJSON['master'], + duplicates: dataKeysJSON['duplicates'], + olids: dataKeysJSON['olids'], + }; - const mrid = dataKeysJSON['mrid']; - const comment = dataKeysJSON['comment']; + const mrid = dataKeysJSON['mrid']; + const comment = dataKeysJSON['comment']; - if (mrid) { - data['mrid'] = mrid; - } - if (comment) { - data['comment'] = comment; - } + if (mrid) { + data['mrid'] = mrid; + } + if (comment) { + data['comment'] = comment; + } - $.ajax({ - url: '/authors/merge.json', - type: 'POST', - data: JSON.stringify(data), - error: () => { - $('#preMerge').fadeOut(); - $('#errorMerge').fadeIn(); - }, - success: () => { - $('#preMerge').fadeOut(); - $('#postMerge').fadeIn(); - }, - }); + $.ajax({ + url: '/authors/merge.json', + type: 'POST', + data: JSON.stringify(data), + error: () => { + $('#preMerge').fadeOut(); + $('#errorMerge').fadeIn(); + }, + success: () => { + $('#preMerge').fadeOut(); + $('#postMerge').fadeIn(); + }, + }); } diff --git a/openlibrary/plugins/openlibrary/js/modals/index.js b/openlibrary/plugins/openlibrary/js/modals/index.js index d52ecfbb08d..2016c3fa3f6 100644 --- a/openlibrary/plugins/openlibrary/js/modals/index.js +++ b/openlibrary/plugins/openlibrary/js/modals/index.js @@ -6,19 +6,19 @@ import '../../../../../static/css/components/metadata-form.css'; * Initializes share modal. */ export function initShareModal($modalLinks) { - addClickListeners($modalLinks, '400px'); - addShareModalButtonListeners(); + addClickListeners($modalLinks, '400px'); + addShareModalButtonListeners(); } /** * Adds click listeners to buttons in all notes modals on a page. */ function addShareModalButtonListeners() { - $('#social-modal-content .copy-url-btn').on('click', (event) => { - event.preventDefault(); - navigator.clipboard.writeText(window.location.href); - showToast('URL copied to clipboard'); - $.colorbox.close(); - }); + $('#social-modal-content .copy-url-btn').on('click', (event) => { + event.preventDefault(); + navigator.clipboard.writeText(window.location.href); + showToast('URL copied to clipboard'); + $.colorbox.close(); + }); } /** @@ -27,68 +27,68 @@ function addShareModalButtonListeners() { * @param {JQuery} $modalLinks A collection of notes modal links. */ export function initNotesModal($modalLinks) { - addClickListeners($modalLinks, '640px'); - addNotesModalButtonListeners(); - addNotesReloadListeners($('.notes-textarea')); + addClickListeners($modalLinks, '640px'); + addNotesModalButtonListeners(); + addNotesReloadListeners($('.notes-textarea')); } /** * Adds click listeners to buttons in all notes modals on a page. */ function addNotesModalButtonListeners() { - $('.update-note-button').on('click', function (event) { - event.preventDefault(); - // Get form data - const formData = new FormData($(this).closest('form')[0]); - if (formData.get('notes')) { - const $deleteButton = $($(this).siblings()[0]); - - // Post data - const workOlid = formData.get('work_id'); - formData.delete('work_id'); - - $.ajax({ - url: `/works/${workOlid}/notes.json`, - data: formData, - type: 'POST', - contentType: false, - processData: false, - success: () => { - showToast('Update successful!'); - $.colorbox.close(); - $deleteButton.removeClass('hidden'); - }, - }); - } - }); - - $('.delete-note-button').on('click', function () { - if (confirm('Really delete this book note?')) { - const $button = $(this); - - // Get form data - const formData = new FormData($button.prop('form')); - - // Post data - const workOlid = formData.get('work_id'); - formData.delete('work_id'); - formData.delete('notes'); + $('.update-note-button').on('click', function (event) { + event.preventDefault(); + // Get form data + const formData = new FormData($(this).closest('form')[0]); + if (formData.get('notes')) { + const $deleteButton = $($(this).siblings()[0]); + + // Post data + const workOlid = formData.get('work_id'); + formData.delete('work_id'); + + $.ajax({ + url: `/works/${workOlid}/notes.json`, + data: formData, + type: 'POST', + contentType: false, + processData: false, + success: () => { + showToast('Update successful!'); + $.colorbox.close(); + $deleteButton.removeClass('hidden'); + }, + }); + } + }); - $.ajax({ - url: `/works/${workOlid}/notes.json`, - data: formData, - type: 'POST', - contentType: false, - processData: false, - success: () => { - showToast('Note deleted.'); - $.colorbox.close(); - $button.toggleClass('hidden'); - $button.closest('form').find('textarea').val(''); - }, - }); - } - }); + $('.delete-note-button').on('click', function () { + if (confirm('Really delete this book note?')) { + const $button = $(this); + + // Get form data + const formData = new FormData($button.prop('form')); + + // Post data + const workOlid = formData.get('work_id'); + formData.delete('work_id'); + formData.delete('notes'); + + $.ajax({ + url: `/works/${workOlid}/notes.json`, + data: formData, + type: 'POST', + contentType: false, + processData: false, + success: () => { + showToast('Note deleted.'); + $.colorbox.close(); + $button.toggleClass('hidden'); + $button.closest('form').find('textarea').val(''); + }, + }); + } + }); } /** @@ -98,65 +98,65 @@ function addNotesModalButtonListeners() { * from the view. */ export function addNotesPageButtonListeners() { - $('.update-note-link-button').on('click', function (event) { - event.preventDefault(); - const workId = $(this).parent().siblings('input')[0].value; - const editionId = $(this).parent().attr('id').split('-')[0]; - const note = $(this).parent().siblings('textarea')[0].value; - - const formData = new FormData(); - formData.append('notes', note); - formData.append('edition_id', `OL${editionId}M`); - - $.ajax({ - url: `/works/OL${workId}W/notes.json`, - data: formData, - type: 'POST', - contentType: false, - processData: false, - success: () => { - showToast('Update successful!'); - }, + $('.update-note-link-button').on('click', function (event) { + event.preventDefault(); + const workId = $(this).parent().siblings('input')[0].value; + const editionId = $(this).parent().attr('id').split('-')[0]; + const note = $(this).parent().siblings('textarea')[0].value; + + const formData = new FormData(); + formData.append('notes', note); + formData.append('edition_id', `OL${editionId}M`); + + $.ajax({ + url: `/works/OL${workId}W/notes.json`, + data: formData, + type: 'POST', + contentType: false, + processData: false, + success: () => { + showToast('Update successful!'); + }, + }); }); - }); - - $('.delete-note-button').on('click', function () { - if (confirm('Really delete this book note?')) { - const $parent = $(this).parent(); - - const workId = $(this).parent().siblings('input')[0].value; - const editionId = $(this).parent().attr('id').split('-')[0]; - - const formData = new FormData(); - formData.append('edition_id', `OL${editionId}M`); - $.ajax({ - url: `/works/OL${workId}W/notes.json`, - data: formData, - type: 'POST', - contentType: false, - processData: false, - success: () => { - showToast('Note deleted.'); - - // Remove list element from UI: - if ($parent.closest('.notes-list').children().length === 1) { - // This is the last edition for a set of notes on a work. - // Remove the work element: - $parent.closest('.main-list-item').remove(); - - if (!$('.main-list-item').length) { - $('.list-container')[0].innerText = 'No notes found.'; - } - } else { - // Notes for other editions of the work exist - // Remove the edition's notes list item: - $parent.closest('.notes-list-item').remove(); - } - }, - }); - } - }); + $('.delete-note-button').on('click', function () { + if (confirm('Really delete this book note?')) { + const $parent = $(this).parent(); + + const workId = $(this).parent().siblings('input')[0].value; + const editionId = $(this).parent().attr('id').split('-')[0]; + + const formData = new FormData(); + formData.append('edition_id', `OL${editionId}M`); + + $.ajax({ + url: `/works/OL${workId}W/notes.json`, + data: formData, + type: 'POST', + contentType: false, + processData: false, + success: () => { + showToast('Note deleted.'); + + // Remove list element from UI: + if ($parent.closest('.notes-list').children().length === 1) { + // This is the last edition for a set of notes on a work. + // Remove the work element: + $parent.closest('.main-list-item').remove(); + + if (!$('.main-list-item').length) { + $('.list-container')[0].innerText = 'No notes found.'; + } + } else { + // Notes for other editions of the work exist + // Remove the edition's notes list item: + $parent.closest('.notes-list-item').remove(); + } + }, + }); + } + }); } /** @@ -168,16 +168,16 @@ export function addNotesPageButtonListeners() { * @param {JQuery} $notesTextareas All notes text areas on a page. */ function addNotesReloadListeners($notesTextareas) { - $notesTextareas.each((_i, textarea) => { - const $textarea = $(textarea); - - $textarea.on('contentReload', () => { - const newValue = $textarea - .parent() - .find('.notes-modal-textarea')[0].value; - $textarea.val(newValue); + $notesTextareas.each((_i, textarea) => { + const $textarea = $(textarea); + + $textarea.on('contentReload', () => { + const newValue = $textarea + .parent() + .find('.notes-modal-textarea')[0].value; + $textarea.val(newValue); + }); }); - }); } /** @@ -187,7 +187,7 @@ function addNotesReloadListeners($notesTextareas) { * @param {JQuery} $parent Mount point for toast component */ function showToast(message, $parent) { - new FadingToast(message, $parent).show(); + new FadingToast(message, $parent).show(); } /** @@ -199,16 +199,16 @@ function showToast(message, $parent) { * @param {JQuery} $modalLinks A collection of observations modal links. */ export function initObservationsModal($modalLinks) { - addClickListeners($modalLinks, '800px'); - addObservationReloadListeners($('.observations-list')); - addDeleteObservationsListeners($('.delete-observations-button')); + addClickListeners($modalLinks, '800px'); + addObservationReloadListeners($('.observations-list')); + addDeleteObservationsListeners($('.delete-observations-button')); - $modalLinks.each((_i, modalLinkElement) => { - const $element = $(modalLinkElement); - const context = JSON.parse(getModalContent($element).dataset['context']); + $modalLinks.each((_i, modalLinkElement) => { + const $element = $(modalLinkElement); + const context = JSON.parse(getModalContent($element).dataset['context']); - addObservationChangeListeners($element.next(), context); - }); + addObservationChangeListeners($element.next(), context); + }); } /** @@ -220,13 +220,13 @@ export function initObservationsModal($modalLinks) { * @param {JQuery} $modalLinks A collection of modal links. */ function addClickListeners($modalLinks, maxWidth) { - $modalLinks.each((_i, modalLinkElement) => { - $(modalLinkElement).on('click', function () { - // Get context, which is attached to the modal content - const content = getModalContent($(this)); - displayModal(content, maxWidth); + $modalLinks.each((_i, modalLinkElement) => { + $(modalLinkElement).on('click', function () { + // Get context, which is attached to the modal content + const content = getModalContent($(this)); + displayModal(content, maxWidth); + }); }); - }); } /** @@ -237,7 +237,7 @@ function addClickListeners($modalLinks, maxWidth) { * @returns {HTMLElement} Reference to a modal's content */ function getModalContent($modalLink) { - return $modalLink.siblings()[0].children[0]; + return $modalLink.siblings()[0].children[0]; } /** @@ -251,61 +251,61 @@ function getModalContent($modalLink) { * @param {JQuery} $observationLists All of the observations lists on a page */ function addObservationReloadListeners($observationLists) { - $observationLists.each((_i, list) => { - $(list).on('contentReload', function () { - const $list = $(this); - const $buttonsDiv = $list.siblings('div').first(); - const id = $list.attr('id'); - const workOlid = `OL${id.split('-')[0]}W`; - - $list.empty(); - $list.append(` + $observationLists.each((_i, list) => { + $(list).on('contentReload', function () { + const $list = $(this); + const $buttonsDiv = $list.siblings('div').first(); + const id = $list.attr('id'); + const workOlid = `OL${id.split('-')[0]}W`; + + $list.empty(); + $list.append(` <li class="throbber-li"> <div class="throbber"><h3>Updating observations</h3></div> </li> `); - $.ajax({ - type: 'GET', - url: `/works/${workOlid}/observations`, - dataType: 'json', - }).done((data) => { - let listItems = ''; - for (const [category, values] of Object.entries(data)) { - let observations = values.join(', '); - observations = + $.ajax({ + type: 'GET', + url: `/works/${workOlid}/observations`, + dataType: 'json', + }).done((data) => { + let listItems = ''; + for (const [category, values] of Object.entries(data)) { + let observations = values.join(', '); + observations = observations.charAt(0).toUpperCase() + observations.slice(1); - listItems += ` + listItems += ` <li> <span class="observation-category">${category.charAt(0).toUpperCase() + category.slice(1)}:</span> ${observations} </li> `; - } + } - $list.empty(); + $list.empty(); - if (listItems.length === 0) { - listItems = ` + if (listItems.length === 0) { + listItems = ` <li> No observations for this work. </li> `; - $list.addClass('no-content'); - $buttonsDiv.removeClass('observation-buttons'); - $buttonsDiv.addClass('no-content'); - $buttonsDiv.children().first().addClass('hidden'); - } else { - $list.removeClass('no-content'); - $buttonsDiv.removeClass('no-content'); - $buttonsDiv.addClass('observation-buttons'); - $buttonsDiv.children().first().removeClass('hidden'); - } - - $list.append(listItems); - }); + $list.addClass('no-content'); + $buttonsDiv.removeClass('observation-buttons'); + $buttonsDiv.addClass('no-content'); + $buttonsDiv.children().first().addClass('hidden'); + } else { + $list.removeClass('no-content'); + $buttonsDiv.removeClass('no-content'); + $buttonsDiv.addClass('observation-buttons'); + $buttonsDiv.children().first().removeClass('hidden'); + } + + $list.append(listItems); + }); + }); }); - }); } /** @@ -319,39 +319,39 @@ function addObservationReloadListeners($observationLists) { * @param {JQuery} $deleteButtons All observation delete buttons found on a page. */ function addDeleteObservationsListeners($deleteButtons) { - $deleteButtons.each((_i, deleteButton) => { - const $button = $(deleteButton); - - $button.on('click', () => { - const workOlid = `OL${$button.prop('id').split('-')[0]}W`; - - $.ajax({ - url: `/works/${workOlid}/observations`, - type: 'DELETE', - contentType: 'application/json', - success: () => { - // Remove observations in view - const $observationsView = $button.closest('.observation-view'); - const $list = $observationsView.find('ul'); - - $list.empty(); - $list.append(` + $deleteButtons.each((_i, deleteButton) => { + const $button = $(deleteButton); + + $button.on('click', () => { + const workOlid = `OL${$button.prop('id').split('-')[0]}W`; + + $.ajax({ + url: `/works/${workOlid}/observations`, + type: 'DELETE', + contentType: 'application/json', + success: () => { + // Remove observations in view + const $observationsView = $button.closest('.observation-view'); + const $list = $observationsView.find('ul'); + + $list.empty(); + $list.append(` <li> No observations for this work. </li> `); - $list.addClass('no-content'); + $list.addClass('no-content'); - $button.parent().removeClass('observation-buttons'); - $button.parent().addClass('no-content'); - $button.addClass('hidden'); + $button.parent().removeClass('observation-buttons'); + $button.parent().addClass('no-content'); + $button.addClass('hidden'); - // find and clear modal selections - clearForm($button.siblings().find('form')); - }, - }); + // find and clear modal selections + clearForm($button.siblings().find('form')); + }, + }); + }); }); - }); } /** @@ -360,11 +360,11 @@ function addDeleteObservationsListeners($deleteButtons) { * @param {JQuery} $form An observations modal form */ function clearForm($form) { - $form.find('input').each((_i, input) => { - if (input.checked) { - input.checked = false; - } - }); + $form.find('input').each((_i, input) => { + if (input.checked) { + input.checked = false; + } + }); } /** @@ -376,24 +376,24 @@ function clearForm($form) { * @param {String} maxWidth The max width of the modal */ function displayModal(content, maxWidth) { - const modalId = `#${content.id}`; - const context = content.dataset['context'] - ? JSON.parse(content.dataset['context']) - : null; - const reloadId = context ? context.reloadId : null; - - $.colorbox({ - inline: true, - opacity: '0.5', - href: modalId, - width: '100%', - maxWidth: maxWidth, - onClosed: () => { - if (reloadId) { - $(`#${reloadId}`).trigger('contentReload'); - } - }, - }); + const modalId = `#${content.id}`; + const context = content.dataset['context'] + ? JSON.parse(content.dataset['context']) + : null; + const reloadId = context ? context.reloadId : null; + + $.colorbox({ + inline: true, + opacity: '0.5', + href: modalId, + width: '100%', + maxWidth: maxWidth, + onClosed: () => { + if (reloadId) { + $(`#${reloadId}`).trigger('contentReload'); + } + }, + }); } /** @@ -408,30 +408,30 @@ function displayModal(content, maxWidth) { * @param {Object} context An object containing the patron's username and the work's OLID. */ function addObservationChangeListeners($parent, context) { - const $questionSections = $parent.find('.aspect-section'); - const username = context.username; - const workOlid = context.work.split('/')[2]; - - $questionSections.each(function () { - const $inputs = $(this).find('input'); - - $inputs.each(function () { - $(this).on('change', function () { - const type = $(this).attr('name'); - const value = $(this).attr('value'); - const observation = {}; - observation[type] = value; - - const data = { - username: username, - action: `${$(this).prop('checked') ? 'add' : 'delete'}`, - observation: observation, - }; - - submitObservation($(this), workOlid, data, type); - }); + const $questionSections = $parent.find('.aspect-section'); + const username = context.username; + const workOlid = context.work.split('/')[2]; + + $questionSections.each(function () { + const $inputs = $(this).find('input'); + + $inputs.each(function () { + $(this).on('change', function () { + const type = $(this).attr('name'); + const value = $(this).attr('value'); + const observation = {}; + observation[type] = value; + + const data = { + username: username, + action: `${$(this).prop('checked') ? 'add' : 'delete'}`, + observation: observation, + }; + + submitObservation($(this), workOlid, data, type); + }); + }); }); - }); } /** @@ -442,24 +442,24 @@ function addObservationChangeListeners($parent, context) { * @param {String} sectionType Name of the input's section. */ function submitObservation($input, workOlid, data, sectionType) { - let toastMessage; - const capitalizedType = + let toastMessage; + const capitalizedType = sectionType[0].toUpperCase() + sectionType.substring(1); - // Make AJAX call - $.ajax({ - type: 'POST', - url: `/works/${workOlid}/observations`, - contentType: 'application/json', - data: JSON.stringify(data), - }) - .done(() => { - toastMessage = `${capitalizedType} saved!`; - }) - .fail(() => { - toastMessage = `${capitalizedType} save failed...`; + // Make AJAX call + $.ajax({ + type: 'POST', + url: `/works/${workOlid}/observations`, + contentType: 'application/json', + data: JSON.stringify(data), }) - .always(() => { - showToast(toastMessage, $input.closest('.metadata-form')); - }); + .done(() => { + toastMessage = `${capitalizedType} saved!`; + }) + .fail(() => { + toastMessage = `${capitalizedType} save failed...`; + }) + .always(() => { + showToast(toastMessage, $input.closest('.metadata-form')); + }); } diff --git a/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js b/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js index 79d35dce3bc..877e7843792 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js +++ b/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js @@ -20,50 +20,50 @@ import myBooksStore from './store'; * @class */ export class CreateListForm { - /** + /** * Creates a new `CreateListForm` object. * * Sets references to form inputs and "Create List" button. * * @param {HTMLElement} form */ - constructor(form) { + constructor(form) { /** * References this form's "Create List" button. * * @member {HTMLElement} */ - this.createListButton = form.querySelector('#create-list-button'); + this.createListButton = form.querySelector('#create-list-button'); - /** + /** * References the form's list title input field. * * @member {HTMLElement} */ - this.listTitleInput = form.querySelector('#list_label'); + this.listTitleInput = form.querySelector('#list_label'); - /** + /** * References the form's list description input field. * * @member {HTMLElement} */ - this.listDescriptionInput = form.querySelector('#list_desc'); + this.listDescriptionInput = form.querySelector('#list_desc'); - // Clear form on page refresh: - this.resetForm(); - } + // Clear form on page refresh: + this.resetForm(); + } - /** + /** * Attaches click listener to the "Create List" button. */ - initialize() { - this.createListButton.addEventListener('click', (event) => { - event.preventDefault(); - this.createNewList(); - }); - } + initialize() { + this.createListButton.addEventListener('click', (event) => { + event.preventDefault(); + this.createNewList(); + }); + } - /** + /** * Creates a new patron list. * * When a new list is created, the list's title and description @@ -78,65 +78,65 @@ export class CreateListForm { * * @async */ - async createNewList() { + async createNewList() { // Construct seed object for first list item: - const listTitle = websafe(this.listTitleInput.value); - const listDescription = websafe(this.listDescriptionInput.value); + const listTitle = websafe(this.listTitleInput.value); + const listDescription = websafe(this.listDescriptionInput.value); - const openDropper = myBooksStore.getOpenDropper(); - const seed = openDropper.readingLists.getSeed(); + const openDropper = myBooksStore.getOpenDropper(); + const seed = openDropper.readingLists.getSeed(); - const postData = { - name: listTitle, - description: listDescription, - seeds: [seed], - }; + const postData = { + name: listTitle, + description: listDescription, + seeds: [seed], + }; - // Call list creation service with seed object: - await createList(myBooksStore.getUserKey(), postData) - .then((response) => response.json()) - .then((data) => { - // Update active lists showcase: - attachNewActiveShowcaseItem(data['key'], seed, listTitle, data['key']); + // Call list creation service with seed object: + await createList(myBooksStore.getUserKey(), postData) + .then((response) => response.json()) + .then((data) => { + // Update active lists showcase: + attachNewActiveShowcaseItem(data['key'], seed, listTitle, data['key']); - // Update all droppers with new list data - this.updateDroppersOnListCreation(data['key'], listTitle, data['key']); + // Update all droppers with new list data + this.updateDroppersOnListCreation(data['key'], listTitle, data['key']); - // Clear list creation form fields, nullify seed - this.resetForm(); - }) - .finally(() => { - // Close the modal - $.colorbox.close(); - }); - } + // Clear list creation form fields, nullify seed + this.resetForm(); + }) + .finally(() => { + // Close the modal + $.colorbox.close(); + }); + } - /** + /** * Updates lists section of each dropper with a new list. * * @param {string} listKey Key of the newly created list * @param {string} listTitle Title of the new list */ - updateDroppersOnListCreation(listKey, listTitle, coverUrl) { - const droppers = myBooksStore.getDroppers(); - const openDropper = myBooksStore.getOpenDropper(); + updateDroppersOnListCreation(listKey, listTitle, coverUrl) { + const droppers = myBooksStore.getDroppers(); + const openDropper = myBooksStore.getOpenDropper(); - for (const dropper of droppers) { - const isActive = dropper === openDropper; - dropper.readingLists.onListCreationSuccess( - listKey, - listTitle, - isActive, - coverUrl, - ); + for (const dropper of droppers) { + const isActive = dropper === openDropper; + dropper.readingLists.onListCreationSuccess( + listKey, + listTitle, + isActive, + coverUrl, + ); + } } - } - /** + /** * Clears the list title and desciption fields in the form. */ - resetForm() { - this.listTitleInput.value = ''; - this.listDescriptionInput.value = ''; - } + resetForm() { + this.listTitleInput.value = ''; + this.listDescriptionInput.value = ''; + } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js index 6ec2cb3cbbb..ee87c8ed538 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js @@ -8,8 +8,8 @@ import { removeChildren } from '../utils'; import { CheckInComponents } from './MyBooksDropper/CheckInComponents'; import { ReadingLists } from './MyBooksDropper/ReadingLists'; import { - ReadingLogForms, - ReadingLogShelves, + ReadingLogForms, + ReadingLogShelves, } from './MyBooksDropper/ReadingLogForms'; import myBooksStore from './store'; @@ -30,119 +30,119 @@ import myBooksStore from './store'; * @augments Dropper */ export class MyBooksDropper extends Dropper { - /** + /** * Creates references to the given dropper's reading log forms, read date affordances, and * list affordances. * * @param {HTMLElement} dropper */ - constructor(dropper) { - super(dropper); + constructor(dropper) { + super(dropper); - const dropperActionCallbacks = { - closeDropper: this.closeDropper.bind(this), - toggleDropper: this.toggleDropper.bind(this), - }; + const dropperActionCallbacks = { + closeDropper: this.closeDropper.bind(this), + toggleDropper: this.toggleDropper.bind(this), + }; - /** + /** * Reference to this dropper's list content. * @member {ReadingLists} */ - this.readingLists = new ReadingLists(dropper, dropperActionCallbacks); + this.readingLists = new ReadingLists(dropper, dropperActionCallbacks); - /** + /** * Reference to the dropper's list loading indicator. * * This is only rendered when the patron is logged in. * @member {HTMLElement|null} */ - this.loadingIndicator = dropper.querySelector('.list-loading-indicator'); + this.loadingIndicator = dropper.querySelector('.list-loading-indicator'); - /** + /** * Reference to the interval ID of the animation `setInterval` call. * @member {NodeJS.Timer|undefined} */ - this.loadingAnimationId; + this.loadingAnimationId; - /** + /** * The work key associated with this dropper, if any. * * @member {string|undefined} */ - this.workKey = this.dropper.dataset.workKey; + this.workKey = this.dropper.dataset.workKey; - const splitKey = this.workKey ? this.workKey.split('/') : ['']; - const workOlid = splitKey[splitKey.length - 1]; + const splitKey = this.workKey ? this.workKey.split('/') : ['']; + const workOlid = splitKey[splitKey.length - 1]; - /** + /** * @type {CheckInComponents|null} */ - this.checkInComponents = workOlid - ? new CheckInComponents( - document.querySelector(`#check-in-container-${workOlid}`), - ) - : null; + this.checkInComponents = workOlid + ? new CheckInComponents( + document.querySelector(`#check-in-container-${workOlid}`), + ) + : null; - /** + /** * References this dropper's reading log buttons. * @member {ReadingLogForms} */ - this.readingLogForms = new ReadingLogForms( - dropper, - this.checkInComponents, - dropperActionCallbacks, - ); - } - - /** + this.readingLogForms = new ReadingLogForms( + dropper, + this.checkInComponents, + dropperActionCallbacks, + ); + } + + /** * Hydrates dropper contents and loads patron's lists. */ - initialize() { - super.initialize(); - - this.readingLogForms.initialize(); - this.readingLists.initialize(); - if (this.checkInComponents) { - this.checkInComponents.initialize(); + initialize() { + super.initialize(); + + this.readingLogForms.initialize(); + this.readingLists.initialize(); + if (this.checkInComponents) { + this.checkInComponents.initialize(); + } + + this.loadingAnimationId = this.initLoadingAnimation( + this.dropper.querySelector('.loading-ellipsis'), + ); } - this.loadingAnimationId = this.initLoadingAnimation( - this.dropper.querySelector('.loading-ellipsis'), - ); - } - - /** + /** * Creates loading animation for list loading indicator. * * @param {HTMLElement} loadingIndicator * @returns {NodeJS.Timer} */ - initLoadingAnimation(loadingIndicator) { - let count = 0; - const intervalId = setInterval(() => { - let ellipsis = ''; - for (let i = 0; i < count % 4; ++i) { - ellipsis += '.'; - } - loadingIndicator.innerText = ellipsis; - ++count; - }, 1500); - - return intervalId; - } - - /** + initLoadingAnimation(loadingIndicator) { + let count = 0; + const intervalId = setInterval(() => { + let ellipsis = ''; + for (let i = 0; i < count % 4; ++i) { + ellipsis += '.'; + } + loadingIndicator.innerText = ellipsis; + ++count; + }, 1500); + + return intervalId; + } + + /** * Replaces dropper loading indicator with the given * partially rendered list affordances. * * @param {string} partialHtml */ - updateReadingLists(partialHtml) { - clearInterval(this.loadingAnimationId); - this.replaceLoadingIndicators(this.loadingIndicator, partialHtml); - } + updateReadingLists(partialHtml) { + clearInterval(this.loadingAnimationId); + this.replaceLoadingIndicators(this.loadingIndicator, partialHtml); + } - /** + /** * Returns an array of seed keys associated with this dropper. * * If the seed identifies a book, there should be both an @@ -151,42 +151,42 @@ export class MyBooksDropper extends Dropper { * * @returns {Array<string>} */ - getSeedKeys() { - const results = [this.readingLists.seedKey]; - if (this.readingLists.workKey) { - results.push(this.readingLists.workKey); + getSeedKeys() { + const results = [this.readingLists.seedKey]; + if (this.readingLists.workKey) { + results.push(this.readingLists.workKey); + } + return results; } - return results; - } - /** + /** * Object returned by the list partials endpoint. * * @typedef {Object} ListPartials * @property {string} dropper HTML string for dropdown list content * @property {string} active HTML string for patron's active lists */ - /** + /** * Replaces list loading indicators with the given partially rendered HTML. * * @param {HTMLElement} dropperListsPlaceholder Loading indicator found inside of the dropdown content * @param {ListPartials} partials */ - replaceLoadingIndicators(dropperListsPlaceholder, partialHTML) { - const dropperParent = dropperListsPlaceholder - ? dropperListsPlaceholder.parentElement - : null; - - if (dropperParent) { - removeChildren(dropperParent); - dropperParent.insertAdjacentHTML('afterbegin', partialHTML); - - const anchors = this.dropper.querySelectorAll('.modify-list'); - this.readingLists.initModifyListAffordances(anchors); + replaceLoadingIndicators(dropperListsPlaceholder, partialHTML) { + const dropperParent = dropperListsPlaceholder + ? dropperListsPlaceholder.parentElement + : null; + + if (dropperParent) { + removeChildren(dropperParent); + dropperParent.insertAdjacentHTML('afterbegin', partialHTML); + + const anchors = this.dropper.querySelectorAll('.modify-list'); + this.readingLists.initModifyListAffordances(anchors); + } } - } - /** + /** * Updates this dropper's primary button's state and display to show that a book is active on the * given shelf. * @@ -195,43 +195,43 @@ export class MyBooksDropper extends Dropper { * * @param shelf {ReadingLogShelf} */ - updateShelfDisplay(shelf) { - this.readingLogForms.updateActivatedStatus(true); - this.readingLogForms.updatePrimaryBookshelfId(Number(shelf)); - this.readingLogForms.updatePrimaryButtonText( - this.readingLogForms.getDisplayString(shelf), - ); - - if (this.checkInComponents) { - if ( - !this.checkInComponents.hasReadDate() && + updateShelfDisplay(shelf) { + this.readingLogForms.updateActivatedStatus(true); + this.readingLogForms.updatePrimaryBookshelfId(Number(shelf)); + this.readingLogForms.updatePrimaryButtonText( + this.readingLogForms.getDisplayString(shelf), + ); + + if (this.checkInComponents) { + if ( + !this.checkInComponents.hasReadDate() && shelf === ReadingLogShelves.ALREADY_READ - ) { - this.checkInComponents.showCheckInDisplay(); - } else { - this.checkInComponents.hideCheckInPrompt(); - } + ) { + this.checkInComponents.showCheckInDisplay(); + } else { + this.checkInComponents.hideCheckInPrompt(); + } + } } - } - // Dropper overrides: - /** + // Dropper overrides: + /** * Updates store with reference to the opened dropper. * * @override */ - onOpen() { - myBooksStore.setOpenDropper(this); - } + onOpen() { + myBooksStore.setOpenDropper(this); + } - /** + /** * Redirects to login page when disabled dropper is clicked. * * My Books droppers are disabled for unauthenticated patrons. * * @override */ - onDisabledClick() { - window.location = `/account/login?redirect=${location.pathname}`; - } + onDisabledClick() { + window.location = `/account/login?redirect=${location.pathname}`; + } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js index 399522928aa..90db2c813bb 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js @@ -21,7 +21,7 @@ const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; * @returns `true` if the given year is a leap year. */ function isLeapYear(year) { - return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); } /** @@ -37,206 +37,206 @@ function isLeapYear(year) { * @class */ export class CheckInComponents { - /** + /** * @param checkInContainer */ - constructor(checkInContainer) { + constructor(checkInContainer) { // HTML for the check-in components is not rendered if // the patron is unauthenticated, or if the dropper // is for an orphaned edition. - if (!checkInContainer) { - return; - } + if (!checkInContainer) { + return; + } - /** + /** * @typedef {object} ReadDateConfig * @property {string} workOlid * @property {string} [editionKey] * @property {string} [lastReadDate] * @property {number} [eventId] */ - /** + /** * @type {ReadDateConfig} */ - this.config = JSON.parse(checkInContainer.dataset.config); + this.config = JSON.parse(checkInContainer.dataset.config); - const checkInPromptElem = + const checkInPromptElem = checkInContainer.querySelector('.check-in-prompt'); - /** + /** * @type {CheckInPrompt} */ - this.checkInPrompt = new CheckInPrompt(checkInPromptElem); + this.checkInPrompt = new CheckInPrompt(checkInPromptElem); - const checkInDisplayElem = + const checkInDisplayElem = checkInContainer.querySelector('.last-read-date'); - /** + /** * @type {CheckInDisplay} */ - this.checkInDisplay = new CheckInDisplay(checkInDisplayElem); + this.checkInDisplay = new CheckInDisplay(checkInDisplayElem); - /** + /** * References element that will be displayed in last read date form modal. * Set during form initialization. * * @type {HTMLElement|undefined} */ - this.modalContent = undefined; + this.modalContent = undefined; - /** + /** * @type {CheckInForm|undefined} */ - this.checkInForm = undefined; - } - - initialize() { - this.checkInPrompt.initialize(); - this.checkInPrompt - .getRootElement() - .addEventListener('submit-check-in', (event) => { - const year = event.detail.year; - const month = event.detail.month; - const day = event.detail.day; - - const eventData = this.prepareEventRequest(year, month, day); - this.postCheckIn(eventData, this.checkInForm.getFormAction()) - .then((resp) => { - if (!resp.ok) { - throw Error(`Check-in request failed. Status: ${resp.status}`); - } - this.updateDateAndShowDisplay(year, month, day); - }) - .catch(() => { - new PersistentToast( - 'Failed to submit check-in. Please try again in a few moments.', - ).show(); - }); - }); - - let hiddenModalContentContainer = document.querySelector( - '#hidden-modal-content-container', - ); - if (!hiddenModalContentContainer) { - hiddenModalContentContainer = document.createElement('div'); - hiddenModalContentContainer.classList.add('hidden'); - hiddenModalContentContainer.id = 'hidden-modal-content-container'; - document.body.appendChild(hiddenModalContentContainer); - } - - const modalContent = this.createModalContentFromTemplate(); - hiddenModalContentContainer.appendChild(modalContent); - - this.modalContent = hiddenModalContentContainer.querySelector( - `#modal-content-${this.config.workOlid}`, - ); - - const formElem = this.modalContent.querySelector('form'); - this.checkInForm = new CheckInForm( - formElem, - this.config.workOlid, - this.config.editionKey || '', - this.config.lastReadDate || '', - this.config.eventId, - ); - this.checkInForm.initialize(); - this.checkInForm - .getRootElement() - .addEventListener('delete-check-in', () => { - this.deleteCheckIn(this.checkInForm.getEventId()) - .then((resp) => { - if (!resp.ok) { - throw Error( - `Check-in delete request failed. Status: ${resp.status}`, - ); - } + this.checkInForm = undefined; + } - this.checkInForm.resetForm(); - this.checkInDisplay.hide(); - this.checkInPrompt.show(); - }) - .catch(() => { - // TODO : Use localized strings - new PersistentToast( - 'Failed to delete check-in. Please try again in a few moments.', - ).show(); - }) - .finally(() => { - this.closeModal(); - }); - }); - this.checkInForm - .getRootElement() - .addEventListener('submit-check-in', (event) => { - const year = event.detail.year; - const month = event.detail.month; - const day = event.detail.day; - - const eventData = this.prepareEventRequest(year, month, day); - this.postCheckIn(eventData, this.checkInForm.getFormAction()) - .then((resp) => { - if (!resp.ok) { - throw Error(`Check-in request failed. Status: ${resp.status}`); - } - this.updateDateAndShowDisplay(year, month, day); - }) - .catch(() => { - // TODO : Use localized strings - new PersistentToast( - 'Failed to submit check-in. Please try again in a few moments.', - ).show(); - }) - .finally(() => { - this.closeModal(); - }); - }); - - const closeModalElements = + initialize() { + this.checkInPrompt.initialize(); + this.checkInPrompt + .getRootElement() + .addEventListener('submit-check-in', (event) => { + const year = event.detail.year; + const month = event.detail.month; + const day = event.detail.day; + + const eventData = this.prepareEventRequest(year, month, day); + this.postCheckIn(eventData, this.checkInForm.getFormAction()) + .then((resp) => { + if (!resp.ok) { + throw Error(`Check-in request failed. Status: ${resp.status}`); + } + this.updateDateAndShowDisplay(year, month, day); + }) + .catch(() => { + new PersistentToast( + 'Failed to submit check-in. Please try again in a few moments.', + ).show(); + }); + }); + + let hiddenModalContentContainer = document.querySelector( + '#hidden-modal-content-container', + ); + if (!hiddenModalContentContainer) { + hiddenModalContentContainer = document.createElement('div'); + hiddenModalContentContainer.classList.add('hidden'); + hiddenModalContentContainer.id = 'hidden-modal-content-container'; + document.body.appendChild(hiddenModalContentContainer); + } + + const modalContent = this.createModalContentFromTemplate(); + hiddenModalContentContainer.appendChild(modalContent); + + this.modalContent = hiddenModalContentContainer.querySelector( + `#modal-content-${this.config.workOlid}`, + ); + + const formElem = this.modalContent.querySelector('form'); + this.checkInForm = new CheckInForm( + formElem, + this.config.workOlid, + this.config.editionKey || '', + this.config.lastReadDate || '', + this.config.eventId, + ); + this.checkInForm.initialize(); + this.checkInForm + .getRootElement() + .addEventListener('delete-check-in', () => { + this.deleteCheckIn(this.checkInForm.getEventId()) + .then((resp) => { + if (!resp.ok) { + throw Error( + `Check-in delete request failed. Status: ${resp.status}`, + ); + } + + this.checkInForm.resetForm(); + this.checkInDisplay.hide(); + this.checkInPrompt.show(); + }) + .catch(() => { + // TODO : Use localized strings + new PersistentToast( + 'Failed to delete check-in. Please try again in a few moments.', + ).show(); + }) + .finally(() => { + this.closeModal(); + }); + }); + this.checkInForm + .getRootElement() + .addEventListener('submit-check-in', (event) => { + const year = event.detail.year; + const month = event.detail.month; + const day = event.detail.day; + + const eventData = this.prepareEventRequest(year, month, day); + this.postCheckIn(eventData, this.checkInForm.getFormAction()) + .then((resp) => { + if (!resp.ok) { + throw Error(`Check-in request failed. Status: ${resp.status}`); + } + this.updateDateAndShowDisplay(year, month, day); + }) + .catch(() => { + // TODO : Use localized strings + new PersistentToast( + 'Failed to submit check-in. Please try again in a few moments.', + ).show(); + }) + .finally(() => { + this.closeModal(); + }); + }); + + const closeModalElements = this.modalContent.querySelectorAll('.dialog--close'); - initDialogClosers(closeModalElements); - } + initDialogClosers(closeModalElements); + } - /** + /** * Creates a new element containing the check-in form and `colorbox` modal content. * * @returns {HTMLElement} */ - createModalContentFromTemplate() { - const templateElem = document.createElement('template'); - const modalContentTemplate = document.querySelector('#check-in-form-modal'); - templateElem.innerHTML = modalContentTemplate.outerHTML; - const modalContent = templateElem.content.firstElementChild; - modalContent.id = `modal-content-${this.config.workOlid}`; + createModalContentFromTemplate() { + const templateElem = document.createElement('template'); + const modalContentTemplate = document.querySelector('#check-in-form-modal'); + templateElem.innerHTML = modalContentTemplate.outerHTML; + const modalContent = templateElem.content.firstElementChild; + modalContent.id = `modal-content-${this.config.workOlid}`; - return modalContent; - } + return modalContent; + } - /** + /** * Updates the date display and form with the given date, and shows the display. * * @param {number} year * @param {number|null} month * @param {number|null} day */ - updateDateAndShowDisplay(year, month = null, day = null) { + updateDateAndShowDisplay(year, month = null, day = null) { // Update last read date display - let dateString = String(year); - if (month) { - dateString += `-${String(month).padStart(2, '0')}`; - if (day) { - dateString += `-${String(day).padStart(2, '0')}`; - } - } - this.checkInDisplay.updateDateDisplay(dateString); + let dateString = String(year); + if (month) { + dateString += `-${String(month).padStart(2, '0')}`; + if (day) { + dateString += `-${String(day).padStart(2, '0')}`; + } + } + this.checkInDisplay.updateDateDisplay(dateString); - // Update component visibility - this.checkInPrompt.hide(); - this.checkInDisplay.show(); + // Update component visibility + this.checkInPrompt.hide(); + this.checkInDisplay.show(); - // Update submission form - this.checkInForm.updateSelectedDate(year, month, day); - this.checkInForm.showDeleteButton(); - } + // Update submission form + this.checkInForm.updateSelectedDate(year, month, day); + this.checkInForm.showDeleteButton(); + } - /** + /** * @typedef {object} CheckInEventPostRequestData * @property {number} event_type * @property {number} year @@ -245,37 +245,37 @@ export class CheckInComponents { * @property {number|null} event_id * @property {string} [edition_key] */ - /** + /** * Posts the given data to the backend check-in handler. * * @param {CheckInEventPostRequestData} eventData * @param {string} url * @returns {Promise<Response>} */ - postCheckIn(eventData, url) { - return fetch(url, { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded', - accept: 'application/json', - }, - body: JSON.stringify(eventData), - }); - } - - /** + postCheckIn(eventData, url) { + return fetch(url, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + accept: 'application/json', + }, + body: JSON.stringify(eventData), + }); + } + + /** * Posts request to delete the read date record with the given ID. * * @param {string} eventId * @returns {Promise<Response>} */ - async deleteCheckIn(eventId) { - return fetch(`/check-ins/${eventId}`, { - method: 'DELETE', - }); - } + async deleteCheckIn(eventId) { + return fetch(`/check-ins/${eventId}`, { + method: 'DELETE', + }); + } - /** + /** * Prepares data for a `postEvent` call. * * @param {number} year @@ -283,79 +283,79 @@ export class CheckInComponents { * @param {number|null} day * @returns {CheckInEventPostRequestData} */ - prepareEventRequest(year, month = null, day = null) { + prepareEventRequest(year, month = null, day = null) { // Get event id - const eventId = this.checkInForm.getEventId(); + const eventId = this.checkInForm.getEventId(); - // Get event type - const eventType = this.checkInForm.getEventType(); + // Get event type + const eventType = this.checkInForm.getEventType(); - const eventRequest = { - event_id: eventId ? Number(eventId) : null, - event_type: Number(eventType), - year: year, - month: month, - day: day, - }; + const eventRequest = { + event_id: eventId ? Number(eventId) : null, + event_type: Number(eventType), + year: year, + month: month, + day: day, + }; - const editionKey = this.checkInForm.getEditionKey() || null; - if (editionKey) { - eventRequest.edition_key = editionKey; - } + const editionKey = this.checkInForm.getEditionKey() || null; + if (editionKey) { + eventRequest.edition_key = editionKey; + } - return eventRequest; - } + return eventRequest; + } - /** + /** * Returns `true` if the check-in display is visible on the screen. * * @returns {boolean} */ - hasReadDate() { - return !this.checkInDisplay.getRootElement().classList.contains('hidden'); - } + hasReadDate() { + return !this.checkInDisplay.getRootElement().classList.contains('hidden'); + } - /** + /** * Resets the check-in form. */ - resetForm() { - this.checkInForm.resetForm(); - } + resetForm() { + this.checkInForm.resetForm(); + } - /** + /** * Show the check-in display. */ - showCheckInDisplay() { - this.checkInDisplay.show(); - } + showCheckInDisplay() { + this.checkInDisplay.show(); + } - /** + /** * Hide the check-in display. */ - hideCheckInDisplay() { - this.checkInDisplay.hide(); - } + hideCheckInDisplay() { + this.checkInDisplay.hide(); + } - /** + /** * Show the check-in prompt. */ - showCheckInPrompt() { - this.checkInPrompt.show(); - } + showCheckInPrompt() { + this.checkInPrompt.show(); + } - /** + /** * Hide the check-in prompt. */ - hideCheckInPrompt() { - this.checkInPrompt.hide(); - } + hideCheckInPrompt() { + this.checkInPrompt.hide(); + } - /** + /** * Closes the opened `colorbox` modal. */ - closeModal() { - $.colorbox.close(); - } + closeModal() { + $.colorbox.close(); + } } /** @@ -365,73 +365,73 @@ export class CheckInComponents { * @class */ class CheckInPrompt { - /** + /** * @param {HTMLElement} checkInPrompt */ - constructor(checkInPrompt) { - this.rootElem = checkInPrompt; - } - - initialize() { - const yearLink = this.rootElem.querySelector('.prompt-current-year'); - yearLink.addEventListener('click', () => { - // Get the current year - const year = new Date().getFullYear(); - - this.dispatchCheckInSubmission(year); - }); - - const todayLink = this.rootElem.querySelector('.prompt-today'); - todayLink.addEventListener('click', () => { - // Get today's date - const now = new Date(); - const year = now.getFullYear(); - const month = now.getMonth() + 1; - const day = now.getDate(); + constructor(checkInPrompt) { + this.rootElem = checkInPrompt; + } - this.dispatchCheckInSubmission(year, month, day); - }); - } + initialize() { + const yearLink = this.rootElem.querySelector('.prompt-current-year'); + yearLink.addEventListener('click', () => { + // Get the current year + const year = new Date().getFullYear(); + + this.dispatchCheckInSubmission(year); + }); + + const todayLink = this.rootElem.querySelector('.prompt-today'); + todayLink.addEventListener('click', () => { + // Get today's date + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth() + 1; + const day = now.getDate(); + + this.dispatchCheckInSubmission(year, month, day); + }); + } - /** + /** * Dispatches a custom `submit-check-in` event with the given date. * * @param {number} year * @param {number|null} month * @param {number|null} day */ - dispatchCheckInSubmission(year, month = null, day = null) { - const submitEvent = new CustomEvent('submit-check-in', { - detail: { - year: year, - month: month, - day: day, - }, - }); - this.rootElem.dispatchEvent(submitEvent); - } - - /** + dispatchCheckInSubmission(year, month = null, day = null) { + const submitEvent = new CustomEvent('submit-check-in', { + detail: { + year: year, + month: month, + day: day, + }, + }); + this.rootElem.dispatchEvent(submitEvent); + } + + /** * Hides this check-in prompt. */ - hide() { - this.rootElem.classList.add('hidden'); - } + hide() { + this.rootElem.classList.add('hidden'); + } - /** + /** * Shows this check-in prompt. */ - show() { - this.rootElem.classList.remove('hidden'); - } + show() { + this.rootElem.classList.remove('hidden'); + } - /** + /** * Returns reference to the root element of this check-in prompt. * @returns {HTMLElement} */ - getRootElement() { - return this.rootElem; - } + getRootElement() { + return this.rootElem; + } } /** @@ -440,43 +440,43 @@ class CheckInPrompt { * @class */ class CheckInDisplay { - /** + /** * @param {HTMLElement} checkInDisplay */ - constructor(checkInDisplay) { - this.rootElem = checkInDisplay; - this.dateDisplayElem = this.rootElem.querySelector('.check-in-date'); - } + constructor(checkInDisplay) { + this.rootElem = checkInDisplay; + this.dateDisplayElem = this.rootElem.querySelector('.check-in-date'); + } - /** + /** * Updates the date displayed to the given string. * * @param {string} date */ - updateDateDisplay(date) { - this.dateDisplayElem.textContent = date; - } + updateDateDisplay(date) { + this.dateDisplayElem.textContent = date; + } - /** + /** * Hides this date display. */ - hide() { - this.rootElem.classList.add('hidden'); - } + hide() { + this.rootElem.classList.add('hidden'); + } - /** + /** * Shows this date display. */ - show() { - this.rootElem.classList.remove('hidden'); - } + show() { + this.rootElem.classList.remove('hidden'); + } - /** + /** * @returns {HTMLElement} */ - getRootElement() { - return this.rootElem; - } + getRootElement() { + return this.rootElem; + } } /** @@ -490,347 +490,347 @@ class CheckInDisplay { * @class */ export class CheckInForm { - /** + /** * @param {HTMLFormElement} formElem * @param {string} workOlid * @param {string|null} editionKey * @param {string|null} lastReadDate * @param {number|null} eventId */ - constructor( - formElem, - workOlid, - editionKey = null, - lastReadDate = null, - eventId = null, - ) { - this.rootElem = formElem; - this.workOlid = workOlid; - this.editionKey = editionKey; - this.lastReadDate = lastReadDate; - this.eventId = eventId; - - /** + constructor( + formElem, + workOlid, + editionKey = null, + lastReadDate = null, + eventId = null, + ) { + this.rootElem = formElem; + this.workOlid = workOlid; + this.editionKey = editionKey; + this.lastReadDate = lastReadDate; + this.eventId = eventId; + + /** * Reference to hidden `event_type` form input. * * @type {HTMLInputElement|undefined} */ - this.eventTypeInput = this.rootElem.querySelector('input[name=event_type]'); + this.eventTypeInput = this.rootElem.querySelector('input[name=event_type]'); - /** + /** * Reference to hidden `event_id` form input. * * @type {HTMLInputElement|undefined} */ - this.eventIdInput = this.rootElem.querySelector('input[name=event_id]'); + this.eventIdInput = this.rootElem.querySelector('input[name=event_id]'); - /** + /** * Reference to hidden `edition_key` form input. * * @type {HTMLInputElement} */ - this.editionKeyInput = this.rootElem.querySelector( - 'input[name=edition_key]', - ); + this.editionKeyInput = this.rootElem.querySelector( + 'input[name=edition_key]', + ); - /** + /** * Reference to the form's year `select` element. * * @type {HTMLSelectElement} */ - this.yearSelect = this.rootElem.querySelector('select[name=year]'); + this.yearSelect = this.rootElem.querySelector('select[name=year]'); - /** + /** * Reference to the form's month `select` element. * * @type {HTMLSelectElement} */ - this.monthSelect = this.rootElem.querySelector('select[name=month]'); + this.monthSelect = this.rootElem.querySelector('select[name=month]'); - /** + /** * Reference to the form's day `select` element. * * @type {HTMLSelectElement} */ - this.daySelect = this.rootElem.querySelector('select[name=day]'); + this.daySelect = this.rootElem.querySelector('select[name=day]'); - /** + /** * Reference to the form's submit button. * @type {HTMLButtonElement} */ - this.submitButton = this.rootElem.querySelector('.check-in__submit-btn'); + this.submitButton = this.rootElem.querySelector('.check-in__submit-btn'); - /** + /** * Reference to the form's delete button. * * @type {HTMLButtonElement} */ - this.deleteButton = this.rootElem.querySelector('.check-in__delete-btn'); - } + this.deleteButton = this.rootElem.querySelector('.check-in__delete-btn'); + } - initialize() { + initialize() { // Set form's action - this.rootElem.action = `/works/${this.workOlid}/check-ins.json`; - // Set form's event ID - if (this.eventId) { - this.setEventId(this.eventId); - this.showDeleteButton(); - } - // Set form's edition_key - if (this.editionKey) { - this.editionKeyInput.value = this.editionKey; - } - // Set date select elements to the last read date - const [yearString, monthString, dayString] = this.lastReadDate - ? this.lastReadDate.split('-') - : [null, null, null]; - this.updateSelectedDate( - Number(yearString), - Number(monthString), - Number(dayString), - ); - - // Update form for new years day - const currentYear = new Date().getFullYear(); - const hiddenYear = this.yearSelect.querySelector('.show-if-local-year'); - // The year select element has a hidden option for next year. This - // option is shown on 1 January if the client's local year is different - // from the server's local year. - if (Number(hiddenYear.value) === currentYear) { - hiddenYear.classList.remove('hidden'); - } - - // Associate labels with select elements - const yearLabel = this.rootElem.querySelector('.check-in__year-label'); - const yearSelectId = `year-select-${this.workOlid}`; - this.yearSelect.id = yearSelectId; - yearLabel.htmlFor = yearSelectId; - - const monthLabel = this.rootElem.querySelector('.check-in__month-label'); - const monthSelectId = `month-select-${this.workOlid}`; - this.monthSelect.id = monthSelectId; - monthLabel.htmlFor = monthSelectId; - - const dayLabel = this.rootElem.querySelector('.check-in__day-label'); - const daySelectId = `day-select-${this.workOlid}`; - this.daySelect.id = daySelectId; - dayLabel.htmlFor = daySelectId; - - // Add listeners to form elements: - this.yearSelect.addEventListener('change', () => { - this.onDateSelectionChange(); - }); - this.monthSelect.addEventListener('change', () => { - this.onDateSelectionChange(); - }); - this.deleteButton.addEventListener('click', (event) => { - event.preventDefault(); - const deleteEvent = new CustomEvent('delete-check-in'); - this.rootElem.dispatchEvent(deleteEvent); - }); - this.submitButton.addEventListener('click', (event) => { - event.preventDefault(); - const submitEvent = new CustomEvent('submit-check-in', { - detail: { - year: this.getSelectedYear(), - month: this.getSelectedMonth(), - day: this.getSelectedDay(), - }, - }); - this.rootElem.dispatchEvent(submitEvent); - }); - const todayLink = this.rootElem.querySelector('.check-in__today'); - todayLink.addEventListener('click', () => { - // Get today's date - const now = new Date(); - const year = now.getFullYear(); - const month = now.getMonth() + 1; - const day = now.getDate(); - - this.updateSelectedDate(year, month, day); - }); - } - - /** + this.rootElem.action = `/works/${this.workOlid}/check-ins.json`; + // Set form's event ID + if (this.eventId) { + this.setEventId(this.eventId); + this.showDeleteButton(); + } + // Set form's edition_key + if (this.editionKey) { + this.editionKeyInput.value = this.editionKey; + } + // Set date select elements to the last read date + const [yearString, monthString, dayString] = this.lastReadDate + ? this.lastReadDate.split('-') + : [null, null, null]; + this.updateSelectedDate( + Number(yearString), + Number(monthString), + Number(dayString), + ); + + // Update form for new years day + const currentYear = new Date().getFullYear(); + const hiddenYear = this.yearSelect.querySelector('.show-if-local-year'); + // The year select element has a hidden option for next year. This + // option is shown on 1 January if the client's local year is different + // from the server's local year. + if (Number(hiddenYear.value) === currentYear) { + hiddenYear.classList.remove('hidden'); + } + + // Associate labels with select elements + const yearLabel = this.rootElem.querySelector('.check-in__year-label'); + const yearSelectId = `year-select-${this.workOlid}`; + this.yearSelect.id = yearSelectId; + yearLabel.htmlFor = yearSelectId; + + const monthLabel = this.rootElem.querySelector('.check-in__month-label'); + const monthSelectId = `month-select-${this.workOlid}`; + this.monthSelect.id = monthSelectId; + monthLabel.htmlFor = monthSelectId; + + const dayLabel = this.rootElem.querySelector('.check-in__day-label'); + const daySelectId = `day-select-${this.workOlid}`; + this.daySelect.id = daySelectId; + dayLabel.htmlFor = daySelectId; + + // Add listeners to form elements: + this.yearSelect.addEventListener('change', () => { + this.onDateSelectionChange(); + }); + this.monthSelect.addEventListener('change', () => { + this.onDateSelectionChange(); + }); + this.deleteButton.addEventListener('click', (event) => { + event.preventDefault(); + const deleteEvent = new CustomEvent('delete-check-in'); + this.rootElem.dispatchEvent(deleteEvent); + }); + this.submitButton.addEventListener('click', (event) => { + event.preventDefault(); + const submitEvent = new CustomEvent('submit-check-in', { + detail: { + year: this.getSelectedYear(), + month: this.getSelectedMonth(), + day: this.getSelectedDay(), + }, + }); + this.rootElem.dispatchEvent(submitEvent); + }); + const todayLink = this.rootElem.querySelector('.check-in__today'); + todayLink.addEventListener('click', () => { + // Get today's date + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth() + 1; + const day = now.getDate(); + + this.updateSelectedDate(year, month, day); + }); + } + + /** * Gets currently selected date, then updates the form. */ - onDateSelectionChange() { - const year = this.yearSelect.selectedIndex - ? Number(this.yearSelect.value) - : null; - this.updateSelectedDate( - year, - this.monthSelect.selectedIndex, - this.daySelect.selectedIndex, - ); - } - - /** + onDateSelectionChange() { + const year = this.yearSelect.selectedIndex + ? Number(this.yearSelect.value) + : null; + this.updateSelectedDate( + year, + this.monthSelect.selectedIndex, + this.daySelect.selectedIndex, + ); + } + + /** * Updates date select elements based on the given year, month, and day. * * @param {number|null} year * @param {number|null} month * @param {number|null} day */ - updateSelectedDate(year = null, month = null, day = null) { - if (!month) { - day = null; - } - if (!year) { - month = null; - day = null; - } - - if (year) { - this.yearSelect.value = year || ''; - this.monthSelect.disabled = false; - this.submitButton.disabled = false; - } else { - this.yearSelect.selectedIndex = 0; - this.monthSelect.disabled = true; - this.submitButton.disabled = true; - } - if (month) { - this.monthSelect.value = month || ''; - this.daySelect.disabled = false; - - // Update daySelect options for month/leap year - let daysInMonth = DAYS_IN_MONTH[month - 1]; - if (month === 2 && isLeapYear(year)) { - ++daysInMonth; - } - this.updateDayOptions(daysInMonth); - } else { - this.monthSelect.selectedIndex = 0; - this.daySelect.disabled = true; - } - if (day) { - const daysInMonth = DAYS_IN_MONTH[this.monthSelect.selectedIndex - 1]; - this.daySelect.selectedIndex = day > daysInMonth ? 0 : day; - } else { - this.daySelect.selectedIndex = 0; - } - } - - /** + updateSelectedDate(year = null, month = null, day = null) { + if (!month) { + day = null; + } + if (!year) { + month = null; + day = null; + } + + if (year) { + this.yearSelect.value = year || ''; + this.monthSelect.disabled = false; + this.submitButton.disabled = false; + } else { + this.yearSelect.selectedIndex = 0; + this.monthSelect.disabled = true; + this.submitButton.disabled = true; + } + if (month) { + this.monthSelect.value = month || ''; + this.daySelect.disabled = false; + + // Update daySelect options for month/leap year + let daysInMonth = DAYS_IN_MONTH[month - 1]; + if (month === 2 && isLeapYear(year)) { + ++daysInMonth; + } + this.updateDayOptions(daysInMonth); + } else { + this.monthSelect.selectedIndex = 0; + this.daySelect.disabled = true; + } + if (day) { + const daysInMonth = DAYS_IN_MONTH[this.monthSelect.selectedIndex - 1]; + this.daySelect.selectedIndex = day > daysInMonth ? 0 : day; + } else { + this.daySelect.selectedIndex = 0; + } + } + + /** * Updates day select options, hiding days greater than the given amount. * * @param {number} daysInMonth */ - updateDayOptions(daysInMonth) { - for (let i = 0; i < this.daySelect.options.length; ++i) { - if (i <= daysInMonth) { - this.daySelect.options[i].classList.remove('hidden'); - } else { - this.daySelect.options[i].classList.add('hidden'); - } + updateDayOptions(daysInMonth) { + for (let i = 0; i < this.daySelect.options.length; ++i) { + if (i <= daysInMonth) { + this.daySelect.options[i].classList.remove('hidden'); + } else { + this.daySelect.options[i].classList.add('hidden'); + } + } } - } - /** + /** * Resets the form. * * Unsets the `event_id` input value, hides the delete button, and * resets the date select elements to their default values. */ - resetForm() { - this.setEventId(''); - this.updateSelectedDate(); - this.hideDeleteButton(); - } + resetForm() { + this.setEventId(''); + this.updateSelectedDate(); + this.hideDeleteButton(); + } - /** + /** * Shows this form's delete button. */ - showDeleteButton() { - this.deleteButton.classList.remove('invisible'); - } + showDeleteButton() { + this.deleteButton.classList.remove('invisible'); + } - /** + /** * Hides this form's delete button. */ - hideDeleteButton() { - this.deleteButton.classList.add('invisible'); - } + hideDeleteButton() { + this.deleteButton.classList.add('invisible'); + } - /** + /** * Returns the numeric value of the selected year. * * @returns {number|null} The selected year, or `null` if none selected */ - getSelectedYear() { - return this.yearSelect.selectedIndex ? Number(this.yearSelect.value) : null; - } + getSelectedYear() { + return this.yearSelect.selectedIndex ? Number(this.yearSelect.value) : null; + } - /** + /** * Returns the numeric value of the selected month. * * @returns {number|null} The selected month, or `null` if none selected */ - getSelectedMonth() { - return this.monthSelect.selectedIndex || null; - } + getSelectedMonth() { + return this.monthSelect.selectedIndex || null; + } - /** + /** * Returns the numeric value of the selected day. * * @returns {number|null} The selected day, or `null` if none selected */ - getSelectedDay() { - return this.daySelect.selectedIndex || null; - } + getSelectedDay() { + return this.daySelect.selectedIndex || null; + } - /** + /** * Returns the value of this form's `event_id` input. * * @returns {string} */ - getEventId() { - return this.eventIdInput.value; - } + getEventId() { + return this.eventIdInput.value; + } - /** + /** * Updates the value of the form's `event_id` input. * * @param value */ - setEventId(value) { - this.eventIdInput.value = value; - } + setEventId(value) { + this.eventIdInput.value = value; + } - /** + /** * Returns the value of this form's `event_type` input. * * @returns {string} */ - getEventType() { - return this.eventTypeInput.value; - } + getEventType() { + return this.eventTypeInput.value; + } - /** + /** * Returns the value of the form's edition key input. * * @returns {string} */ - getEditionKey() { - return this.editionKeyInput.value; - } + getEditionKey() { + return this.editionKeyInput.value; + } - /** + /** * Returns this form's `action` * * @returns {string} */ - getFormAction() { - return this.rootElem.action; - } + getFormAction() { + return this.rootElem.action; + } - /** + /** * Returns a reference to this check-in form. * * @returns {HTMLFormElement} */ - getRootElement() { - return this.rootElem; - } + getRootElement() { + return this.rootElem; + } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js index dbc68b5ce87..778867eac80 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js @@ -6,8 +6,8 @@ import 'jquery-colorbox'; import { addItem, removeItem } from '../../lists/ListService'; import { - attachNewActiveShowcaseItem, - toggleActiveShowcaseItems, + attachNewActiveShowcaseItem, + toggleActiveShowcaseItems, } from '../../lists/ShowcaseItem'; import { FadingToast } from '../../Toast'; import myBooksStore from '../store'; @@ -21,59 +21,59 @@ const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; * @class */ export class ReadingLists { - /** + /** * Adds functionality to the given dropper's list affordances. * @param {HTMLElement} dropper */ - constructor(dropper) { + constructor(dropper) { /** * References the given My Books Dropper root element. * * @member {HTMLElement} */ - this.dropper = dropper; + this.dropper = dropper; - /** + /** * Reference to the "Use work" checkbox. * * @member {HTMLElement|null} */ - this.workCheckBox = dropper.querySelector('.work-checkbox'); - if (this.workCheckBox) { - // Uncheck "Use work" checkbox on page refresh - this.workCheckBox.checked = false; - } + this.workCheckBox = dropper.querySelector('.work-checkbox'); + if (this.workCheckBox) { + // Uncheck "Use work" checkbox on page refresh + this.workCheckBox.checked = false; + } - /** + /** * Reference to the "My Reading Lists" section of the dropdown content. * * @member {HTMLElement} */ - this.dropperListsElement = dropper.querySelector('.my-lists'); + this.dropperListsElement = dropper.querySelector('.my-lists'); - /** + /** * Key of the document that will be added to or removed from a list. * * @member {string} */ - this.seedKey = this.dropperListsElement.dataset.seedKey; + this.seedKey = this.dropperListsElement.dataset.seedKey; - /** + /** * Key of the work associated with this dropper. Will be an empty * string if no work is associated. * * @member {string} */ - this.workKey = this.dropperListsElement.dataset.workKey; + this.workKey = this.dropperListsElement.dataset.workKey; - /** + /** * The patron's user key. * * @member {string} */ - this.userKey = this.dropperListsElement.dataset.userKey; + this.userKey = this.dropperListsElement.dataset.userKey; - /** + /** * Stores information about a single list. * * @typedef ActiveListData @@ -84,53 +84,53 @@ export class ReadingLists { * @property {boolean} workOnList True if the list contains a reference to a work * @property {HTMLElement} dropperListAffordance Reference to the "Add to list" dropdown affordance */ - /** + /** * Maps list keys to objects containing more data about the list. * * @member {Record<string, ActiveListData>} */ - this.patronLists = {}; - } + this.patronLists = {}; + } - /** + /** * Adds functionality to all of the dropper's list affordances. */ - initialize() { - this.initModifyListAffordances( - this.dropper.querySelectorAll('.modify-list'), - ); + initialize() { + this.initModifyListAffordances( + this.dropper.querySelectorAll('.modify-list'), + ); - const openListModalButton = this.dropper.querySelector('.create-new-list'); + const openListModalButton = this.dropper.querySelector('.create-new-list'); - if (openListModalButton) { - this.addOpenListModalClickListener(openListModalButton); - } + if (openListModalButton) { + this.addOpenListModalClickListener(openListModalButton); + } - if (this.workCheckBox) { - this.workCheckBox.addEventListener('click', () => { - this.updateListDisplays(); - toggleActiveShowcaseItems(this.workCheckBox.checked); - }); + if (this.workCheckBox) { + this.workCheckBox.addEventListener('click', () => { + this.updateListDisplays(); + toggleActiveShowcaseItems(this.workCheckBox.checked); + }); + } } - } - /** + /** * Updates dropdown list affordances when an update occurs. */ - updateListDisplays() { - const isWorkSelected = this.workCheckBox && this.workCheckBox.checked; - for (const key of Object.keys(this.patronLists)) { - const listData = this.patronLists[key]; - - if (isWorkSelected) { - this.toggleDisplayedType(listData.workOnList, key); - } else { - this.toggleDisplayedType(listData.itemOnList, key); - } + updateListDisplays() { + const isWorkSelected = this.workCheckBox && this.workCheckBox.checked; + for (const key of Object.keys(this.patronLists)) { + const listData = this.patronLists[key]; + + if (isWorkSelected) { + this.toggleDisplayedType(listData.workOnList, key); + } else { + this.toggleDisplayedType(listData.itemOnList, key); + } + } } - } - /** + /** * Changes list affordance visibility in the dropper and "Already list" * list based on an item's membership to the given list. * @@ -140,17 +140,17 @@ export class ReadingLists { * @param {boolean} isListMember True if the item is on the list * @param {string} listKey Unique identifier for a list */ - toggleDisplayedType(isListMember, listKey) { - const listData = this.patronLists[listKey]; + toggleDisplayedType(isListMember, listKey) { + const listData = this.patronLists[listKey]; - if (isListMember) { - listData.dropperListAffordance.classList.add('list--active'); - } else { - listData.dropperListAffordance.classList.remove('list--active'); + if (isListMember) { + listData.dropperListAffordance.classList.add('list--active'); + } else { + listData.dropperListAffordance.classList.remove('list--active'); + } } - } - /** + /** * Hydrates the given dropdown list affordance elements and stores list data. * * Each given element is decorated with additional information about the list. @@ -158,133 +158,133 @@ export class ReadingLists { * * @param {NodeList<HTMLElement>} modifyListElements */ - initModifyListAffordances(modifyListElements) { - for (const elem of modifyListElements) { - const listItemKeys = elem.dataset.listItems; - const listKey = elem.dataset.listKey; - const itemOnList = listItemKeys.includes(this.seedKey); - const elemParent = elem.parentElement; - - this.patronLists[listKey] = { - title: elem.innerText, - coverUrl: elem.dataset.listCoverUrl, - itemOnList: itemOnList, - dropperListAffordance: elemParent, // The .list element - }; - if (!this.patronLists[listKey].coverUrl) { - this.patronLists[listKey].coverUrl = DEFAULT_COVER_URL; - } - if (this.workCheckBox) { - // Check for work key membership: - const workOnList = listItemKeys.includes(this.workKey); - this.patronLists[listKey].workOnList = workOnList; - - if (this.workCheckBox.checked) { - if (workOnList) { - elemParent.classList.add('list--active'); - } - } else { - if (itemOnList) { - elemParent.classList.add('list--active'); - } - } - } else { - if (itemOnList) { - elemParent.classList.add('list--active'); - } - } - - elem.addEventListener('click', (event) => { - event.preventDefault(); - const isAddingItem = + initModifyListAffordances(modifyListElements) { + for (const elem of modifyListElements) { + const listItemKeys = elem.dataset.listItems; + const listKey = elem.dataset.listKey; + const itemOnList = listItemKeys.includes(this.seedKey); + const elemParent = elem.parentElement; + + this.patronLists[listKey] = { + title: elem.innerText, + coverUrl: elem.dataset.listCoverUrl, + itemOnList: itemOnList, + dropperListAffordance: elemParent, // The .list element + }; + if (!this.patronLists[listKey].coverUrl) { + this.patronLists[listKey].coverUrl = DEFAULT_COVER_URL; + } + if (this.workCheckBox) { + // Check for work key membership: + const workOnList = listItemKeys.includes(this.workKey); + this.patronLists[listKey].workOnList = workOnList; + + if (this.workCheckBox.checked) { + if (workOnList) { + elemParent.classList.add('list--active'); + } + } else { + if (itemOnList) { + elemParent.classList.add('list--active'); + } + } + } else { + if (itemOnList) { + elemParent.classList.add('list--active'); + } + } + + elem.addEventListener('click', (event) => { + event.preventDefault(); + const isAddingItem = !this.patronLists[listKey].dropperListAffordance.classList.contains( - 'list--active', + 'list--active', ); - this.modifyList(listKey, isAddingItem); - }); + this.modifyList(listKey, isAddingItem); + }); + } } - } - /** + /** * Adds or removes a document to or from the list identified by the given key. * * @async * @param {string} listKey Unique key for list * @param {boolean} isAddingItem `true` if an item is being added to a list */ - async modifyList(listKey, isAddingItem) { - let seed; - const isWork = this.workCheckBox && this.workCheckBox.checked; - - // Seed will be a string if its type is 'subject' - const isSubjectSeed = this.seedKey[0] !== '/'; - - if (isWork) { - seed = { key: this.workKey }; - } else if (isSubjectSeed) { - seed = this.seedKey; - } else { - seed = { key: this.seedKey }; - } + async modifyList(listKey, isAddingItem) { + let seed; + const isWork = this.workCheckBox && this.workCheckBox.checked; - const makeChange = isAddingItem ? addItem : removeItem; - this.patronLists[listKey].dropperListAffordance.classList.remove( - 'list--active', - ); - this.patronLists[listKey].dropperListAffordance.classList.add( - 'list--pending', - ); - - await makeChange(listKey, seed) - .then((response) => { - if (response.status >= 400) { - throw new Error('List update failed'); - } - response.json(); - }) - .then(() => { - this.updateViewAfterModifyingList(listKey, isWork, isAddingItem); - - const seedKey = isWork ? this.workKey : this.seedKey; - if (isAddingItem) { - // make new active showcase item - const listTitle = this.patronLists[listKey].title; - attachNewActiveShowcaseItem( - listKey, - seedKey, - listTitle, - this.patronLists[listKey].coverUrl, - ); + // Seed will be a string if its type is 'subject' + const isSubjectSeed = this.seedKey[0] !== '/'; + + if (isWork) { + seed = { key: this.workKey }; + } else if (isSubjectSeed) { + seed = this.seedKey; } else { - // remove existing showcase items - const showcases = myBooksStore.getShowcases(); - const matchingShowcases = showcases.filter( - (item) => item.listKey === listKey && item.seedKey === seedKey, - ); - for (const item of matchingShowcases) { - item.removeSelf(); - } - } - }) - .catch(() => { - if (!isAddingItem) { - // Replace check mark if patron was removing an item from a list - this.patronLists[listKey].dropperListAffordance.classList.add( - 'list--active', - ); + seed = { key: this.seedKey }; } - new FadingToast( - 'Could not update list. Please try again later.', - ).show(); - }) - .finally(() => + + const makeChange = isAddingItem ? addItem : removeItem; this.patronLists[listKey].dropperListAffordance.classList.remove( - 'list--pending', - ), - ); - } + 'list--active', + ); + this.patronLists[listKey].dropperListAffordance.classList.add( + 'list--pending', + ); - /** + await makeChange(listKey, seed) + .then((response) => { + if (response.status >= 400) { + throw new Error('List update failed'); + } + response.json(); + }) + .then(() => { + this.updateViewAfterModifyingList(listKey, isWork, isAddingItem); + + const seedKey = isWork ? this.workKey : this.seedKey; + if (isAddingItem) { + // make new active showcase item + const listTitle = this.patronLists[listKey].title; + attachNewActiveShowcaseItem( + listKey, + seedKey, + listTitle, + this.patronLists[listKey].coverUrl, + ); + } else { + // remove existing showcase items + const showcases = myBooksStore.getShowcases(); + const matchingShowcases = showcases.filter( + (item) => item.listKey === listKey && item.seedKey === seedKey, + ); + for (const item of matchingShowcases) { + item.removeSelf(); + } + } + }) + .catch(() => { + if (!isAddingItem) { + // Replace check mark if patron was removing an item from a list + this.patronLists[listKey].dropperListAffordance.classList.add( + 'list--active', + ); + } + new FadingToast( + 'Could not update list. Please try again later.', + ).show(); + }) + .finally(() => + this.patronLists[listKey].dropperListAffordance.classList.remove( + 'list--pending', + ), + ); + } + + /** * Updates `patronLists` with the new list membership information, * then updates the view. * @@ -292,17 +292,17 @@ export class ReadingLists { * @param {boolean} isWork `true` if a work was added or removed * @param {boolean} wasItemAdded `true` if item was added to list */ - updateViewAfterModifyingList(listKey, isWork, wasItemAdded) { - if (isWork) { - this.patronLists[listKey].workOnList = wasItemAdded; - } else { - this.patronLists[listKey].itemOnList = wasItemAdded; - } + updateViewAfterModifyingList(listKey, isWork, wasItemAdded) { + if (isWork) { + this.patronLists[listKey].workOnList = wasItemAdded; + } else { + this.patronLists[listKey].itemOnList = wasItemAdded; + } - this.updateListDisplays(); - } + this.updateListDisplays(); + } - /** + /** * Adds click listener to the given "Create a new list" button. * * When the button is clicked, a modal containing the list creation form @@ -310,19 +310,19 @@ export class ReadingLists { * * @param {HTMLElement} openListModalButton */ - addOpenListModalClickListener(openListModalButton) { - openListModalButton.addEventListener('click', (event) => { - event.preventDefault(); - - $.colorbox({ - inline: true, - opacity: '0.5', - href: '#addList', - }); - }); - } - - /** + addOpenListModalClickListener(openListModalButton) { + openListModalButton.addEventListener('click', (event) => { + event.preventDefault(); + + $.colorbox({ + inline: true, + opacity: '0.5', + href: '#addList', + }); + }); + } + + /** * Adds new entry to `patronLists` record and updates list dropdown. * * Creates and hydrates an "Add to list" dropdown affordance. @@ -332,31 +332,31 @@ export class ReadingLists { * @param {boolean} isActive `True` if this dropper's seed is on the list * @param {string} coverUrl URL for the list's cover image */ - onListCreationSuccess(listKey, listTitle, isActive, coverUrl) { - const dropperListAffordance = this.createDropdownListAffordance( - listKey, - listTitle, - isActive, - ); - - this.patronLists[listKey] = { - title: listTitle, - coverUrl: coverUrl, - dropperListAffordance: dropperListAffordance, - }; - - if (isActive) { - if (this.workCheckBox && this.workCheckBox.checked) { - this.patronLists[listKey].itemOnList = false; - this.patronLists[listKey].workOnList = true; - } else { - this.patronLists[listKey].itemOnList = true; - this.patronLists[listKey].workOnList = false; - } + onListCreationSuccess(listKey, listTitle, isActive, coverUrl) { + const dropperListAffordance = this.createDropdownListAffordance( + listKey, + listTitle, + isActive, + ); + + this.patronLists[listKey] = { + title: listTitle, + coverUrl: coverUrl, + dropperListAffordance: dropperListAffordance, + }; + + if (isActive) { + if (this.workCheckBox && this.workCheckBox.checked) { + this.patronLists[listKey].itemOnList = false; + this.patronLists[listKey].workOnList = true; + } else { + this.patronLists[listKey].itemOnList = true; + this.patronLists[listKey].workOnList = false; + } + } } - } - /** + /** * Creates and hydrates a new "Add to list" dropdown affordance. * * @param {string} listKey Unique identifier for a list @@ -364,42 +364,42 @@ export class ReadingLists { * @param {boolean} isActive `true` if the seed is on this list * @returns {HTMLElement} Reference to the newly created element */ - createDropdownListAffordance(listKey, listTitle, isActive) { - const itemMarkUp = `<span class="list__status-indicator"></span> + createDropdownListAffordance(listKey, listTitle, isActive) { + const itemMarkUp = `<span class="list__status-indicator"></span> <a href="${listKey}" class="modify-list dropper__close" data-list-cover-url="${listKey}" data-list-key="${listKey}">${listTitle}</a> `; - const p = document.createElement('p'); - p.classList.add('list'); - if (isActive) { - p.classList.add('list--active'); - } - p.innerHTML = itemMarkUp; - this.dropperListsElement.appendChild(p); - const listAffordance = p.querySelector('.modify-list'); + const p = document.createElement('p'); + p.classList.add('list'); + if (isActive) { + p.classList.add('list--active'); + } + p.innerHTML = itemMarkUp; + this.dropperListsElement.appendChild(p); + const listAffordance = p.querySelector('.modify-list'); - listAffordance.addEventListener('click', (event) => { - event.preventDefault(); - const isAddingItem = + listAffordance.addEventListener('click', (event) => { + event.preventDefault(); + const isAddingItem = !this.patronLists[listKey].dropperListAffordance.classList.contains( - 'list--active', + 'list--active', ); - this.modifyList(listKey, isAddingItem); - }); + this.modifyList(listKey, isAddingItem); + }); - return p; - } + return p; + } - /** + /** * Returns the seed of the object that can be added to this list. * * @returns {string} The seed key */ - getSeed() { - if (this.workCheckBox && this.workCheckBox.checked) { - // seed is the work key: - return this.workKey; - } + getSeed() { + if (this.workCheckBox && this.workCheckBox.checked) { + // seed is the work key: + return this.workKey; + } - return this.seedKey; - } + return this.seedKey; + } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLogForms.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLogForms.js index af9028388bb..e50020ed14f 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLogForms.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLogForms.js @@ -12,9 +12,9 @@ * @enum {ReadingLogShelf} */ export const ReadingLogShelves = { - WANT_TO_READ: '1', - CURRENTLY_READING: '2', - ALREADY_READ: '3', + WANT_TO_READ: '1', + CURRENTLY_READING: '2', + ALREADY_READ: '3', }; /** @@ -36,14 +36,14 @@ export const ReadingLogShelves = { * @class */ export class ReadingLogForms { - /** + /** * Adds functionality to a single dropper's reading log forms. * * @param {HTMLElement} dropper * @param {import('./CheckInComponents')} checkInComponents * @param {Record<string, CallableFunction>} dropperActionCallbacks */ - constructor(dropper, checkInComponents, dropperActionCallbacks) { + constructor(dropper, checkInComponents, dropperActionCallbacks) { /** * Contains references to the parent dropper's close and * toggle functions. These functions are bound to the @@ -51,177 +51,177 @@ export class ReadingLogForms { * * @member {Record<string, CallableFunction>} */ - this.dropperActions = dropperActionCallbacks; + this.dropperActions = dropperActionCallbacks; - /** + /** * Reference to each reading log submit button. This includes the * primary dropper button and the buttons in the dropdown. * * @member {NodeList<HTMLElement>} */ - this.submitButtons = dropper.querySelectorAll('.reading-log button'); + this.submitButtons = dropper.querySelectorAll('.reading-log button'); - /** + /** * Reference to this dropper's primary form. * * @member {HTMLFormElement} */ - this.primaryForm = null; + this.primaryForm = null; - /** + /** * Reference to this dropper's primary button. * * @member {HTMLButtonElement} */ - this.primaryButton = null; + this.primaryButton = null; - /** + /** * Reference to this dropper's "Remove from shelf" button. * * @member {HTMLButtonElement} */ - this.removeButton = null; - - for (const button of this.submitButtons) { - if (button.classList.contains('primary-action')) { - this.primaryButton = button; - this.primaryForm = button.closest('form'); - } else if (button.classList.contains('remove-from-list')) { - // XXX : Rename class `remove-from-shelf`? - this.removeButton = button; - } - } + this.removeButton = null; + + for (const button of this.submitButtons) { + if (button.classList.contains('primary-action')) { + this.primaryButton = button; + this.primaryForm = button.closest('form'); + } else if (button.classList.contains('remove-from-list')) { + // XXX : Rename class `remove-from-shelf`? + this.removeButton = button; + } + } - if (!this.primaryButton) { - // This dropper only contains list affordances - this.primaryButton = dropper.querySelector('.primary-action'); - } + if (!this.primaryButton) { + // This dropper only contains list affordances + this.primaryButton = dropper.querySelector('.primary-action'); + } - /** + /** * @member {import('./CheckInComponents') | null} */ - this.checkInComponents = checkInComponents; + this.checkInComponents = checkInComponents; - this.readingLogForms = dropper.querySelectorAll('form.reading-log'); - this.isDropperDisabled = dropper.classList.contains( - 'generic-dropper--disabled', - ); - } + this.readingLogForms = dropper.querySelectorAll('form.reading-log'); + this.isDropperDisabled = dropper.classList.contains( + 'generic-dropper--disabled', + ); + } - /** + /** * Adds click listeners to each of the form's submit buttons. * * If dropper is disabled, no event listeners will be added. */ - initialize() { - if (!this.isDropperDisabled) { - if (this.readingLogForms.length) { - for (const form of this.readingLogForms) { - const submitButton = form.querySelector('button[type=submit]'); - submitButton.addEventListener('click', (event) => { - event.preventDefault(); - this.updateReadingLog(form); - - // Close the dropper - this.dropperActions.closeDropper(); - }); + initialize() { + if (!this.isDropperDisabled) { + if (this.readingLogForms.length) { + for (const form of this.readingLogForms) { + const submitButton = form.querySelector('button[type=submit]'); + submitButton.addEventListener('click', (event) => { + event.preventDefault(); + this.updateReadingLog(form); + + // Close the dropper + this.dropperActions.closeDropper(); + }); + } + } else { + // Toggle the dropper when there is no "Reading Log" primary action: + this.primaryButton.addEventListener('click', () => { + this.dropperActions.toggleDropper(); + }); + } } - } else { - // Toggle the dropper when there is no "Reading Log" primary action: - this.primaryButton.addEventListener('click', () => { - this.dropperActions.toggleDropper(); - }); - } } - } - /** + /** * POSTs the given form and updates the dropper accordingly. * * @param {HTMLFormElement} form */ - updateReadingLog(form) { - let newPrimaryButtonText = + updateReadingLog(form) { + let newPrimaryButtonText = this.primaryButton.querySelector('.btn-text').innerText; - // XXX: Use i18n strings - this.updatePrimaryButtonText('saving...'); + // XXX: Use i18n strings + this.updatePrimaryButtonText('saving...'); - const formData = new FormData(form); - const url = form.getAttribute('action'); + const formData = new FormData(form); + const url = form.getAttribute('action'); - const hasAddedBook = formData.get('action') === 'add'; + const hasAddedBook = formData.get('action') === 'add'; - let canUpdateShelf = true; + let canUpdateShelf = true; - if ( - !hasAddedBook && + if ( + !hasAddedBook && this.checkInComponents && this.checkInComponents.hasReadDate() - ) { - // XXX: Use i18n strings - canUpdateShelf = confirm( - 'Removing this book from your shelves will delete your check-ins for this work. Continue?', - ); - } + ) { + // XXX: Use i18n strings + canUpdateShelf = confirm( + 'Removing this book from your shelves will delete your check-ins for this work. Continue?', + ); + } - if (canUpdateShelf) { - fetch(url, { - method: 'post', - body: formData, - }) - .then((response) => response.json()) - .then((data) => { - if (!('error' in data)) { - // XXX: Serve correct HTTP codes to avoid this - this.updateActivatedStatus(hasAddedBook); - - if (hasAddedBook) { - const primaryButtonClicked = + if (canUpdateShelf) { + fetch(url, { + method: 'post', + body: formData, + }) + .then((response) => response.json()) + .then((data) => { + if (!('error' in data)) { + // XXX: Serve correct HTTP codes to avoid this + this.updateActivatedStatus(hasAddedBook); + + if (hasAddedBook) { + const primaryButtonClicked = form.classList.contains('primary-action'); - const newBookshelfId = form.querySelector( - 'input[name=bookshelf_id]', - ).value; + const newBookshelfId = form.querySelector( + 'input[name=bookshelf_id]', + ).value; - if (!primaryButtonClicked) { - // A book has been added to a shelf chosen from the dropdown. - // The primary form and dropdown selections must now be updated. - const clickedButton = form.querySelector('button[type=submit]'); - newPrimaryButtonText = clickedButton.innerText; + if (!primaryButtonClicked) { + // A book has been added to a shelf chosen from the dropdown. + // The primary form and dropdown selections must now be updated. + const clickedButton = form.querySelector('button[type=submit]'); + newPrimaryButtonText = clickedButton.innerText; - this.updatePrimaryBookshelfId(newBookshelfId); + this.updatePrimaryBookshelfId(newBookshelfId); - this.updateDropdownButtonVisibility(clickedButton); - } + this.updateDropdownButtonVisibility(clickedButton); + } - // Update check-ins: - if (this.checkInComponents) { - if ( - !this.checkInComponents.hasReadDate() && + // Update check-ins: + if (this.checkInComponents) { + if ( + !this.checkInComponents.hasReadDate() && newBookshelfId === ReadingLogShelves.ALREADY_READ - ) { - this.checkInComponents.showCheckInPrompt(); - } else { - this.checkInComponents.hideCheckInPrompt(); - } - } - } else if (this.checkInComponents) { - // Update check-ins: - this.checkInComponents.hideCheckInPrompt(); - this.checkInComponents.hideCheckInDisplay(); - this.checkInComponents.resetForm(); - } - } - - // Remove "saving..." message from button: - this.updatePrimaryButtonText(newPrimaryButtonText); - }); - } else { - // Remove "saving..." message from button if shelf cannot be updated: - this.updatePrimaryButtonText(newPrimaryButtonText); + ) { + this.checkInComponents.showCheckInPrompt(); + } else { + this.checkInComponents.hideCheckInPrompt(); + } + } + } else if (this.checkInComponents) { + // Update check-ins: + this.checkInComponents.hideCheckInPrompt(); + this.checkInComponents.hideCheckInDisplay(); + this.checkInComponents.resetForm(); + } + } + + // Remove "saving..." message from button: + this.updatePrimaryButtonText(newPrimaryButtonText); + }); + } else { + // Remove "saving..." message from button if shelf cannot be updated: + this.updatePrimaryButtonText(newPrimaryButtonText); + } } - } - /** + /** * Updates "active" status of the primary form. * * An "active" dropper will display a checkmark in the primary button, and a remove @@ -232,73 +232,73 @@ export class ReadingLogForms { * * @param {boolean} isActivated `true` if the dropper is changing to an "active" status */ - updateActivatedStatus(isActivated) { - if (isActivated) { - this.primaryButton - .querySelector('.activated-check') - .classList.remove('hidden'); - this.removeButton.classList.remove('hidden'); - this.primaryForm.querySelector('input[name=action]').value = 'remove'; - } else { - this.primaryButton - .querySelector('.activated-check') - .classList.add('hidden'); - this.removeButton.classList.add('hidden'); - this.primaryForm.querySelector('input[name=action]').value = 'add'; - } + updateActivatedStatus(isActivated) { + if (isActivated) { + this.primaryButton + .querySelector('.activated-check') + .classList.remove('hidden'); + this.removeButton.classList.remove('hidden'); + this.primaryForm.querySelector('input[name=action]').value = 'remove'; + } else { + this.primaryButton + .querySelector('.activated-check') + .classList.add('hidden'); + this.removeButton.classList.add('hidden'); + this.primaryForm.querySelector('input[name=action]').value = 'add'; + } - this.primaryButton.classList.toggle('activated'); - this.primaryButton.classList.toggle('unactivated'); - } + this.primaryButton.classList.toggle('activated'); + this.primaryButton.classList.toggle('unactivated'); + } - /** + /** * Sets that primary button's text to the given string. * * @param {string} newText */ - updatePrimaryButtonText(newText) { - this.primaryButton.querySelector('.btn-text').innerText = newText; - } + updatePrimaryButtonText(newText) { + this.primaryButton.querySelector('.btn-text').innerText = newText; + } - /** + /** * Changes value of primary form's `bookshelf_id` input to the given number. * * @param {number} newId */ - updatePrimaryBookshelfId(newId) { - this.primaryForm.querySelector('input[name=bookshelf_id]').value = newId; - } + updatePrimaryBookshelfId(newId) { + this.primaryForm.querySelector('input[name=bookshelf_id]').value = newId; + } - /** + /** * Updates the visibility of dropdown buttons, hiding the given button. * * All other dropdown buttons will be visible after this method exits. * * @param {HTMLButtonElement} transitioningButton */ - updateDropdownButtonVisibility(transitioningButton) { - for (const button of this.submitButtons) { - button.classList.remove('hidden'); - } + updateDropdownButtonVisibility(transitioningButton) { + for (const button of this.submitButtons) { + button.classList.remove('hidden'); + } - transitioningButton.classList.add('hidden'); - } + transitioningButton.classList.add('hidden'); + } - /** + /** * Returns the display string used to denote the given reading log shelf ID. * * @param shelfId {ReadingLogShelf} */ - getDisplayString(shelfId) { - const matchingFormElem = Array.from(this.readingLogForms).find((elem) => { - if (elem === this.primaryForm) { - return false; - } - const bookshelfInput = elem.querySelector('input[name=bookshelf_id]'); - return shelfId === bookshelfInput.value; - }); - - const formButton = matchingFormElem.querySelector('button'); - return formButton.textContent; - } + getDisplayString(shelfId) { + const matchingFormElem = Array.from(this.readingLogForms).find((elem) => { + if (elem === this.primaryForm) { + return false; + } + const bookshelfInput = elem.querySelector('input[name=bookshelf_id]'); + return shelfId === bookshelfInput.value; + }); + + const formButton = matchingFormElem.querySelector('button'); + return formButton.textContent; + } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/index.js b/openlibrary/plugins/openlibrary/js/my-books/index.js index 5be7fe28fb5..25ea487319a 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/index.js +++ b/openlibrary/plugins/openlibrary/js/my-books/index.js @@ -1,8 +1,8 @@ import { getListPartials } from '../lists/ListService'; import { - createActiveShowcaseItem, - ShowcaseItem, - toggleActiveShowcaseItems, + createActiveShowcaseItem, + ShowcaseItem, + toggleActiveShowcaseItems, } from '../lists/ShowcaseItem'; import { removeChildren } from '../utils'; import { CreateListForm } from './CreateListForm'; @@ -12,88 +12,88 @@ import myBooksStore from './store'; // XXX : jsdoc // XXX : decompose export function initMyBooksAffordances(dropperElements, showcaseElements) { - const showcases = []; - for (const elem of showcaseElements) { - const showcase = new ShowcaseItem(elem); - showcase.initialize(); - - showcases.push(showcase); - } - - myBooksStore.setShowcases(showcases); - - const form = document.querySelector('#create-list-form'); - const createListForm = new CreateListForm(form); - createListForm.initialize(); - - const droppers = []; - const seedKeys = []; - for (const dropper of dropperElements) { - const myBooksDropper = new MyBooksDropper(dropper); - myBooksDropper.initialize(); - - droppers.push(myBooksDropper); - seedKeys.push(...myBooksDropper.getSeedKeys()); - } - - // Remove duplicate keys: - const seedKeySet = new Set(seedKeys); - - // Get user key from first Dropper and add to store: - const userKey = droppers[0].readingLists.userKey; - myBooksStore.setUserKey(userKey); - myBooksStore.setDroppers(droppers); - - getListPartials() - .then((response) => response.json()) - .then((data) => { - // XXX : convert this block to one or two function calls - const listData = data.listData; - const activeShowcaseItems = []; - for (const listKey in listData) { - // Check for matches between seed keys and list members - // If match, create new active showcase item - - for (const seedKey of listData[listKey].members) { - if (seedKeySet.has(seedKey)) { - const key = listData[listKey].members[0]; - const coverID = key.slice(key.indexOf('OL')); - const cover = `https://covers.openlibrary.org/b/olid/${coverID}-S.jpg`; - - activeShowcaseItems.push( - createActiveShowcaseItem( - listKey, - seedKey, - listData[listKey].listName, - cover, - ), - ); - } - } - } - - const activeListsShowcaseElem = document.querySelector('.already-lists'); - - if (activeListsShowcaseElem) { - // Remove the loading indicator: - removeChildren(activeListsShowcaseElem); - - for (const li of activeShowcaseItems) { - activeListsShowcaseElem.appendChild(li); - - const showcase = new ShowcaseItem(li); - showcase.initialize(); - - showcases.push(showcase); - } - toggleActiveShowcaseItems(false); - } - - // Update dropper content: - for (const dropper of droppers) { - dropper.updateReadingLists(data['dropper']); - } - }); + const showcases = []; + for (const elem of showcaseElements) { + const showcase = new ShowcaseItem(elem); + showcase.initialize(); + + showcases.push(showcase); + } + + myBooksStore.setShowcases(showcases); + + const form = document.querySelector('#create-list-form'); + const createListForm = new CreateListForm(form); + createListForm.initialize(); + + const droppers = []; + const seedKeys = []; + for (const dropper of dropperElements) { + const myBooksDropper = new MyBooksDropper(dropper); + myBooksDropper.initialize(); + + droppers.push(myBooksDropper); + seedKeys.push(...myBooksDropper.getSeedKeys()); + } + + // Remove duplicate keys: + const seedKeySet = new Set(seedKeys); + + // Get user key from first Dropper and add to store: + const userKey = droppers[0].readingLists.userKey; + myBooksStore.setUserKey(userKey); + myBooksStore.setDroppers(droppers); + + getListPartials() + .then((response) => response.json()) + .then((data) => { + // XXX : convert this block to one or two function calls + const listData = data.listData; + const activeShowcaseItems = []; + for (const listKey in listData) { + // Check for matches between seed keys and list members + // If match, create new active showcase item + + for (const seedKey of listData[listKey].members) { + if (seedKeySet.has(seedKey)) { + const key = listData[listKey].members[0]; + const coverID = key.slice(key.indexOf('OL')); + const cover = `https://covers.openlibrary.org/b/olid/${coverID}-S.jpg`; + + activeShowcaseItems.push( + createActiveShowcaseItem( + listKey, + seedKey, + listData[listKey].listName, + cover, + ), + ); + } + } + } + + const activeListsShowcaseElem = document.querySelector('.already-lists'); + + if (activeListsShowcaseElem) { + // Remove the loading indicator: + removeChildren(activeListsShowcaseElem); + + for (const li of activeShowcaseItems) { + activeListsShowcaseElem.appendChild(li); + + const showcase = new ShowcaseItem(li); + showcase.initialize(); + + showcases.push(showcase); + } + toggleActiveShowcaseItems(false); + } + + // Update dropper content: + for (const dropper of droppers) { + dropper.updateReadingLists(data['dropper']); + } + }); } /** @@ -103,7 +103,7 @@ export function initMyBooksAffordances(dropperElements, showcaseElements) { * @returns {MyBooksDropper|undefined} */ export function findDropperForWork(workKey) { - return myBooksStore.getDroppers().find((dropper) => { - return workKey === dropper.workKey; - }); + return myBooksStore.getDroppers().find((dropper) => { + return workKey === dropper.workKey; + }); } diff --git a/openlibrary/plugins/openlibrary/js/my-books/store/index.js b/openlibrary/plugins/openlibrary/js/my-books/store/index.js index 35246e04c3e..78c6cb73e97 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/store/index.js +++ b/openlibrary/plugins/openlibrary/js/my-books/store/index.js @@ -8,73 +8,73 @@ * @class */ class MyBooksStore { - /** + /** * Initializes the store. */ - constructor() { - this._store = { - droppers: [], - showcases: [], - userKey: '', - openDropper: null, - }; - } + constructor() { + this._store = { + droppers: [], + showcases: [], + userKey: '', + openDropper: null, + }; + } - /** + /** * @returns {Array<MyBooksDropper>} */ - getDroppers() { - return this._store.droppers; - } + getDroppers() { + return this._store.droppers; + } - /** + /** * @param {Array<MyBooksDropper>} droppers */ - setDroppers(droppers) { - this._store.droppers = droppers; - } + setDroppers(droppers) { + this._store.droppers = droppers; + } - /** + /** * @returns {Array<ShowcaseItem>} */ - getShowcases() { - return this._store.showcases; - } + getShowcases() { + return this._store.showcases; + } - /** + /** * @param {Array<ShowcaseItem>} showcases */ - setShowcases(showcases) { - this._store.showcases = showcases; - } + setShowcases(showcases) { + this._store.showcases = showcases; + } - /** + /** * @returns {string} */ - getUserKey() { - return this._store.userKey; - } + getUserKey() { + return this._store.userKey; + } - /** + /** * @param {string} userKey */ - setUserKey(userKey) { - this._store.userKey = userKey; - } + setUserKey(userKey) { + this._store.userKey = userKey; + } - /** + /** * @returns {MyBooksDropper} */ - getOpenDropper() { - return this._store.openDropper; - } + getOpenDropper() { + return this._store.openDropper; + } - /** + /** * @param {MyBooksDropper} dropper */ - setOpenDropper(dropper) { - this._store.openDropper = dropper; - } + setOpenDropper(dropper) { + this._store.openDropper = dropper; + } } const myBooksStore = new MyBooksStore(); diff --git a/openlibrary/plugins/openlibrary/js/native-dialog/index.js b/openlibrary/plugins/openlibrary/js/native-dialog/index.js index e30b2ba6aac..37d3f80ecf1 100644 --- a/openlibrary/plugins/openlibrary/js/native-dialog/index.js +++ b/openlibrary/plugins/openlibrary/js/native-dialog/index.js @@ -7,26 +7,26 @@ * @param {HTMLCollection<HTMLDialogElement>} elems */ export function initDialogs(elems) { - for (const elem of elems) { - elem.addEventListener('click', (event) => { - // Event target exclusions needed for FireFox, which sets mouse positions to zero on - // <select> and <option> clicks - if ( - isOutOfBounds(event, elem) && + for (const elem of elems) { + elem.addEventListener('click', (event) => { + // Event target exclusions needed for FireFox, which sets mouse positions to zero on + // <select> and <option> clicks + if ( + isOutOfBounds(event, elem) && event.target.nodeName !== 'SELECT' && event.target.nodeName !== 'OPTION' - ) { - elem.close(); - } - }); - elem.addEventListener('close-dialog', () => { - elem.close(); - }); - const closeIcon = elem.querySelector('.native-dialog--close'); - closeIcon.addEventListener('click', () => { - elem.close(); - }); - } + ) { + elem.close(); + } + }); + elem.addEventListener('close-dialog', () => { + elem.close(); + }); + const closeIcon = elem.querySelector('.native-dialog--close'); + closeIcon.addEventListener('click', () => { + elem.close(); + }); + } } /** @@ -37,11 +37,11 @@ export function initDialogs(elems) { * @returns `true` if the click was out of bounds. */ function isOutOfBounds(event, dialog) { - const rect = dialog.getBoundingClientRect(); - return ( - event.clientX < rect.left || + const rect = dialog.getBoundingClientRect(); + return ( + event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom - ); + ); } diff --git a/openlibrary/plugins/openlibrary/js/nonjquery_utils.js b/openlibrary/plugins/openlibrary/js/nonjquery_utils.js index 2276dea46d4..3b2c05bd4a3 100644 --- a/openlibrary/plugins/openlibrary/js/nonjquery_utils.js +++ b/openlibrary/plugins/openlibrary/js/nonjquery_utils.js @@ -13,20 +13,20 @@ * @returns {Function} */ export function debounce(func, threshold = 100, execAsap = false) { - let timeout; - return function debounced() { - const obj = this, - args = arguments; - function delayed() { - if (!execAsap) func.apply(obj, args); - timeout = null; - } + let timeout; + return function debounced() { + const obj = this, + args = arguments; + function delayed() { + if (!execAsap) func.apply(obj, args); + timeout = null; + } - if (timeout) { - clearTimeout(timeout); - } else if (execAsap) { - func.apply(obj, args); - } - timeout = setTimeout(delayed, threshold); - }; + if (timeout) { + clearTimeout(timeout); + } else if (execAsap) { + func.apply(obj, args); + } + timeout = setTimeout(delayed, threshold); + }; } diff --git a/openlibrary/plugins/openlibrary/js/offline-banner.js b/openlibrary/plugins/openlibrary/js/offline-banner.js index 30622eb2f46..6083652f731 100644 --- a/openlibrary/plugins/openlibrary/js/offline-banner.js +++ b/openlibrary/plugins/openlibrary/js/offline-banner.js @@ -1,6 +1,6 @@ export function initOfflineBanner() { - window.addEventListener('offline', () => { - $('#offline-info').slideDown(); - $('#offline-info').fadeTo(5000, 1).slideUp(); - }); + window.addEventListener('offline', () => { + $('#offline-info').slideDown(); + $('#offline-info').fadeTo(5000, 1).slideUp(); + }); } diff --git a/openlibrary/plugins/openlibrary/js/ol.analytics.js b/openlibrary/plugins/openlibrary/js/ol.analytics.js index bfd776a5072..67fce11f4de 100644 --- a/openlibrary/plugins/openlibrary/js/ol.analytics.js +++ b/openlibrary/plugins/openlibrary/js/ol.analytics.js @@ -6,54 +6,54 @@ */ export default function initAnalytics() { - var vs, i; - var startTime = new Date(); - if (window.archive_analytics) { + var vs, i; + var startTime = new Date(); + if (window.archive_analytics) { // Setup analytics, depends on script loaded from CDN - window.archive_analytics.set_up_event_tracking(); + window.archive_analytics.set_up_event_tracking(); - window.archive_analytics.ol_send_event_ping = (values) => { - var endTime = new Date(); - window.archive_analytics.send_ping({ - service: 'ol', - kind: 'event', - ec: values['category'], - ea: values['action'], - el: values['label'] || location.pathname, - ev: 1, - loadtime: endTime.getTime() - startTime.getTime(), - cache_bust: Math.random(), - }); - }; + window.archive_analytics.ol_send_event_ping = (values) => { + var endTime = new Date(); + window.archive_analytics.send_ping({ + service: 'ol', + kind: 'event', + ec: values['category'], + ea: values['action'], + el: values['label'] || location.pathname, + ev: 1, + loadtime: endTime.getTime() - startTime.getTime(), + cache_bust: Math.random(), + }); + }; - vs = window.archive_analytics.get_data_packets(); - for (i in vs) { - vs[i]['cache_bust'] = Math.random(); - vs[i]['server_ms'] = $('.analytics-stats-time-calculator').data('time'); - vs[i]['server_name'] = 'ol-web.us.archive.org'; - vs[i]['service'] = 'ol'; + vs = window.archive_analytics.get_data_packets(); + for (i in vs) { + vs[i]['cache_bust'] = Math.random(); + vs[i]['server_ms'] = $('.analytics-stats-time-calculator').data('time'); + vs[i]['server_name'] = 'ol-web.us.archive.org'; + vs[i]['service'] = 'ol'; + } + if (window.flights) { + window.flights.init(); + } + $(document).on('click', '[data-ol-link-track]', function () { + var category_action = $(this).attr('data-ol-link-track').split('|'); + // for testing, + // console.log(category_action[0], category_action[1]); + window.archive_analytics.ol_send_event_ping({ + category: category_action[0], + action: category_action[1], + label: category_action[2], + }); + }); } - if (window.flights) { - window.flights.init(); - } - $(document).on('click', '[data-ol-link-track]', function () { - var category_action = $(this).attr('data-ol-link-track').split('|'); - // for testing, - // console.log(category_action[0], category_action[1]); - window.archive_analytics.ol_send_event_ping({ - category: category_action[0], - action: category_action[1], - label: category_action[2], - }); - }); - } - window.vs = vs; + window.vs = vs; - // NOTE: This might cause issues if this script is made async #4474 - window.addEventListener( - 'DOMContentLoaded', - function send_analytics_pageview() { - window.archive_analytics.send_pageview({}); - }, - ); + // NOTE: This might cause issues if this script is made async #4474 + window.addEventListener( + 'DOMContentLoaded', + function send_analytics_pageview() { + window.archive_analytics.send_pageview({}); + }, + ); } diff --git a/openlibrary/plugins/openlibrary/js/partner_ol_lib.js b/openlibrary/plugins/openlibrary/js/partner_ol_lib.js index b14b9d1bead..7f7cc55628c 100644 --- a/openlibrary/plugins/openlibrary/js/partner_ol_lib.js +++ b/openlibrary/plugins/openlibrary/js/partner_ol_lib.js @@ -2,16 +2,16 @@ * @param {string} container */ function getIsbnToElementMap(container) { - const reISBN = /(978)?[0-9]{9}[0-9X]/i; - const elements = Array.from(document.querySelectorAll(container)); - const isbnElementMap = {}; - elements.forEach((e) => { - const isbnMatches = e.outerHTML.match(reISBN); - if (isbnMatches) { - isbnElementMap[isbnMatches[0]] = e; - } - }); - return isbnElementMap; + const reISBN = /(978)?[0-9]{9}[0-9X]/i; + const elements = Array.from(document.querySelectorAll(container)); + const isbnElementMap = {}; + elements.forEach((e) => { + const isbnMatches = e.outerHTML.match(reISBN); + if (isbnMatches) { + isbnElementMap[isbnMatches[0]] = e; + } + }); + return isbnElementMap; } /** @@ -19,19 +19,19 @@ function getIsbnToElementMap(container) { * @returns {Promise<Array>} */ async function getAvailabilityDataFromOpenLibrary(isbnList) { - const apiBaseUrl = 'https://openlibrary.org/search.json'; - const apiUrl = `${apiBaseUrl}?fields=*,availability&q=isbn:${isbnList.join('+OR+')}`; - const response = await fetch(apiUrl); - const jsonResponse = await response.json(); - const olDocs = jsonResponse.docs; - const isbnToAvailabilityDataMap = {}; - olDocs.forEach((doc) => { - const isbnList = doc.isbn; - isbnList.forEach((isbn) => { - isbnToAvailabilityDataMap[isbn] = doc?.availability; + const apiBaseUrl = 'https://openlibrary.org/search.json'; + const apiUrl = `${apiBaseUrl}?fields=*,availability&q=isbn:${isbnList.join('+OR+')}`; + const response = await fetch(apiUrl); + const jsonResponse = await response.json(); + const olDocs = jsonResponse.docs; + const isbnToAvailabilityDataMap = {}; + olDocs.forEach((doc) => { + const isbnList = doc.isbn; + isbnList.forEach((isbn) => { + isbnToAvailabilityDataMap[isbn] = doc?.availability; + }); }); - }); - return isbnToAvailabilityDataMap; + return isbnToAvailabilityDataMap; } /** @@ -48,30 +48,30 @@ async function getAvailabilityDataFromOpenLibrary(isbnList) { * }); */ async function addOpenLibraryButtons(options) { - const { bookContainer, selectorToPlaceBtnIn, textOnBtn } = options; - if (bookContainer === undefined) { - throw Error( - 'book container must be specified in options for open library buttons to populate!', - ); - } - const foundIsbnElementsMap = getIsbnToElementMap(bookContainer); - const availabilityResults = await getAvailabilityDataFromOpenLibrary( - Object.keys(foundIsbnElementsMap), - ); - Object.keys(foundIsbnElementsMap).map((isbn) => { - const availability = availabilityResults[isbn]; - if (availability && availability.status !== 'error') { - const e = foundIsbnElementsMap[isbn]; - const buttons = selectorToPlaceBtnIn - ? e.querySelector(selectorToPlaceBtnIn) - : e; - const openLibraryBtnLink = document.createElement('a'); - openLibraryBtnLink.href = `https://openlibrary.org/works/${availability.openlibrary_work}`; - openLibraryBtnLink.text = textOnBtn || 'Open Library'; - openLibraryBtnLink.classList.add('openlibrary-btn'); - buttons.append(openLibraryBtnLink); + const { bookContainer, selectorToPlaceBtnIn, textOnBtn } = options; + if (bookContainer === undefined) { + throw Error( + 'book container must be specified in options for open library buttons to populate!', + ); } - }); + const foundIsbnElementsMap = getIsbnToElementMap(bookContainer); + const availabilityResults = await getAvailabilityDataFromOpenLibrary( + Object.keys(foundIsbnElementsMap), + ); + Object.keys(foundIsbnElementsMap).map((isbn) => { + const availability = availabilityResults[isbn]; + if (availability && availability.status !== 'error') { + const e = foundIsbnElementsMap[isbn]; + const buttons = selectorToPlaceBtnIn + ? e.querySelector(selectorToPlaceBtnIn) + : e; + const openLibraryBtnLink = document.createElement('a'); + openLibraryBtnLink.href = `https://openlibrary.org/works/${availability.openlibrary_work}`; + openLibraryBtnLink.text = textOnBtn || 'Open Library'; + openLibraryBtnLink.classList.add('openlibrary-btn'); + buttons.append(openLibraryBtnLink); + } + }); } // Expose globally so clients can use this method diff --git a/openlibrary/plugins/openlibrary/js/password-toggle.js b/openlibrary/plugins/openlibrary/js/password-toggle.js index c325e525f75..781f2591b58 100644 --- a/openlibrary/plugins/openlibrary/js/password-toggle.js +++ b/openlibrary/plugins/openlibrary/js/password-toggle.js @@ -4,16 +4,16 @@ * @param {HTMLElement} elem Reference to affordance that toggles a password input's visibility */ export function initPasswordToggling(elem) { - const passwordInput = document.querySelector('input[type=password]'); + const passwordInput = document.querySelector('input[type=password]'); - elem.addEventListener('click', () => { - if (passwordInput.type === 'password') { - passwordInput.type = 'text'; - elem.querySelector('img').src = '/static/images/icons/icon_eye-open.svg'; - } else { - passwordInput.type = 'password'; - elem.querySelector('img').src = + elem.addEventListener('click', () => { + if (passwordInput.type === 'password') { + passwordInput.type = 'text'; + elem.querySelector('img').src = '/static/images/icons/icon_eye-open.svg'; + } else { + passwordInput.type = 'password'; + elem.querySelector('img').src = '/static/images/icons/icon_eye-closed.svg'; - } - }); + } + }); } diff --git a/openlibrary/plugins/openlibrary/js/patron_exports.js b/openlibrary/plugins/openlibrary/js/patron_exports.js index ed3602f4c24..7d9638ff88f 100644 --- a/openlibrary/plugins/openlibrary/js/patron_exports.js +++ b/openlibrary/plugins/openlibrary/js/patron_exports.js @@ -4,8 +4,8 @@ * @param {HTMLElement} buttonElement */ function disableButton(buttonElement) { - buttonElement.setAttribute('disabled', 'true'); - buttonElement.setAttribute('aria-disabled', 'true'); + buttonElement.setAttribute('disabled', 'true'); + buttonElement.setAttribute('aria-disabled', 'true'); } /** @@ -17,10 +17,10 @@ function disableButton(buttonElement) { * @param {NodeList<HTMLFormElement>} elems */ export function initPatronExportForms(elems) { - elems.forEach((form) => { - const submitButton = form.querySelector('input[type=submit]'); - form.addEventListener('submit', () => { - disableButton(submitButton); + elems.forEach((form) => { + const submitButton = form.querySelector('input[type=submit]'); + form.addEventListener('submit', () => { + disableButton(submitButton); + }); }); - }); } diff --git a/openlibrary/plugins/openlibrary/js/private-button.js b/openlibrary/plugins/openlibrary/js/private-button.js index 07a8ddc42aa..ce9000db51a 100644 --- a/openlibrary/plugins/openlibrary/js/private-button.js +++ b/openlibrary/plugins/openlibrary/js/private-button.js @@ -1,15 +1,15 @@ import { FadingToast } from './Toast'; export function initPrivateButtons(buttons) { - buttons.forEach((button) => { - button.addEventListener('click', (event) => { - event.preventDefault(); - const toast = new FadingToast( - window.$_('This patron has not enabled following'), - null, - 3000, - ); - toast.show(); + buttons.forEach((button) => { + button.addEventListener('click', (event) => { + event.preventDefault(); + const toast = new FadingToast( + window.$_('This patron has not enabled following'), + null, + 3000, + ); + toast.show(); + }); }); - }); } diff --git a/openlibrary/plugins/openlibrary/js/python.js b/openlibrary/plugins/openlibrary/js/python.js index c59bff54be0..2398f8d1d5a 100644 --- a/openlibrary/plugins/openlibrary/js/python.js +++ b/openlibrary/plugins/openlibrary/js/python.js @@ -8,31 +8,31 @@ * @return {string} */ export function commify(n) { - var text = n.toString(); - var re = /(\d+)(\d{3})/; + var text = n.toString(); + var re = /(\d+)(\d{3})/; - while (re.test(text)) { - text = text.replace(re, '$1,$2'); - } + while (re.test(text)) { + text = text.replace(re, '$1,$2'); + } - return text; + return text; } // Implementation of Python urllib.urlencode in Javascript. export function urlencode(query) { - var parts = []; - var k; - for (k in query) { - parts.push(`${k}=${query[k]}`); - } - return parts.join('&'); + var parts = []; + var k; + for (k in query) { + parts.push(`${k}=${query[k]}`); + } + return parts.join('&'); } export function slice(array, begin, end) { - var a = []; - var i; - for (i = begin; i < Math.min(array.length, end); i++) { - a.push(array[i]); - } - return a; + var a = []; + var i; + for (i = begin; i < Math.min(array.length, end); i++) { + a.push(array[i]); + } + return a; } diff --git a/openlibrary/plugins/openlibrary/js/reading-goals/index.js b/openlibrary/plugins/openlibrary/js/reading-goals/index.js index f863dd6c248..1c8481e4a62 100644 --- a/openlibrary/plugins/openlibrary/js/reading-goals/index.js +++ b/openlibrary/plugins/openlibrary/js/reading-goals/index.js @@ -7,19 +7,19 @@ import { buildPartialsUrl } from '../utils'; * @param {HTMLCollection<HTMLElement>} links Prompts for adding a reading goal */ export function initYearlyGoalPrompt(links) { - for (const link of links) { - if (!link.classList.contains('goal-set')) { - link.addEventListener('click', onYearlyGoalClick); + for (const link of links) { + if (!link.classList.contains('goal-set')) { + link.addEventListener('click', onYearlyGoalClick); + } } - } } /** * Finds and shows the yearly goal modal. */ function onYearlyGoalClick() { - const yearlyGoalModal = document.querySelector('#yearly-goal-modal'); - yearlyGoalModal.showModal(); + const yearlyGoalModal = document.querySelector('#yearly-goal-modal'); + yearlyGoalModal.showModal(); } /** @@ -34,13 +34,13 @@ function onYearlyGoalClick() { * @param {HTMLCollection<HTMLElement>} elems ELements which display only the current year */ export function displayLocalYear(elems) { - const localYear = new Date().getFullYear(); - for (const elem of elems) { - const serverYear = Number(elem.dataset.serverYear); - if (localYear !== serverYear) { - elem.textContent = localYear; + const localYear = new Date().getFullYear(); + for (const elem of elems) { + const serverYear = Number(elem.dataset.serverYear); + if (localYear !== serverYear) { + elem.textContent = localYear; + } } - } } /** @@ -49,11 +49,11 @@ export function displayLocalYear(elems) { * @param {HTMLCollection<HTMLElement>} editLinks Edit goal links */ export function initGoalEditLinks(editLinks) { - for (const link of editLinks) { - const parent = link.closest('.reading-goal-progress'); - const modal = parent.querySelector('dialog'); - addGoalEditClickListener(link, modal); - } + for (const link of editLinks) { + const parent = link.closest('.reading-goal-progress'); + const modal = parent.querySelector('dialog'); + addGoalEditClickListener(link, modal); + } } /** @@ -65,9 +65,9 @@ export function initGoalEditLinks(editLinks) { * @param {HTMLDialogElement} modal The modal that will be shown */ function addGoalEditClickListener(editLink, modal) { - editLink.addEventListener('click', () => { - modal.showModal(); - }); + editLink.addEventListener('click', () => { + modal.showModal(); + }); } /** @@ -77,9 +77,9 @@ function addGoalEditClickListener(editLink, modal) { * @param {HTMLCollection<HTMLElement>} submitButtons Submit goal buttons */ export function initGoalSubmitButtons(submitButtons) { - for (const button of submitButtons) { - addGoalSubmissionListener(button); - } + for (const button of submitButtons) { + addGoalSubmissionListener(button); + } } /** @@ -90,77 +90,77 @@ export function initGoalSubmitButtons(submitButtons) { * @param {HTMLELement} submitButton Reading goal form submit button */ function addGoalSubmissionListener(submitButton) { - submitButton.addEventListener('click', (event) => { - event.preventDefault(); + submitButton.addEventListener('click', (event) => { + event.preventDefault(); - const form = submitButton.closest('form'); + const form = submitButton.closest('form'); - if (!form.checkValidity()) { - form.reportValidity(); - throw new Error('Form invalid'); - } - const formData = new FormData(form); - - fetch(form.action, { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams(formData), - }).then((response) => { - if (!response.ok) { - throw new Error('Failed to set reading goal'); - } - const modal = form.closest('dialog'); - if (modal) { - modal.close(); - } - - const yearlyGoalSections = document.querySelectorAll( - '.yearly-goal-section', - ); - if (formData.get('is_update')) { - // Progress component exists on page - yearlyGoalSections.forEach((yearlyGoalSection) => { - const goalInput = form.querySelector('input[name=goal]'); - const isDeleted = Number(goalInput.value) === 0; - - if (isDeleted) { - const chipGroup = yearlyGoalSection.querySelector('.chip-group'); - const goalContainer = yearlyGoalSection.querySelector( - '#reading-goal-container', - ); - if (chipGroup) { - chipGroup.classList.remove('hidden'); + if (!form.checkValidity()) { + form.reportValidity(); + throw new Error('Form invalid'); + } + const formData = new FormData(form); + + fetch(form.action, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams(formData), + }).then((response) => { + if (!response.ok) { + throw new Error('Failed to set reading goal'); } - if (goalContainer) { - goalContainer.remove(); + const modal = form.closest('dialog'); + if (modal) { + modal.close(); } - // Restore "Set reading goal" link hidden when goal was first set - const setGoalLink = yearlyGoalSection.querySelector( - '.set-reading-goal-link', + + const yearlyGoalSections = document.querySelectorAll( + '.yearly-goal-section', ); - if (setGoalLink) { - setGoalLink.classList.remove('hidden'); + if (formData.get('is_update')) { + // Progress component exists on page + yearlyGoalSections.forEach((yearlyGoalSection) => { + const goalInput = form.querySelector('input[name=goal]'); + const isDeleted = Number(goalInput.value) === 0; + + if (isDeleted) { + const chipGroup = yearlyGoalSection.querySelector('.chip-group'); + const goalContainer = yearlyGoalSection.querySelector( + '#reading-goal-container', + ); + if (chipGroup) { + chipGroup.classList.remove('hidden'); + } + if (goalContainer) { + goalContainer.remove(); + } + // Restore "Set reading goal" link hidden when goal was first set + const setGoalLink = yearlyGoalSection.querySelector( + '.set-reading-goal-link', + ); + if (setGoalLink) { + setGoalLink.classList.remove('hidden'); + } + } else { + const progressComponent = modal.closest('.reading-goal-progress'); + updateProgressComponent( + progressComponent, + Number(formData.get('goal')), + ); + } + }); + } else { + const goalYear = formData.get('year'); + fetchProgressAndUpdateViews(yearlyGoalSections, goalYear); + const banner = document.querySelector('.page-banner-mybooks'); + if (banner) { + banner.remove(); + } } - } else { - const progressComponent = modal.closest('.reading-goal-progress'); - updateProgressComponent( - progressComponent, - Number(formData.get('goal')), - ); - } }); - } else { - const goalYear = formData.get('year'); - fetchProgressAndUpdateViews(yearlyGoalSections, goalYear); - const banner = document.querySelector('.page-banner-mybooks'); - if (banner) { - banner.remove(); - } - } }); - }); } /** @@ -171,18 +171,18 @@ function addGoalSubmissionListener(submitButton) { * @param {Number} goal The new reading goal */ function updateProgressComponent(elem, goal) { - // Calculate new percentage: - const booksReadSpan = elem.querySelector( - '.reading-goal-progress__books-read', - ); - const booksRead = Number(booksReadSpan.textContent); - const percentComplete = Math.floor((booksRead / goal) * 100); - - // Update view: - const goalSpan = elem.querySelector('.reading-goal-progress__goal'); - const completedBar = elem.querySelector('.reading-goal-progress__completed'); - goalSpan.textContent = goal; - completedBar.style.width = `${Math.min(100, percentComplete)}%`; + // Calculate new percentage: + const booksReadSpan = elem.querySelector( + '.reading-goal-progress__books-read', + ); + const booksRead = Number(booksReadSpan.textContent); + const percentComplete = Math.floor((booksRead / goal) * 100); + + // Update view: + const goalSpan = elem.querySelector('.reading-goal-progress__goal'); + const completedBar = elem.querySelector('.reading-goal-progress__completed'); + goalSpan.textContent = goal; + completedBar.style.width = `${Math.min(100, percentComplete)}%`; } /** @@ -195,42 +195,42 @@ function updateProgressComponent(elem, goal) { * @param {string} goalYear Year that the goal is set for. */ function fetchProgressAndUpdateViews(yearlyGoalElems, goalYear) { - fetch(buildPartialsUrl('ReadingGoalProgress', { year: goalYear })) - .then((response) => { - if (!response.ok) { - throw new Error('Failed to fetch progress element'); - } - return response.json(); - }) - .then((data) => { - const html = data['partials']; - yearlyGoalElems.forEach((yearlyGoalElem) => { - const progress = document.createElement('SPAN'); - progress.id = 'reading-goal-container'; - progress.innerHTML = html; - yearlyGoalElem.appendChild(progress); - - const link = yearlyGoalElem.querySelector('.set-reading-goal-link'); - if (link) { - if (link.classList.contains('li-title-desktop')) { - // Remove click listener in mobile views - link.removeEventListener('click', onYearlyGoalClick); - } else { - // Hide desktop "set 20XX reading goal" link - link.classList.add('hidden'); - } - } - - const progressEditLink = progress.querySelector( - '.edit-reading-goal-link', - ); - const updateModal = progress.querySelector('dialog'); - initDialogs([updateModal]); - addGoalEditClickListener(progressEditLink, updateModal); - const submitButton = updateModal.querySelector( - '.reading-goal-submit-button', - ); - addGoalSubmissionListener(submitButton); - }); - }); + fetch(buildPartialsUrl('ReadingGoalProgress', { year: goalYear })) + .then((response) => { + if (!response.ok) { + throw new Error('Failed to fetch progress element'); + } + return response.json(); + }) + .then((data) => { + const html = data['partials']; + yearlyGoalElems.forEach((yearlyGoalElem) => { + const progress = document.createElement('SPAN'); + progress.id = 'reading-goal-container'; + progress.innerHTML = html; + yearlyGoalElem.appendChild(progress); + + const link = yearlyGoalElem.querySelector('.set-reading-goal-link'); + if (link) { + if (link.classList.contains('li-title-desktop')) { + // Remove click listener in mobile views + link.removeEventListener('click', onYearlyGoalClick); + } else { + // Hide desktop "set 20XX reading goal" link + link.classList.add('hidden'); + } + } + + const progressEditLink = progress.querySelector( + '.edit-reading-goal-link', + ); + const updateModal = progress.querySelector('dialog'); + initDialogs([updateModal]); + addGoalEditClickListener(progressEditLink, updateModal); + const submitButton = updateModal.querySelector( + '.reading-goal-submit-button', + ); + addGoalSubmissionListener(submitButton); + }); + }); } diff --git a/openlibrary/plugins/openlibrary/js/readinglog_stats.js b/openlibrary/plugins/openlibrary/js/readinglog_stats.js index 716530f2c46..84d95c0ee72 100644 --- a/openlibrary/plugins/openlibrary/js/readinglog_stats.js +++ b/openlibrary/plugins/openlibrary/js/readinglog_stats.js @@ -41,106 +41,106 @@ import 'chartjs-plugin-datalabels'; * @param {Config} config */ export function init(config) { - Chart.scaleService.updateScaleDefaults('linear', { - ticks: { beginAtZero: true, stepSize: 1 }, - }); - const authors_by_id = fromPairs(config.authors.map((a) => [a.key, a])); + Chart.scaleService.updateScaleDefaults('linear', { + ticks: { beginAtZero: true, stepSize: 1 }, + }); + const authors_by_id = fromPairs(config.authors.map((a) => [a.key, a])); - /** + /** * * @param {Config} config * @param {ChartConfig} chartConfig * @param {Element} container * @param {HTMLCanvasElement} canvas */ - function createWorkChart(config, chartConfig, container, canvas) { + function createWorkChart(config, chartConfig, container, canvas) { /** @type {{[key: string]: Work[]}} */ - const grouped = {}; - /** @type {Work[]} */ - const excluded = []; + const grouped = {}; + /** @type {Work[]} */ + const excluded = []; - for (const work of config.works) { - const allKeys = getPath(work, chartConfig.key) || []; - const validKeys = uniq( - allKeys.filter( - (key) => !isUndefined(key) && !includes(chartConfig.exclude, key), - ), - ); - if (!validKeys.length) { - excluded.push(work); - continue; - } - for (const key of validKeys) { - grouped[key] = grouped[key] || []; - grouped[key].push(work); - } - } + for (const work of config.works) { + const allKeys = getPath(work, chartConfig.key) || []; + const validKeys = uniq( + allKeys.filter( + (key) => !isUndefined(key) && !includes(chartConfig.exclude, key), + ), + ); + if (!validKeys.length) { + excluded.push(work); + continue; + } + for (const key of validKeys) { + grouped[key] = grouped[key] || []; + grouped[key].push(work); + } + } - const bars = orderBy(entries(grouped), (x) => x[1].length, 'desc').slice( - 0, - 20, - ); - canvas.height = bars.length * 20 + 5; - canvas.width = 400; - new Chart(canvas.getContext('2d'), { - type: 'horizontalBar', - data: { - labels: bars.map((b) => b[0]), - datasets: [ - { - backgroundColor: 'rgb(255, 99, 132)', - borderColor: 'rgb(255, 99, 132)', - borderWidth: 0, - data: bars.map((b) => b[1].length), - }, - ], - }, - options: { - responsive: false, - legend: { display: false }, - scales: { - xAxes: [{ display: false }], - yAxes: [ - { barPercentage: 1, gridLines: { display: false }, stacked: true }, - ], - }, - onClick: (e, [chartEl]) => { - if (chartEl) { - const bar = bars[chartEl._index]; - document.querySelector('.selected-works--list').innerHTML = + const bars = orderBy(entries(grouped), (x) => x[1].length, 'desc').slice( + 0, + 20, + ); + canvas.height = bars.length * 20 + 5; + canvas.width = 400; + new Chart(canvas.getContext('2d'), { + type: 'horizontalBar', + data: { + labels: bars.map((b) => b[0]), + datasets: [ + { + backgroundColor: 'rgb(255, 99, 132)', + borderColor: 'rgb(255, 99, 132)', + borderWidth: 0, + data: bars.map((b) => b[1].length), + }, + ], + }, + options: { + responsive: false, + legend: { display: false }, + scales: { + xAxes: [{ display: false }], + yAxes: [ + { barPercentage: 1, gridLines: { display: false }, stacked: true }, + ], + }, + onClick: (e, [chartEl]) => { + if (chartEl) { + const bar = bars[chartEl._index]; + document.querySelector('.selected-works--list').innerHTML = window.render_works_list(bar[1]); - } else { - document.querySelector('.selected-works--list').innerHTML = ''; - } - }, - plugins: { - datalabels: { - color: '#FFF', - anchor: 'end', - align: 'left', - offset: 0, - }, - }, - }, - }); + } else { + document.querySelector('.selected-works--list').innerHTML = ''; + } + }, + plugins: { + datalabels: { + color: '#FFF', + anchor: 'end', + align: 'left', + offset: 0, + }, + }, + }, + }); - $( - window.render_excluded_works_list(excluded, config.works.length), - ).appendTo(container); - } + $( + window.render_excluded_works_list(excluded, config.works.length), + ).appendTo(container); + } - const defaultFieldRender = (field) => - `OPTIONAL { ?x ${field.relation} ?${field.name}. }`; + const defaultFieldRender = (field) => + `OPTIONAL { ?x ${field.relation} ?${field.name}. }`; - const SPARQL_FIELDS = [ - { name: 'sex', type: 'uri', relation: 'wdt:P21' }, - { name: 'dob', type: 'literal', relation: 'wdt:P569' }, - { name: 'country_of_citizenship', type: 'uri', relation: 'wdt:P27' }, - { - name: 'country_of_birth', - type: 'uri', - relation: 'wdt:P19/wdt:P131*/wdt:P17', - render: (field) => ` + const SPARQL_FIELDS = [ + { name: 'sex', type: 'uri', relation: 'wdt:P21' }, + { name: 'dob', type: 'literal', relation: 'wdt:P569' }, + { name: 'country_of_citizenship', type: 'uri', relation: 'wdt:P27' }, + { + name: 'country_of_birth', + type: 'uri', + relation: 'wdt:P19/wdt:P131*/wdt:P17', + render: (field) => ` OPTIONAL { ?x ${field.relation} ?${field.name}. OPTIONAL { ?country_of_birth wdt:P571 ?country_inception. } @@ -150,95 +150,95 @@ export function init(config) { # of the author's birth FILTER(!BOUND(?country_dissolution) || !BOUND(?dob) || (?dob >= ?country_inception && ?dob <= ?country_dissolution) ). `, - }, - ]; + }, + ]; - function buildSparql(authors) { - return ` + function buildSparql(authors) { + return ` SELECT DISTINCT ?x ?xLabel ?olid ${SPARQL_FIELDS.map( - (f) => - `?${f.name} ${f.type === 'uri' ? `?${f.name}Label ` : ''}`, - ).join('')} + (f) => + `?${f.name} ${f.type === 'uri' ? `?${f.name}Label ` : ''}`, + ).join('')} WHERE { VALUES ?olids { ${authors.map((a) => `"${a.key.split('/')[2]}"`).join(' ')} } ?x wdt:P648 ?olids; wdt:P648 ?olid. ${SPARQL_FIELDS.map((f) => - (f.render || defaultFieldRender)(f), - ).join('\n')} + (f.render || defaultFieldRender)(f), + ).join('\n')} SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],${config.lang},en". } } `; - } + } - document.getElementById('wd-query-sample').href = + document.getElementById('wd-query-sample').href = `https://query.wikidata.org/#${encodeURIComponent(buildSparql(config.authors.slice(0, 20)))}`; - const wdPromise = fetch('https://query.wikidata.org/sparql?format=json', { - method: 'POST', - body: new URLSearchParams({ query: buildSparql(config.authors) }), - }) - .then((r) => r.json()) - .then((resp) => { - const bindings = resp.results.bindings; - const grouped = groupBy(bindings, (o) => o.x.value.split('/')[4]); - const records = entries(grouped).map(([qid, bindings]) => { - const record = { qid, olids: uniq(bindings.map((x) => x.olid.value)) }; - // { qid: Q123, olids: [ { value: }, {value: }], blah: [ {value:}, {value:} ], blahLabel: [{value:}, {value:}, - for (const { name, type } of SPARQL_FIELDS) { - if (type === 'uri') { - // need to dedupe whilst keeping labels in mind - const deduped = uniqBy( - bindings - .filter((x) => x[name]) - .map((x) => ({ - [name]: x[name], - [`${name}Label`]: x[`${name}Label`], - })), - (x) => x[name].value, - ); - record[name] = deduped.map((x) => x[name]); - record[`${name}Label`] = deduped.map((x) => x[`${name}Label`]); - } else { - record[name] = uniqBy( - bindings.map((x) => x[name]), - 'value', - ); - } - } - return record; - }); + const wdPromise = fetch('https://query.wikidata.org/sparql?format=json', { + method: 'POST', + body: new URLSearchParams({ query: buildSparql(config.authors) }), + }) + .then((r) => r.json()) + .then((resp) => { + const bindings = resp.results.bindings; + const grouped = groupBy(bindings, (o) => o.x.value.split('/')[4]); + const records = entries(grouped).map(([qid, bindings]) => { + const record = { qid, olids: uniq(bindings.map((x) => x.olid.value)) }; + // { qid: Q123, olids: [ { value: }, {value: }], blah: [ {value:}, {value:} ], blahLabel: [{value:}, {value:}, + for (const { name, type } of SPARQL_FIELDS) { + if (type === 'uri') { + // need to dedupe whilst keeping labels in mind + const deduped = uniqBy( + bindings + .filter((x) => x[name]) + .map((x) => ({ + [name]: x[name], + [`${name}Label`]: x[`${name}Label`], + })), + (x) => x[name].value, + ); + record[name] = deduped.map((x) => x[name]); + record[`${name}Label`] = deduped.map((x) => x[`${name}Label`]); + } else { + record[name] = uniqBy( + bindings.map((x) => x[name]), + 'value', + ); + } + } + return record; + }); - for (const record of records) { - for (const olid of record.olids) { - if (`/authors/${olid}` in authors_by_id) { - authors_by_id[`/authors/${olid}`].wd = record; - } - } - } - }); + for (const record of records) { + for (const olid of record.olids) { + if (`/authors/${olid}` in authors_by_id) { + authors_by_id[`/authors/${olid}`].wd = record; + } + } + } + }); - // Add full authors to the works objects for easy reference - for (const work of config.works) { - work.authors = work.author_keys.map((key) => authors_by_id[key]); - } + // Add full authors to the works objects for easy reference + for (const work of config.works) { + work.authors = work.author_keys.map((key) => authors_by_id[key]); + } - for (const container of document.querySelectorAll(config.charts_selector)) { - const chartConfig = JSON.parse(container.dataset['config']); - const canvas = document.createElement('canvas'); - container.append(canvas); + for (const container of document.querySelectorAll(config.charts_selector)) { + const chartConfig = JSON.parse(container.dataset['config']); + const canvas = document.createElement('canvas'); + container.append(canvas); - if (chartConfig.type === 'work-chart') { - createWorkChart(config, chartConfig, container, canvas); - } else if (chartConfig.type === 'wd-chart') { - wdPromise.then(() => - createWorkChart(config, chartConfig, container, canvas), - ); + if (chartConfig.type === 'work-chart') { + createWorkChart(config, chartConfig, container, canvas); + } else if (chartConfig.type === 'wd-chart') { + wdPromise.then(() => + createWorkChart(config, chartConfig, container, canvas), + ); + } } - } } /** @@ -247,17 +247,17 @@ export function init(config) { * @return {any} */ function getPath(obj, key) { - /** + /** * @param {object} obj * @param {string[]} param1 * @return {any} */ - function main(obj, [head, ...rest]) { - if (typeof obj === 'undefined') return undefined; - if (!head) return obj; - if (head.endsWith('[]')) - return obj[head.slice(0, -2)].flatMap((x) => main(x, rest)); - else return main(obj[head], rest); - } - return main(obj, key.split('.')); + function main(obj, [head, ...rest]) { + if (typeof obj === 'undefined') return undefined; + if (!head) return obj; + if (head.endsWith('[]')) + return obj[head.slice(0, -2)].flatMap((x) => main(x, rest)); + else return main(obj[head], rest); + } + return main(obj, key.split('.')); } diff --git a/openlibrary/plugins/openlibrary/js/return-form/index.js b/openlibrary/plugins/openlibrary/js/return-form/index.js index 2447441c25d..ade5d8a1a45 100644 --- a/openlibrary/plugins/openlibrary/js/return-form/index.js +++ b/openlibrary/plugins/openlibrary/js/return-form/index.js @@ -5,12 +5,12 @@ * @param {NodeList<HTMLElement>} returnForms */ export function initReturnForms(returnForms) { - for (const form of returnForms) { - const i18nStrings = JSON.parse(form.dataset.i18n); - form.addEventListener('submit', (event) => { - if (!confirm(i18nStrings['confirm_return'])) { - event.preventDefault(); - } - }); - } + for (const form of returnForms) { + const i18nStrings = JSON.parse(form.dataset.i18n); + form.addEventListener('submit', (event) => { + if (!confirm(i18nStrings['confirm_return'])) { + event.preventDefault(); + } + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/search.js b/openlibrary/plugins/openlibrary/js/search.js index 5753ae3c902..ca641c0507c 100644 --- a/openlibrary/plugins/openlibrary/js/search.js +++ b/openlibrary/plugins/openlibrary/js/search.js @@ -12,18 +12,18 @@ import { buildPartialsUrl } from './utils'; * @param {Number} facet_inc number of hidden facets to be displayed */ export function more(header, start_facet_count, facet_inc) { - const facetEntry = `div.${header} div.facetEntry`; - const shown = $(`${facetEntry}:not(:hidden)`).length; - const total = $(facetEntry).length; - if (shown === start_facet_count) { - $(`#${header}_less`).show(); - $(`#${header}_bull`).show(); - } - if (shown + facet_inc >= total) { - $(`#${header}_more`).hide(); - $(`#${header}_bull`).hide(); - } - $(`${facetEntry}:hidden`).slice(0, facet_inc).removeClass('ui-helper-hidden'); + const facetEntry = `div.${header} div.facetEntry`; + const shown = $(`${facetEntry}:not(:hidden)`).length; + const total = $(facetEntry).length; + if (shown === start_facet_count) { + $(`#${header}_less`).show(); + $(`#${header}_bull`).show(); + } + if (shown + facet_inc >= total) { + $(`#${header}_more`).hide(); + $(`#${header}_bull`).hide(); + } + $(`${facetEntry}:hidden`).slice(0, facet_inc).removeClass('ui-helper-hidden'); } /** @@ -34,23 +34,23 @@ export function more(header, start_facet_count, facet_inc) { * @param {Number} facet_inc number of displayed facets to be hidden */ export function less(header, start_facet_count, facet_inc) { - const facetEntry = `div.${header} div.facetEntry`; - const shown = $(`${facetEntry}:not(:hidden)`).length; - const total = $(facetEntry).length; - const increment_extra = (shown - start_facet_count) % facet_inc; - const facet_dec = increment_extra === 0 ? facet_inc : increment_extra; - const next_shown = Math.max(start_facet_count, shown - facet_dec); - if (shown === total) { - $(`#${header}_more`).show(); - $(`#${header}_bull`).show(); - } - if (next_shown === start_facet_count) { - $(`#${header}_less`).hide(); - $(`#${header}_bull`).hide(); - } - $(`${facetEntry}:not(:hidden)`) - .slice(next_shown, shown) - .addClass('ui-helper-hidden'); + const facetEntry = `div.${header} div.facetEntry`; + const shown = $(`${facetEntry}:not(:hidden)`).length; + const total = $(facetEntry).length; + const increment_extra = (shown - start_facet_count) % facet_inc; + const facet_dec = increment_extra === 0 ? facet_inc : increment_extra; + const next_shown = Math.max(start_facet_count, shown - facet_dec); + if (shown === total) { + $(`#${header}_more`).show(); + $(`#${header}_bull`).show(); + } + if (next_shown === start_facet_count) { + $(`#${header}_less`).hide(); + $(`#${header}_bull`).hide(); + } + $(`${facetEntry}:not(:hidden)`) + .slice(next_shown, shown) + .addClass('ui-helper-hidden'); } /** @@ -67,50 +67,50 @@ export function less(header, start_facet_count, facet_inc) { * @param {HTMLElement} facetsElem Root element of the search facets sidebar component */ export async function initSearchFacets(facetsElem) { - const asyncLoad = facetsElem.dataset.asyncLoad; + const asyncLoad = facetsElem.dataset.asyncLoad; - if (asyncLoad) { - const param = JSON.parse(facetsElem.dataset.param); - await whenVisible(facetsElem); + if (asyncLoad) { + const param = JSON.parse(facetsElem.dataset.param); + await whenVisible(facetsElem); - fetchPartials(param) - .then((data) => { - if (data.activeFacets) { - const activeFacetsElem = createElementFromMarkup(data.activeFacets); - const activeFacetsContainer = document.querySelector( - '.selected-search-facets-container', - ); - activeFacetsContainer.replaceChildren(activeFacetsElem); - } - const newFacetsElem = createElementFromMarkup(data.sidebar); - facetsElem.replaceWith(newFacetsElem); - hydrateFacets(); + fetchPartials(param) + .then((data) => { + if (data.activeFacets) { + const activeFacetsElem = createElementFromMarkup(data.activeFacets); + const activeFacetsContainer = document.querySelector( + '.selected-search-facets-container', + ); + activeFacetsContainer.replaceChildren(activeFacetsElem); + } + const newFacetsElem = createElementFromMarkup(data.sidebar); + facetsElem.replaceWith(newFacetsElem); + hydrateFacets(); - document.title = data.title; - }) - .catch(() => { - // XXX : Handle case where `/partials` response is not `2XX` here - }); - } else { - hydrateFacets(); - } + document.title = data.title; + }) + .catch(() => { + // XXX : Handle case where `/partials` response is not `2XX` here + }); + } else { + hydrateFacets(); + } } /** * Adds click listeners to the "show more" and "show less" facet affordances. */ function hydrateFacets() { - const data_config_json = $('#searchFacets').data('config'); - const start_facet_count = data_config_json['start_facet_count']; - const facet_inc = data_config_json['facet_inc']; + const data_config_json = $('#searchFacets').data('config'); + const start_facet_count = data_config_json['start_facet_count']; + const facet_inc = data_config_json['facet_inc']; - $('.header_bull').hide(); - $('.header_more').on('click', function () { - more($(this).data('header'), start_facet_count, facet_inc); - }); - $('.header_less').on('click', function () { - less($(this).data('header'), start_facet_count, facet_inc); - }); + $('.header_bull').hide(); + $('.header_more').on('click', function () { + more($(this).data('header'), start_facet_count, facet_inc); + }); + $('.header_less').on('click', function () { + less($(this).data('header'), start_facet_count, facet_inc); + }); } /** @@ -131,20 +131,20 @@ function hydrateFacets() { * @throws Error when `/partials` response is not in 200-299 range. */ function fetchPartials(param) { - const data = { - param: param, - path: location.pathname, - query: location.search, - }; + const data = { + param: param, + path: location.pathname, + query: location.search, + }; - return fetch( - buildPartialsUrl('SearchFacets', { data: JSON.stringify(data) }), - ).then((resp) => { - if (!resp.ok) { - throw new Error(`Failed to fetch partials. Status code: ${resp.status}`); - } - return resp.json(); - }); + return fetch( + buildPartialsUrl('SearchFacets', { data: JSON.stringify(data) }), + ).then((resp) => { + if (!resp.ok) { + throw new Error(`Failed to fetch partials. Status code: ${resp.status}`); + } + return resp.json(); + }); } /** @@ -157,9 +157,9 @@ function fetchPartials(param) { * @returns {HTMLElement} */ function createElementFromMarkup(markup) { - const template = document.createElement('template'); - template.innerHTML = markup; - return template.content.children[0]; + const template = document.createElement('template'); + template.innerHTML = markup; + return template.content.children[0]; } /** @@ -170,30 +170,30 @@ function createElementFromMarkup(markup) { * @returns {Promise<void>} */ async function whenVisible(elem, options = {}) { - return new Promise((resolve) => { - const intersectionObserver = new IntersectionObserver( - (entries, observer) => { - entries.forEach((entry) => { - if (!entry.isIntersecting) { - return; - } + return new Promise((resolve) => { + const intersectionObserver = new IntersectionObserver( + (entries, observer) => { + entries.forEach((entry) => { + if (!entry.isIntersecting) { + return; + } - // Stop observing once the element is visible - observer.unobserve(entry.target); - observer.disconnect(); - resolve(); - }); - }, - Object.assign( - { - root: null, - rootMargin: '200px', - threshold: 0, - }, - options, - ), - ); + // Stop observing once the element is visible + observer.unobserve(entry.target); + observer.disconnect(); + resolve(); + }); + }, + Object.assign( + { + root: null, + rootMargin: '200px', + threshold: 0, + }, + options, + ), + ); - intersectionObserver.observe(elem); - }); + intersectionObserver.observe(elem); + }); } diff --git a/openlibrary/plugins/openlibrary/js/service-worker-init.js b/openlibrary/plugins/openlibrary/js/service-worker-init.js index e0827ebc291..80d4c1dbc7f 100644 --- a/openlibrary/plugins/openlibrary/js/service-worker-init.js +++ b/openlibrary/plugins/openlibrary/js/service-worker-init.js @@ -1,17 +1,17 @@ export default function initServiceWorker() { - if ('serviceWorker' in navigator) { - window.addEventListener('load', () => { - navigator.serviceWorker - .register('/sw.js') - .catch((error) => { - // eslint-disable-next-line no-console - console.error(`Service worker registration failed: ${error}`); + if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker + .register('/sw.js') + .catch((error) => { + // eslint-disable-next-line no-console + console.error(`Service worker registration failed: ${error}`); + }); }); - }); - } + } - window.addEventListener('beforeinstallprompt', (e) => { + window.addEventListener('beforeinstallprompt', (e) => { // Prevent the mini-infobar from appearing on mobile - e.preventDefault(); - }); + e.preventDefault(); + }); } diff --git a/openlibrary/plugins/openlibrary/js/service-worker-matchers.js b/openlibrary/plugins/openlibrary/js/service-worker-matchers.js index d9ab3297a64..ba1f41f99ad 100644 --- a/openlibrary/plugins/openlibrary/js/service-worker-matchers.js +++ b/openlibrary/plugins/openlibrary/js/service-worker-matchers.js @@ -7,38 +7,38 @@ It is in a is a separate file to avoid this error when writing tests: */ export function matchMiscFiles({ url }) { - const miscFiles = [ - '/favicon.ico', - '/static/manifest.json', - '/cdn/archive.org/athena.js', - '/cdn/archive.org/donate.js', - ]; - return miscFiles.includes(url.pathname); + const miscFiles = [ + '/favicon.ico', + '/static/manifest.json', + '/cdn/archive.org/athena.js', + '/cdn/archive.org/donate.js', + ]; + return miscFiles.includes(url.pathname); } export function matchSmallMediumCovers({ url }) { - const regex = /-[SM].jpg$/; - return regex.test(url.pathname); + const regex = /-[SM].jpg$/; + return regex.test(url.pathname); } export function matchLargeCovers({ url }) { - const regex = /-L.jpg$/; - return regex.test(url.pathname); + const regex = /-L.jpg$/; + return regex.test(url.pathname); } export function matchStaticImages({ url }) { - const regex = /^\/images\/|^\/static\/images\//; - return regex.test(url.pathname); + const regex = /^\/images\/|^\/static\/images\//; + return regex.test(url.pathname); } export function matchStaticBuild({ url }) { - const regex = /^\/static\/build\/.*(\.js|\.css)/; - const localhost = url.origin.includes('localhost'); - return !localhost && regex.test(url.pathname); + const regex = /^\/static\/build\/.*(\.js|\.css)/; + const localhost = url.origin.includes('localhost'); + return !localhost && regex.test(url.pathname); } export function matchArchiveOrgImage({ url }) { - // most importantly, to cache your profile picture from loading every time - // also caches some covers - return url.href.startsWith('https://archive.org/services/img/'); + // most importantly, to cache your profile picture from loading every time + // also caches some covers + return url.href.startsWith('https://archive.org/services/img/'); } diff --git a/openlibrary/plugins/openlibrary/js/service-worker.js b/openlibrary/plugins/openlibrary/js/service-worker.js index 773b730e096..68f5fc23002 100644 --- a/openlibrary/plugins/openlibrary/js/service-worker.js +++ b/openlibrary/plugins/openlibrary/js/service-worker.js @@ -5,12 +5,12 @@ import { offlineFallback } from 'workbox-recipes'; import { registerRoute, setDefaultHandler } from 'workbox-routing'; import { CacheFirst, NetworkOnly } from 'workbox-strategies'; import { - matchArchiveOrgImage, - matchLargeCovers, - matchMiscFiles, - matchSmallMediumCovers, - matchStaticBuild, - matchStaticImages, + matchArchiveOrgImage, + matchLargeCovers, + matchMiscFiles, + matchSmallMediumCovers, + matchStaticBuild, + matchStaticImages, } from './service-worker-matchers'; self.skipWaiting(); @@ -20,104 +20,104 @@ clientsClaim(); setDefaultHandler(new NetworkOnly()); offlineFallback({ - pageFallback: '/static/offline.html', - imageFallback: '/static/images/logo_OL-lg.png', + pageFallback: '/static/offline.html', + imageFallback: '/static/images/logo_OL-lg.png', }); const HOUR_SECONDS = 60 * 60; const DAY_SECONDS = 24 * HOUR_SECONDS; // only cache if it the request returns 0 or 200 status const cacheableResponses = new CacheableResponsePlugin({ - statuses: [0, 200], + statuses: [0, 200], }); registerRoute( - matchMiscFiles, - new CacheFirst({ - cacheName: 'misc-files-cache', - plugins: [ - new ExpirationPlugin({ - maxAgeSeconds: DAY_SECONDS, - }), - cacheableResponses, - ], - }), + matchMiscFiles, + new CacheFirst({ + cacheName: 'misc-files-cache', + plugins: [ + new ExpirationPlugin({ + maxAgeSeconds: DAY_SECONDS, + }), + cacheableResponses, + ], + }), ); registerRoute( - matchStaticImages, - new CacheFirst({ - cacheName: 'static-images-cache', - plugins: [ - new ExpirationPlugin({ - maxEntries: 100, - maxAgeSeconds: DAY_SECONDS * 365, - }), - cacheableResponses, - ], - }), + matchStaticImages, + new CacheFirst({ + cacheName: 'static-images-cache', + plugins: [ + new ExpirationPlugin({ + maxEntries: 100, + maxAgeSeconds: DAY_SECONDS * 365, + }), + cacheableResponses, + ], + }), ); registerRoute( - matchStaticBuild, - // This has all the JS and CSS that changes on build - // We use cache first because it rarely changes - // But we only cache it for 10 minutes in case of deploy - // TODO: We should increase this a lot and make it change on deploy (clear it out when the deploy hash changes) - // it includes a .* at the end because some items have versions ?v=123 after - new CacheFirst({ - cacheName: 'static-build-cache', - plugins: [ - new ExpirationPlugin({ - maxAgeSeconds: 60 * 10, - }), - cacheableResponses, - ], - }), + matchStaticBuild, + // This has all the JS and CSS that changes on build + // We use cache first because it rarely changes + // But we only cache it for 10 minutes in case of deploy + // TODO: We should increase this a lot and make it change on deploy (clear it out when the deploy hash changes) + // it includes a .* at the end because some items have versions ?v=123 after + new CacheFirst({ + cacheName: 'static-build-cache', + plugins: [ + new ExpirationPlugin({ + maxAgeSeconds: 60 * 10, + }), + cacheableResponses, + ], + }), ); registerRoute( - matchSmallMediumCovers, - // S/M covers - cache 150 of them. They take up no more than 2.25mb of space. There are ~150 covers on the homepage. - new CacheFirst({ - cacheName: 'covers-small-medium-cache', - plugins: [ - new ExpirationPlugin({ - maxEntries: 150, - purgeOnQuotaError: true, - }), - cacheableResponses, - ], - }), + matchSmallMediumCovers, + // S/M covers - cache 150 of them. They take up no more than 2.25mb of space. There are ~150 covers on the homepage. + new CacheFirst({ + cacheName: 'covers-small-medium-cache', + plugins: [ + new ExpirationPlugin({ + maxEntries: 150, + purgeOnQuotaError: true, + }), + cacheableResponses, + ], + }), ); registerRoute( - matchLargeCovers, - // L covers - cache 5 of them but with a very short timeout so if you go to a few pages they stay. - new CacheFirst({ - cacheName: 'covers-large-cache', - plugins: [ - new ExpirationPlugin({ - maxEntries: 5, - maxAgeSeconds: HOUR_SECONDS, - purgeOnQuotaError: true, - }), - cacheableResponses, - ], - }), + matchLargeCovers, + // L covers - cache 5 of them but with a very short timeout so if you go to a few pages they stay. + new CacheFirst({ + cacheName: 'covers-large-cache', + plugins: [ + new ExpirationPlugin({ + maxEntries: 5, + maxAgeSeconds: HOUR_SECONDS, + purgeOnQuotaError: true, + }), + cacheableResponses, + ], + }), ); registerRoute( - matchArchiveOrgImage, - new CacheFirst({ - cacheName: 'archive-org-images-cache', - plugins: [ - new ExpirationPlugin({ - maxEntries: 50, - maxAgeSeconds: DAY_SECONDS, - purgeOnQuotaError: true, - }), - cacheableResponses, - ], - }), + matchArchiveOrgImage, + new CacheFirst({ + cacheName: 'archive-org-images-cache', + plugins: [ + new ExpirationPlugin({ + maxEntries: 50, + maxAgeSeconds: DAY_SECONDS, + purgeOnQuotaError: true, + }), + cacheableResponses, + ], + }), ); diff --git a/openlibrary/plugins/openlibrary/js/signup.js b/openlibrary/plugins/openlibrary/js/signup.js index 374072a0f1d..92e42615a53 100644 --- a/openlibrary/plugins/openlibrary/js/signup.js +++ b/openlibrary/plugins/openlibrary/js/signup.js @@ -1,280 +1,280 @@ import { debounce } from './nonjquery_utils.js'; export function initSignupForm() { - const signupForm = document.querySelector('form[name=signup]'); - const submitBtn = document.querySelector('button[name=signup]'); - const rpdCheckbox = document.querySelector('#pd-request'); - const pdaSelectorContainer = document.querySelector('#pda-selector'); - const pdaSelector = document.querySelector('#pd_program'); - const i18nStrings = JSON.parse(signupForm.dataset.i18n); - const emailLoadingIcon = $( - '.ol-signup-form__input--emailAddr .ol-signup-form__icon--loading', - ); - const usernameLoadingIcon = $( - '.ol-signup-form__input--username .ol-signup-form__icon--loading', - ); - const emailSuccessIcon = $( - '.ol-signup-form__input--emailAddr .ol-signup-form__icon--success', - ); - const usernameSuccessIcon = $( - '.ol-signup-form__input--username .ol-signup-form__icon--success', - ); - - // Keep the same with openlibrary/plugins/upstream/forms.py - const VALID_EMAIL_RE = /^.*@.*\..*$/; - const VALID_USERNAME_RE = /^[a-z0-9-_]{3,20}$/i; - const PASSWORD_MINLENGTH = 3; - const PASSWORD_MAXLENGTH = 20; - const USERNAME_MINLENGTH = 3; - const USERNAME_MAXLENGTH = 20; - - // Callback that is called when grecaptcha.execute() is successful - function submitCreateAccountForm() { - signupForm.submit(); - } - window.submitCreateAccountForm = submitCreateAccountForm; - - // Checks whether reportValidity exists for cross-browser compatibility - // Includes invalid input count to account for checks not covered by reportValidity - $(signupForm).on('submit', (e) => { - e.preventDefault(); - validatePDSelection(); - const numInvalidInputs = signupForm.querySelectorAll('.invalid').length; - const isFormattingValid = - !signupForm.reportValidity || signupForm.reportValidity(); - if (numInvalidInputs === 0 && isFormattingValid && window.grecaptcha) { - $(submitBtn).prop('disabled', true).text(i18nStrings['loading_text']); - window.grecaptcha.execute(); + const signupForm = document.querySelector('form[name=signup]'); + const submitBtn = document.querySelector('button[name=signup]'); + const rpdCheckbox = document.querySelector('#pd-request'); + const pdaSelectorContainer = document.querySelector('#pda-selector'); + const pdaSelector = document.querySelector('#pd_program'); + const i18nStrings = JSON.parse(signupForm.dataset.i18n); + const emailLoadingIcon = $( + '.ol-signup-form__input--emailAddr .ol-signup-form__icon--loading', + ); + const usernameLoadingIcon = $( + '.ol-signup-form__input--username .ol-signup-form__icon--loading', + ); + const emailSuccessIcon = $( + '.ol-signup-form__input--emailAddr .ol-signup-form__icon--success', + ); + const usernameSuccessIcon = $( + '.ol-signup-form__input--username .ol-signup-form__icon--success', + ); + + // Keep the same with openlibrary/plugins/upstream/forms.py + const VALID_EMAIL_RE = /^.*@.*\..*$/; + const VALID_USERNAME_RE = /^[a-z0-9-_]{3,20}$/i; + const PASSWORD_MINLENGTH = 3; + const PASSWORD_MAXLENGTH = 20; + const USERNAME_MINLENGTH = 3; + const USERNAME_MAXLENGTH = 20; + + // Callback that is called when grecaptcha.execute() is successful + function submitCreateAccountForm() { + signupForm.submit(); } - }); + window.submitCreateAccountForm = submitCreateAccountForm; + + // Checks whether reportValidity exists for cross-browser compatibility + // Includes invalid input count to account for checks not covered by reportValidity + $(signupForm).on('submit', (e) => { + e.preventDefault(); + validatePDSelection(); + const numInvalidInputs = signupForm.querySelectorAll('.invalid').length; + const isFormattingValid = + !signupForm.reportValidity || signupForm.reportValidity(); + if (numInvalidInputs === 0 && isFormattingValid && window.grecaptcha) { + $(submitBtn).prop('disabled', true).text(i18nStrings['loading_text']); + window.grecaptcha.execute(); + } + }); - $('#username').on('keyup', function () { - const value = $(this).val(); - $('#userUrl').addClass('darkgreen').text(value).css('font-weight', '700'); - }); + $('#username').on('keyup', function () { + const value = $(this).val(); + $('#userUrl').addClass('darkgreen').text(value).css('font-weight', '700'); + }); - /** + /** * Renders an error message for a given input in a given error div. * * @param {string} inputId The ID (incl #) of the input the error relates to * @param {string} errorDiv The ID (incl #) of the div where the error msg will be rendered * @param {string} errorMsg The error message text */ - function renderError(inputId, errorDiv, errorMsg) { - $(inputId).addClass('invalid'); - $(`label[for=${inputId.slice(1)}]`).addClass('invalid'); - $(errorDiv).text(errorMsg); - } + function renderError(inputId, errorDiv, errorMsg) { + $(inputId).addClass('invalid'); + $(`label[for=${inputId.slice(1)}]`).addClass('invalid'); + $(errorDiv).text(errorMsg); + } - /** + /** * Clears error styling and message for a given input and error div. * * @param {string} inputId The ID (incl #) of the input the error relates to * @param {string} errorDiv The ID (incl #) of the div where the error msg is currently rendered */ - function clearError(inputId, errorDiv) { - $(inputId).removeClass('invalid'); - $(`label[for=${inputId.slice(1)}]`).removeClass('invalid'); - $(errorDiv).text(''); - } + function clearError(inputId, errorDiv) { + $(inputId).removeClass('invalid'); + $(`label[for=${inputId.slice(1)}]`).removeClass('invalid'); + $(errorDiv).text(''); + } - function validateUsername() { - const value_username = $('#username').val(); + function validateUsername() { + const value_username = $('#username').val(); - usernameSuccessIcon.hide(); + usernameSuccessIcon.hide(); - if (value_username === '') { - clearError('#username', '#usernameMessage'); - return; - } + if (value_username === '') { + clearError('#username', '#usernameMessage'); + return; + } - if ( - value_username.length < USERNAME_MINLENGTH || + if ( + value_username.length < USERNAME_MINLENGTH || value_username.length > USERNAME_MAXLENGTH - ) { - renderError( - '#username', - '#usernameMessage', - i18nStrings['username_length_err'], - ); - return; - } + ) { + renderError( + '#username', + '#usernameMessage', + i18nStrings['username_length_err'], + ); + return; + } + + if (!VALID_USERNAME_RE.test(value_username)) { + renderError( + '#username', + '#usernameMessage', + i18nStrings['username_char_err'], + ); + return; + } - if (!VALID_USERNAME_RE.test(value_username)) { - renderError( - '#username', - '#usernameMessage', - i18nStrings['username_char_err'], - ); - return; + usernameLoadingIcon.show(); + + $.ajax({ + url: '/account/validate', + data: { username: value_username }, + type: 'GET', + success: (errors) => { + usernameLoadingIcon.hide(); + + if (errors.username) { + renderError('#username', '#usernameMessage', errors.username); + } else { + clearError('#username', '#usernameMessage'); + usernameSuccessIcon.show(); + } + }, + }); } - usernameLoadingIcon.show(); + function validateEmail() { + const value_email = $('#emailAddr').val(); - $.ajax({ - url: '/account/validate', - data: { username: value_username }, - type: 'GET', - success: (errors) => { - usernameLoadingIcon.hide(); + emailSuccessIcon.hide(); - if (errors.username) { - renderError('#username', '#usernameMessage', errors.username); - } else { - clearError('#username', '#usernameMessage'); - usernameSuccessIcon.show(); + if (value_email === '') { + clearError('#emailAddr', '#emailAddrMessage'); + return; } - }, - }); - } - - function validateEmail() { - const value_email = $('#emailAddr').val(); - emailSuccessIcon.hide(); - - if (value_email === '') { - clearError('#emailAddr', '#emailAddrMessage'); - return; - } + if (!VALID_EMAIL_RE.test(value_email)) { + renderError( + '#emailAddr', + '#emailAddrMessage', + i18nStrings['invalid_email_format'], + ); + return; + } - if (!VALID_EMAIL_RE.test(value_email)) { - renderError( - '#emailAddr', - '#emailAddrMessage', - i18nStrings['invalid_email_format'], - ); - return; + emailLoadingIcon.show(); + + $.ajax({ + url: '/account/validate', + data: { email: value_email }, + type: 'GET', + success: (errors) => { + emailLoadingIcon.hide(); + + if (errors.email) { + renderError('#emailAddr', '#emailAddrMessage', errors.email); + } else { + clearError('#emailAddr', '#emailAddrMessage'); + emailSuccessIcon.show(); + } + }, + }); } - emailLoadingIcon.show(); + function validatePassword() { + const value_password = $('#password').val(); - $.ajax({ - url: '/account/validate', - data: { email: value_email }, - type: 'GET', - success: (errors) => { - emailLoadingIcon.hide(); - - if (errors.email) { - renderError('#emailAddr', '#emailAddrMessage', errors.email); - } else { - clearError('#emailAddr', '#emailAddrMessage'); - emailSuccessIcon.show(); + if (value_password === '') { + clearError('#password', '#passwordMessage'); + return; } - }, - }); - } - - function validatePassword() { - const value_password = $('#password').val(); - - if (value_password === '') { - clearError('#password', '#passwordMessage'); - return; - } - if ( - value_password.length < PASSWORD_MINLENGTH || + if ( + value_password.length < PASSWORD_MINLENGTH || value_password.length > PASSWORD_MAXLENGTH - ) { - renderError( - '#password', - '#passwordMessage', - i18nStrings['password_length_err'], - ); - return; + ) { + renderError( + '#password', + '#passwordMessage', + i18nStrings['password_length_err'], + ); + return; + } + + clearError('#password', '#passwordMessage'); } - clearError('#password', '#passwordMessage'); - } + function validatePDSelection() { + if (!rpdCheckbox.checked) { + clearError('#pd_program', '#pd_programMessage'); + pdaSelector.setAttribute('aria-invalid', 'false'); + return; + } + if (pdaSelector.value === '') { + renderError( + '#pd_program', + '#pd_programMessage', + i18nStrings['missing_pda_err'], + ); + pdaSelector.setAttribute('aria-invalid', 'true'); + return; + } - function validatePDSelection() { - if (!rpdCheckbox.checked) { - clearError('#pd_program', '#pd_programMessage'); - pdaSelector.setAttribute('aria-invalid', 'false'); - return; - } - if (pdaSelector.value === '') { - renderError( - '#pd_program', - '#pd_programMessage', - i18nStrings['missing_pda_err'], - ); - pdaSelector.setAttribute('aria-invalid', 'true'); - return; + clearError('#pd_program', '#pd_programMessage'); + pdaSelector.setAttribute('aria-invalid', 'false'); } - clearError('#pd_program', '#pd_programMessage'); - pdaSelector.setAttribute('aria-invalid', 'false'); - } - - // Maps input ID attribute to corresponding validation function - function validateInput(input) { - const id = $(input).attr('id'); - if (id === 'emailAddr') { - validateEmail(); - } else if (id === 'username') { - validateUsername(); - } else if (id === 'password') { - validatePassword(); - } else { - throw new Error('Input validation function not found'); - } - } - - const $nonCheckboxInputs = $( - 'form[name=signup] input:not([type="checkbox"])', - ); - - // Validates input fields already marked as invalid on value change - $nonCheckboxInputs.on( - 'input', - debounce(function () { - if ($(this).hasClass('invalid')) { - validateInput(this); - } - }, 50), - ); - - // Validates all other input fields (i.e. not already marked as invalid) on blur - $nonCheckboxInputs.on('blur', function () { - if (!$(this).hasClass('invalid')) { - validateInput(this); + // Maps input ID attribute to corresponding validation function + function validateInput(input) { + const id = $(input).attr('id'); + if (id === 'emailAddr') { + validateEmail(); + } else if (id === 'username') { + validateUsername(); + } else if (id === 'password') { + validatePassword(); + } else { + throw new Error('Input validation function not found'); + } } - }); - // Validates the print-disability authority selection when the selection changes - $('form[name=signup] select').on('change', () => { - validatePDSelection(); - }); - - function updateSelectorVisibility() { - if (rpdCheckbox.checked) { - pdaSelectorContainer.classList.remove('hidden'); - rpdCheckbox.setAttribute('aria-expanded', 'true'); - pdaSelectorContainer.setAttribute('aria-hidden', 'false'); - pdaSelector.setAttribute('aria-required', 'true'); - } else { - pdaSelectorContainer.classList.add('hidden'); - rpdCheckbox.setAttribute('aria-expanded', 'false'); - pdaSelectorContainer.setAttribute('aria-hidden', 'true'); - pdaSelector.setAttribute('aria-required', 'false'); + const $nonCheckboxInputs = $( + 'form[name=signup] input:not([type="checkbox"])', + ); + + // Validates input fields already marked as invalid on value change + $nonCheckboxInputs.on( + 'input', + debounce(function () { + if ($(this).hasClass('invalid')) { + validateInput(this); + } + }, 50), + ); + + // Validates all other input fields (i.e. not already marked as invalid) on blur + $nonCheckboxInputs.on('blur', function () { + if (!$(this).hasClass('invalid')) { + validateInput(this); + } + }); + + // Validates the print-disability authority selection when the selection changes + $('form[name=signup] select').on('change', () => { + validatePDSelection(); + }); + + function updateSelectorVisibility() { + if (rpdCheckbox.checked) { + pdaSelectorContainer.classList.remove('hidden'); + rpdCheckbox.setAttribute('aria-expanded', 'true'); + pdaSelectorContainer.setAttribute('aria-hidden', 'false'); + pdaSelector.setAttribute('aria-required', 'true'); + } else { + pdaSelectorContainer.classList.add('hidden'); + rpdCheckbox.setAttribute('aria-expanded', 'false'); + pdaSelectorContainer.setAttribute('aria-hidden', 'true'); + pdaSelector.setAttribute('aria-required', 'false'); + } } - } - rpdCheckbox.addEventListener('change', updateSelectorVisibility); + rpdCheckbox.addEventListener('change', updateSelectorVisibility); - // On page reload, display PD program options and validate selection - updateSelectorVisibility(); - validatePDSelection(); + // On page reload, display PD program options and validate selection + updateSelectorVisibility(); + validatePDSelection(); } export function initLoginForm() { - const loginForm = $('form[name=login]'); - const loadingText = loginForm.data('i18n')['loading_text']; + const loginForm = $('form[name=login]'); + const loadingText = loginForm.data('i18n')['loading_text']; - loginForm.on('submit', () => { - $('button[type=submit]').prop('disabled', true).text(loadingText); - }); + loginForm.on('submit', () => { + $('button[type=submit]').prop('disabled', true).text(loadingText); + }); } diff --git a/openlibrary/plugins/openlibrary/js/star-ratings/index.js b/openlibrary/plugins/openlibrary/js/star-ratings/index.js index ba616161ca2..94e1f72053c 100644 --- a/openlibrary/plugins/openlibrary/js/star-ratings/index.js +++ b/openlibrary/plugins/openlibrary/js/star-ratings/index.js @@ -3,72 +3,72 @@ import { ReadingLogShelves } from '../my-books/MyBooksDropper/ReadingLogForms'; import { FadingToast } from '../Toast.js'; export function initRatingHandlers(ratingForms) { - for (const form of ratingForms) { - form.addEventListener('submit', (e) => { - handleRatingSubmission(e, form); - }); - } + for (const form of ratingForms) { + form.addEventListener('submit', (e) => { + handleRatingSubmission(e, form); + }); + } } function handleRatingSubmission(event, form) { - event.preventDefault(); - // Continue only if selected star is different from previous rating - if (!event.submitter.classList.contains('star-selected')) { + event.preventDefault(); + // Continue only if selected star is different from previous rating + if (!event.submitter.classList.contains('star-selected')) { // Construct form data object: - const formData = new FormData(form); - let rating; - if (event.submitter.value) { - rating = Number(event.submitter.value); - formData.append('rating', event.submitter.value); - } - - // Make AJAX call - fetch(form.action, { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams(formData), - }) - .then((response) => { - if (response.status === 401) { - throw new Error('You must be logged in to rate books'); - } - if (!response.ok) { - throw new Error('Ratings update failed'); + const formData = new FormData(form); + let rating; + if (event.submitter.value) { + rating = Number(event.submitter.value); + formData.append('rating', event.submitter.value); } - // Update view to deselect all stars - form.querySelectorAll('.star-selected').forEach((elem) => { - elem.classList.remove('star-selected'); - if (elem.hasAttribute('property')) { - elem.removeAttribute('property'); - } - }); - const clearButton = form.querySelector('.star-messaging'); - if (rating) { - // A rating was added or updated - // Update view to show patron's new star rating: - clearButton.classList.remove('hidden'); - form.querySelectorAll(`.star-${rating}`).forEach((elem) => { - elem.classList.add('star-selected'); - if (elem.tagName === 'LABEL') { - elem.setAttribute('property', 'ratingValue'); - } - }); + // Make AJAX call + fetch(form.action, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams(formData), + }) + .then((response) => { + if (response.status === 401) { + throw new Error('You must be logged in to rate books'); + } + if (!response.ok) { + throw new Error('Ratings update failed'); + } + // Update view to deselect all stars + form.querySelectorAll('.star-selected').forEach((elem) => { + elem.classList.remove('star-selected'); + if (elem.hasAttribute('property')) { + elem.removeAttribute('property'); + } + }); - // Find dropper that is associated with this star rating affordance: - const dropper = findDropperForWork(form.dataset.workKey); - if (dropper) { - dropper.updateShelfDisplay(ReadingLogShelves.ALREADY_READ); - } - } else { - // A rating was deleted - clearButton.classList.add('hidden'); - } - }) - .catch((error) => { - new FadingToast(error.message).show(); - }); - } + const clearButton = form.querySelector('.star-messaging'); + if (rating) { + // A rating was added or updated + // Update view to show patron's new star rating: + clearButton.classList.remove('hidden'); + form.querySelectorAll(`.star-${rating}`).forEach((elem) => { + elem.classList.add('star-selected'); + if (elem.tagName === 'LABEL') { + elem.setAttribute('property', 'ratingValue'); + } + }); + + // Find dropper that is associated with this star rating affordance: + const dropper = findDropperForWork(form.dataset.workKey); + if (dropper) { + dropper.updateShelfDisplay(ReadingLogShelves.ALREADY_READ); + } + } else { + // A rating was deleted + clearButton.classList.add('hidden'); + } + }) + .catch((error) => { + new FadingToast(error.message).show(); + }); + } } diff --git a/openlibrary/plugins/openlibrary/js/stats/index.js b/openlibrary/plugins/openlibrary/js/stats/index.js index 1de4cda8e44..8c0b30d6d72 100644 --- a/openlibrary/plugins/openlibrary/js/stats/index.js +++ b/openlibrary/plugins/openlibrary/js/stats/index.js @@ -6,21 +6,21 @@ * @see /openlibrary/templates/admin/index.html */ export async function initUniqueLoginCounts(containerElem) { - const loadingIndicator = containerElem.querySelector('.loadingIndicator'); - const i18nStrings = JSON.parse(containerElem.dataset.i18n); + const loadingIndicator = containerElem.querySelector('.loadingIndicator'); + const i18nStrings = JSON.parse(containerElem.dataset.i18n); - const counts = await fetchCounts().then((resp) => { - if (resp.status !== 200) { - throw new Error(`Failed to fetch partials. Status code: ${resp.status}`); - } - return resp.json(); - }); + const counts = await fetchCounts().then((resp) => { + if (resp.status !== 200) { + throw new Error(`Failed to fetch partials. Status code: ${resp.status}`); + } + return resp.json(); + }); - const countDiv = document.createElement('DIV'); - countDiv.innerHTML = i18nStrings.uniqueLoginsCopy; - const countSpan = countDiv.querySelector('.login-counts'); - countSpan.textContent = counts.loginCount; - loadingIndicator.replaceWith(countDiv); + const countDiv = document.createElement('DIV'); + countDiv.innerHTML = i18nStrings.uniqueLoginsCopy; + const countSpan = countDiv.querySelector('.login-counts'); + countSpan.textContent = counts.loginCount; + loadingIndicator.replaceWith(countDiv); } /** @@ -30,5 +30,5 @@ export async function initUniqueLoginCounts(containerElem) { * @see `monthly_logins` class in /openlibrary/plugins/openlibrary/api.py */ async function fetchCounts() { - return fetch('/api/monthly_logins.json'); + return fetch('/api/monthly_logins.json'); } diff --git a/openlibrary/plugins/openlibrary/js/tabs.js b/openlibrary/plugins/openlibrary/js/tabs.js index afd7f0a89f4..47c3565451d 100644 --- a/openlibrary/plugins/openlibrary/js/tabs.js +++ b/openlibrary/plugins/openlibrary/js/tabs.js @@ -2,8 +2,8 @@ const TABS_OPTIONS = { fx: { opacity: 'toggle' } }; import 'jquery-ui/ui/widgets/tabs'; export function initTabs($node) { - $node.tabs(TABS_OPTIONS); - $node.filter('.autohash').on('tabsselect', (event, ui) => { - document.location.hash = ui.panel.id; - }); + $node.tabs(TABS_OPTIONS); + $node.filter('.autohash').on('tabsselect', (event, ui) => { + document.location.hash = ui.panel.id; + }); } diff --git a/openlibrary/plugins/openlibrary/js/team.js b/openlibrary/plugins/openlibrary/js/team.js index a248daea4ea..84ddf9dff9a 100644 --- a/openlibrary/plugins/openlibrary/js/team.js +++ b/openlibrary/plugins/openlibrary/js/team.js @@ -1,320 +1,320 @@ import team from '../../../templates/about/team.json'; import { updateURLParameters } from './utils'; export function initTeamFilter() { - const currentYear = new Date().getFullYear().toString(); - // Photos - const default_profile_image = + const currentYear = new Date().getFullYear().toString(); + // Photos + const default_profile_image = '../../../static/images/openlibrary-180x180.png'; - const bookUrlIcon = '../../../static/images/icons/icon_book-lg.png'; - const personalUrlIcon = '../../../static/images/globe-solid.svg'; - const initialSearchParams = new URL(window.location.href).searchParams; - const initialRole = initialSearchParams.get('role') || 'All'; - const initialDepartment = initialSearchParams.get('department') || 'All'; - - // Team sorted by last name - const sortByLastName = (array) => { - array.sort((a, b) => { - const aName = a.name.split(' '); - const bName = b.name.split(' '); - const aLastName = aName[aName.length - 1]; - const bLastName = bName[bName.length - 1]; - if (aLastName < bLastName) { - return -1; - } else if (aLastName > bLastName) { - return 1; - } else { - return 0; - } - }); - }; - sortByLastName(team); - - // Match a substring in each person's role - const matchSubstring = (array, substring) => { - return array.some((item) => item.includes(substring)); - }; - - // *************************************** Team sorted by role *************************************** - // ********** STAFF ********** - const staff = team.filter((person) => matchSubstring(person.roles, 'staff')); - const staffEmeritus = staff.filter((person) => - matchSubstring(person.roles, 'emeritus'), - ); - const staffCurrent = staff.filter( - (person) => !matchSubstring(person.roles, 'emeritus'), - ); - - // ********** FELLOWS ********** - const fellows = team.filter( - (person) => - matchSubstring(person.roles, 'fellow') && - !matchSubstring(person.roles, 'staff'), - ); - const currentFellows = fellows.filter((person) => - matchSubstring(person.roles, currentYear), - ); - const pastFellows = fellows.filter( - (person) => !matchSubstring(person.roles, currentYear), - ); - - // ********** VOLUNTEERS ********** - const volunteers = team.filter( - (person) => - matchSubstring(person.roles, 'volunteer') && - !matchSubstring(person.roles, 'fellow'), - ); - - // *************************************** Selectors and eventListeners *************************************** - const roleFilter = document.getElementById('role'); - const departmentFilter = document.getElementById('department'); - roleFilter.value = initialRole; - roleFilter.addEventListener('change', (e) => { - filterTeam(e.target.value, departmentFilter.value); - updateURLParameters({ - role: e.target.value, - department: departmentFilter.value, - }); - }); - departmentFilter.value = initialDepartment; - departmentFilter.addEventListener('change', (e) => { - filterTeam(roleFilter.value, e.target.value); - updateURLParameters({ - role: roleFilter.value, - department: departmentFilter.value, - }); - }); - const cardsContainer = document.querySelector('.teamCards_container'); - - // *************************************** Functions *************************************** - const showError = () => { - const noResults = document.createElement('h3'); - noResults.classList = 'noResults'; - noResults.textContent = - "It looks like we don't have anyone with those specifications."; - cardsContainer.append(noResults); - }; - - const createCards = (array) => { - array.map((member) => { - // create - const teamCardContainer = document.createElement('div'); - const teamCard = document.createElement('div'); - - const teamCardPhotoContainer = document.createElement('div'); - const teamCardPhoto = document.createElement('img'); - - const teamCardDescription = document.createElement('div'); - const memberOlLink = document.createElement('a'); - const memberName = document.createElement('h2'); - // const memberRole = document.createElement('h4'); - // const memberDepartment = document.createElement('h3'); - const memberTitle = document.createElement('h3'); - - const descriptionLinks = document.createElement('div'); - - //modify - teamCardContainer.classList = 'teamCard__container'; - teamCard.classList = 'teamCard'; - - teamCardPhotoContainer.classList = 'teamCard__photoContainer'; - teamCardPhoto.classList = 'teamCard__photo'; - teamCardPhoto.src = `${ - member.photo_path ? member.photo_path : default_profile_image - }`; - - teamCardDescription.classList.add('teamCard__description'); - if (member.ol_key) { - memberOlLink.href = `https://openlibrary.org/people/${member.ol_key}`; - } - member.name.length >= 18 - ? (memberName.classList = 'description__name--length-long') - : (memberName.classList = 'description__name--length-short'); - - memberName.textContent = `${member.name}`; - memberTitle.classList = 'description__title'; - memberTitle.textContent = `${member.title}`; - - descriptionLinks.classList = 'description__links'; - if (member.personal_url) { - const memberPersonalA = document.createElement('a'); - const memberPersonalImg = document.createElement('img'); - - memberPersonalA.href = `${member.personal_url}`; - memberPersonalImg.src = personalUrlIcon; - memberPersonalImg.classList = 'links__site'; - - memberPersonalA.append(memberPersonalImg); - descriptionLinks.append(memberPersonalA); - } - - if (member.favorite_book_url) { - const memberBookA = document.createElement('a'); - const memberBookImg = document.createElement('img'); - - memberBookA.href = `${member.favorite_book_url}`; - memberBookImg.src = bookUrlIcon; - memberBookImg.classList = 'links__book'; - - memberBookA.append(memberBookImg); - descriptionLinks.append(memberBookA); - } - - // append - teamCardPhotoContainer.append(teamCardPhoto); - memberOlLink.append(memberName); - teamCardDescription.append( - memberOlLink, - // memberRole, - // memberDepartment, - memberTitle, - descriptionLinks, - ); - teamCard.append(teamCardPhotoContainer, teamCardDescription); - teamCardContainer.append(teamCard); - cardsContainer.append(teamCardContainer); - }); - }; - - const createSectionHeading = (text) => { - const sectionSeparator = document.createElement('div'); - sectionSeparator.textContent = `${text}`; - sectionSeparator.classList = 'sectionSeparator'; - cardsContainer.append(sectionSeparator); - }; - - const createsubSection = (array, text) => { - const subsectionSeparator = document.createElement('div'); - subsectionSeparator.textContent = `${text}`; - subsectionSeparator.classList = 'subsectionSeparator'; - cardsContainer.append(subsectionSeparator); - createCards(array); - }; - - const filterTeam = (role, department) => { - cardsContainer.textContent = ''; - // **************************************** default sort ***************************************** - if (role === 'All' && department === 'All') { - createSectionHeading('Staff'); - createsubSection(staffCurrent, 'Current'); - createsubSection(staffEmeritus, 'Emeritus'); - - createSectionHeading('Fellows'); - createsubSection(currentFellows, 'Current'); - createsubSection(pastFellows, 'Past'); - - createSectionHeading('Volunteers'); - createCards(volunteers); - } - // ************************************* sort by department *************************************** - else if (role === 'All' && department !== 'All') { - role = ''; - const filteredTeam = team.filter( - (person) => - matchSubstring(person.roles, role) && - matchSubstring(person.departments, department), - ); - - const staff = filteredTeam.filter((person) => - matchSubstring(person.roles, 'staff'), - ); - const staffEmeritus = staff.filter((person) => + const bookUrlIcon = '../../../static/images/icons/icon_book-lg.png'; + const personalUrlIcon = '../../../static/images/globe-solid.svg'; + const initialSearchParams = new URL(window.location.href).searchParams; + const initialRole = initialSearchParams.get('role') || 'All'; + const initialDepartment = initialSearchParams.get('department') || 'All'; + + // Team sorted by last name + const sortByLastName = (array) => { + array.sort((a, b) => { + const aName = a.name.split(' '); + const bName = b.name.split(' '); + const aLastName = aName[aName.length - 1]; + const bLastName = bName[bName.length - 1]; + if (aLastName < bLastName) { + return -1; + } else if (aLastName > bLastName) { + return 1; + } else { + return 0; + } + }); + }; + sortByLastName(team); + + // Match a substring in each person's role + const matchSubstring = (array, substring) => { + return array.some((item) => item.includes(substring)); + }; + + // *************************************** Team sorted by role *************************************** + // ********** STAFF ********** + const staff = team.filter((person) => matchSubstring(person.roles, 'staff')); + const staffEmeritus = staff.filter((person) => matchSubstring(person.roles, 'emeritus'), - ); - const staffCurrent = staff.filter( + ); + const staffCurrent = staff.filter( (person) => !matchSubstring(person.roles, 'emeritus'), - ); + ); - const fellows = filteredTeam.filter( + // ********** FELLOWS ********** + const fellows = team.filter( (person) => - matchSubstring(person.roles, 'fellow') && - !matchSubstring(person.roles, 'staff'), - ); - const currentFellows = fellows.filter((person) => + matchSubstring(person.roles, 'fellow') && + !matchSubstring(person.roles, 'staff'), + ); + const currentFellows = fellows.filter((person) => matchSubstring(person.roles, currentYear), - ); - const pastFellows = fellows.filter( + ); + const pastFellows = fellows.filter( (person) => !matchSubstring(person.roles, currentYear), - ); + ); - const volunteers = filteredTeam.filter( + // ********** VOLUNTEERS ********** + const volunteers = team.filter( (person) => - matchSubstring(person.roles, 'volunteer') && - !matchSubstring(person.roles, 'fellow'), - ); - - staff.length && createSectionHeading('Staff'); - staffCurrent.length && createsubSection(staffCurrent, 'Current'); - staffEmeritus.length && createsubSection(staffEmeritus, 'Emeritus'); - - fellows.length && createSectionHeading('Fellows'); - currentFellows.length && createsubSection(currentFellows, 'Current'); - pastFellows.length && createsubSection(pastFellows, 'Past'); - - volunteers.length && createSectionHeading('Volunteers'); - createCards(volunteers); - } - // ****************************** sort by role and/or department ******************************* - else { - department === 'All' ? (department = '') : department; - createSectionHeading(capitalize(role)); - if (role === 'volunteer') { - const filteredVolunteers = volunteers.filter((person) => - matchSubstring(person.departments, department), - ); - filteredVolunteers.length !== 0 - ? createCards(filteredVolunteers) - : showError(); - } else if (role === 'staff') { - const filteredCurrentStaff = staffCurrent.filter((person) => - matchSubstring(person.departments, department), - ); - const filteredStaffEmeritus = staffEmeritus.filter((person) => + matchSubstring(person.roles, 'volunteer') && + !matchSubstring(person.roles, 'fellow'), + ); + + // *************************************** Selectors and eventListeners *************************************** + const roleFilter = document.getElementById('role'); + const departmentFilter = document.getElementById('department'); + roleFilter.value = initialRole; + roleFilter.addEventListener('change', (e) => { + filterTeam(e.target.value, departmentFilter.value); + updateURLParameters({ + role: e.target.value, + department: departmentFilter.value, + }); + }); + departmentFilter.value = initialDepartment; + departmentFilter.addEventListener('change', (e) => { + filterTeam(roleFilter.value, e.target.value); + updateURLParameters({ + role: roleFilter.value, + department: departmentFilter.value, + }); + }); + const cardsContainer = document.querySelector('.teamCards_container'); + + // *************************************** Functions *************************************** + const showError = () => { + const noResults = document.createElement('h3'); + noResults.classList = 'noResults'; + noResults.textContent = + 'It looks like we don\'t have anyone with those specifications.'; + cardsContainer.append(noResults); + }; + + const createCards = (array) => { + array.map((member) => { + // create + const teamCardContainer = document.createElement('div'); + const teamCard = document.createElement('div'); + + const teamCardPhotoContainer = document.createElement('div'); + const teamCardPhoto = document.createElement('img'); + + const teamCardDescription = document.createElement('div'); + const memberOlLink = document.createElement('a'); + const memberName = document.createElement('h2'); + // const memberRole = document.createElement('h4'); + // const memberDepartment = document.createElement('h3'); + const memberTitle = document.createElement('h3'); + + const descriptionLinks = document.createElement('div'); + + //modify + teamCardContainer.classList = 'teamCard__container'; + teamCard.classList = 'teamCard'; + + teamCardPhotoContainer.classList = 'teamCard__photoContainer'; + teamCardPhoto.classList = 'teamCard__photo'; + teamCardPhoto.src = `${ + member.photo_path ? member.photo_path : default_profile_image + }`; + + teamCardDescription.classList.add('teamCard__description'); + if (member.ol_key) { + memberOlLink.href = `https://openlibrary.org/people/${member.ol_key}`; + } + member.name.length >= 18 + ? (memberName.classList = 'description__name--length-long') + : (memberName.classList = 'description__name--length-short'); + + memberName.textContent = `${member.name}`; + memberTitle.classList = 'description__title'; + memberTitle.textContent = `${member.title}`; + + descriptionLinks.classList = 'description__links'; + if (member.personal_url) { + const memberPersonalA = document.createElement('a'); + const memberPersonalImg = document.createElement('img'); + + memberPersonalA.href = `${member.personal_url}`; + memberPersonalImg.src = personalUrlIcon; + memberPersonalImg.classList = 'links__site'; + + memberPersonalA.append(memberPersonalImg); + descriptionLinks.append(memberPersonalA); + } + + if (member.favorite_book_url) { + const memberBookA = document.createElement('a'); + const memberBookImg = document.createElement('img'); + + memberBookA.href = `${member.favorite_book_url}`; + memberBookImg.src = bookUrlIcon; + memberBookImg.classList = 'links__book'; + + memberBookA.append(memberBookImg); + descriptionLinks.append(memberBookA); + } + + // append + teamCardPhotoContainer.append(teamCardPhoto); + memberOlLink.append(memberName); + teamCardDescription.append( + memberOlLink, + // memberRole, + // memberDepartment, + memberTitle, + descriptionLinks, + ); + teamCard.append(teamCardPhotoContainer, teamCardDescription); + teamCardContainer.append(teamCard); + cardsContainer.append(teamCardContainer); + }); + }; + + const createSectionHeading = (text) => { + const sectionSeparator = document.createElement('div'); + sectionSeparator.textContent = `${text}`; + sectionSeparator.classList = 'sectionSeparator'; + cardsContainer.append(sectionSeparator); + }; + + const createsubSection = (array, text) => { + const subsectionSeparator = document.createElement('div'); + subsectionSeparator.textContent = `${text}`; + subsectionSeparator.classList = 'subsectionSeparator'; + cardsContainer.append(subsectionSeparator); + createCards(array); + }; + + const filterTeam = (role, department) => { + cardsContainer.textContent = ''; + // **************************************** default sort ***************************************** + if (role === 'All' && department === 'All') { + createSectionHeading('Staff'); + createsubSection(staffCurrent, 'Current'); + createsubSection(staffEmeritus, 'Emeritus'); + + createSectionHeading('Fellows'); + createsubSection(currentFellows, 'Current'); + createsubSection(pastFellows, 'Past'); + + createSectionHeading('Volunteers'); + createCards(volunteers); + } + // ************************************* sort by department *************************************** + else if (role === 'All' && department !== 'All') { + role = ''; + const filteredTeam = team.filter( + (person) => + matchSubstring(person.roles, role) && matchSubstring(person.departments, department), - ); - filteredCurrentStaff.length && + ); + + const staff = filteredTeam.filter((person) => + matchSubstring(person.roles, 'staff'), + ); + const staffEmeritus = staff.filter((person) => + matchSubstring(person.roles, 'emeritus'), + ); + const staffCurrent = staff.filter( + (person) => !matchSubstring(person.roles, 'emeritus'), + ); + + const fellows = filteredTeam.filter( + (person) => + matchSubstring(person.roles, 'fellow') && + !matchSubstring(person.roles, 'staff'), + ); + const currentFellows = fellows.filter((person) => + matchSubstring(person.roles, currentYear), + ); + const pastFellows = fellows.filter( + (person) => !matchSubstring(person.roles, currentYear), + ); + + const volunteers = filteredTeam.filter( + (person) => + matchSubstring(person.roles, 'volunteer') && + !matchSubstring(person.roles, 'fellow'), + ); + + staff.length && createSectionHeading('Staff'); + staffCurrent.length && createsubSection(staffCurrent, 'Current'); + staffEmeritus.length && createsubSection(staffEmeritus, 'Emeritus'); + + fellows.length && createSectionHeading('Fellows'); + currentFellows.length && createsubSection(currentFellows, 'Current'); + pastFellows.length && createsubSection(pastFellows, 'Past'); + + volunteers.length && createSectionHeading('Volunteers'); + createCards(volunteers); + } + // ****************************** sort by role and/or department ******************************* + else { + department === 'All' ? (department = '') : department; + createSectionHeading(capitalize(role)); + if (role === 'volunteer') { + const filteredVolunteers = volunteers.filter((person) => + matchSubstring(person.departments, department), + ); + filteredVolunteers.length !== 0 + ? createCards(filteredVolunteers) + : showError(); + } else if (role === 'staff') { + const filteredCurrentStaff = staffCurrent.filter((person) => + matchSubstring(person.departments, department), + ); + const filteredStaffEmeritus = staffEmeritus.filter((person) => + matchSubstring(person.departments, department), + ); + filteredCurrentStaff.length && createsubSection(filteredCurrentStaff, 'Current'); - filteredStaffEmeritus.length && + filteredStaffEmeritus.length && createsubSection(filteredStaffEmeritus, 'Emeritus'); - !filteredCurrentStaff.length && + !filteredCurrentStaff.length && !filteredStaffEmeritus.length && showError(); - } else { - const filteredCurrentFellows = currentFellows.filter((person) => - matchSubstring(person.departments, department), - ); - const filteredPastFellows = pastFellows.filter((person) => - matchSubstring(person.departments, department), - ); - filteredCurrentFellows.length && + } else { + const filteredCurrentFellows = currentFellows.filter((person) => + matchSubstring(person.departments, department), + ); + const filteredPastFellows = pastFellows.filter((person) => + matchSubstring(person.departments, department), + ); + filteredCurrentFellows.length && createsubSection(filteredCurrentFellows, 'Current'); - filteredPastFellows.length && + filteredPastFellows.length && createsubSection(filteredPastFellows, 'Past'); - !filteredCurrentFellows.length && + !filteredCurrentFellows.length && !filteredPastFellows.length && showError(); - } - } - }; - - const capitalize = (text) => { - const firstLetter = text[0].toUpperCase(); - if (text === 'fellow' || text === 'volunteer') { - return `${firstLetter + text.slice(1)}s`; - } else { - return firstLetter + text.slice(1); - } - }; - - // on page load - createSectionHeading('Staff'); - createsubSection(staffCurrent, 'Current'); - createsubSection(staffEmeritus, 'Emeritus'); - - createSectionHeading('Fellows'); - createsubSection(currentFellows, 'Current'); - createsubSection(pastFellows, 'Past'); - - createSectionHeading('Volunteers'); - createCards(volunteers); - filterTeam(initialRole, initialDepartment); + } + } + }; + + const capitalize = (text) => { + const firstLetter = text[0].toUpperCase(); + if (text === 'fellow' || text === 'volunteer') { + return `${firstLetter + text.slice(1)}s`; + } else { + return firstLetter + text.slice(1); + } + }; + + // on page load + createSectionHeading('Staff'); + createsubSection(staffCurrent, 'Current'); + createsubSection(staffEmeritus, 'Emeritus'); + + createSectionHeading('Fellows'); + createsubSection(currentFellows, 'Current'); + createsubSection(pastFellows, 'Past'); + + createSectionHeading('Volunteers'); + createCards(volunteers); + filterTeam(initialRole, initialDepartment); } diff --git a/openlibrary/plugins/openlibrary/js/template.js b/openlibrary/plugins/openlibrary/js/template.js index bbd764c11f9..371b0546ce8 100644 --- a/openlibrary/plugins/openlibrary/js/template.js +++ b/openlibrary/plugins/openlibrary/js/template.js @@ -3,39 +3,39 @@ // Inspired by http://ejohn.org/blog/javascript-micro-templating/ export default function Template(tmpl_text) { - var s = []; - var js = ['var _p=[];', 'with(env) {']; - var tokens, i, t, f, g; + var s = []; + var js = ['var _p=[];', 'with(env) {']; + var tokens, i, t, f, g; - function addCode(text) { - js.push(text); - } - function addExpr(text) { - js.push(`_p.push(htmlquote(${text}));`); - } - function addText(text) { - js.push(`_p.push(__s[${s.length}]);`); - s.push(text); - } + function addCode(text) { + js.push(text); + } + function addExpr(text) { + js.push(`_p.push(htmlquote(${text}));`); + } + function addText(text) { + js.push(`_p.push(__s[${s.length}]);`); + s.push(text); + } - tokens = tmpl_text.split('<%'); + tokens = tmpl_text.split('<%'); - addText(tokens[0]); - for (i = 1; i < tokens.length; i++) { - t = tokens[i].split('%>'); + addText(tokens[0]); + for (i = 1; i < tokens.length; i++) { + t = tokens[i].split('%>'); - if (t[0][0] === '=') { - addExpr(t[0].substr(1)); - } else { - addCode(t[0]); + if (t[0][0] === '=') { + addExpr(t[0].substr(1)); + } else { + addCode(t[0]); + } + addText(t[1]); } - addText(t[1]); - } - js.push('}', "return _p.join('');"); + js.push('}', 'return _p.join(\'\');'); - f = new Function(['__s', 'env'], js.join('\n')); - g = (env) => f(s, env); - g.toString = () => tmpl_text; - g.toCode = () => f.toString(); - return g; + f = new Function(['__s', 'env'], js.join('\n')); + g = (env) => f(s, env); + g.toString = () => tmpl_text; + g.toCode = () => f.toString(); + return g; } diff --git a/openlibrary/plugins/openlibrary/js/type_changer.js b/openlibrary/plugins/openlibrary/js/type_changer.js index 106d9891333..234125460f1 100644 --- a/openlibrary/plugins/openlibrary/js/type_changer.js +++ b/openlibrary/plugins/openlibrary/js/type_changer.js @@ -3,17 +3,17 @@ */ export function initTypeChanger(elem) { - // /about?m=edit - where this code is run + // /about?m=edit - where this code is run - function changeTemplate() { + function changeTemplate() { // Change the template of the page based on the selected value - const searchParams = new URLSearchParams(window.location.search); - const t = elem.value; - searchParams.set('t', t); + const searchParams = new URLSearchParams(window.location.search); + const t = elem.value; + searchParams.set('t', t); - // Update the URL and navigate to the new page - window.location.search = searchParams.toString(); - } + // Update the URL and navigate to the new page + window.location.search = searchParams.toString(); + } - elem.addEventListener('change', changeTemplate); + elem.addEventListener('change', changeTemplate); } diff --git a/openlibrary/plugins/openlibrary/js/waitlist.js b/openlibrary/plugins/openlibrary/js/waitlist.js index 1e2689eaffa..29d223ad35a 100644 --- a/openlibrary/plugins/openlibrary/js/waitlist.js +++ b/openlibrary/plugins/openlibrary/js/waitlist.js @@ -6,14 +6,14 @@ import 'jquery-ui/ui/widgets/dialog'; * @param {NodeList<HTMLElement>} leaveWaitlistLinks - NodeList of leave waitlist links */ export function initLeaveWaitlist(leaveWaitlistLinks) { - for (const link of leaveWaitlistLinks) { - link.addEventListener('click', () => { - const $link = $(link); - const title = $link.parents('tr').find('.book').text(); - $('#leave-waitinglist-dialog strong').text(title); - // We remove the hidden class here because otherwise it flashes for a moment on page load - $('#leave-waitinglist-dialog').removeClass('hidden'); - $('#leave-waitinglist-dialog').data('origin', $link).dialog('open'); - }); - } + for (const link of leaveWaitlistLinks) { + link.addEventListener('click', () => { + const $link = $(link); + const title = $link.parents('tr').find('.book').text(); + $('#leave-waitinglist-dialog strong').text(title); + // We remove the hidden class here because otherwise it flashes for a moment on page load + $('#leave-waitinglist-dialog').removeClass('hidden'); + $('#leave-waitinglist-dialog').data('origin', $link).dialog('open'); + }); + } } diff --git a/stories/Button.stories.js b/stories/Button.stories.js index 41137aa1cd3..562adb35229 100644 --- a/stories/Button.stories.js +++ b/stories/Button.stories.js @@ -2,67 +2,67 @@ import '../static/css/components/buttonCta.css'; import '../static/css/components/buttonCta--js.css'; export default { - title: 'Legacy/Button', + title: 'Legacy/Button', }; const ButtonTemplate = (buttonType, text, badgeCount = null) => - `<div class="cta-btn${ButtonTypes[buttonType]}">${text}${badgeCount ? BadgeTemplate(badgeCount) : ''}</div>`; + `<div class="cta-btn${ButtonTypes[buttonType]}">${text}${badgeCount ? BadgeTemplate(badgeCount) : ''}</div>`; const BadgeTemplate = (badgeCount) => - ` <span class="cta-btn__badge">${badgeCount}</span>`; + ` <span class="cta-btn__badge">${badgeCount}</span>`; const ButtonTypes = { - default: '', - unavailable: ' cta-btn--unavailable', - available: ' cta-btn--available', - preview: ' cta-btn--shell cta-btn--preview', + default: '', + unavailable: ' cta-btn--unavailable', + available: ' cta-btn--available', + preview: ' cta-btn--shell cta-btn--preview', }; export const CtaBtn = () => ButtonTemplate('default', 'Leave waitlist'); CtaBtn.parameters = { - docs: { - source: { - code: ButtonTemplate('default', 'Leave waitlist'), + docs: { + source: { + code: ButtonTemplate('default', 'Leave waitlist'), + }, }, - }, }; export const CtaBtnUnavailable = () => - ButtonTemplate('unavailable', 'Join waitlist'); + ButtonTemplate('unavailable', 'Join waitlist'); CtaBtnUnavailable.parameters = { - docs: { - source: { - code: ButtonTemplate('unavailable', 'Join waitlist'), + docs: { + source: { + code: ButtonTemplate('unavailable', 'Join waitlist'), + }, }, - }, }; export const CtaBtnAvailable = () => ButtonTemplate('available', 'Borrow'); CtaBtnAvailable.parameters = { - docs: { - source: { - code: ButtonTemplate('available', 'Borrow'), + docs: { + source: { + code: ButtonTemplate('available', 'Borrow'), + }, }, - }, }; export const CtaBtnPreview = () => ButtonTemplate('preview', 'Preview'); CtaBtnPreview.parameters = { - docs: { - source: { - code: ButtonTemplate('preview', 'Preview'), + docs: { + source: { + code: ButtonTemplate('preview', 'Preview'), + }, }, - }, }; export const CtaBtnWithBadge = () => - ButtonTemplate('unavailable', 'Join waiting list', 4); + ButtonTemplate('unavailable', 'Join waiting list', 4); CtaBtnWithBadge.parameters = { - docs: { - source: { - code: ButtonTemplate('unavailable', 'Join waiting list', 4), + docs: { + source: { + code: ButtonTemplate('unavailable', 'Join waiting list', 4), + }, }, - }, }; export const CtaBtnGroup = () => `<div class="cta-button-group"> diff --git a/tests/unit/js/Browser.test.js b/tests/unit/js/Browser.test.js index e90ad35efd7..f8defa24145 100644 --- a/tests/unit/js/Browser.test.js +++ b/tests/unit/js/Browser.test.js @@ -1,59 +1,59 @@ import { - getJsonFromUrl, - removeURLParameter, + getJsonFromUrl, + removeURLParameter, } from '../../../openlibrary/plugins/openlibrary/js/Browser'; describe('removeURLParameter', () => { - const fn = removeURLParameter; - - test('URL with no parameters', () => { - expect(fn('http://foo.com', 'x')).toBe('http://foo.com'); - }); - - test('URL with the given parameter', () => { - expect(fn('http://foo.com?x=3', 'x')).toBe('http://foo.com'); - expect(fn('http://foo.com?x=3&y=4&z=5', 'x')).toBe( - 'http://foo.com?y=4&z=5', - ); - expect(fn('http://foo.com?x=3&y=4&z=5', 'y')).toBe( - 'http://foo.com?x=3&z=5', - ); - expect(fn('http://foo.com?x=3&y=4&z=5', 'z')).toBe( - 'http://foo.com?x=3&y=4', - ); - }); - - test('URL without the given parameter', () => { - expect(fn('http://foo.com?x=3', 'y')).toBe('http://foo.com?x=3'); - expect(fn('http://foo.com?x=3&y=4&z=5', 'w')).toBe( - 'http://foo.com?x=3&y=4&z=5', - ); - }); - - test('URL with multiple occurences of param', () => { - expect(fn('http://foo.com?x=3&x=4&x=5', 'x')).toBe('http://foo.com'); - expect(fn('http://foo.com?x=3&x=4&z=5', 'x')).toBe('http://foo.com?z=5'); - }); + const fn = removeURLParameter; + + test('URL with no parameters', () => { + expect(fn('http://foo.com', 'x')).toBe('http://foo.com'); + }); + + test('URL with the given parameter', () => { + expect(fn('http://foo.com?x=3', 'x')).toBe('http://foo.com'); + expect(fn('http://foo.com?x=3&y=4&z=5', 'x')).toBe( + 'http://foo.com?y=4&z=5', + ); + expect(fn('http://foo.com?x=3&y=4&z=5', 'y')).toBe( + 'http://foo.com?x=3&z=5', + ); + expect(fn('http://foo.com?x=3&y=4&z=5', 'z')).toBe( + 'http://foo.com?x=3&y=4', + ); + }); + + test('URL without the given parameter', () => { + expect(fn('http://foo.com?x=3', 'y')).toBe('http://foo.com?x=3'); + expect(fn('http://foo.com?x=3&y=4&z=5', 'w')).toBe( + 'http://foo.com?x=3&y=4&z=5', + ); + }); + + test('URL with multiple occurences of param', () => { + expect(fn('http://foo.com?x=3&x=4&x=5', 'x')).toBe('http://foo.com'); + expect(fn('http://foo.com?x=3&x=4&z=5', 'x')).toBe('http://foo.com?z=5'); + }); }); describe('getJsonFromUrl', () => { - const fn = getJsonFromUrl; + const fn = getJsonFromUrl; - test('Handles empty strings', () => { - expect(fn('')).toEqual({}); - expect(fn('?')).toEqual({}); - }); + test('Handles empty strings', () => { + expect(fn('')).toEqual({}); + expect(fn('?')).toEqual({}); + }); - test('Handles normal params', () => { - expect(fn('?hello=world')).toEqual({ hello: 'world' }); - expect(fn('?x=3&y=4&z=5')).toEqual({ x: '3', y: '4', z: '5' }); - }); + test('Handles normal params', () => { + expect(fn('?hello=world')).toEqual({ hello: 'world' }); + expect(fn('?x=3&y=4&z=5')).toEqual({ x: '3', y: '4', z: '5' }); + }); - test('Decodes parameter values', () => { - expect(fn('?q=foo%20bar')).toEqual({ q: 'foo bar' }); - }); + test('Decodes parameter values', () => { + expect(fn('?q=foo%20bar')).toEqual({ q: 'foo bar' }); + }); - test('Parameters override each other', () => { - expect(fn('?x=1&x=2&x=3')).toEqual({ x: '3' }); - }); + test('Parameters override each other', () => { + expect(fn('?x=1&x=2&x=3')).toEqual({ x: '3' }); + }); }); diff --git a/tests/unit/js/SearchBar.test.js b/tests/unit/js/SearchBar.test.js index a731ffcc667..2f2e13259e9 100644 --- a/tests/unit/js/SearchBar.test.js +++ b/tests/unit/js/SearchBar.test.js @@ -4,7 +4,7 @@ import { SearchBar } from '../../../openlibrary/plugins/openlibrary/js/SearchBar import * as SearchUtils from '../../../openlibrary/plugins/openlibrary/js/SearchUtils'; describe('SearchBar', () => { - const DUMMY_COMPONENT_HTML = ` + const DUMMY_COMPONENT_HTML = ` <div> <form class="search-bar-input" action="https://openlibrary.org/search?q=foo"> <input type="text"> @@ -12,305 +12,305 @@ describe('SearchBar', () => { <ul class="search-results"></ul> </div>`; - describe('initFromUrlParams', () => { + describe('initFromUrlParams', () => { /** @type {SearchBar} */ - let sb; - beforeEach(() => { - sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - sinon - .stub(sb, 'getCurUrl') - .returns(new URL('https://openlibrary.org/search')); + let sb; + beforeEach(() => { + sb = new SearchBar($(DUMMY_COMPONENT_HTML)); + sinon + .stub(sb, 'getCurUrl') + .returns(new URL('https://openlibrary.org/search')); + }); + afterEach(() => localStorage.clear()); + test('Does not throw on empty params', () => { + sb.initFromUrlParams({}); + }); + + test('Updates facet from params', () => { + expect(sb.facet.read()).not.toBe('title'); + sb.initFromUrlParams({ facet: 'title' }); + expect(sb.facet.read()).toBe('title'); + }); + + test('Ignore invalid facets', () => { + const originalValue = sb.facet.read(); + sb.initFromUrlParams({ facet: 'spam' }); + expect(sb.facet.read()).toBe(originalValue); + }); + + test('Sets input value from q param', () => { + sb.initFromUrlParams({ q: 'Harry Potter' }); + expect(sb.$input.val()).toBe('Harry Potter'); + }); + + test('Remove title prefix from q param', () => { + sb.initFromUrlParams({ q: 'title:"Harry Potter"', facet: 'title' }); + expect(sb.$input.val()).toBe('Harry Potter'); + sb.initFromUrlParams({ q: 'title: "Harry"', facet: 'title' }); + expect(sb.$input.val()).toBe('Harry'); + }); + + test('Persists value in url param', () => { + expect(localStorage.getItem('facet')).not.toBe('title'); + sb.initFromUrlParams({ facet: 'title' }); + expect(localStorage.getItem('facet')).toBe('title'); + }); }); - afterEach(() => localStorage.clear()); - test('Does not throw on empty params', () => { - sb.initFromUrlParams({}); - }); - - test('Updates facet from params', () => { - expect(sb.facet.read()).not.toBe('title'); - sb.initFromUrlParams({ facet: 'title' }); - expect(sb.facet.read()).toBe('title'); - }); - - test('Ignore invalid facets', () => { - const originalValue = sb.facet.read(); - sb.initFromUrlParams({ facet: 'spam' }); - expect(sb.facet.read()).toBe(originalValue); - }); - - test('Sets input value from q param', () => { - sb.initFromUrlParams({ q: 'Harry Potter' }); - expect(sb.$input.val()).toBe('Harry Potter'); - }); - - test('Remove title prefix from q param', () => { - sb.initFromUrlParams({ q: 'title:"Harry Potter"', facet: 'title' }); - expect(sb.$input.val()).toBe('Harry Potter'); - sb.initFromUrlParams({ q: 'title: "Harry"', facet: 'title' }); - expect(sb.$input.val()).toBe('Harry'); - }); - - test('Persists value in url param', () => { - expect(localStorage.getItem('facet')).not.toBe('title'); - sb.initFromUrlParams({ facet: 'title' }); - expect(localStorage.getItem('facet')).toBe('title'); - }); - }); - describe('submitForm', () => { - let sb; - beforeEach(() => { - sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - }); - afterEach(() => localStorage.clear()); - - test('Queries are marshalled before submit for titles', () => { - sb.initFromUrlParams({ facet: 'title' }); - const spy = sinon.spy(SearchBar, 'marshalBookSearchQuery'); - sb.submitForm(); - expect(spy.callCount).toBe(1); - spy.restore(); - }); - - test('Form action is updated on submit', () => { - sb.initFromUrlParams({ facet: 'title' }); - const originalAction = sb.$form[0].action; - sb.submitForm(); - expect(sb.$form[0].action).not.toBe(originalAction); + describe('submitForm', () => { + let sb; + beforeEach(() => { + sb = new SearchBar($(DUMMY_COMPONENT_HTML)); + }); + afterEach(() => localStorage.clear()); + + test('Queries are marshalled before submit for titles', () => { + sb.initFromUrlParams({ facet: 'title' }); + const spy = sinon.spy(SearchBar, 'marshalBookSearchQuery'); + sb.submitForm(); + expect(spy.callCount).toBe(1); + spy.restore(); + }); + + test('Form action is updated on submit', () => { + sb.initFromUrlParams({ facet: 'title' }); + const originalAction = sb.$form[0].action; + sb.submitForm(); + expect(sb.$form[0].action).not.toBe(originalAction); + }); + + test('Special inputs are added to the form on submit', () => { + const spy = sinon.spy(SearchUtils, 'addModeInputsToForm'); + sb.submitForm(); + expect(spy.callCount).toBe(1); + }); }); - test('Special inputs are added to the form on submit', () => { - const spy = sinon.spy(SearchUtils, 'addModeInputsToForm'); - sb.submitForm(); - expect(spy.callCount).toBe(1); - }); - }); - - describe('toggleCollapsibleModeForSmallScreens', () => { + describe('toggleCollapsibleModeForSmallScreens', () => { /** @type {SearchBar?} */ - let sb; - beforeEach(() => (sb = new SearchBar($(DUMMY_COMPONENT_HTML)))); - afterEach(() => localStorage.clear()); - - test('Only enters collapsible mode if not already there', () => { - sb.inCollapsibleMode = true; - const spy = sinon.spy(sb, 'enableCollapsibleMode'); - sb.toggleCollapsibleModeForSmallScreens(100); - expect(spy.callCount).toBe(0); + let sb; + beforeEach(() => (sb = new SearchBar($(DUMMY_COMPONENT_HTML)))); + afterEach(() => localStorage.clear()); + + test('Only enters collapsible mode if not already there', () => { + sb.inCollapsibleMode = true; + const spy = sinon.spy(sb, 'enableCollapsibleMode'); + sb.toggleCollapsibleModeForSmallScreens(100); + expect(spy.callCount).toBe(0); + }); + + test('Only exits collapsible mode if not already exited', () => { + sb.inCollapsibleMode = false; + const spy = sinon.spy(sb, 'disableCollapsibleMode'); + sb.toggleCollapsibleModeForSmallScreens(1000); + expect(spy.callCount).toBe(0); + }); }); - test('Only exits collapsible mode if not already exited', () => { - sb.inCollapsibleMode = false; - const spy = sinon.spy(sb, 'disableCollapsibleMode'); - sb.toggleCollapsibleModeForSmallScreens(1000); - expect(spy.callCount).toBe(0); - }); - }); + describe('marshalBookSearchQuery', () => { + const fn = SearchBar.marshalBookSearchQuery; + test('Empty string', () => { + expect(fn('')).toBe(''); + }); - describe('marshalBookSearchQuery', () => { - const fn = SearchBar.marshalBookSearchQuery; - test('Empty string', () => { - expect(fn('')).toBe(''); - }); - - test('Adds title prefix to plain strings', () => { - expect(fn('Harry Potter')).toBe('title: "Harry Potter"'); - }); - - test('Does not add title prefix to lucene-style queries', () => { - expect(fn('author:"Harry Potter"')).toBe('author:"Harry Potter"'); - expect(fn('"Harry Potter"')).toBe('"Harry Potter"'); - }); - }); - - describe('Misc', () => { - const sandbox = sinon.createSandbox(); - afterEach(() => { - sandbox.restore(); - localStorage.clear(); - }); - - test('When localStorage empty, defaults to facet=all', () => { - localStorage.clear(); - const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - expect(sb.facet.read()).toBe('all'); - }); - - test('Facet persists between page loads', () => { - localStorage.setItem('facet', 'title'); - const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - expect(sb.facet.read()).toBe('title'); - const sb2 = new SearchBar($(DUMMY_COMPONENT_HTML)); - expect(sb2.facet.read()).toBe('title'); - }); - - test('Advanced facet triggers redirect', () => { - const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - const navigateToStub = sandbox.stub(sb, 'navigateTo'); - const event = Object.assign(new $.Event(), { - target: { value: 'advanced' }, - }); - sb.handleFacetSelectChange(event); - expect(navigateToStub.callCount).toBe(1); - expect(navigateToStub.args[0]).toEqual(['/advancedsearch']); - }); - - for (const facet of ['title', 'author', 'all']) { - test(`Facet "${facet}" searches tigger autocomplete`, () => { - // Stub debounce to avoid have to manipulate time (!) - sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); - const sb = new SearchBar($(DUMMY_COMPONENT_HTML), { facet }); - const getJSONStub = sandbox.stub($, 'getJSON'); - - sb.$input.val('Harry'); - sb.$input.triggerHandler('focus'); - expect(getJSONStub.callCount).toBe(1); - }); - } - - test('Title searches tigger autocomplete even if containing title: prefix', () => { - // Stub debounce to avoid have to manipulate time (!) - sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); - const sb = new SearchBar($(DUMMY_COMPONENT_HTML), { facet: 'title' }); - const getJSONStub = sandbox.stub($, 'getJSON'); - sb.$input.val('title:"Harry"'); - sb.$input.triggerHandler('focus'); - expect(getJSONStub.callCount).toBe(1); - }); - - test('Focussing on input when empty does not trigger autocomplete', () => { - // Stub debounce to avoid have to manipulate time (!) - sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); - const sb = new SearchBar($(DUMMY_COMPONENT_HTML), { facet: 'title' }); - const getJSONStub = sandbox.stub($, 'getJSON'); - sb.$input.val(''); - sb.$input.triggerHandler('focus'); - expect(getJSONStub.callCount).toBe(0); - }); - - for (const facet of ['lists', 'subject', 'text']) { - test(`Facet "${facet}" does not tigger autocomplete`, () => { - // Stub debounce to avoid have to manipulate time (!) - sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); - const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - const getJSONStub = sandbox.stub($, 'getJSON'); - - sb.$input.val('foo bar'); - sb.facet.write(facet); - sb.$input.triggerHandler('focus'); - expect(getJSONStub.callCount).toBe(0); - }); - } - - test('Tabbing out of search input clears autocomplete results', () => { - const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - - // Spy on the clearAutocompletionResults method - const clearResultsSpy = sandbox.spy(sb, 'clearAutocompletionResults'); - - // Simulate tab keydown event on the form - const tabEvent = $.Event('keydown', { key: 'Tab' }); - sb.$form.trigger(tabEvent); - - // Verify clearAutocompletionResults was called - expect(clearResultsSpy.callCount).toBe(1); - }); - - test('Autocomplete rendering behavior depends on existing results', () => { - sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); - const sb = new SearchBar($(DUMMY_COMPONENT_HTML), { facet: 'title' }); - const renderSpy = sandbox.spy(sb, 'renderAutocompletionResults'); - - // Should render when results are empty - sb.$input.triggerHandler('focus'); - expect(renderSpy.callCount).toBe( - 1, - 'Should render when no results exist', - ); - - renderSpy.resetHistory(); - - // Should not render when results exist - sb.$results.append('<li>Some result</li>'); - sb.$input.triggerHandler('focus'); - expect(renderSpy.callCount).toBe( - 0, - 'Should not render when results exist', - ); - }); + test('Adds title prefix to plain strings', () => { + expect(fn('Harry Potter')).toBe('title: "Harry Potter"'); + }); - test('Tabbing from search result focuses search submit button and clears results', () => { - const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - - // Add a dummy result and focus on it - sb.$results.append('<li tabindex="0">Test Result</li>'); - const $resultItem = sb.$results.children().first(); - $resultItem.trigger('focus'); - - // Spy on the clearAutocompletionResults method - const clearResultsSpy = sandbox.spy(sb, 'clearAutocompletionResults'); - - // Spy on the focus trigger for search submit - const focusSpy = sandbox.spy(sb.$searchSubmit, 'trigger'); - - // Simulate tab keydown event on the result item - const tabEvent = $.Event('keydown', { key: 'Tab', shiftKey: false }); - $resultItem.trigger(tabEvent); - - // Verify clearAutocompletionResults was called - expect(clearResultsSpy.callCount).toBe( - 1, - 'Should clear autocomplete results', - ); - - // Verify search submit was focused - expect(focusSpy.calledWith('focus')).toBe( - true, - 'Should focus search submit button', - ); - - // Verify event default was prevented - expect(tabEvent.isDefaultPrevented()).toBe( - true, - 'Should prevent default tab behavior', - ); + test('Does not add title prefix to lucene-style queries', () => { + expect(fn('author:"Harry Potter"')).toBe('author:"Harry Potter"'); + expect(fn('"Harry Potter"')).toBe('"Harry Potter"'); + }); }); - test('Shift+tabbing from search result focuses facet select and clears results', () => { - const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - - // Add a dummy result and focus on it - sb.$results.append('<li tabindex="0">Test Result</li>'); - const $resultItem = sb.$results.children().first(); - $resultItem.trigger('focus'); - - // Spy on the clearAutocompletionResults method - const clearResultsSpy = sandbox.spy(sb, 'clearAutocompletionResults'); - - // Spy on the focus trigger for facet select - const focusSpy = sandbox.spy(sb.$facetSelect, 'trigger'); - - // Simulate shift+tab keydown event on the result item - const shiftTabEvent = $.Event('keydown', { key: 'Tab', shiftKey: true }); - $resultItem.trigger(shiftTabEvent); - - // Verify clearAutocompletionResults was called - expect(clearResultsSpy.callCount).toBe( - 1, - 'Should clear autocomplete results', - ); - - // Verify facet select was focused - expect(focusSpy.calledWith('focus')).toBe( - true, - 'Should focus facet select', - ); - - // Verify event default was prevented - expect(shiftTabEvent.isDefaultPrevented()).toBe( - true, - 'Should prevent default tab behavior', - ); + describe('Misc', () => { + const sandbox = sinon.createSandbox(); + afterEach(() => { + sandbox.restore(); + localStorage.clear(); + }); + + test('When localStorage empty, defaults to facet=all', () => { + localStorage.clear(); + const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); + expect(sb.facet.read()).toBe('all'); + }); + + test('Facet persists between page loads', () => { + localStorage.setItem('facet', 'title'); + const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); + expect(sb.facet.read()).toBe('title'); + const sb2 = new SearchBar($(DUMMY_COMPONENT_HTML)); + expect(sb2.facet.read()).toBe('title'); + }); + + test('Advanced facet triggers redirect', () => { + const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); + const navigateToStub = sandbox.stub(sb, 'navigateTo'); + const event = Object.assign(new $.Event(), { + target: { value: 'advanced' }, + }); + sb.handleFacetSelectChange(event); + expect(navigateToStub.callCount).toBe(1); + expect(navigateToStub.args[0]).toEqual(['/advancedsearch']); + }); + + for (const facet of ['title', 'author', 'all']) { + test(`Facet "${facet}" searches tigger autocomplete`, () => { + // Stub debounce to avoid have to manipulate time (!) + sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); + const sb = new SearchBar($(DUMMY_COMPONENT_HTML), { facet }); + const getJSONStub = sandbox.stub($, 'getJSON'); + + sb.$input.val('Harry'); + sb.$input.triggerHandler('focus'); + expect(getJSONStub.callCount).toBe(1); + }); + } + + test('Title searches tigger autocomplete even if containing title: prefix', () => { + // Stub debounce to avoid have to manipulate time (!) + sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); + const sb = new SearchBar($(DUMMY_COMPONENT_HTML), { facet: 'title' }); + const getJSONStub = sandbox.stub($, 'getJSON'); + sb.$input.val('title:"Harry"'); + sb.$input.triggerHandler('focus'); + expect(getJSONStub.callCount).toBe(1); + }); + + test('Focussing on input when empty does not trigger autocomplete', () => { + // Stub debounce to avoid have to manipulate time (!) + sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); + const sb = new SearchBar($(DUMMY_COMPONENT_HTML), { facet: 'title' }); + const getJSONStub = sandbox.stub($, 'getJSON'); + sb.$input.val(''); + sb.$input.triggerHandler('focus'); + expect(getJSONStub.callCount).toBe(0); + }); + + for (const facet of ['lists', 'subject', 'text']) { + test(`Facet "${facet}" does not tigger autocomplete`, () => { + // Stub debounce to avoid have to manipulate time (!) + sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); + const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); + const getJSONStub = sandbox.stub($, 'getJSON'); + + sb.$input.val('foo bar'); + sb.facet.write(facet); + sb.$input.triggerHandler('focus'); + expect(getJSONStub.callCount).toBe(0); + }); + } + + test('Tabbing out of search input clears autocomplete results', () => { + const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); + + // Spy on the clearAutocompletionResults method + const clearResultsSpy = sandbox.spy(sb, 'clearAutocompletionResults'); + + // Simulate tab keydown event on the form + const tabEvent = $.Event('keydown', { key: 'Tab' }); + sb.$form.trigger(tabEvent); + + // Verify clearAutocompletionResults was called + expect(clearResultsSpy.callCount).toBe(1); + }); + + test('Autocomplete rendering behavior depends on existing results', () => { + sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); + const sb = new SearchBar($(DUMMY_COMPONENT_HTML), { facet: 'title' }); + const renderSpy = sandbox.spy(sb, 'renderAutocompletionResults'); + + // Should render when results are empty + sb.$input.triggerHandler('focus'); + expect(renderSpy.callCount).toBe( + 1, + 'Should render when no results exist', + ); + + renderSpy.resetHistory(); + + // Should not render when results exist + sb.$results.append('<li>Some result</li>'); + sb.$input.triggerHandler('focus'); + expect(renderSpy.callCount).toBe( + 0, + 'Should not render when results exist', + ); + }); + + test('Tabbing from search result focuses search submit button and clears results', () => { + const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); + + // Add a dummy result and focus on it + sb.$results.append('<li tabindex="0">Test Result</li>'); + const $resultItem = sb.$results.children().first(); + $resultItem.trigger('focus'); + + // Spy on the clearAutocompletionResults method + const clearResultsSpy = sandbox.spy(sb, 'clearAutocompletionResults'); + + // Spy on the focus trigger for search submit + const focusSpy = sandbox.spy(sb.$searchSubmit, 'trigger'); + + // Simulate tab keydown event on the result item + const tabEvent = $.Event('keydown', { key: 'Tab', shiftKey: false }); + $resultItem.trigger(tabEvent); + + // Verify clearAutocompletionResults was called + expect(clearResultsSpy.callCount).toBe( + 1, + 'Should clear autocomplete results', + ); + + // Verify search submit was focused + expect(focusSpy.calledWith('focus')).toBe( + true, + 'Should focus search submit button', + ); + + // Verify event default was prevented + expect(tabEvent.isDefaultPrevented()).toBe( + true, + 'Should prevent default tab behavior', + ); + }); + + test('Shift+tabbing from search result focuses facet select and clears results', () => { + const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); + + // Add a dummy result and focus on it + sb.$results.append('<li tabindex="0">Test Result</li>'); + const $resultItem = sb.$results.children().first(); + $resultItem.trigger('focus'); + + // Spy on the clearAutocompletionResults method + const clearResultsSpy = sandbox.spy(sb, 'clearAutocompletionResults'); + + // Spy on the focus trigger for facet select + const focusSpy = sandbox.spy(sb.$facetSelect, 'trigger'); + + // Simulate shift+tab keydown event on the result item + const shiftTabEvent = $.Event('keydown', { key: 'Tab', shiftKey: true }); + $resultItem.trigger(shiftTabEvent); + + // Verify clearAutocompletionResults was called + expect(clearResultsSpy.callCount).toBe( + 1, + 'Should clear autocomplete results', + ); + + // Verify facet select was focused + expect(focusSpy.calledWith('focus')).toBe( + true, + 'Should focus facet select', + ); + + // Verify event default was prevented + expect(shiftTabEvent.isDefaultPrevented()).toBe( + true, + 'Should prevent default tab behavior', + ); + }); }); - }); }); diff --git a/tests/unit/js/SearchUtils.test.js b/tests/unit/js/SearchUtils.test.js index 5197145a223..d91f0c9ae1e 100644 --- a/tests/unit/js/SearchUtils.test.js +++ b/tests/unit/js/SearchUtils.test.js @@ -2,98 +2,98 @@ import sinon from 'sinon'; import * as SearchUtils from '../../../openlibrary/plugins/openlibrary/js/SearchUtils'; describe('PersistentValue', () => { - const PV = SearchUtils.PersistentValue; - afterEach(() => localStorage.clear()); + const PV = SearchUtils.PersistentValue; + afterEach(() => localStorage.clear()); - test('Saves to localStorage', () => { - const pv = new PV('foo'); - pv.write('bar'); - expect(localStorage.getItem('foo')).toBe('bar'); - }); + test('Saves to localStorage', () => { + const pv = new PV('foo'); + pv.write('bar'); + expect(localStorage.getItem('foo')).toBe('bar'); + }); - test('Reads from localStorage', () => { - localStorage.setItem('foo', 'bar'); - const pv = new PV('foo'); - expect(pv.read()).toBe('bar'); - }); + test('Reads from localStorage', () => { + localStorage.setItem('foo', 'bar'); + const pv = new PV('foo'); + expect(pv.read()).toBe('bar'); + }); - test('Writes default on init', () => { - const pv = new PV('foo', { default: 'blue' }); - expect(pv.read()).toBe('blue'); - }); + test('Writes default on init', () => { + const pv = new PV('foo', { default: 'blue' }); + expect(pv.read()).toBe('blue'); + }); - test('Does not writes default on init if already set', () => { - localStorage.setItem('foo', 'green'); - const pv = new PV('foo', { default: 'blue' }); - expect(pv.read()).toBe('green'); - }); + test('Does not writes default on init if already set', () => { + localStorage.setItem('foo', 'green'); + const pv = new PV('foo', { default: 'blue' }); + expect(pv.read()).toBe('green'); + }); - test('Writes default on invalid init', () => { - localStorage.setItem('foo', 'anything'); - const pv = new PV('foo', { - default: 'blue', - initValidation: () => false, + test('Writes default on invalid init', () => { + localStorage.setItem('foo', 'anything'); + const pv = new PV('foo', { + default: 'blue', + initValidation: () => false, + }); + expect(pv.read()).toBe('blue'); }); - expect(pv.read()).toBe('blue'); - }); - test('Writes null on invalid init', () => { - localStorage.setItem('foo', 'anything'); - const pv = new PV('foo', { - initValidation: () => false, + test('Writes null on invalid init', () => { + localStorage.setItem('foo', 'anything'); + const pv = new PV('foo', { + initValidation: () => false, + }); + expect(pv.read()).toBe(null); }); - expect(pv.read()).toBe(null); - }); - test('Does not writes default on valid init', () => { - localStorage.setItem('foo', 'anything'); - const pv = new PV('foo', { - default: 'blue', - initValidation: () => true, + test('Does not writes default on valid init', () => { + localStorage.setItem('foo', 'anything'); + const pv = new PV('foo', { + default: 'blue', + initValidation: () => true, + }); + expect(pv.read()).toBe('anything'); }); - expect(pv.read()).toBe('anything'); - }); - test('Writing applies transformation', () => { - localStorage.setItem('foo', 'blue'); - const pv = new PV('foo', { - writeTransformation: (newVal, oldVal) => oldVal + newVal, + test('Writing applies transformation', () => { + localStorage.setItem('foo', 'blue'); + const pv = new PV('foo', { + writeTransformation: (newVal, oldVal) => oldVal + newVal, + }); + pv.write('green'); + expect(pv.read()).toBe('bluegreen'); }); - pv.write('green'); - expect(pv.read()).toBe('bluegreen'); - }); - test('Writing removes when null', () => { - const pv = new PV('foo'); - pv.write(null); - expect(pv.read()).toBe(null); - }); + test('Writing removes when null', () => { + const pv = new PV('foo'); + pv.write(null); + expect(pv.read()).toBe(null); + }); - test('Writing triggers on change', () => { - const pv = new PV('foo'); - pv.write('a'); - const spy = sinon.spy(); - pv.sync(spy, false); - pv.write('b'); - expect(spy.callCount).toBe(1); - expect(spy.args[0]).toEqual(['b']); - }); + test('Writing triggers on change', () => { + const pv = new PV('foo'); + pv.write('a'); + const spy = sinon.spy(); + pv.sync(spy, false); + pv.write('b'); + expect(spy.callCount).toBe(1); + expect(spy.args[0]).toEqual(['b']); + }); - test('Writing does not trigger when same', () => { - const pv = new PV('foo'); - pv.write('a'); - const spy = sinon.spy(); - pv.sync(spy, false); - pv.write('a'); - expect(spy.callCount).toBe(0); - }); + test('Writing does not trigger when same', () => { + const pv = new PV('foo'); + pv.write('a'); + const spy = sinon.spy(); + pv.sync(spy, false); + pv.write('a'); + expect(spy.callCount).toBe(0); + }); - test('Change fires automatically if so specified', () => { - const pv = new PV('foo'); - pv.write('a'); - const spy = sinon.spy(); - pv.sync(spy, true); - expect(spy.callCount).toBe(1); - expect(spy.args[0]).toEqual(['a']); - }); + test('Change fires automatically if so specified', () => { + const pv = new PV('foo'); + pv.write('a'); + const spy = sinon.spy(); + pv.sync(spy, true); + expect(spy.callCount).toBe(1); + expect(spy.args[0]).toEqual(['a']); + }); }); diff --git a/tests/unit/js/SelectionManager.test.js b/tests/unit/js/SelectionManager.test.js index a7b18b1d86c..5bd56ea470c 100644 --- a/tests/unit/js/SelectionManager.test.js +++ b/tests/unit/js/SelectionManager.test.js @@ -1,86 +1,86 @@ import SelectionManager from '../../../openlibrary/plugins/openlibrary/js/ile/utils/SelectionManager/SelectionManager.js'; function createTestElementsForProcessClick() { - const listItem = document.createElement('li'); - listItem.classList.add('searchResultItem', 'ile-selectable'); + const listItem = document.createElement('li'); + listItem.classList.add('searchResultItem', 'ile-selectable'); - const link = document.createElement('a'); - listItem.appendChild(link); + const link = document.createElement('a'); + listItem.appendChild(link); - const bookTitle = document.createElement('div'); - bookTitle.classList.add('booktitle'); - const bookLink = document.createElement('a'); - bookLink.href = 'OL12345W'; // Mock href value - bookTitle.appendChild(bookLink); + const bookTitle = document.createElement('div'); + bookTitle.classList.add('booktitle'); + const bookLink = document.createElement('a'); + bookLink.href = 'OL12345W'; // Mock href value + bookTitle.appendChild(bookLink); - listItem.appendChild(bookTitle); + listItem.appendChild(bookTitle); - return { listItem, link }; + return { listItem, link }; } function setupSelectionManager() { - const sm = new SelectionManager(null, '/search'); - sm.ile = { $statusImages: { append: jest.fn() } }; - sm.selectedItems = { work: [] }; - sm.updateToolbar = jest.fn(); - return sm; + const sm = new SelectionManager(null, '/search'); + sm.ile = { $statusImages: { append: jest.fn() } }; + sm.selectedItems = { work: [] }; + sm.updateToolbar = jest.fn(); + return sm; } describe('SelectionManager', () => { - afterEach(() => { - window.sessionStorage.clear(); - }); - - test('getSelectedItems initializes selected item types', () => { - const sm = new SelectionManager(null, '/search'); - sm.getSelectedItems(); - expect(sm.selectedItems).toEqual({ - work: [], - edition: [], - author: [], + afterEach(() => { + window.sessionStorage.clear(); }); - }); - test('addSelectedItem', () => { - const sm = new SelectionManager(null, '/search'); - sm.getSelectedItems(); // to initialize types for push to work - sm.addSelectedItem('OL1W'); - expect(sm.selectedItems).toEqual({ - work: ['OL1W'], - edition: [], - author: [], + test('getSelectedItems initializes selected item types', () => { + const sm = new SelectionManager(null, '/search'); + sm.getSelectedItems(); + expect(sm.selectedItems).toEqual({ + work: [], + edition: [], + author: [], + }); }); - }); - test('processClick - clicking on a link or button', () => { - const sm = setupSelectionManager(); - const { listItem, link } = createTestElementsForProcessClick(); - - link.addEventListener('click', () => { - sm.processClick({ target: link, currentTarget: listItem }); + test('addSelectedItem', () => { + const sm = new SelectionManager(null, '/search'); + sm.getSelectedItems(); // to initialize types for push to work + sm.addSelectedItem('OL1W'); + expect(sm.selectedItems).toEqual({ + work: ['OL1W'], + edition: [], + author: [], + }); }); - expect(listItem.classList.contains('ile-selected')).toBe(false); - link.click(); - expect(listItem.classList.contains('ile-selected')).toBe(false); + test('processClick - clicking on a link or button', () => { + const sm = setupSelectionManager(); + const { listItem, link } = createTestElementsForProcessClick(); - jest.clearAllMocks(); - }); + link.addEventListener('click', () => { + sm.processClick({ target: link, currentTarget: listItem }); + }); - test('processClick - clicking on listItem', () => { - const sm = setupSelectionManager(); - const { listItem } = createTestElementsForProcessClick(); + expect(listItem.classList.contains('ile-selected')).toBe(false); + link.click(); + expect(listItem.classList.contains('ile-selected')).toBe(false); - listItem.addEventListener('click', () => { - sm.processClick({ target: listItem, currentTarget: listItem }); + jest.clearAllMocks(); }); - expect(listItem.classList.contains('ile-selected')).toBe(false); - listItem.click(); - expect(listItem.classList.contains('ile-selected')).toBe(true); - listItem.click(); - expect(listItem.classList.contains('ile-selected')).toBe(false); + test('processClick - clicking on listItem', () => { + const sm = setupSelectionManager(); + const { listItem } = createTestElementsForProcessClick(); + + listItem.addEventListener('click', () => { + sm.processClick({ target: listItem, currentTarget: listItem }); + }); - jest.clearAllMocks(); - }); + expect(listItem.classList.contains('ile-selected')).toBe(false); + listItem.click(); + expect(listItem.classList.contains('ile-selected')).toBe(true); + listItem.click(); + expect(listItem.classList.contains('ile-selected')).toBe(false); + + jest.clearAllMocks(); + }); }); diff --git a/tests/unit/js/autocomplete.test.js b/tests/unit/js/autocomplete.test.js index 5afc4666013..48cc2d72703 100644 --- a/tests/unit/js/autocomplete.test.js +++ b/tests/unit/js/autocomplete.test.js @@ -1,57 +1,57 @@ import { - highlight, - mapApiResultsToAutocompleteSuggestions, + highlight, + mapApiResultsToAutocompleteSuggestions, } from '../../../openlibrary/plugins/openlibrary/js/autocomplete.js'; describe('highlight', () => { - test('Highlights terms with strong tag', () => { - [ - ['Jon Robson', 'Jon', '<strong>Jon</strong> Robson'], - ['No match', 'abcde', 'No match'], - ].forEach((test) => { - const highlightedText = highlight(test[0], test[1]); - expect(highlightedText).toStrictEqual(test[2]); + test('Highlights terms with strong tag', () => { + [ + ['Jon Robson', 'Jon', '<strong>Jon</strong> Robson'], + ['No match', 'abcde', 'No match'], + ].forEach((test) => { + const highlightedText = highlight(test[0], test[1]); + expect(highlightedText).toStrictEqual(test[2]); + }); }); - }); }); describe('mapApiResultsToAutocompleteSuggestions', () => { - test('API results are converted to suggestions using label function', () => { - const suggestions = mapApiResultsToAutocompleteSuggestions( - [ - { - key: 1, - name: 'Test', - }, - ], - (r) => r.name, - ); + test('API results are converted to suggestions using label function', () => { + const suggestions = mapApiResultsToAutocompleteSuggestions( + [ + { + key: 1, + name: 'Test', + }, + ], + (r) => r.name, + ); - expect(suggestions).toStrictEqual([ - { - key: 1, - label: 'Test', - value: 'Test', - }, - ]); - }); + expect(suggestions).toStrictEqual([ + { + key: 1, + label: 'Test', + value: 'Test', + }, + ]); + }); - test('Add new item field can be added', () => { - const suggestions = mapApiResultsToAutocompleteSuggestions( - [ - { - key: 1, - name: 'Test', - }, - ], - (r) => r.name, - 'Add new item', - ); + test('Add new item field can be added', () => { + const suggestions = mapApiResultsToAutocompleteSuggestions( + [ + { + key: 1, + name: 'Test', + }, + ], + (r) => r.name, + 'Add new item', + ); - expect(suggestions[1]).toStrictEqual({ - key: '__new__', - label: 'Add new item', - value: 'Add new item', + expect(suggestions[1]).toStrictEqual({ + key: '__new__', + label: 'Add new item', + value: 'Add new item', + }); }); - }); }); diff --git a/tests/unit/js/droppers.test.js b/tests/unit/js/droppers.test.js index 7f1e7512dcd..33433911e1e 100644 --- a/tests/unit/js/droppers.test.js +++ b/tests/unit/js/droppers.test.js @@ -1,352 +1,352 @@ import sinon from 'sinon'; import { - initDroppers, - initGenericDroppers, + initDroppers, + initGenericDroppers, } from '../../../openlibrary/plugins/openlibrary/js/dropper'; import { Dropper } from '../../../openlibrary/plugins/openlibrary/js/dropper/Dropper'; import * as nonjquery_utils from '../../../openlibrary/plugins/openlibrary/js/nonjquery_utils.js'; import { - closedDropperMarkup, - disabledDropperMarkup, - legacyBookDropperMarkup, - openDropperMarkup, + closedDropperMarkup, + disabledDropperMarkup, + legacyBookDropperMarkup, + openDropperMarkup, } from './sample-html/dropper-test-data'; describe('initDroppers', () => { - test('dropdown changes arrow direction on click', () => { + test('dropdown changes arrow direction on click', () => { // Stub debounce to avoid have to manipulate time (!) - const stub = sinon.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); + const stub = sinon.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); - $(document.body).html(legacyBookDropperMarkup); - const $dropclick = $('.dropclick'); - const $arrow = $dropclick.find('.arrow'); - initDroppers(document.querySelectorAll('.dropper')); + $(document.body).html(legacyBookDropperMarkup); + const $dropclick = $('.dropclick'); + const $arrow = $dropclick.find('.arrow'); + initDroppers(document.querySelectorAll('.dropper')); - for (let i = 0; i < 2; i++) { - $dropclick.trigger('click'); - expect($arrow.hasClass('up')).toBe(true); + for (let i = 0; i < 2; i++) { + $dropclick.trigger('click'); + expect($arrow.hasClass('up')).toBe(true); - $dropclick.trigger('click'); - expect($arrow.hasClass('up')).toBe(false); - } + $dropclick.trigger('click'); + expect($arrow.hasClass('up')).toBe(false); + } - stub.restore(); - }); + stub.restore(); + }); }); describe('Generic Droppers', () => { - test('Clicking dropclick element toggles the dropper', () => { + test('Clicking dropclick element toggles the dropper', () => { // Setup - document.body.innerHTML = closedDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const dropper = new Dropper(wrapper); - dropper.initialize(); - - const dropClick = document.querySelector('.generic-dropper__dropclick'); - const arrow = dropClick.querySelector('.arrow'); - - // Dropper should be closed at the start - expect(arrow.classList.contains('up')).toBe(false); - expect(wrapper.classList.contains('dropper-wrapper--active')).toBe(false); - - // Open dropper - dropClick.click(); - expect(arrow.classList.contains('up')).toBe(true); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - true, - ); - - // Close dropper - dropClick.click(); - expect(arrow.classList.contains('up')).toBe(false); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - }); - - test('Opened droppers close if they are not the target of a click', () => { + document.body.innerHTML = closedDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + dropper.initialize(); + + const dropClick = document.querySelector('.generic-dropper__dropclick'); + const arrow = dropClick.querySelector('.arrow'); + + // Dropper should be closed at the start + expect(arrow.classList.contains('up')).toBe(false); + expect(wrapper.classList.contains('dropper-wrapper--active')).toBe(false); + + // Open dropper + dropClick.click(); + expect(arrow.classList.contains('up')).toBe(true); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + true, + ); + + // Close dropper + dropClick.click(); + expect(arrow.classList.contains('up')).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + }); + + test('Opened droppers close if they are not the target of a click', () => { // Setup - document.body.innerHTML = openDropperMarkup.concat( - openDropperMarkup, - openDropperMarkup, - ); - const wrappers = document.querySelectorAll('.generic-dropper-wrapper'); - initGenericDroppers(wrappers); - - // Ensure that all three droppers are open - expect(wrappers.length).toBe(3); - for (const wrapper of wrappers) { - const arrow = wrapper.querySelector('.arrow'); - expect( - wrapper.classList.contains('generic-dropper-wrapper--active'), - ).toBe(true); - expect(arrow.classList.contains('up')).toBe(true); - } - - // After clicking the dropdown content of the first dropper: - const dropdownContent = wrappers[0].querySelector( - '.generic-dropper__dropdown', - ); - dropdownContent.click(); - - // First dropper should be open - expect( - wrappers[0].classList.contains('generic-dropper-wrapper--active'), - ).toBe(true); - expect(wrappers[0].querySelector('.arrow').classList.contains('up')).toBe( - true, - ); - - // ...while other droppers should be closed - for (let i = 1; i < wrappers.length; ++i) { - const arrow = wrappers[i].querySelector('.arrow'); - expect( - wrappers[i].classList.contains('generic-dropper-wrapper--active'), - ).toBe(false); - expect(arrow.classList.contains('up')).toBe(false); - } - }); - - test('Disabled droppers cannot be opened nor closed', () => { - document.body.innerHTML = disabledDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const dropper = new Dropper(wrapper); - dropper.initialize(); - const dropclick = wrapper.querySelector('.generic-dropper__dropclick'); - const arrow = wrapper.querySelector('.arrow'); - - // Sanity checks - expect(wrapper.classList.contains('generic-dropper--disabled')).toBe(true); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - expect(arrow.classList.contains('up')).toBe(false); - - // Click on the dropclick: - dropclick.click(); - - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - expect(arrow.classList.contains('up')).toBe(false); - }); -}); + document.body.innerHTML = openDropperMarkup.concat( + openDropperMarkup, + openDropperMarkup, + ); + const wrappers = document.querySelectorAll('.generic-dropper-wrapper'); + initGenericDroppers(wrappers); + + // Ensure that all three droppers are open + expect(wrappers.length).toBe(3); + for (const wrapper of wrappers) { + const arrow = wrapper.querySelector('.arrow'); + expect( + wrapper.classList.contains('generic-dropper-wrapper--active'), + ).toBe(true); + expect(arrow.classList.contains('up')).toBe(true); + } + + // After clicking the dropdown content of the first dropper: + const dropdownContent = wrappers[0].querySelector( + '.generic-dropper__dropdown', + ); + dropdownContent.click(); + + // First dropper should be open + expect( + wrappers[0].classList.contains('generic-dropper-wrapper--active'), + ).toBe(true); + expect(wrappers[0].querySelector('.arrow').classList.contains('up')).toBe( + true, + ); + + // ...while other droppers should be closed + for (let i = 1; i < wrappers.length; ++i) { + const arrow = wrappers[i].querySelector('.arrow'); + expect( + wrappers[i].classList.contains('generic-dropper-wrapper--active'), + ).toBe(false); + expect(arrow.classList.contains('up')).toBe(false); + } + }); -describe('Dropper.js class', () => { - test('Dropper references set correctly on instantiation', () => { - document.body.innerHTML = closedDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const dropper = new Dropper(wrapper); - - // Reference to component root stored - expect(dropper.dropper === wrapper).toBe(true); - - // Dropclick reference stored - const dropClick = wrapper.querySelector('.generic-dropper__dropclick'); - expect(dropper.dropClick === dropClick).toBe(true); - - // Dropper is closed - expect(dropper.isDropperOpen).toBe(false); - - // This dropper is not disabled - expect(dropper.isDropperDisabled).toBe(false); - }); - - it('is not functional until initialize() is called', () => { - document.body.innerHTML = closedDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const dropClick = wrapper.querySelector('.generic-dropper__dropclick'); - const arrow = wrapper.querySelector('.arrow'); - - const dropper = new Dropper(wrapper); - const spy = jest.spyOn(dropper, 'toggleDropper'); - - // Dropper should be closed initially: - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - expect(arrow.classList.contains('up')).toBe(false); - - // Clicking should not do anything yet: - dropClick.click(); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - expect(arrow.classList.contains('up')).toBe(false); - expect(spy).not.toHaveBeenCalled(); - - // Test again after initialization: - dropper.initialize(); - dropClick.click(); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - true, - ); - expect(arrow.classList.contains('up')).toBe(true); - expect(spy).toHaveBeenCalled(); - - jest.restoreAllMocks(); - }); - - it('can be closed if not disabled', () => { - document.body.innerHTML = openDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const arrow = wrapper.querySelector('.arrow'); - - const dropper = new Dropper(wrapper); - dropper.initialize(); - - // Check initial state: - expect(dropper.isDropperDisabled).toBe(false); - expect(dropper.isDropperOpen).toBe(true); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - true, - ); - expect(arrow.classList.contains('up')).toBe(true); - - // Check again after closing: - dropper.closeDropper(); - expect(dropper.isDropperOpen).toBe(false); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - expect(arrow.classList.contains('up')).toBe(false); - }); - - it('can be toggled if not disabled', () => { - document.body.innerHTML = closedDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const arrow = wrapper.querySelector('.arrow'); - - const dropper = new Dropper(wrapper); - dropper.initialize(); - - // Check initial state: - expect(dropper.isDropperDisabled).toBe(false); - expect(dropper.isDropperOpen).toBe(false); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - expect(arrow.classList.contains('up')).toBe(false); - - // Check after toggling open: - dropper.toggleDropper(); - expect(dropper.isDropperOpen).toBe(true); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - true, - ); - expect(arrow.classList.contains('up')).toBe(true); - - // Check after toggling once more: - dropper.toggleDropper(); - expect(dropper.isDropperOpen).toBe(false); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - expect(arrow.classList.contains('up')).toBe(false); - }); - - it('cannot be opened while disabled', () => { - document.body.innerHTML = disabledDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const dropper = new Dropper(wrapper); - dropper.initialize(); - const arrow = wrapper.querySelector('.arrow'); - - // Check initial state: - expect(dropper.isDropperDisabled).toBe(true); - expect(arrow.classList.contains('up')).toBe(false); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - - // Check state after toggling: - dropper.toggleDropper(); - expect(arrow.classList.contains('up')).toBe(false); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - }); - - describe('Dropper event methods', () => { - afterEach(() => { - jest.clearAllMocks(); + test('Disabled droppers cannot be opened nor closed', () => { + document.body.innerHTML = disabledDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + dropper.initialize(); + const dropclick = wrapper.querySelector('.generic-dropper__dropclick'); + const arrow = wrapper.querySelector('.arrow'); + + // Sanity checks + expect(wrapper.classList.contains('generic-dropper--disabled')).toBe(true); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + expect(arrow.classList.contains('up')).toBe(false); + + // Click on the dropclick: + dropclick.click(); + + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + expect(arrow.classList.contains('up')).toBe(false); }); +}); - it('calls `onDisabledClick()` when dropper is clicked while disabled', () => { - document.body.innerHTML = disabledDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const dropper = new Dropper(wrapper); - dropper.initialize(); +describe('Dropper.js class', () => { + test('Dropper references set correctly on instantiation', () => { + document.body.innerHTML = closedDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); - const onDisabledClickFn = jest.spyOn(dropper, 'onDisabledClick'); + // Reference to component root stored + expect(dropper.dropper === wrapper).toBe(true); - // Check initial state: - expect(dropper.isDropperDisabled).toBe(true); - expect(onDisabledClickFn).not.toHaveBeenCalled(); + // Dropclick reference stored + const dropClick = wrapper.querySelector('.generic-dropper__dropclick'); + expect(dropper.dropClick === dropClick).toBe(true); - // Check state after toggling: - dropper.toggleDropper(); - expect(dropper.isDropperDisabled).toBe(true); - expect(onDisabledClickFn).toHaveBeenCalledTimes(1); + // Dropper is closed + expect(dropper.isDropperOpen).toBe(false); - // Check state after closing: - dropper.closeDropper(); - expect(dropper.isDropperDisabled).toBe(true); - expect(onDisabledClickFn).toHaveBeenCalledTimes(2); + // This dropper is not disabled + expect(dropper.isDropperDisabled).toBe(false); }); - it('calls `onClose()` when active dropper is closed', () => { - document.body.innerHTML = openDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const dropper = new Dropper(wrapper); - dropper.initialize(); - - const onCloseFn = jest.spyOn(dropper, 'onClose'); - - // Check initial state: - expect(dropper.isDropperOpen).toBe(true); - expect(onCloseFn).not.toHaveBeenCalled(); + it('is not functional until initialize() is called', () => { + document.body.innerHTML = closedDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropClick = wrapper.querySelector('.generic-dropper__dropclick'); + const arrow = wrapper.querySelector('.arrow'); + + const dropper = new Dropper(wrapper); + const spy = jest.spyOn(dropper, 'toggleDropper'); + + // Dropper should be closed initially: + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + expect(arrow.classList.contains('up')).toBe(false); + + // Clicking should not do anything yet: + dropClick.click(); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + expect(arrow.classList.contains('up')).toBe(false); + expect(spy).not.toHaveBeenCalled(); + + // Test again after initialization: + dropper.initialize(); + dropClick.click(); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + true, + ); + expect(arrow.classList.contains('up')).toBe(true); + expect(spy).toHaveBeenCalled(); + + jest.restoreAllMocks(); + }); - // Check state after closing: - dropper.closeDropper(); - expect(dropper.isDropperOpen).toBe(false); - expect(onCloseFn).toHaveBeenCalledTimes(1); + it('can be closed if not disabled', () => { + document.body.innerHTML = openDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const arrow = wrapper.querySelector('.arrow'); + + const dropper = new Dropper(wrapper); + dropper.initialize(); + + // Check initial state: + expect(dropper.isDropperDisabled).toBe(false); + expect(dropper.isDropperOpen).toBe(true); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + true, + ); + expect(arrow.classList.contains('up')).toBe(true); + + // Check again after closing: + dropper.closeDropper(); + expect(dropper.isDropperOpen).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + expect(arrow.classList.contains('up')).toBe(false); + }); - // Check state after toggling open then closed: - dropper.toggleDropper(); - expect(dropper.isDropperOpen).toBe(true); - expect(onCloseFn).toHaveBeenCalledTimes(1); // Should not be called when dropper is closed + it('can be toggled if not disabled', () => { + document.body.innerHTML = closedDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const arrow = wrapper.querySelector('.arrow'); + + const dropper = new Dropper(wrapper); + dropper.initialize(); + + // Check initial state: + expect(dropper.isDropperDisabled).toBe(false); + expect(dropper.isDropperOpen).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + expect(arrow.classList.contains('up')).toBe(false); + + // Check after toggling open: + dropper.toggleDropper(); + expect(dropper.isDropperOpen).toBe(true); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + true, + ); + expect(arrow.classList.contains('up')).toBe(true); + + // Check after toggling once more: + dropper.toggleDropper(); + expect(dropper.isDropperOpen).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + expect(arrow.classList.contains('up')).toBe(false); + }); - dropper.toggleDropper(); - expect(dropper.isDropperOpen).toBe(false); - expect(onCloseFn).toHaveBeenCalledTimes(2); + it('cannot be opened while disabled', () => { + document.body.innerHTML = disabledDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + dropper.initialize(); + const arrow = wrapper.querySelector('.arrow'); + + // Check initial state: + expect(dropper.isDropperDisabled).toBe(true); + expect(arrow.classList.contains('up')).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); + + // Check state after toggling: + dropper.toggleDropper(); + expect(arrow.classList.contains('up')).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( + false, + ); }); - test('toggling dropper results in correct event method being called', () => { - document.body.innerHTML = closedDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const dropper = new Dropper(wrapper); - dropper.initialize(); - - const onCloseFn = jest.spyOn(dropper, 'onClose'); - const onOpenFn = jest.spyOn(dropper, 'onOpen'); - - // Check initial state: - expect(dropper.isDropperOpen).toBe(false); - expect(onCloseFn).not.toHaveBeenCalled(); - expect(onOpenFn).not.toHaveBeenCalled(); - - // Check after toggling open: - dropper.toggleDropper(); - expect(dropper.isDropperOpen).toBe(true); - expect(onCloseFn).toHaveBeenCalledTimes(0); - expect(onOpenFn).toHaveBeenCalledTimes(1); - - // Check after toggling closed: - dropper.toggleDropper(); - expect(dropper.isDropperOpen).toBe(false); - expect(onCloseFn).toHaveBeenCalledTimes(1); - expect(onOpenFn).toHaveBeenCalledTimes(1); + describe('Dropper event methods', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('calls `onDisabledClick()` when dropper is clicked while disabled', () => { + document.body.innerHTML = disabledDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + dropper.initialize(); + + const onDisabledClickFn = jest.spyOn(dropper, 'onDisabledClick'); + + // Check initial state: + expect(dropper.isDropperDisabled).toBe(true); + expect(onDisabledClickFn).not.toHaveBeenCalled(); + + // Check state after toggling: + dropper.toggleDropper(); + expect(dropper.isDropperDisabled).toBe(true); + expect(onDisabledClickFn).toHaveBeenCalledTimes(1); + + // Check state after closing: + dropper.closeDropper(); + expect(dropper.isDropperDisabled).toBe(true); + expect(onDisabledClickFn).toHaveBeenCalledTimes(2); + }); + + it('calls `onClose()` when active dropper is closed', () => { + document.body.innerHTML = openDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + dropper.initialize(); + + const onCloseFn = jest.spyOn(dropper, 'onClose'); + + // Check initial state: + expect(dropper.isDropperOpen).toBe(true); + expect(onCloseFn).not.toHaveBeenCalled(); + + // Check state after closing: + dropper.closeDropper(); + expect(dropper.isDropperOpen).toBe(false); + expect(onCloseFn).toHaveBeenCalledTimes(1); + + // Check state after toggling open then closed: + dropper.toggleDropper(); + expect(dropper.isDropperOpen).toBe(true); + expect(onCloseFn).toHaveBeenCalledTimes(1); // Should not be called when dropper is closed + + dropper.toggleDropper(); + expect(dropper.isDropperOpen).toBe(false); + expect(onCloseFn).toHaveBeenCalledTimes(2); + }); + + test('toggling dropper results in correct event method being called', () => { + document.body.innerHTML = closedDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + dropper.initialize(); + + const onCloseFn = jest.spyOn(dropper, 'onClose'); + const onOpenFn = jest.spyOn(dropper, 'onOpen'); + + // Check initial state: + expect(dropper.isDropperOpen).toBe(false); + expect(onCloseFn).not.toHaveBeenCalled(); + expect(onOpenFn).not.toHaveBeenCalled(); + + // Check after toggling open: + dropper.toggleDropper(); + expect(dropper.isDropperOpen).toBe(true); + expect(onCloseFn).toHaveBeenCalledTimes(0); + expect(onOpenFn).toHaveBeenCalledTimes(1); + + // Check after toggling closed: + dropper.toggleDropper(); + expect(dropper.isDropperOpen).toBe(false); + expect(onCloseFn).toHaveBeenCalledTimes(1); + expect(onOpenFn).toHaveBeenCalledTimes(1); + }); }); - }); }); diff --git a/tests/unit/js/editionEditPageClassification.test.js b/tests/unit/js/editionEditPageClassification.test.js index b6b0666a2fc..de2d2f6fba6 100644 --- a/tests/unit/js/editionEditPageClassification.test.js +++ b/tests/unit/js/editionEditPageClassification.test.js @@ -7,52 +7,52 @@ import * as testData from './html-test-data'; let sandbox; beforeEach(() => { - // Clear session storage - sandbox = sinon.createSandbox(); - global.htmlquote = htmlquote; - // htmlquote is used inside an eval expression (yuck) so is an implied dependency - sandbox.stub(global, 'htmlquote').callsFake(htmlquote); - // setup Query repeat - init(); - // setup the HTML - $(document.body).html(testData.readClassification); - initClassificationValidation(); + // Clear session storage + sandbox = sinon.createSandbox(); + global.htmlquote = htmlquote; + // htmlquote is used inside an eval expression (yuck) so is an implied dependency + sandbox.stub(global, 'htmlquote').callsFake(htmlquote); + // setup Query repeat + init(); + // setup the HTML + $(document.body).html(testData.readClassification); + initClassificationValidation(); }); describe('initClassificationValidation', () => { - test.each([ + test.each([ // format: [testName, selectValue, classificationValue, expectedDisplay] - [ - 'Can have a classification and any value', - 'lc_classifications', - 'anything at all', - 'none', - ], - [ - 'Cannot have both an empty classification and classification value', - '', - '', - 'block', - ], - ['Cannot have an empty classification', '', 'Test', 'block'], - [ - 'Cannot have an empty classification value', - 'lc_classifications', - '', - 'block', - ], - [ - 'Cannot have --- as a classification WITHOUT a value', - '---', - 'test', - 'block', - ], - ['Cannot have --- as a classification with a value', '---', '', 'block'], - ])('Test: %s', (testName, selectValue, classificationValue, expectedDisplay) => { - $('#select-classification').val(selectValue); - $('#classification-value').val(classificationValue); - $('.repeat-add').trigger('click'); - const displayError = $('#classification-errors').css('display'); - expect(displayError).toBe(expectedDisplay); - }); + [ + 'Can have a classification and any value', + 'lc_classifications', + 'anything at all', + 'none', + ], + [ + 'Cannot have both an empty classification and classification value', + '', + '', + 'block', + ], + ['Cannot have an empty classification', '', 'Test', 'block'], + [ + 'Cannot have an empty classification value', + 'lc_classifications', + '', + 'block', + ], + [ + 'Cannot have --- as a classification WITHOUT a value', + '---', + 'test', + 'block', + ], + ['Cannot have --- as a classification with a value', '---', '', 'block'], + ])('Test: %s', (testName, selectValue, classificationValue, expectedDisplay) => { + $('#select-classification').val(selectValue); + $('#classification-value').val(classificationValue); + $('.repeat-add').trigger('click'); + const displayError = $('#classification-errors').css('display'); + expect(displayError).toBe(expectedDisplay); + }); }); diff --git a/tests/unit/js/editionsEditPage.test.js b/tests/unit/js/editionsEditPage.test.js index 5d53fe8b737..e8cabcbc092 100644 --- a/tests/unit/js/editionsEditPage.test.js +++ b/tests/unit/js/editionsEditPage.test.js @@ -25,199 +25,199 @@ let sandbox; // Adapted from jquery.repeat.test.js beforeEach(() => { - // Clear session storage - sandbox = sinon.createSandbox(); - global.htmlquote = htmlquote; - // htmlquote is used inside an eval expression (yuck) so is an implied dependency - sandbox.stub(global, 'htmlquote').callsFake(htmlquote); - // setup Query repeat - init(); - // setup the HTML - $(document.body).html(testData.editionIdentifiersSample); - $('#identifiers').repeat({ - vars: { prefix: 'edition--' }, - validate: (data) => validateIdentifiers(data), - }); + // Clear session storage + sandbox = sinon.createSandbox(); + global.htmlquote = htmlquote; + // htmlquote is used inside an eval expression (yuck) so is an implied dependency + sandbox.stub(global, 'htmlquote').callsFake(htmlquote); + // setup Query repeat + init(); + // setup the HTML + $(document.body).html(testData.editionIdentifiersSample); + $('#identifiers').repeat({ + vars: { prefix: 'edition--' }, + validate: (data) => validateIdentifiers(data), + }); }); // Per the test data used, and beforeEach(), the length always starts out at 5. describe('initIdentifierValidation', () => { - // ISBN 10 - it('does add a valid ISBN 10 ending in X', () => { - $('#select-id').val('isbn_10'); - $('#id-value').val('0-8044-2957-X'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - it('does add a valid ISBN 10 NOT ending in X', () => { - $('#select-id').val('isbn_10'); - $('#id-value').val('0596520689'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - it('does NOT add an invalid ISBN 10 with a failed check digit', () => { - $('#select-id').val('isbn_10'); - $('#id-value').val('1234567890'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(5); - }); - - it('does NOT prompt to add a formally invalid ISBN 10', () => { - $('#select-id').val('isbn_10'); - $('#id-value').val('12345'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(5); - expect($('.repeat-add').length).toBe(1); - const errorDivText = $('#id-errors').text(); - const expected = 'Add it anyway?'; - expect(errorDivText).toEqual(expect.not.stringContaining(expected)); - }); - - it('clears the invalid ISBN 10 error prompt and does not add an ISBN if a user clicks no', () => { - $('#select-id').val('isbn_10'); - $('#id-value').val('2121212121'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(5); - expect($('.repeat-add').length).toBe(2); - $('#do-not-add-isbn').trigger('click'); - expect($('.repeat-item').length).toBe(5); - const cssDisplay = $('#id-errors').css('display'); - expect(cssDisplay).toEqual('none'); - }); - - it('does NOT add a duplicate ISBN 10', () => { - $('#select-id').val('isbn_10'); - $('#id-value').val('0063162024'); - $('.repeat-add').trigger('click'); - $('#select-id').val('isbn_10'); - $('#id-value').val('0063162024'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - it('does properly strip spaces and hypens from a valid ISBN 10 and add it', () => { - $('#select-id').val('isbn_10'); - $('#id-value').val('09- 8478---2869 '); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - it('does count identical stripped and unstripped ISBN 10s as the same ISBN', () => { - $('#select-id').val('isbn_10'); - $('#id-value').val(' 144--93-55730 '); - $('.repeat-add').trigger('click'); - $('#select-id').val('isbn_10'); - $('#id-value').val('1449355730'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - // ISBN 13 - it('does add a valid ISBN 13', () => { - $('#select-id').val('isbn_13'); - $('#id-value').val('9781789801217'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - it('does NOT add an invalid ISBN 13', () => { - $('#select-id').val('isbn_13'); - $('#id-value').val('1111111111111'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(5); - }); - - it('does NOT prompt to add a formally invalid ISBN 13', () => { - $('#select-id').val('isbn_13'); - $('#id-value').val('12345'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(5); - expect($('.repeat-add').length).toBe(1); - const errorDivText = $('#id-errors').text(); - const expected = 'Add it anyway?'; - expect(errorDivText).toEqual(expect.not.stringContaining(expected)); - }); - - it('clears the invalid ISBN 13 error prompt and does not add an ISBN if a user clicks no', () => { - $('#select-id').val('isbn_13'); - $('#id-value').val('0123456789123'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(5); - expect($('.repeat-add').length).toBe(2); - $('#do-not-add-isbn').trigger('click'); - expect($('.repeat-item').length).toBe(5); - const cssDisplay = $('#id-errors').css('display'); - expect(cssDisplay).toEqual('none'); - }); - - it('does NOT add a duplicate ISBN 13', () => { - $('#select-id').val('isbn_13'); - $('#id-value').val('9780984782857'); - $('.repeat-add').trigger('click'); - $('#select-id').val('isbn_13'); - $('#id-value').val('9780984782857'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - it('does properly strip spaces and hypens from a valid ISBN 13 and add it', () => { - $('#select-id').val('isbn_13'); - $('#id-value').val('978-16172--95 980 '); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - it('does count identical stripped and unstripped ISBN 13s as the same ISBN', () => { - $('#select-id').val('isbn_13'); - $('#id-value').val('-979-86 -64653403 '); - $('.repeat-add').trigger('click'); - $('#select-id').val('isbn_13'); - $('#id-value').val('9798664653403'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - //LCCN - it('does add a valid LCCN', () => { - $('#select-id').val('lccn'); - $('#id-value').val('n78-890351'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - it('does NOT add an invalid LCCN', () => { - $('#select-id').val('lccn'); - $('#id-value').val('12345'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(5); - }); - - it('does NOT add a duplicate LCCN', () => { - $('#select-id').val('lccn'); - $('#id-value').val('n78-890351'); - $('.repeat-add').trigger('click'); - $('#select-id').val('lccn'); - $('#id-value').val('n78-890351'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - it('does properly normalize a valid LCCN and add it', () => { - $('#select-id').val('lccn'); - $('#id-value').val(' 75-425165//r75'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); - - it('does count identical normalized and non-normalized LCCNs as the same LCCN', () => { - $('#select-id').val('lccn'); - $('#id-value').val(' 75-425165//r75'); - $('.repeat-add').trigger('click'); - $('#select-id').val('lccn'); - $('#id-value').val('75425165'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - }); + // ISBN 10 + it('does add a valid ISBN 10 ending in X', () => { + $('#select-id').val('isbn_10'); + $('#id-value').val('0-8044-2957-X'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does add a valid ISBN 10 NOT ending in X', () => { + $('#select-id').val('isbn_10'); + $('#id-value').val('0596520689'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does NOT add an invalid ISBN 10 with a failed check digit', () => { + $('#select-id').val('isbn_10'); + $('#id-value').val('1234567890'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(5); + }); + + it('does NOT prompt to add a formally invalid ISBN 10', () => { + $('#select-id').val('isbn_10'); + $('#id-value').val('12345'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(5); + expect($('.repeat-add').length).toBe(1); + const errorDivText = $('#id-errors').text(); + const expected = 'Add it anyway?'; + expect(errorDivText).toEqual(expect.not.stringContaining(expected)); + }); + + it('clears the invalid ISBN 10 error prompt and does not add an ISBN if a user clicks no', () => { + $('#select-id').val('isbn_10'); + $('#id-value').val('2121212121'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(5); + expect($('.repeat-add').length).toBe(2); + $('#do-not-add-isbn').trigger('click'); + expect($('.repeat-item').length).toBe(5); + const cssDisplay = $('#id-errors').css('display'); + expect(cssDisplay).toEqual('none'); + }); + + it('does NOT add a duplicate ISBN 10', () => { + $('#select-id').val('isbn_10'); + $('#id-value').val('0063162024'); + $('.repeat-add').trigger('click'); + $('#select-id').val('isbn_10'); + $('#id-value').val('0063162024'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does properly strip spaces and hypens from a valid ISBN 10 and add it', () => { + $('#select-id').val('isbn_10'); + $('#id-value').val('09- 8478---2869 '); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does count identical stripped and unstripped ISBN 10s as the same ISBN', () => { + $('#select-id').val('isbn_10'); + $('#id-value').val(' 144--93-55730 '); + $('.repeat-add').trigger('click'); + $('#select-id').val('isbn_10'); + $('#id-value').val('1449355730'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + // ISBN 13 + it('does add a valid ISBN 13', () => { + $('#select-id').val('isbn_13'); + $('#id-value').val('9781789801217'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does NOT add an invalid ISBN 13', () => { + $('#select-id').val('isbn_13'); + $('#id-value').val('1111111111111'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(5); + }); + + it('does NOT prompt to add a formally invalid ISBN 13', () => { + $('#select-id').val('isbn_13'); + $('#id-value').val('12345'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(5); + expect($('.repeat-add').length).toBe(1); + const errorDivText = $('#id-errors').text(); + const expected = 'Add it anyway?'; + expect(errorDivText).toEqual(expect.not.stringContaining(expected)); + }); + + it('clears the invalid ISBN 13 error prompt and does not add an ISBN if a user clicks no', () => { + $('#select-id').val('isbn_13'); + $('#id-value').val('0123456789123'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(5); + expect($('.repeat-add').length).toBe(2); + $('#do-not-add-isbn').trigger('click'); + expect($('.repeat-item').length).toBe(5); + const cssDisplay = $('#id-errors').css('display'); + expect(cssDisplay).toEqual('none'); + }); + + it('does NOT add a duplicate ISBN 13', () => { + $('#select-id').val('isbn_13'); + $('#id-value').val('9780984782857'); + $('.repeat-add').trigger('click'); + $('#select-id').val('isbn_13'); + $('#id-value').val('9780984782857'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does properly strip spaces and hypens from a valid ISBN 13 and add it', () => { + $('#select-id').val('isbn_13'); + $('#id-value').val('978-16172--95 980 '); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does count identical stripped and unstripped ISBN 13s as the same ISBN', () => { + $('#select-id').val('isbn_13'); + $('#id-value').val('-979-86 -64653403 '); + $('.repeat-add').trigger('click'); + $('#select-id').val('isbn_13'); + $('#id-value').val('9798664653403'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + //LCCN + it('does add a valid LCCN', () => { + $('#select-id').val('lccn'); + $('#id-value').val('n78-890351'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does NOT add an invalid LCCN', () => { + $('#select-id').val('lccn'); + $('#id-value').val('12345'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(5); + }); + + it('does NOT add a duplicate LCCN', () => { + $('#select-id').val('lccn'); + $('#id-value').val('n78-890351'); + $('.repeat-add').trigger('click'); + $('#select-id').val('lccn'); + $('#id-value').val('n78-890351'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does properly normalize a valid LCCN and add it', () => { + $('#select-id').val('lccn'); + $('#id-value').val(' 75-425165//r75'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); + + it('does count identical normalized and non-normalized LCCNs as the same LCCN', () => { + $('#select-id').val('lccn'); + $('#id-value').val(' 75-425165//r75'); + $('.repeat-add').trigger('click'); + $('#select-id').val('lccn'); + $('#id-value').val('75425165'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + }); }); diff --git a/tests/unit/js/idValidation.test.js b/tests/unit/js/idValidation.test.js index 298e40e4c25..983ccd6a730 100644 --- a/tests/unit/js/idValidation.test.js +++ b/tests/unit/js/idValidation.test.js @@ -1,157 +1,157 @@ import { - isChecksumValidIsbn10, - isChecksumValidIsbn13, - isFormatValidIsbn10, - isFormatValidIsbn13, - isValidLccn, - parseIsbn, - parseLccn, + isChecksumValidIsbn10, + isChecksumValidIsbn13, + isFormatValidIsbn10, + isFormatValidIsbn13, + isValidLccn, + parseIsbn, + parseLccn, } from '../../../openlibrary/plugins/openlibrary/js/idValidation.js'; describe('parseIsbn', () => { - it('correctly parses ISBN 10 with dashes', () => { - expect(parseIsbn('0-553-38168-7')).toBe('0553381687'); - }); - it('correctly parses ISBN 13 with dashes', () => { - expect(parseIsbn('978-0-553-38168-9')).toBe('9780553381689'); - }); + it('correctly parses ISBN 10 with dashes', () => { + expect(parseIsbn('0-553-38168-7')).toBe('0553381687'); + }); + it('correctly parses ISBN 13 with dashes', () => { + expect(parseIsbn('978-0-553-38168-9')).toBe('9780553381689'); + }); }); // testing from examples listed here: // https://www.loc.gov/marc/lccn-namespace.html describe('parseLccn', () => { - it('correctly parses LCCN example 1', () => { - expect(parseLccn('n78-890351')).toBe('n78890351'); - }); - it('correctly parses LCCN example 2', () => { - expect(parseLccn('n78-89035')).toBe('n78089035'); - }); - it('correctly parses LCCN example 3', () => { - expect(parseLccn('n 78890351 ')).toBe('n78890351'); - }); - it('correctly parses LCCN example 4', () => { - expect(parseLccn(' 85000002')).toBe('85000002'); - }); - it('correctly parses LCCN example 5', () => { - expect(parseLccn('85-2 ')).toBe('85000002'); - }); - it('correctly parses LCCN example 6', () => { - expect(parseLccn('2001-000002')).toBe('2001000002'); - }); - it('correctly parses LCCN example 7', () => { - expect(parseLccn('75-425165//r75')).toBe('75425165'); - }); - it('correctly parses LCCN example 8', () => { - expect(parseLccn(' 79139101 /AC/r932')).toBe('79139101'); - }); + it('correctly parses LCCN example 1', () => { + expect(parseLccn('n78-890351')).toBe('n78890351'); + }); + it('correctly parses LCCN example 2', () => { + expect(parseLccn('n78-89035')).toBe('n78089035'); + }); + it('correctly parses LCCN example 3', () => { + expect(parseLccn('n 78890351 ')).toBe('n78890351'); + }); + it('correctly parses LCCN example 4', () => { + expect(parseLccn(' 85000002')).toBe('85000002'); + }); + it('correctly parses LCCN example 5', () => { + expect(parseLccn('85-2 ')).toBe('85000002'); + }); + it('correctly parses LCCN example 6', () => { + expect(parseLccn('2001-000002')).toBe('2001000002'); + }); + it('correctly parses LCCN example 7', () => { + expect(parseLccn('75-425165//r75')).toBe('75425165'); + }); + it('correctly parses LCCN example 8', () => { + expect(parseLccn(' 79139101 /AC/r932')).toBe('79139101'); + }); }); describe('isChecksumValidIsbn10', () => { - it('returns true with valid ISBN 10 (X check character)', () => { - expect(isChecksumValidIsbn10('080442957X')).toBe(true); - }); - it('returns true with valid ISBN 10 (numerical check character, check 1)', () => { - expect(isChecksumValidIsbn10('1593279280')).toBe(true); - }); - it('returns true with valid ISBN 10 (numerical check character, check 2)', () => { - expect(isChecksumValidIsbn10('1617295981')).toBe(true); - }); + it('returns true with valid ISBN 10 (X check character)', () => { + expect(isChecksumValidIsbn10('080442957X')).toBe(true); + }); + it('returns true with valid ISBN 10 (numerical check character, check 1)', () => { + expect(isChecksumValidIsbn10('1593279280')).toBe(true); + }); + it('returns true with valid ISBN 10 (numerical check character, check 2)', () => { + expect(isChecksumValidIsbn10('1617295981')).toBe(true); + }); - it('returns false with an invalid ISBN 10', () => { - expect(isChecksumValidIsbn10('1234567890')).toBe(false); - }); + it('returns false with an invalid ISBN 10', () => { + expect(isChecksumValidIsbn10('1234567890')).toBe(false); + }); }); describe('isChecksumValidIsbn13', () => { - it('returns true with valid ISBN 13 (check 1)', () => { - expect(isChecksumValidIsbn13('9781789801217')).toBe(true); - }); - it('returns true with valid ISBN 13 (check 2)', () => { - expect(isChecksumValidIsbn13('9798430918002')).toBe(true); - }); + it('returns true with valid ISBN 13 (check 1)', () => { + expect(isChecksumValidIsbn13('9781789801217')).toBe(true); + }); + it('returns true with valid ISBN 13 (check 2)', () => { + expect(isChecksumValidIsbn13('9798430918002')).toBe(true); + }); - it('returns false with an invalid ISBN 13 (check 1)', () => { - expect(isChecksumValidIsbn13('1234567890123')).toBe(false); - }); - it('returns false with an invalid ISBN 13 (check 2)', () => { - expect(isChecksumValidIsbn13('9790000000000')).toBe(false); - }); + it('returns false with an invalid ISBN 13 (check 1)', () => { + expect(isChecksumValidIsbn13('1234567890123')).toBe(false); + }); + it('returns false with an invalid ISBN 13 (check 2)', () => { + expect(isChecksumValidIsbn13('9790000000000')).toBe(false); + }); }); describe('isFormatValidIsbn10', () => { - it('returns true with valid ISBN 10 (X check character)', () => { - expect(isFormatValidIsbn10('080442957X')).toBe(true); - }); - it('returns true with valid ISBN 10', () => { - expect(isFormatValidIsbn10('1593279280')).toBe(true); - }); + it('returns true with valid ISBN 10 (X check character)', () => { + expect(isFormatValidIsbn10('080442957X')).toBe(true); + }); + it('returns true with valid ISBN 10', () => { + expect(isFormatValidIsbn10('1593279280')).toBe(true); + }); - it('returns false with invalid ISBN 10', () => { - expect(isFormatValidIsbn10('a234567890')).toBe(false); - }); - it('returns false with blank value', () => { - expect(isFormatValidIsbn10('')).toBe(false); - }); + it('returns false with invalid ISBN 10', () => { + expect(isFormatValidIsbn10('a234567890')).toBe(false); + }); + it('returns false with blank value', () => { + expect(isFormatValidIsbn10('')).toBe(false); + }); }); describe('isFormatValidIsbn13', () => { - it('returns true with valid ISBN 13', () => { - expect(isFormatValidIsbn13('9781789801217')).toBe(true); - }); + it('returns true with valid ISBN 13', () => { + expect(isFormatValidIsbn13('9781789801217')).toBe(true); + }); - it('returns false with invalid ISBN 13 (too long)', () => { - expect(isFormatValidIsbn13('97918430918002')).toBe(false); - }); - it('returns false with invalid ISBN 13 (too short)', () => { - expect(isFormatValidIsbn13('979843091802')).toBe(false); - }); - it('returns false with invalis ISBN 13 (non-numeric)', () => { - expect(isFormatValidIsbn13('979a430918002')).toBe(false); - }); + it('returns false with invalid ISBN 13 (too long)', () => { + expect(isFormatValidIsbn13('97918430918002')).toBe(false); + }); + it('returns false with invalid ISBN 13 (too short)', () => { + expect(isFormatValidIsbn13('979843091802')).toBe(false); + }); + it('returns false with invalis ISBN 13 (non-numeric)', () => { + expect(isFormatValidIsbn13('979a430918002')).toBe(false); + }); }); // testing from examples listed here: // https://www.loc.gov/marc/lccn-namespace.html // https://www.oclc.org/bibformats/en/0xx/010.html describe('isValidLccn', () => { - it('returns true for LCCN of length 8', () => { - expect(isValidLccn('85000002')).toBe(true); - }); - it('returns true for LCCN of length 9', () => { - expect(isValidLccn('n78890351')).toBe(true); - }); - it('returns true for LCCN of length 10 (all digits)', () => { - expect(isValidLccn('2001000002')).toBe(true); - }); - it('returns true for LCCN of length 10 (alpha prefix)', () => { - expect(isValidLccn('sn85000678')).toBe(true); - }); - it('returns true for LCCN of length 11 (alpha-numeric prefix)', () => { - expect(isValidLccn('a2500000003')).toBe(true); - }); - it('returns true for LCCN of length 11 (alpha prefix)', () => { - expect(isValidLccn('agr25000003')).toBe(true); - }); - it('returns true for LCCN of length 12', () => { - expect(isValidLccn('mm2002084896')).toBe(true); - }); + it('returns true for LCCN of length 8', () => { + expect(isValidLccn('85000002')).toBe(true); + }); + it('returns true for LCCN of length 9', () => { + expect(isValidLccn('n78890351')).toBe(true); + }); + it('returns true for LCCN of length 10 (all digits)', () => { + expect(isValidLccn('2001000002')).toBe(true); + }); + it('returns true for LCCN of length 10 (alpha prefix)', () => { + expect(isValidLccn('sn85000678')).toBe(true); + }); + it('returns true for LCCN of length 11 (alpha-numeric prefix)', () => { + expect(isValidLccn('a2500000003')).toBe(true); + }); + it('returns true for LCCN of length 11 (alpha prefix)', () => { + expect(isValidLccn('agr25000003')).toBe(true); + }); + it('returns true for LCCN of length 12', () => { + expect(isValidLccn('mm2002084896')).toBe(true); + }); - it('returns false for LCCN below minimum length', () => { - expect(isValidLccn('8500002')).toBe(false); - }); - it('returns false for LCCN of length 9 with all digits', () => { - expect(isValidLccn('178890351')).toBe(false); - }); - it('returns false for LCCN of length 10 with alpha characters', () => { - expect(isValidLccn('a001000002')).toBe(false); - }); - it('returns false for LCCN of length 11 with all digits', () => { - expect(isValidLccn('12500000003')).toBe(false); - }); - it('returns false for LCCN of length 12 with all digits', () => { - expect(isValidLccn('125000000003')).toBe(false); - }); - it('returns false for LCCN of length 13', () => { - expect(isValidLccn('1250000000003')).toBe(false); - }); + it('returns false for LCCN below minimum length', () => { + expect(isValidLccn('8500002')).toBe(false); + }); + it('returns false for LCCN of length 9 with all digits', () => { + expect(isValidLccn('178890351')).toBe(false); + }); + it('returns false for LCCN of length 10 with alpha characters', () => { + expect(isValidLccn('a001000002')).toBe(false); + }); + it('returns false for LCCN of length 11 with all digits', () => { + expect(isValidLccn('12500000003')).toBe(false); + }); + it('returns false for LCCN of length 12 with all digits', () => { + expect(isValidLccn('125000000003')).toBe(false); + }); + it('returns false for LCCN of length 13', () => { + expect(isValidLccn('1250000000003')).toBe(false); + }); }); diff --git a/tests/unit/js/jquery.repeat.test.js b/tests/unit/js/jquery.repeat.test.js index 97db55c4d4a..ed7214e6765 100644 --- a/tests/unit/js/jquery.repeat.test.js +++ b/tests/unit/js/jquery.repeat.test.js @@ -6,38 +6,38 @@ import * as testData from './html-test-data'; let sandbox; beforeEach(() => { - sandbox = sinon.createSandbox(); - global.htmlquote = htmlquote; - // htmlquote is used inside an eval expression (yuck) so is an implied dependency - sandbox.stub(global, 'htmlquote').callsFake(htmlquote); + sandbox = sinon.createSandbox(); + global.htmlquote = htmlquote; + // htmlquote is used inside an eval expression (yuck) so is an implied dependency + sandbox.stub(global, 'htmlquote').callsFake(htmlquote); }); test('identifiers of repeated elements are never the same.', () => { - // setup Query repeat - init(); - // setup the HTML - $(document.body).html(testData.editionIdentifiersSample); - // turn on jQuery repeat - $('#identifiers').repeat({ - vars: { - prefix: 'edition--', - }, - validate: () => {}, - }); + // setup Query repeat + init(); + // setup the HTML + $(document.body).html(testData.editionIdentifiersSample); + // turn on jQuery repeat + $('#identifiers').repeat({ + vars: { + prefix: 'edition--', + }, + validate: () => {}, + }); - expect($('.repeat-item').length).toBe(5); - $('#select-id').val('google'); - $('#id-value').text('fo4rzdaHDAwC'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - $('#identifiers--3 .repeat-remove').trigger('click'); - expect($('.repeat-item').length).toBe(5); - $('#select-id').val('goodreads'); - $('#id-value').text('44415839'); - $('.repeat-add').trigger('click'); - expect($('.repeat-item').length).toBe(6); - const ids = $('[id]') - .map((_, node) => node.getAttribute('id')) - .toArray(); - expect(ids.length).toBe(new Set(ids).size); + expect($('.repeat-item').length).toBe(5); + $('#select-id').val('google'); + $('#id-value').text('fo4rzdaHDAwC'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + $('#identifiers--3 .repeat-remove').trigger('click'); + expect($('.repeat-item').length).toBe(5); + $('#select-id').val('goodreads'); + $('#id-value').text('44415839'); + $('.repeat-add').trigger('click'); + expect($('.repeat-item').length).toBe(6); + const ids = $('[id]') + .map((_, node) => node.getAttribute('id')) + .toArray(); + expect(ids.length).toBe(new Set(ids).size); }); diff --git a/tests/unit/js/jsdef.test.js b/tests/unit/js/jsdef.test.js index d1dcee0e01f..25ec37d76da 100644 --- a/tests/unit/js/jsdef.test.js +++ b/tests/unit/js/jsdef.test.js @@ -1,64 +1,64 @@ import { - enumerate, - foreach, - htmlquote, - join, - len, - range, - websafe, + enumerate, + foreach, + htmlquote, + join, + len, + range, + websafe, } from '../../../openlibrary/plugins/openlibrary/js/jsdef'; test('jsdef: python range function', () => { - expect(range(2, 5)).toEqual([2, 3, 4]); - expect(range(5)).toEqual([0, 1, 2, 3, 4]); - expect(range(0, 10, 2)).toEqual([0, 2, 4, 6, 8]); + expect(range(2, 5)).toEqual([2, 3, 4]); + expect(range(5)).toEqual([0, 1, 2, 3, 4]); + expect(range(0, 10, 2)).toEqual([0, 2, 4, 6, 8]); }); test('jsdef: enumerate', () => { - expect(enumerate([1, 2, 3])).toEqual([ - ['0', 1], - ['1', 2], - ['2', 3], - ]); + expect(enumerate([1, 2, 3])).toEqual([ + ['0', 1], + ['1', 2], + ['2', 3], + ]); }); test('jsdef: foreach', () => { - let called = 0; - const loop = []; - const listToLoop = [1, 2, 3]; - expect.assertions(1); - return new Promise((resolve) => { - foreach(listToLoop, loop, () => { - called += 1; - if (called === 3) { - expect(called).toBe(3); - resolve(); - } + let called = 0; + const loop = []; + const listToLoop = [1, 2, 3]; + expect.assertions(1); + return new Promise((resolve) => { + foreach(listToLoop, loop, () => { + called += 1; + if (called === 3) { + expect(called).toBe(3); + resolve(); + } + }); }); - }); }); test('jsdef: join', () => { - const str = '-'; - const joinFn = join.bind(str); - expect(joinFn(['1', '2'])).toBe('1-2'); + const str = '-'; + const joinFn = join.bind(str); + expect(joinFn(['1', '2'])).toBe('1-2'); }); test('jsdef: len', () => { - expect(len(['1', '2'])).toBe(2); + expect(len(['1', '2'])).toBe(2); }); test('jsdef: htmlquote', () => { - expect(htmlquote(5)).toBe('5'); - expect(htmlquote('<foo>')).toBe('<foo>'); - expect(htmlquote('\'foo\': "bar"')).toBe(''foo': "bar"'); - expect(htmlquote('a&b')).toBe('a&b'); + expect(htmlquote(5)).toBe('5'); + expect(htmlquote('<foo>')).toBe('<foo>'); + expect(htmlquote('\'foo\': "bar"')).toBe(''foo': "bar"'); + expect(htmlquote('a&b')).toBe('a&b'); }); test('jsdef: websafe', () => { - expect(websafe('<script>')).toBe('<script>'); - // not sure if these are really necessary, but they document the current behaviour - expect(websafe(undefined)).toBe(''); - expect(websafe(null)).toBe(''); - expect(websafe({ toString: undefined })).toBe(''); + expect(websafe('<script>')).toBe('<script>'); + // not sure if these are really necessary, but they document the current behaviour + expect(websafe(undefined)).toBe(''); + expect(websafe(null)).toBe(''); + expect(websafe({ toString: undefined })).toBe(''); }); diff --git a/tests/unit/js/lists.test.js b/tests/unit/js/lists.test.js index 43fc7f228f3..5574867bccf 100644 --- a/tests/unit/js/lists.test.js +++ b/tests/unit/js/lists.test.js @@ -1,269 +1,269 @@ import { - createActiveShowcaseItem, - ShowcaseItem, + createActiveShowcaseItem, + ShowcaseItem, } from '../../../openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js'; import { CreateListForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js'; import { - activeListShowcase, - authorShowcase, - editionShowcase, - filledListCreationForm, - listCreationForm, - listsSectionShowcase, - showcaseI18nInput, - subjectShowcase, - workShowcase, + activeListShowcase, + authorShowcase, + editionShowcase, + filledListCreationForm, + listCreationForm, + listsSectionShowcase, + showcaseI18nInput, + subjectShowcase, + workShowcase, } from './sample-html/lists-test-data'; describe('CreateListForm class tests', () => { - test('CreateListForm fields correctly set', () => { - document.body.innerHTML = listCreationForm; - const formElem = document.querySelector('form'); - const listForm = new CreateListForm(formElem); - - const createListButton = document.querySelector('#create-list-button'); - expect(listForm.createListButton === createListButton).toBe(true); - - const listTitleInput = document.querySelector('#list_label'); - expect(listForm.listTitleInput === listTitleInput).toBe(true); - - const listDescriptionInput = document.querySelector('#list_desc'); - expect(listForm.listDescriptionInput === listDescriptionInput).toBe(true); - }); - - test('`resetForm()` clears a filled form', () => { - document.body.innerHTML = listCreationForm; - const formElem = document.querySelector('form'); - const listForm = new CreateListForm(formElem); - - // Initial checks - expect(listForm.listTitleInput.value).not.toBeTruthy(); - expect(listForm.listDescriptionInput.value).not.toBeTruthy(); - - // After setting input values - listForm.listTitleInput.value = 'New List'; - listForm.listDescriptionInput.value = 'My new list.'; - expect(listForm.listTitleInput.value).toBeTruthy(); - expect(listForm.listDescriptionInput.value).toBeTruthy(); - - // After clearing the form: - listForm.resetForm(); - expect(listForm.listTitleInput.value).not.toBeTruthy(); - expect(listForm.listDescriptionInput.value).not.toBeTruthy(); - }); - - it('should have empty inputs after instantiation', () => { - document.body.innerHTML = filledListCreationForm; - const formElem = document.querySelector('form'); - const titleInput = formElem.querySelector('#list_label'); - const descriptionInput = formElem.querySelector('#list_desc'); - - // Form is initially filled - expect(titleInput.value).toBeTruthy(); - expect(descriptionInput.value).toBeTruthy(); - - // Creating new CreateListForm should clear the form - // eslint-disable-next-line no-unused-vars - const listForm = new CreateListForm(formElem); - expect(titleInput.value).not.toBeTruthy(); - expect(descriptionInput.value).not.toBeTruthy(); - }); + test('CreateListForm fields correctly set', () => { + document.body.innerHTML = listCreationForm; + const formElem = document.querySelector('form'); + const listForm = new CreateListForm(formElem); + + const createListButton = document.querySelector('#create-list-button'); + expect(listForm.createListButton === createListButton).toBe(true); + + const listTitleInput = document.querySelector('#list_label'); + expect(listForm.listTitleInput === listTitleInput).toBe(true); + + const listDescriptionInput = document.querySelector('#list_desc'); + expect(listForm.listDescriptionInput === listDescriptionInput).toBe(true); + }); + + test('`resetForm()` clears a filled form', () => { + document.body.innerHTML = listCreationForm; + const formElem = document.querySelector('form'); + const listForm = new CreateListForm(formElem); + + // Initial checks + expect(listForm.listTitleInput.value).not.toBeTruthy(); + expect(listForm.listDescriptionInput.value).not.toBeTruthy(); + + // After setting input values + listForm.listTitleInput.value = 'New List'; + listForm.listDescriptionInput.value = 'My new list.'; + expect(listForm.listTitleInput.value).toBeTruthy(); + expect(listForm.listDescriptionInput.value).toBeTruthy(); + + // After clearing the form: + listForm.resetForm(); + expect(listForm.listTitleInput.value).not.toBeTruthy(); + expect(listForm.listDescriptionInput.value).not.toBeTruthy(); + }); + + it('should have empty inputs after instantiation', () => { + document.body.innerHTML = filledListCreationForm; + const formElem = document.querySelector('form'); + const titleInput = formElem.querySelector('#list_label'); + const descriptionInput = formElem.querySelector('#list_desc'); + + // Form is initially filled + expect(titleInput.value).toBeTruthy(); + expect(descriptionInput.value).toBeTruthy(); + + // Creating new CreateListForm should clear the form + // eslint-disable-next-line no-unused-vars + const listForm = new CreateListForm(formElem); + expect(titleInput.value).not.toBeTruthy(); + expect(descriptionInput.value).not.toBeTruthy(); + }); }); describe('createActiveShowcaseItem() tests', () => { - test('createActiveShowcaseItem() results are as expected', () => { - document.body.innerHTML = showcaseI18nInput; - const listKey = '/people/openlibrary/lists/OL1L'; - const seedKey = '/books/OL3421846M'; - const listTitle = 'My First List'; - const coverUrl = '/images/icons/avatar_book-sm.png'; - - const li = createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl); - const anchors = li.querySelectorAll('a'); - const [imageLink, titleLink, removeLink] = anchors; - const inputs = li.querySelectorAll('input'); - const [titleInput, seedKeyInput, seedTypeInput] = inputs; - - // Must have `actionable-item` class - expect(li.classList.contains('actionable-item')).toBe(true); - - // List key has been set - expect(removeLink.dataset.listKey === listKey).toBe(true); - expect(imageLink.href.endsWith(listKey)).toBe(true); - expect(titleLink.href.endsWith(listKey)).toBe(true); - expect(removeLink.href.endsWith(listKey)).toBe(true); - - // Seed key has been set - expect(seedKeyInput.value === seedKey).toBe(true); - expect(seedTypeInput.value === 'edition').toBe(true); - - // List title has been set - expect(titleLink.dataset.listTitle === listTitle).toBe(true); - expect(titleLink.textContent === listTitle).toBe(true); - expect(titleInput.value === listTitle).toBe(true); - - // Cover URL has been set - expect(imageLink.children[0].src.endsWith(coverUrl)).toBe(true); - }); - - test('createActiveShowcaseItem() sets the correct seed type', () => { - const listKey = '/people/openlibrary/lists/OL1L'; - const listTitle = 'My First List'; - const coverUrl = '/images/icons/avatar_book-sm.png'; - - const editionKey = '/books/OL3421846M'; - const workKey = '/works/OL54120W'; - const authorKey = '/authors/OL18319A'; - const subjectKey = 'quotations'; - const bogusKey = '/bogus/OL38475839B'; - - const editionItem = createActiveShowcaseItem( - listKey, - editionKey, - listTitle, - coverUrl, - ); - expect(editionItem.querySelector('input[name=seed-type]').value).toBe( - 'edition', - ); - - const workItem = createActiveShowcaseItem( - listKey, - workKey, - listTitle, - coverUrl, - ); - expect(workItem.querySelector('input[name=seed-type]').value).toBe('work'); - - const authorItem = createActiveShowcaseItem( - listKey, - authorKey, - listTitle, - coverUrl, - ); - expect(authorItem.querySelector('input[name=seed-type]').value).toBe( - 'author', - ); - - const subjectItem = createActiveShowcaseItem( - listKey, - subjectKey, - listTitle, - coverUrl, - ); - expect(subjectItem.querySelector('input[name=seed-type]').value).toBe( - 'subject', - ); - - const bogusItem = createActiveShowcaseItem( - listKey, - bogusKey, - listTitle, - coverUrl, - ); - expect(bogusItem.querySelector('input[name=seed-type]').value).toBe( - 'undefined', - ); - }); - - it('sets the correct default value for `coverUrl`', () => { - document.body.innerHTML = showcaseI18nInput; - const listKey = '/people/openlibrary/lists/OL1L'; - const seedKey = '/books/OL3421846M'; - const listTitle = 'My First List'; - - const li = createActiveShowcaseItem(listKey, seedKey, listTitle); - const coverImage = li.querySelector('img'); - - const expectedCoverUrl = '/images/icons/avatar_book-sm.png'; - expect(coverImage.src.endsWith(expectedCoverUrl)).toBe(true); - }); + test('createActiveShowcaseItem() results are as expected', () => { + document.body.innerHTML = showcaseI18nInput; + const listKey = '/people/openlibrary/lists/OL1L'; + const seedKey = '/books/OL3421846M'; + const listTitle = 'My First List'; + const coverUrl = '/images/icons/avatar_book-sm.png'; + + const li = createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl); + const anchors = li.querySelectorAll('a'); + const [imageLink, titleLink, removeLink] = anchors; + const inputs = li.querySelectorAll('input'); + const [titleInput, seedKeyInput, seedTypeInput] = inputs; + + // Must have `actionable-item` class + expect(li.classList.contains('actionable-item')).toBe(true); + + // List key has been set + expect(removeLink.dataset.listKey === listKey).toBe(true); + expect(imageLink.href.endsWith(listKey)).toBe(true); + expect(titleLink.href.endsWith(listKey)).toBe(true); + expect(removeLink.href.endsWith(listKey)).toBe(true); + + // Seed key has been set + expect(seedKeyInput.value === seedKey).toBe(true); + expect(seedTypeInput.value === 'edition').toBe(true); + + // List title has been set + expect(titleLink.dataset.listTitle === listTitle).toBe(true); + expect(titleLink.textContent === listTitle).toBe(true); + expect(titleInput.value === listTitle).toBe(true); + + // Cover URL has been set + expect(imageLink.children[0].src.endsWith(coverUrl)).toBe(true); + }); + + test('createActiveShowcaseItem() sets the correct seed type', () => { + const listKey = '/people/openlibrary/lists/OL1L'; + const listTitle = 'My First List'; + const coverUrl = '/images/icons/avatar_book-sm.png'; + + const editionKey = '/books/OL3421846M'; + const workKey = '/works/OL54120W'; + const authorKey = '/authors/OL18319A'; + const subjectKey = 'quotations'; + const bogusKey = '/bogus/OL38475839B'; + + const editionItem = createActiveShowcaseItem( + listKey, + editionKey, + listTitle, + coverUrl, + ); + expect(editionItem.querySelector('input[name=seed-type]').value).toBe( + 'edition', + ); + + const workItem = createActiveShowcaseItem( + listKey, + workKey, + listTitle, + coverUrl, + ); + expect(workItem.querySelector('input[name=seed-type]').value).toBe('work'); + + const authorItem = createActiveShowcaseItem( + listKey, + authorKey, + listTitle, + coverUrl, + ); + expect(authorItem.querySelector('input[name=seed-type]').value).toBe( + 'author', + ); + + const subjectItem = createActiveShowcaseItem( + listKey, + subjectKey, + listTitle, + coverUrl, + ); + expect(subjectItem.querySelector('input[name=seed-type]').value).toBe( + 'subject', + ); + + const bogusItem = createActiveShowcaseItem( + listKey, + bogusKey, + listTitle, + coverUrl, + ); + expect(bogusItem.querySelector('input[name=seed-type]').value).toBe( + 'undefined', + ); + }); + + it('sets the correct default value for `coverUrl`', () => { + document.body.innerHTML = showcaseI18nInput; + const listKey = '/people/openlibrary/lists/OL1L'; + const seedKey = '/books/OL3421846M'; + const listTitle = 'My First List'; + + const li = createActiveShowcaseItem(listKey, seedKey, listTitle); + const coverImage = li.querySelector('img'); + + const expectedCoverUrl = '/images/icons/avatar_book-sm.png'; + expect(coverImage.src.endsWith(expectedCoverUrl)).toBe(true); + }); }); describe('ShowcaseItem class tests', () => { - test('ShowcaseItem fields correctly set', () => { - document.body.innerHTML = activeListShowcase; - const showcaseElem = document.querySelector('.actionable-item'); - const showcase = new ShowcaseItem(showcaseElem); - const removeAffordance = showcaseElem.querySelector('.remove-from-list'); - - expect(showcase.showcaseElem === showcaseElem).toBe(true); - expect(showcase.isActiveShowcase).toBe(true); - expect(showcase.removeFromListAffordance === removeAffordance).toBe(true); - expect(showcase.listKey === '/people/openlibrary/lists/OL1L').toBe(true); - expect(showcase.seedKey === '/works/OL54120W').toBe(true); - expect(showcase.type).toBe('work'); - expect(showcase.seed).toMatchObject({ key: '/works/OL54120W' }); - }); - - it('correctly infers if it is an active showcase', () => { - document.body.innerHTML = activeListShowcase + listsSectionShowcase; - const [activeShowcaseElem, otherShowcaseElem] = - document.querySelectorAll('.actionable-item'); - const activeShowcase = new ShowcaseItem(activeShowcaseElem); - const otherShowcase = new ShowcaseItem(otherShowcaseElem); - - expect(activeShowcase.isActiveShowcase).toBe(true); - expect(otherShowcase.isActiveShowcase).toBe(false); - }); - - describe('Seed type inference', () => { - const cases = [ - { - markup: subjectShowcase, - expectedType: 'subject', - expectedIsWorkValue: false, - expectedIsSubjectValue: true, - }, - { - markup: authorShowcase, - expectedType: 'author', - expectedIsWorkValue: false, - expectedIsSubjectValue: false, - }, - { - markup: workShowcase, - expectedType: 'work', - expectedIsWorkValue: true, - expectedIsSubjectValue: false, - }, - { - markup: editionShowcase, - expectedType: 'edition', - expectedIsWorkValue: false, - expectedIsSubjectValue: false, - }, - ]; - - test.each(cases)('Type is $expectedType', ({ markup, expectedType }) => { - document.body.innerHTML = markup; - const showcaseElem = document.querySelector('.actionable-item'); - const showcase = new ShowcaseItem(showcaseElem); - expect(showcase.type).toBe(expectedType); + test('ShowcaseItem fields correctly set', () => { + document.body.innerHTML = activeListShowcase; + const showcaseElem = document.querySelector('.actionable-item'); + const showcase = new ShowcaseItem(showcaseElem); + const removeAffordance = showcaseElem.querySelector('.remove-from-list'); + + expect(showcase.showcaseElem === showcaseElem).toBe(true); + expect(showcase.isActiveShowcase).toBe(true); + expect(showcase.removeFromListAffordance === removeAffordance).toBe(true); + expect(showcase.listKey === '/people/openlibrary/lists/OL1L').toBe(true); + expect(showcase.seedKey === '/works/OL54120W').toBe(true); + expect(showcase.type).toBe('work'); + expect(showcase.seed).toMatchObject({ key: '/works/OL54120W' }); }); - test.each(cases)('`isWork` value expected to be $expectedIsWorkValue', ({ - markup, - expectedIsWorkValue, - }) => { - document.body.innerHTML = markup; - const showcaseElem = document.querySelector('.actionable-item'); - const showcase = new ShowcaseItem(showcaseElem); - expect(showcase.isWork).toBe(expectedIsWorkValue); + it('correctly infers if it is an active showcase', () => { + document.body.innerHTML = activeListShowcase + listsSectionShowcase; + const [activeShowcaseElem, otherShowcaseElem] = + document.querySelectorAll('.actionable-item'); + const activeShowcase = new ShowcaseItem(activeShowcaseElem); + const otherShowcase = new ShowcaseItem(otherShowcaseElem); + + expect(activeShowcase.isActiveShowcase).toBe(true); + expect(otherShowcase.isActiveShowcase).toBe(false); }); - test.each( - cases, - )('`isSubject` value expected to be $expectedIsSubjectValue', ({ - markup, - expectedIsSubjectValue, - }) => { - document.body.innerHTML = markup; - const showcaseElem = document.querySelector('.actionable-item'); - const showcase = new ShowcaseItem(showcaseElem); - expect(showcase.isSubject).toBe(expectedIsSubjectValue); + describe('Seed type inference', () => { + const cases = [ + { + markup: subjectShowcase, + expectedType: 'subject', + expectedIsWorkValue: false, + expectedIsSubjectValue: true, + }, + { + markup: authorShowcase, + expectedType: 'author', + expectedIsWorkValue: false, + expectedIsSubjectValue: false, + }, + { + markup: workShowcase, + expectedType: 'work', + expectedIsWorkValue: true, + expectedIsSubjectValue: false, + }, + { + markup: editionShowcase, + expectedType: 'edition', + expectedIsWorkValue: false, + expectedIsSubjectValue: false, + }, + ]; + + test.each(cases)('Type is $expectedType', ({ markup, expectedType }) => { + document.body.innerHTML = markup; + const showcaseElem = document.querySelector('.actionable-item'); + const showcase = new ShowcaseItem(showcaseElem); + expect(showcase.type).toBe(expectedType); + }); + + test.each(cases)('`isWork` value expected to be $expectedIsWorkValue', ({ + markup, + expectedIsWorkValue, + }) => { + document.body.innerHTML = markup; + const showcaseElem = document.querySelector('.actionable-item'); + const showcase = new ShowcaseItem(showcaseElem); + expect(showcase.isWork).toBe(expectedIsWorkValue); + }); + + test.each( + cases, + )('`isSubject` value expected to be $expectedIsSubjectValue', ({ + markup, + expectedIsSubjectValue, + }) => { + document.body.innerHTML = markup; + const showcaseElem = document.querySelector('.actionable-item'); + const showcase = new ShowcaseItem(showcaseElem); + expect(showcase.isSubject).toBe(expectedIsSubjectValue); + }); }); - }); - // XXX : test : removeSelf() fails safely when myBooksStore has not been created? + // XXX : test : removeSelf() fails safely when myBooksStore has not been created? }); diff --git a/tests/unit/js/my-books.test.js b/tests/unit/js/my-books.test.js index e58d0a12c00..5fbf042b6cd 100644 --- a/tests/unit/js/my-books.test.js +++ b/tests/unit/js/my-books.test.js @@ -6,144 +6,144 @@ import { listCreationForm } from './sample-html/lists-test-data'; jest.mock('jquery-ui/ui/widgets/dialog', () => {}); describe('CreateListForm.js class', () => { - let form; - let formElem; - - beforeEach(() => { - document.body.innerHTML = listCreationForm; - formElem = document.querySelector('form'); - form = new CreateListForm(formElem); - }); - - test('References are set correctly', () => { - const createListButton = formElem.querySelector('#create-list-button'); - const nameInput = formElem.querySelector('#list_label'); - const descriptionInput = formElem.querySelector('#list_desc'); - - expect(createListButton === form.createListButton).toBe(true); - expect(nameInput === form.listTitleInput).toBe(true); - expect(descriptionInput === form.listDescriptionInput).toBe(true); - }); - - it('it clears the form after a resetForm() call', () => { - const nameInput = document.querySelector('#list_label'); - const descriptionInput = document.querySelector('#list_desc'); - - // Form should be empty initially - expect(nameInput.value.length).toBe(0); - expect(descriptionInput.value.length).toBe(0); - - // Add values to each input - nameInput.value = 'My New List'; - descriptionInput.value = 'The best list ever'; - expect(nameInput.value.length).toBeGreaterThan(0); - expect(descriptionInput.value.length).toBeGreaterThan(0); - - // After clearing the form - form.resetForm(); - expect(nameInput.value.length).toBe(0); - expect(descriptionInput.value.length).toBe(0); - }); + let form; + let formElem; + + beforeEach(() => { + document.body.innerHTML = listCreationForm; + formElem = document.querySelector('form'); + form = new CreateListForm(formElem); + }); + + test('References are set correctly', () => { + const createListButton = formElem.querySelector('#create-list-button'); + const nameInput = formElem.querySelector('#list_label'); + const descriptionInput = formElem.querySelector('#list_desc'); + + expect(createListButton === form.createListButton).toBe(true); + expect(nameInput === form.listTitleInput).toBe(true); + expect(descriptionInput === form.listDescriptionInput).toBe(true); + }); + + it('it clears the form after a resetForm() call', () => { + const nameInput = document.querySelector('#list_label'); + const descriptionInput = document.querySelector('#list_desc'); + + // Form should be empty initially + expect(nameInput.value.length).toBe(0); + expect(descriptionInput.value.length).toBe(0); + + // Add values to each input + nameInput.value = 'My New List'; + descriptionInput.value = 'The best list ever'; + expect(nameInput.value.length).toBeGreaterThan(0); + expect(descriptionInput.value.length).toBeGreaterThan(0); + + // After clearing the form + form.resetForm(); + expect(nameInput.value.length).toBe(0); + expect(descriptionInput.value.length).toBe(0); + }); }); describe('CheckInForm class', () => { - let formElem; - let submitButton; - let yearSelect; - let monthSelect; - let daySelect; - - const workOlid = 'OL123W'; - const editionKey = '/books/OL456M'; - - beforeEach(() => { - document.body.innerHTML = checkInForm; - formElem = document.querySelector('form'); - submitButton = document.querySelector('.check-in__submit-btn'); - yearSelect = document.querySelector('select[name=year]'); - monthSelect = document.querySelector('select[name=month]'); - daySelect = document.querySelector('select[name=day]'); - }); - - test('Submit button, month select, and day select are initially disabled when read date is absent', () => { - const form = new CheckInForm(formElem, workOlid, editionKey); - form.initialize(); - expect(submitButton.disabled).toBe(true); - expect(monthSelect.disabled).toBe(true); - expect(daySelect.disabled).toBe(true); - - expect(yearSelect.disabled).toBe(false); - expect(yearSelect.value).toBe(''); - }); - - it('Sets correct values and enables selects and submit button', () => { - const form = new CheckInForm(formElem, workOlid, editionKey); - form.initialize(); - form.updateSelectedDate(2022, 1, 31); - expect(submitButton.disabled).toBe(false); - expect(monthSelect.disabled).toBe(false); - expect(daySelect.disabled).toBe(false); - - expect(yearSelect.value).toBe('2022'); - expect(monthSelect.value).toBe('1'); - expect(daySelect.value).toBe('31'); - }); - - it('Hides impossible day options', () => { - const form = new CheckInForm(formElem, workOlid, editionKey); - form.initialize(); - form.updateSelectedDate(2022, 2, 20); - - // The 28th day should be visible: - expect(daySelect.options[28].classList.contains('hidden')).toBe(false); - - // Subsequent days should not be visible - expect(daySelect.options[29].classList.contains('hidden')).toBe(true); - expect(daySelect.options[30].classList.contains('hidden')).toBe(true); - expect(daySelect.options[31].classList.contains('hidden')).toBe(true); - }); - - it('Shows 29 days in February when there is a leap year', () => { - const form = new CheckInForm(formElem, workOlid, editionKey); - form.initialize(); - form.updateSelectedDate(2020, 2, 1); - - expect(daySelect.options[29].classList.contains('hidden')).toBe(false); - expect(daySelect.options[30].classList.contains('hidden')).toBe(true); - expect(daySelect.options[31].classList.contains('hidden')).toBe(true); - }); - - it('Associates labels with select elements during initialization', () => { - const form = new CheckInForm(formElem, workOlid, editionKey); - - // Get reference to each label: - const yearLabel = formElem.querySelector('.check-in__year-label'); - const monthLabel = formElem.querySelector('.check-in__month-label'); - const dayLabel = formElem.querySelector('.check-in__day-label'); - - // Verify labels have no `for` initially: - expect(yearLabel.htmlFor).toBe(''); - expect(monthLabel.htmlFor).toBe(''); - expect(dayLabel.htmlFor).toBe(''); - - // Verify select elements have no `id` initially: - expect(yearSelect.id).toBe(''); - expect(monthSelect.id).toBe(''); - expect(daySelect.id).toBe(''); - - // Verify labels associated with selects after initialization: - form.initialize(); - - const expectedYearId = `year-select-${workOlid}`; - const expectedMonthId = `month-select-${workOlid}`; - const expectedDayId = `day-select-${workOlid}`; - - expect(yearLabel.htmlFor).toBe(expectedYearId); - expect(monthLabel.htmlFor).toBe(expectedMonthId); - expect(dayLabel.htmlFor).toBe(expectedDayId); - - expect(yearSelect.id).toBe(expectedYearId); - expect(monthSelect.id).toBe(expectedMonthId); - expect(daySelect.id).toBe(expectedDayId); - }); + let formElem; + let submitButton; + let yearSelect; + let monthSelect; + let daySelect; + + const workOlid = 'OL123W'; + const editionKey = '/books/OL456M'; + + beforeEach(() => { + document.body.innerHTML = checkInForm; + formElem = document.querySelector('form'); + submitButton = document.querySelector('.check-in__submit-btn'); + yearSelect = document.querySelector('select[name=year]'); + monthSelect = document.querySelector('select[name=month]'); + daySelect = document.querySelector('select[name=day]'); + }); + + test('Submit button, month select, and day select are initially disabled when read date is absent', () => { + const form = new CheckInForm(formElem, workOlid, editionKey); + form.initialize(); + expect(submitButton.disabled).toBe(true); + expect(monthSelect.disabled).toBe(true); + expect(daySelect.disabled).toBe(true); + + expect(yearSelect.disabled).toBe(false); + expect(yearSelect.value).toBe(''); + }); + + it('Sets correct values and enables selects and submit button', () => { + const form = new CheckInForm(formElem, workOlid, editionKey); + form.initialize(); + form.updateSelectedDate(2022, 1, 31); + expect(submitButton.disabled).toBe(false); + expect(monthSelect.disabled).toBe(false); + expect(daySelect.disabled).toBe(false); + + expect(yearSelect.value).toBe('2022'); + expect(monthSelect.value).toBe('1'); + expect(daySelect.value).toBe('31'); + }); + + it('Hides impossible day options', () => { + const form = new CheckInForm(formElem, workOlid, editionKey); + form.initialize(); + form.updateSelectedDate(2022, 2, 20); + + // The 28th day should be visible: + expect(daySelect.options[28].classList.contains('hidden')).toBe(false); + + // Subsequent days should not be visible + expect(daySelect.options[29].classList.contains('hidden')).toBe(true); + expect(daySelect.options[30].classList.contains('hidden')).toBe(true); + expect(daySelect.options[31].classList.contains('hidden')).toBe(true); + }); + + it('Shows 29 days in February when there is a leap year', () => { + const form = new CheckInForm(formElem, workOlid, editionKey); + form.initialize(); + form.updateSelectedDate(2020, 2, 1); + + expect(daySelect.options[29].classList.contains('hidden')).toBe(false); + expect(daySelect.options[30].classList.contains('hidden')).toBe(true); + expect(daySelect.options[31].classList.contains('hidden')).toBe(true); + }); + + it('Associates labels with select elements during initialization', () => { + const form = new CheckInForm(formElem, workOlid, editionKey); + + // Get reference to each label: + const yearLabel = formElem.querySelector('.check-in__year-label'); + const monthLabel = formElem.querySelector('.check-in__month-label'); + const dayLabel = formElem.querySelector('.check-in__day-label'); + + // Verify labels have no `for` initially: + expect(yearLabel.htmlFor).toBe(''); + expect(monthLabel.htmlFor).toBe(''); + expect(dayLabel.htmlFor).toBe(''); + + // Verify select elements have no `id` initially: + expect(yearSelect.id).toBe(''); + expect(monthSelect.id).toBe(''); + expect(daySelect.id).toBe(''); + + // Verify labels associated with selects after initialization: + form.initialize(); + + const expectedYearId = `year-select-${workOlid}`; + const expectedMonthId = `month-select-${workOlid}`; + const expectedDayId = `day-select-${workOlid}`; + + expect(yearLabel.htmlFor).toBe(expectedYearId); + expect(monthLabel.htmlFor).toBe(expectedMonthId); + expect(dayLabel.htmlFor).toBe(expectedDayId); + + expect(yearSelect.id).toBe(expectedYearId); + expect(monthSelect.id).toBe(expectedMonthId); + expect(daySelect.id).toBe(expectedDayId); + }); }); diff --git a/tests/unit/js/nonjquery_utils.test.js b/tests/unit/js/nonjquery_utils.test.js index f35b8f0669b..908ebd346b2 100644 --- a/tests/unit/js/nonjquery_utils.test.js +++ b/tests/unit/js/nonjquery_utils.test.js @@ -2,55 +2,55 @@ import sinon from 'sinon'; import { debounce } from '../../../openlibrary/plugins/openlibrary/js/nonjquery_utils.js'; describe('debounce', () => { - test('func not called during initialization', () => { - const spy = sinon.spy(); - debounce(spy, 100, false); - expect(spy.callCount).toBe(0); - }); + test('func not called during initialization', () => { + const spy = sinon.spy(); + debounce(spy, 100, false); + expect(spy.callCount).toBe(0); + }); - test('func called after threshold when !execAsap', () => { - const clock = sinon.useFakeTimers(); - const spy = sinon.spy(); - const debouncedSpy = debounce(spy, 100, false); - debouncedSpy(); - expect(spy.callCount).toBe(0); - clock.tick(99); - expect(spy.callCount).toBe(0); - clock.tick(1); - expect(spy.callCount).toBe(1); - clock.restore(); - }); + test('func called after threshold when !execAsap', () => { + const clock = sinon.useFakeTimers(); + const spy = sinon.spy(); + const debouncedSpy = debounce(spy, 100, false); + debouncedSpy(); + expect(spy.callCount).toBe(0); + clock.tick(99); + expect(spy.callCount).toBe(0); + clock.tick(1); + expect(spy.callCount).toBe(1); + clock.restore(); + }); - test('func called immediately when execAsap', () => { - const clock = sinon.useFakeTimers(); - const spy = sinon.spy(); - const debouncedSpy = debounce(spy, 100, true); - debouncedSpy(); - expect(spy.callCount).toBe(1); - clock.tick(100); - expect(spy.callCount).toBe(1); - clock.restore(); - }); + test('func called immediately when execAsap', () => { + const clock = sinon.useFakeTimers(); + const spy = sinon.spy(); + const debouncedSpy = debounce(spy, 100, true); + debouncedSpy(); + expect(spy.callCount).toBe(1); + clock.tick(100); + expect(spy.callCount).toBe(1); + clock.restore(); + }); - test('func called with correct context and arguments', () => { - const spy = sinon.spy(); - const debouncedSpy = debounce(spy, 100, true); - const context = {}; - debouncedSpy.call(context, 1, 2, 3); - expect(spy.thisValues[0]).toBe(context); - expect(spy.args[0]).toEqual([1, 2, 3]); - }); + test('func called with correct context and arguments', () => { + const spy = sinon.spy(); + const debouncedSpy = debounce(spy, 100, true); + const context = {}; + debouncedSpy.call(context, 1, 2, 3); + expect(spy.thisValues[0]).toBe(context); + expect(spy.args[0]).toEqual([1, 2, 3]); + }); - test('func only called once when spammed', () => { - const clock = sinon.useFakeTimers(); - const spy = sinon.spy(); - const debouncedSpy = debounce(spy, 100, false); - for (let i = 0; i < 10; i++) { - debouncedSpy(); - expect(spy.callCount).toBe(0); - } - clock.tick(100); - expect(spy.callCount).toBe(1); - clock.restore(); - }); + test('func only called once when spammed', () => { + const clock = sinon.useFakeTimers(); + const spy = sinon.spy(); + const debouncedSpy = debounce(spy, 100, false); + for (let i = 0; i < 10; i++) { + debouncedSpy(); + expect(spy.callCount).toBe(0); + } + clock.tick(100); + expect(spy.callCount).toBe(1); + clock.restore(); + }); }); diff --git a/tests/unit/js/python.test.js b/tests/unit/js/python.test.js index 4674f95e90d..929176715f7 100644 --- a/tests/unit/js/python.test.js +++ b/tests/unit/js/python.test.js @@ -1,45 +1,45 @@ import { - commify, - slice, - urlencode, + commify, + slice, + urlencode, } from '../../../openlibrary/plugins/openlibrary/js/python'; test('commify', () => { - expect(commify('5443232')).toBe('5,443,232'); - expect(commify('50')).toBe('50'); - expect(commify('5000')).toBe('5,000'); - expect(commify(['1', '2', '3', '45'])).toBe('1,2,3,45'); - expect(commify([1, 20, 3])).toBe('1,20,3'); + expect(commify('5443232')).toBe('5,443,232'); + expect(commify('50')).toBe('50'); + expect(commify('5000')).toBe('5,000'); + expect(commify(['1', '2', '3', '45'])).toBe('1,2,3,45'); + expect(commify([1, 20, 3])).toBe('1,20,3'); }); describe('urlencode', () => { - test('empty array', () => { - expect(urlencode([])).toEqual(''); - }); - test('array of 1', () => { - expect(urlencode(['apple'])).toEqual('0=apple'); - }); - test('array of 3', () => { - expect(urlencode(['apple', 'grapes', 'orange'])).toEqual( - '0=apple&1=grapes&2=orange', - ); - }); + test('empty array', () => { + expect(urlencode([])).toEqual(''); + }); + test('array of 1', () => { + expect(urlencode(['apple'])).toEqual('0=apple'); + }); + test('array of 3', () => { + expect(urlencode(['apple', 'grapes', 'orange'])).toEqual( + '0=apple&1=grapes&2=orange', + ); + }); }); describe('slice', () => { - test('empty array', () => { - expect(slice([], 0, 0)).toEqual([]); - }); - test('array of 2', () => { - expect(slice([1, 2], 0, 1)).toEqual([1]); - }); - test('arr length less than end', () => { - expect(slice([1, 2, 3], 0, 5)).toEqual([1, 2, 3]); - }); - test('beginning greater than end', () => { - expect(slice([1, 2, 3, 4, 5], 4, 3)).toEqual([]); - }); - test('array of 5', () => { - expect(slice([1, 2, 3, 4, 5], 0, 3)).toEqual([1, 2, 3]); - }); + test('empty array', () => { + expect(slice([], 0, 0)).toEqual([]); + }); + test('array of 2', () => { + expect(slice([1, 2], 0, 1)).toEqual([1]); + }); + test('arr length less than end', () => { + expect(slice([1, 2, 3], 0, 5)).toEqual([1, 2, 3]); + }); + test('beginning greater than end', () => { + expect(slice([1, 2, 3, 4, 5], 4, 3)).toEqual([]); + }); + test('array of 5', () => { + expect(slice([1, 2, 3, 4, 5], 0, 3)).toEqual([1, 2, 3]); + }); }); diff --git a/tests/unit/js/sample-html/dropper-test-data.js b/tests/unit/js/sample-html/dropper-test-data.js index da9b83e515a..23ded486df3 100644 --- a/tests/unit/js/sample-html/dropper-test-data.js +++ b/tests/unit/js/sample-html/dropper-test-data.js @@ -14,19 +14,19 @@ export const closedDropperMarkup = generateDropperMarkup(false); export const disabledDropperMarkup = generateDropperMarkup(false, true); function generateDropperMarkup(isDropperOpen, isDropperDisabled = false) { - let wrapperClasses = 'generic-dropper-wrapper'; - let arrowClasses = 'arrow'; + let wrapperClasses = 'generic-dropper-wrapper'; + let arrowClasses = 'arrow'; - if (isDropperOpen) { - wrapperClasses += ' generic-dropper-wrapper--active'; - arrowClasses += ' up'; - } + if (isDropperOpen) { + wrapperClasses += ' generic-dropper-wrapper--active'; + arrowClasses += ' up'; + } - if (isDropperDisabled) { - wrapperClasses += ' generic-dropper--disabled'; - } + if (isDropperDisabled) { + wrapperClasses += ' generic-dropper--disabled'; + } - return ` + return ` <div class="${wrapperClasses}"> <div class="generic-dropper"> <div class="generic-dropper__actions"> diff --git a/tests/unit/js/sample-html/lists-test-data.js b/tests/unit/js/sample-html/lists-test-data.js index 504cf5cd1f3..d281127bcd3 100644 --- a/tests/unit/js/sample-html/lists-test-data.js +++ b/tests/unit/js/sample-html/lists-test-data.js @@ -1,8 +1,8 @@ function createListFormMarkup(isFilled) { - const listName = isFilled ? 'My New List' : ''; - const listDescription = isFilled ? 'A list for all of my books' : ''; + const listName = isFilled ? 'My New List' : ''; + const listDescription = isFilled ? 'A list for all of my books' : ''; - return ` + return ` <form method="post" class="floatform" name="new-list" id="new-list"> <div class="formElement"> <div class="label"> @@ -53,15 +53,15 @@ const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; * @param {Array<ShowcaseDetails>} showcaseData */ function createShowcaseMarkup(isActiveShowcase, showcaseData) { - const listId = isActiveShowcase ? 'already-lists' : 'list-lists'; - const listClasses = 'listLists'.concat( - isActiveShowcase ? ' already-lists' : '', - ); + const listId = isActiveShowcase ? 'already-lists' : 'list-lists'; + const listClasses = 'listLists'.concat( + isActiveShowcase ? ' already-lists' : '', + ); - let showcaseMarkup = ''; + let showcaseMarkup = ''; - for (const data of showcaseData) { - showcaseMarkup += `<li class="actionable-item"> + for (const data of showcaseData) { + showcaseMarkup += `<li class="actionable-item"> <span class="image"> <a href="${data.listKey}"><img src="${DEFAULT_COVER_URL}" alt="Cover of: ${data.listTitle}" title="Cover of: ${data.listTitle}"></a> </span> @@ -78,70 +78,70 @@ function createShowcaseMarkup(isActiveShowcase, showcaseData) { </span> </li> `; - } + } - return `<ul id="${listId}" class="${listClasses}"> + return `<ul id="${listId}" class="${listClasses}"> ${showcaseMarkup} </ul> `; } export const showcaseDetailsData = [ - { - listKey: '/people/openlibrary/lists/OL1L', - seedKey: '/works/OL54120W', - listTitle: 'My First List', - listOwner: '/people/openlibrary', - seedType: 'work', - }, - { - listKey: '/people/openlibrary/lists/OL1L', - seedKey: '/books/OL3421846M', - listTitle: 'My First List', - listOwner: '/people/openlibrary', - seedType: 'edition', - }, - { - listKey: '/people/openlibrary/lists/OL2L', - seedKey: '/works/OL54120W', - listTitle: 'Another List', - listOwner: '/people/openlibrary', - seedType: 'work', - }, - { - listKey: '/people/openlibrary/lists/OL1L', - seedKey: '/authors/OL18319A', - listTitle: 'My First List', - listOwner: '/people/openlibrary', - seedType: 'author', - }, - { - listKey: '/people/openlibrary/lists/OL1L', - seedKey: 'quotations', - listTitle: 'My First List', - listOwner: '/people/openlibrary', - seedType: 'subject', - }, + { + listKey: '/people/openlibrary/lists/OL1L', + seedKey: '/works/OL54120W', + listTitle: 'My First List', + listOwner: '/people/openlibrary', + seedType: 'work', + }, + { + listKey: '/people/openlibrary/lists/OL1L', + seedKey: '/books/OL3421846M', + listTitle: 'My First List', + listOwner: '/people/openlibrary', + seedType: 'edition', + }, + { + listKey: '/people/openlibrary/lists/OL2L', + seedKey: '/works/OL54120W', + listTitle: 'Another List', + listOwner: '/people/openlibrary', + seedType: 'work', + }, + { + listKey: '/people/openlibrary/lists/OL1L', + seedKey: '/authors/OL18319A', + listTitle: 'My First List', + listOwner: '/people/openlibrary', + seedType: 'author', + }, + { + listKey: '/people/openlibrary/lists/OL1L', + seedKey: 'quotations', + listTitle: 'My First List', + listOwner: '/people/openlibrary', + seedType: 'subject', + }, ]; export const multipleShowcasesOnPage = createShowcaseMarkup(true, [showcaseDetailsData[0], showcaseDetailsData[2]]) + createShowcaseMarkup(false, [showcaseDetailsData[0], showcaseDetailsData[1]]); export const activeListShowcase = createShowcaseMarkup(true, [ - showcaseDetailsData[0], + showcaseDetailsData[0], ]); export const listsSectionShowcase = createShowcaseMarkup(false, [ - showcaseDetailsData[0], + showcaseDetailsData[0], ]); export const subjectShowcase = createShowcaseMarkup(false, [ - showcaseDetailsData[4], + showcaseDetailsData[4], ]); export const authorShowcase = createShowcaseMarkup(false, [ - showcaseDetailsData[3], + showcaseDetailsData[3], ]); export const workShowcase = createShowcaseMarkup(false, [ - showcaseDetailsData[0], + showcaseDetailsData[0], ]); export const editionShowcase = createShowcaseMarkup(false, [ - showcaseDetailsData[1], + showcaseDetailsData[1], ]); diff --git a/tests/unit/js/search.test.js b/tests/unit/js/search.test.js index 481b0894197..77ac8a7faac 100644 --- a/tests/unit/js/search.test.js +++ b/tests/unit/js/search.test.js @@ -1,6 +1,6 @@ import { - less, - more, + less, + more, } from '../../../openlibrary/plugins/openlibrary/js/search.js'; /** Creates a dummy search facets section with a list of 'facetEntry' element and a @@ -12,32 +12,32 @@ import { * @return {String} HTML search facets section */ function createSearchFacets( - totalFacet = 2, - visibleFacet = 2, - minVisibleFacet = 2, + totalFacet = 2, + visibleFacet = 2, + minVisibleFacet = 2, ) { - const divSearchFacets = document.createElement('DIV'); - divSearchFacets.setAttribute('id', 'searchFacets'); - divSearchFacets.innerHTML = ` + const divSearchFacets = document.createElement('DIV'); + divSearchFacets.setAttribute('id', 'searchFacets'); + divSearchFacets.innerHTML = ` <div class="facet test"> <h4 class="facetHead">Facet Label</h4> </div> `; - const divTestFacet = divSearchFacets.querySelector('div.test'); - for (let i = 0; i < totalFacet; i++) { - const facetNb = i + 1; - divTestFacet.innerHTML += ` + const divTestFacet = divSearchFacets.querySelector('div.test'); + for (let i = 0; i < totalFacet; i++) { + const facetNb = i + 1; + divTestFacet.innerHTML += ` <div class="facetEntry"> <span><a>facet_${facetNb}</a></span> </div> `; - if (i >= visibleFacet) { - divTestFacet.lastElementChild.classList.add('ui-helper-hidden'); + if (i >= visibleFacet) { + divTestFacet.lastElementChild.classList.add('ui-helper-hidden'); + } } - } - divTestFacet.innerHTML += ` + divTestFacet.innerHTML += ` <div class="facetMoreLess"> <span class="header_more small" data-header="test"> <a id="test_more">more</a> @@ -49,16 +49,16 @@ function createSearchFacets( </div> `; - if (visibleFacet === minVisibleFacet) { - divTestFacet.querySelector('#test_bull').style.display = 'none'; - divTestFacet.querySelector('#test_less').style.display = 'none'; - } - if (visibleFacet === totalFacet) { - divTestFacet.querySelector('#test_more').style.display = 'none'; - divTestFacet.querySelector('#test_bull').style.display = 'none'; - } + if (visibleFacet === minVisibleFacet) { + divTestFacet.querySelector('#test_bull').style.display = 'none'; + divTestFacet.querySelector('#test_less').style.display = 'none'; + } + if (visibleFacet === totalFacet) { + divTestFacet.querySelector('#test_more').style.display = 'none'; + divTestFacet.querySelector('#test_bull').style.display = 'none'; + } - return divSearchFacets.outerHTML; + return divSearchFacets.outerHTML; } /** Runs visibility tests for all 'facetEntry' elements in document. @@ -67,27 +67,27 @@ function createSearchFacets( * @param {Number} expectedVisibleFacet expected number of visible facet */ function checkFacetVisibility(totalFacet, expectedVisibleFacet) { - const facetEntryList = document.getElementsByClassName('facetEntry'); - - test('facetEntry element number', () => { - expect(facetEntryList).toHaveLength(totalFacet); - }); - - for (let i = 0; i < totalFacet; i++) { - if (i < expectedVisibleFacet) { - test(`element "facet_${i + 1}" displayed`, () => { - expect(facetEntryList[i].classList.contains('ui-helper-hidden')).toBe( - false, - ); - }); - } else { - test(`element "facet_${i + 1}" hidden`, () => { - expect(facetEntryList[i].classList.contains('ui-helper-hidden')).toBe( - true, - ); - }); + const facetEntryList = document.getElementsByClassName('facetEntry'); + + test('facetEntry element number', () => { + expect(facetEntryList).toHaveLength(totalFacet); + }); + + for (let i = 0; i < totalFacet; i++) { + if (i < expectedVisibleFacet) { + test(`element "facet_${i + 1}" displayed`, () => { + expect(facetEntryList[i].classList.contains('ui-helper-hidden')).toBe( + false, + ); + }); + } else { + test(`element "facet_${i + 1}" hidden`, () => { + expect(facetEntryList[i].classList.contains('ui-helper-hidden')).toBe( + true, + ); + }); + } } - } } /** Runs visibility tests for 'less', 'bull' and 'more' elements in document @@ -97,123 +97,123 @@ function checkFacetVisibility(totalFacet, expectedVisibleFacet) { * @param {Number} expectedVisibleFacet expected number of visible facet */ function checkFacetMoreLessVisibility( - totalFacet, - minVisibleFacet, - expectedVisibleFacet, + totalFacet, + minVisibleFacet, + expectedVisibleFacet, ) { - if (expectedVisibleFacet <= minVisibleFacet) { - test('element "test_more"', () => { - expect(document.getElementById('test_more').style.display).not.toBe( - 'none', - ); - }); - test('element "test_bull"', () => { - expect(document.getElementById('test_bull').style.display).toBe('none'); - }); - test('element "test_less"', () => { - expect(document.getElementById('test_less').style.display).toBe('none'); - }); - } else if (expectedVisibleFacet >= totalFacet) { - test('element "test_more"', () => { - expect(document.getElementById('test_more').style.display).toBe('none'); - }); - test('element "test_bull"', () => { - expect(document.getElementById('test_bull').style.display).toBe('none'); - }); - test('element "test_less"', () => { - expect(document.getElementById('test_less').style.display).not.toBe( - 'none', - ); - }); - } else { - test('element "test_more"', () => { - expect(document.getElementById('test_more').style.display).not.toBe( - 'none', - ); - }); - test('element "test_bull"', () => { - expect(document.getElementById('test_bull').style.display).not.toBe( - 'none', - ); - }); - test('element "test_less"', () => { - expect(document.getElementById('test_less').style.display).not.toBe( - 'none', - ); - }); - } + if (expectedVisibleFacet <= minVisibleFacet) { + test('element "test_more"', () => { + expect(document.getElementById('test_more').style.display).not.toBe( + 'none', + ); + }); + test('element "test_bull"', () => { + expect(document.getElementById('test_bull').style.display).toBe('none'); + }); + test('element "test_less"', () => { + expect(document.getElementById('test_less').style.display).toBe('none'); + }); + } else if (expectedVisibleFacet >= totalFacet) { + test('element "test_more"', () => { + expect(document.getElementById('test_more').style.display).toBe('none'); + }); + test('element "test_bull"', () => { + expect(document.getElementById('test_bull').style.display).toBe('none'); + }); + test('element "test_less"', () => { + expect(document.getElementById('test_less').style.display).not.toBe( + 'none', + ); + }); + } else { + test('element "test_more"', () => { + expect(document.getElementById('test_more').style.display).not.toBe( + 'none', + ); + }); + test('element "test_bull"', () => { + expect(document.getElementById('test_bull').style.display).not.toBe( + 'none', + ); + }); + test('element "test_less"', () => { + expect(document.getElementById('test_less').style.display).not.toBe( + 'none', + ); + }); + } } const _originalGetClientRects = window.Element.prototype.getClientRects; // Stubbed getClientRects to enable jQuery ':hidden' selector used by 'more' and 'less' functions const _stubbedGetClientRects = function () { - let node = this; - while (node) { - if (node === document) { - break; - } - if ( - !node.style || + let node = this; + while (node) { + if (node === document) { + break; + } + if ( + !node.style || node.style.display === 'none' || node.style.visibility === 'hidden' || node.classList.contains('ui-helper-hidden') - ) { - return []; + ) { + return []; + } + node = node.parentNode; } - node = node.parentNode; - } - return [{ width: 1, height: 1 }]; + return [{ width: 1, height: 1 }]; }; describe('more', () => { - [ + [ /*[ totalFacet, minVisibleFacet, facetInc, visibleFacet, expectedVisibleFacet ]*/ - [7, 2, 3, 2, 5], - [9, 2, 3, 5, 8], - [7, 2, 3, 5, 7], - [7, 2, 3, 7, 7], - ].forEach((test) => { - const label = `Facet setup [total: ${test[0]}, visible: ${test[3]}, min: ${test[1]}]`; - describe(label, () => { - beforeAll(() => { - document.body.innerHTML = createSearchFacets(test[0], test[3], test[1]); - window.Element.prototype.getClientRects = _stubbedGetClientRects; - more('test', test[1], test[2]); - }); - - afterAll(() => { - window.Element.prototype.getClientRects = _originalGetClientRects; - }); - - checkFacetVisibility(test[0], test[4]); - checkFacetMoreLessVisibility(test[0], test[1], test[4]); + [7, 2, 3, 2, 5], + [9, 2, 3, 5, 8], + [7, 2, 3, 5, 7], + [7, 2, 3, 7, 7], + ].forEach((test) => { + const label = `Facet setup [total: ${test[0]}, visible: ${test[3]}, min: ${test[1]}]`; + describe(label, () => { + beforeAll(() => { + document.body.innerHTML = createSearchFacets(test[0], test[3], test[1]); + window.Element.prototype.getClientRects = _stubbedGetClientRects; + more('test', test[1], test[2]); + }); + + afterAll(() => { + window.Element.prototype.getClientRects = _originalGetClientRects; + }); + + checkFacetVisibility(test[0], test[4]); + checkFacetMoreLessVisibility(test[0], test[1], test[4]); + }); }); - }); }); describe('less', () => { - [ + [ /*[ totalFacet, minVisibleFacet, facetInc, visibleFacet, expectedVisibleFacet ]*/ - [5, 2, 3, 2, 2], - [7, 2, 3, 5, 2], - [9, 2, 3, 8, 5], - [7, 2, 3, 7, 5], - ].forEach((test) => { - const label = `Facet setup [total: ${test[0]}, visible: ${test[3]}, min: ${test[1]}]`; - describe(label, () => { - beforeAll(() => { - document.body.innerHTML = createSearchFacets(test[0], test[3], test[1]); - window.Element.prototype.getClientRects = _stubbedGetClientRects; - less('test', test[1], test[2]); - }); - - afterAll(() => { - window.Element.prototype.getClientRects = _originalGetClientRects; - }); - - checkFacetVisibility(test[0], test[4]); - checkFacetMoreLessVisibility(test[0], test[1], test[4]); + [5, 2, 3, 2, 2], + [7, 2, 3, 5, 2], + [9, 2, 3, 8, 5], + [7, 2, 3, 7, 5], + ].forEach((test) => { + const label = `Facet setup [total: ${test[0]}, visible: ${test[3]}, min: ${test[1]}]`; + describe(label, () => { + beforeAll(() => { + document.body.innerHTML = createSearchFacets(test[0], test[3], test[1]); + window.Element.prototype.getClientRects = _stubbedGetClientRects; + less('test', test[1], test[2]); + }); + + afterAll(() => { + window.Element.prototype.getClientRects = _originalGetClientRects; + }); + + checkFacetVisibility(test[0], test[4]); + checkFacetMoreLessVisibility(test[0], test[1], test[4]); + }); }); - }); }); diff --git a/tests/unit/js/service-worker-matchers.test.js b/tests/unit/js/service-worker-matchers.test.js index 659f65140a3..49af0942536 100644 --- a/tests/unit/js/service-worker-matchers.test.js +++ b/tests/unit/js/service-worker-matchers.test.js @@ -1,169 +1,169 @@ import { - matchArchiveOrgImage, - matchLargeCovers, - matchMiscFiles, - matchSmallMediumCovers, - matchStaticBuild, - matchStaticImages, + matchArchiveOrgImage, + matchLargeCovers, + matchMiscFiles, + matchSmallMediumCovers, + matchStaticBuild, + matchStaticImages, } from '../../../openlibrary/plugins/openlibrary/js/service-worker-matchers'; // Helper function to create a URL object function _u(url) { - return { url: new URL(url) }; + return { url: new URL(url) }; } // Group related tests together describe('URL Matchers', () => { - describe('matchMiscFiles', () => { - test('matches miscellaneous files', () => { - expect(matchMiscFiles(_u('https://openlibrary.org/favicon.ico'))).toBe( - true, - ); - expect( - matchMiscFiles(_u('https://openlibrary.org/static/manifest.json')), - ).toBe(true); - }); + describe('matchMiscFiles', () => { + test('matches miscellaneous files', () => { + expect(matchMiscFiles(_u('https://openlibrary.org/favicon.ico'))).toBe( + true, + ); + expect( + matchMiscFiles(_u('https://openlibrary.org/static/manifest.json')), + ).toBe(true); + }); - test('does not match homepage', () => { - expect(matchMiscFiles(_u('https://openlibrary.org/'))).toBe(false); + test('does not match homepage', () => { + expect(matchMiscFiles(_u('https://openlibrary.org/'))).toBe(false); + }); }); - }); - describe('matchSmallMediumCovers', () => { - test('matches small and medium cover sizes', () => { - expect( - matchSmallMediumCovers( - _u('https://covers.openlibrary.org/a/id/6257045-M.jpg'), - ), - ).toBe(true); - expect( - matchSmallMediumCovers( - _u('https://covers.openlibrary.org/a/id/6257045-S.jpg'), - ), - ).toBe(true); - expect( - matchSmallMediumCovers( - _u('https://covers.openlibrary.org/b/id/1852327-M.jpg'), - ), - ).toBe(true); - expect( - matchSmallMediumCovers( - _u('https://covers.openlibrary.org/a/olid/OL2838765A-M.jpg'), - ), - ).toBe(true); - }); + describe('matchSmallMediumCovers', () => { + test('matches small and medium cover sizes', () => { + expect( + matchSmallMediumCovers( + _u('https://covers.openlibrary.org/a/id/6257045-M.jpg'), + ), + ).toBe(true); + expect( + matchSmallMediumCovers( + _u('https://covers.openlibrary.org/a/id/6257045-S.jpg'), + ), + ).toBe(true); + expect( + matchSmallMediumCovers( + _u('https://covers.openlibrary.org/b/id/1852327-M.jpg'), + ), + ).toBe(true); + expect( + matchSmallMediumCovers( + _u('https://covers.openlibrary.org/a/olid/OL2838765A-M.jpg'), + ), + ).toBe(true); + }); - test('does not match large covers', () => { - expect( - matchSmallMediumCovers( - _u('https://covers.openlibrary.org/a/id/6257045-L.jpg'), - ), - ).toBe(false); + test('does not match large covers', () => { + expect( + matchSmallMediumCovers( + _u('https://covers.openlibrary.org/a/id/6257045-L.jpg'), + ), + ).toBe(false); + }); }); - }); - describe('matchLargeCovers', () => { - test('matches large cover sizes', () => { - expect( - matchLargeCovers( - _u('https://covers.openlibrary.org/a/id/6257045-L.jpg'), - ), - ).toBe(true); - }); + describe('matchLargeCovers', () => { + test('matches large cover sizes', () => { + expect( + matchLargeCovers( + _u('https://covers.openlibrary.org/a/id/6257045-L.jpg'), + ), + ).toBe(true); + }); - test('does not match small or medium covers', () => { - expect( - matchLargeCovers( - _u('https://covers.openlibrary.org/a/id/6257045-S.jpg'), - ), - ).toBe(false); - expect( - matchLargeCovers( - _u('https://covers.openlibrary.org/a/id/6257045-M.jpg'), - ), - ).toBe(false); - expect( - matchLargeCovers(_u('https://covers.openlibrary.org/a/id/6257045.jpg')), - ).toBe(false); + test('does not match small or medium covers', () => { + expect( + matchLargeCovers( + _u('https://covers.openlibrary.org/a/id/6257045-S.jpg'), + ), + ).toBe(false); + expect( + matchLargeCovers( + _u('https://covers.openlibrary.org/a/id/6257045-M.jpg'), + ), + ).toBe(false); + expect( + matchLargeCovers(_u('https://covers.openlibrary.org/a/id/6257045.jpg')), + ).toBe(false); + }); }); - }); - describe('matchStaticImages', () => { - test('matches static images', () => { - expect( - matchStaticImages( - _u('https://openlibrary.org/static/images/down-arrow.png'), - ), - ).toBe(true); - expect( - matchStaticImages( - _u( - 'https://testing.openlibrary.org/static/images/icons/barcode_scanner.svg', - ), - ), - ).toBe(true); - }); + describe('matchStaticImages', () => { + test('matches static images', () => { + expect( + matchStaticImages( + _u('https://openlibrary.org/static/images/down-arrow.png'), + ), + ).toBe(true); + expect( + matchStaticImages( + _u( + 'https://testing.openlibrary.org/static/images/icons/barcode_scanner.svg', + ), + ), + ).toBe(true); + }); - test('does not match other URLs', () => { - expect( - matchStaticImages( - _u( - 'https://openlibrary.org/static/build/js/4290.a0ae80aacde14696d322.js', - ), - ), - ).toBe(false); - expect( - matchStaticImages( - _u('https://covers.openlibrary.org/w/id/14348537-L.jpg'), - ), - ).toBe(false); + test('does not match other URLs', () => { + expect( + matchStaticImages( + _u( + 'https://openlibrary.org/static/build/js/4290.a0ae80aacde14696d322.js', + ), + ), + ).toBe(false); + expect( + matchStaticImages( + _u('https://covers.openlibrary.org/w/id/14348537-L.jpg'), + ), + ).toBe(false); + }); }); - }); - describe('matchStaticBuild', () => { - test('matches static build files', () => { - expect( - matchStaticBuild( - _u( - 'https://openlibrary.org/static/build/js/4290.a0ae80aacde14696d322.js', - ), - ), - ).toBe(true); - expect( - matchStaticBuild( - _u( - 'https://testing.openlibrary.org/static/build/css/page-book.css?v=097b69dc350c972d96da0c70cebe7b75', - ), - ), - ).toBe(true); - }); + describe('matchStaticBuild', () => { + test('matches static build files', () => { + expect( + matchStaticBuild( + _u( + 'https://openlibrary.org/static/build/js/4290.a0ae80aacde14696d322.js', + ), + ), + ).toBe(true); + expect( + matchStaticBuild( + _u( + 'https://testing.openlibrary.org/static/build/css/page-book.css?v=097b69dc350c972d96da0c70cebe7b75', + ), + ), + ).toBe(true); + }); - test('does not match localhost URLs', () => { - expect( - matchStaticBuild( - _u( - 'http://localhost:8080/static/build/js/4290.a0ae80aacde14696d322.js', - ), - ), - ).toBe(false); + test('does not match localhost URLs', () => { + expect( + matchStaticBuild( + _u( + 'http://localhost:8080/static/build/js/4290.a0ae80aacde14696d322.js', + ), + ), + ).toBe(false); + }); }); - }); - describe('matchArchiveOrgImage', () => { - test('matches archive.org images', () => { - expect( - matchArchiveOrgImage(_u('https://archive.org/services/img/@raybb')), - ).toBe(true); - expect( - matchArchiveOrgImage( - _u('https://archive.org/services/img/courtofmistfury0000maas'), - ), - ).toBe(true); - }); + describe('matchArchiveOrgImage', () => { + test('matches archive.org images', () => { + expect( + matchArchiveOrgImage(_u('https://archive.org/services/img/@raybb')), + ).toBe(true); + expect( + matchArchiveOrgImage( + _u('https://archive.org/services/img/courtofmistfury0000maas'), + ), + ).toBe(true); + }); - test('does not match other URLs', () => { - expect(matchArchiveOrgImage(_u('https://archive.org/services/'))).toBe( - false, - ); + test('does not match other URLs', () => { + expect(matchArchiveOrgImage(_u('https://archive.org/services/'))).toBe( + false, + ); + }); }); - }); }); diff --git a/tests/unit/js/setup.js b/tests/unit/js/setup.js index 722814e5e7a..a57332a9bba 100644 --- a/tests/unit/js/setup.js +++ b/tests/unit/js/setup.js @@ -6,5 +6,5 @@ window.$ = $; // Improve error reporting for unhandled promise rejections process.on('unhandledRejection', (error) => { - throw error; + throw error; }); diff --git a/tests/unit/js/signup.test.js b/tests/unit/js/signup.test.js index 6d358719b4f..8bce04035b3 100644 --- a/tests/unit/js/signup.test.js +++ b/tests/unit/js/signup.test.js @@ -1,7 +1,7 @@ import { initSignupForm } from '../../../openlibrary/plugins/openlibrary/js/signup'; beforeEach(() => { - document.body.innerHTML = ` + document.body.innerHTML = ` <form id="signup" name="signup" data-i18n={}> <label for="emailAddr">Email</label> <div id="emailAddrMessage" class="ol-signup-form__error"></div> @@ -26,217 +26,217 @@ beforeEach(() => { }); describe('Email tests', () => { - let emailLabel, emailField; + let emailLabel, emailField; - beforeEach(() => { + beforeEach(() => { // call the function - initSignupForm(); + initSignupForm(); - //declare the elements - emailLabel = document.querySelector('label[for="emailAddr"]'); - emailField = document.getElementById('emailAddr'); - }); + //declare the elements + emailLabel = document.querySelector('label[for="emailAddr"]'); + emailField = document.getElementById('emailAddr'); + }); - test('validateEmail should update elements correctly on success', () => { + test('validateEmail should update elements correctly on success', () => { // set the email value - emailField.value = 'testemail@archive.org'; + emailField.value = 'testemail@archive.org'; - // Trigger the blur event on the email field - emailField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the email field + emailField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(emailField.classList.contains('invalid')).toBe(false); - expect(emailLabel.classList.contains('invalid')).toBe(false); - }); + // Assert that the elements have the expected classes + expect(emailField.classList.contains('invalid')).toBe(false); + expect(emailLabel.classList.contains('invalid')).toBe(false); + }); - test('validateEmail should update elements correctly for empty fields', () => { + test('validateEmail should update elements correctly for empty fields', () => { // set the email value - emailField.value = ''; + emailField.value = ''; - // Trigger the blur event on the email field - emailField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the email field + emailField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(emailField.classList.contains('invalid')).toBe(false); - expect(emailLabel.classList.contains('invalid')).toBe(false); - }); + // Assert that the elements have the expected classes + expect(emailField.classList.contains('invalid')).toBe(false); + expect(emailLabel.classList.contains('invalid')).toBe(false); + }); - test('validateEmail should update elements correctly for emails with plus signs', () => { + test('validateEmail should update elements correctly for emails with plus signs', () => { // set the email value - emailField.value = 'testemail+01@archive.org'; + emailField.value = 'testemail+01@archive.org'; - // Trigger the blur event on the email field - emailField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the email field + emailField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(emailField.classList.contains('invalid')).toBe(false); - expect(emailLabel.classList.contains('invalid')).toBe(false); - }); + // Assert that the elements have the expected classes + expect(emailField.classList.contains('invalid')).toBe(false); + expect(emailLabel.classList.contains('invalid')).toBe(false); + }); - test('validateEmail should update elements correctly for emails with no punctuation', () => { + test('validateEmail should update elements correctly for emails with no punctuation', () => { // set the password values - emailField.value = 'testemail'; + emailField.value = 'testemail'; - // Trigger the blur event on the email fields - emailField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the email fields + emailField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(emailField.classList.contains('invalid')).toBe(true); - expect(emailLabel.classList.contains('invalid')).toBe(true); - }); + // Assert that the elements have the expected classes + expect(emailField.classList.contains('invalid')).toBe(true); + expect(emailLabel.classList.contains('invalid')).toBe(true); + }); - test('validateEmail should update elements correctly for emails with invalid punctuation', () => { + test('validateEmail should update elements correctly for emails with invalid punctuation', () => { // set the email values - emailField.value = 'testemail@archive-org'; + emailField.value = 'testemail@archive-org'; - // Trigger the blur event on the email fields - emailField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the email fields + emailField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(emailField.classList.contains('invalid')).toBe(true); - expect(emailLabel.classList.contains('invalid')).toBe(true); - }); + // Assert that the elements have the expected classes + expect(emailField.classList.contains('invalid')).toBe(true); + expect(emailLabel.classList.contains('invalid')).toBe(true); + }); }); describe('Username tests', () => { - let usernameLabel, usernameField; + let usernameLabel, usernameField; - beforeEach(() => { + beforeEach(() => { // call the function - initSignupForm(); + initSignupForm(); - //declare the elements - usernameLabel = document.querySelector('label[for="username"]'); - usernameField = document.getElementById('username'); - }); + //declare the elements + usernameLabel = document.querySelector('label[for="username"]'); + usernameField = document.getElementById('username'); + }); - test('validateUsername should update elements correctly on success', () => { + test('validateUsername should update elements correctly on success', () => { // set the username value - usernameField.value = 'username123'; + usernameField.value = 'username123'; - // Trigger the blur event on the username field - usernameField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the username field + usernameField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(usernameField.classList.contains('invalid')).toBe(false); - expect(usernameLabel.classList.contains('invalid')).toBe(false); - }); + // Assert that the elements have the expected classes + expect(usernameField.classList.contains('invalid')).toBe(false); + expect(usernameLabel.classList.contains('invalid')).toBe(false); + }); - test('validateUsername should update elements correctly for empty fields', () => { + test('validateUsername should update elements correctly for empty fields', () => { // set the username value - usernameField.value = ''; + usernameField.value = ''; - // Trigger the blur event on the username field - usernameField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the username field + usernameField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(usernameField.classList.contains('invalid')).toBe(false); - expect(usernameLabel.classList.contains('invalid')).toBe(false); - }); + // Assert that the elements have the expected classes + expect(usernameField.classList.contains('invalid')).toBe(false); + expect(usernameLabel.classList.contains('invalid')).toBe(false); + }); - test('validateUsername should update elements correctly for usernames over 20 chars', () => { + test('validateUsername should update elements correctly for usernames over 20 chars', () => { // set the username values - usernameField.value = 'username1234567891011'; + usernameField.value = 'username1234567891011'; - // Trigger the blur event on the username fields - usernameField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the username fields + usernameField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(usernameField.classList.contains('invalid')).toBe(true); - expect(usernameLabel.classList.contains('invalid')).toBe(true); - }); + // Assert that the elements have the expected classes + expect(usernameField.classList.contains('invalid')).toBe(true); + expect(usernameLabel.classList.contains('invalid')).toBe(true); + }); - test('validateusername should update elements correctly for usernames under 3 chars', () => { + test('validateusername should update elements correctly for usernames under 3 chars', () => { // set the username values - usernameField.value = 'us'; + usernameField.value = 'us'; - // Trigger the blur event on the username fields - usernameField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the username fields + usernameField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(usernameField.classList.contains('invalid')).toBe(true); - expect(usernameLabel.classList.contains('invalid')).toBe(true); - }); + // Assert that the elements have the expected classes + expect(usernameField.classList.contains('invalid')).toBe(true); + expect(usernameLabel.classList.contains('invalid')).toBe(true); + }); }); describe('Password tests', () => { - let passwordLabel, passwordField; + let passwordLabel, passwordField; - beforeEach(() => { + beforeEach(() => { // call the function - initSignupForm(); + initSignupForm(); - //declare the elements - passwordLabel = document.querySelector('label[for="password"]'); - passwordField = document.getElementById('password'); - }); + //declare the elements + passwordLabel = document.querySelector('label[for="password"]'); + passwordField = document.getElementById('password'); + }); - test('validatePassword should update elements correctly on success', () => { + test('validatePassword should update elements correctly on success', () => { // set the password value - passwordField.value = 'password123'; + passwordField.value = 'password123'; - // Trigger the blur event on the password field - passwordField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the password field + passwordField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(passwordField.classList.contains('invalid')).toBe(false); - expect(passwordLabel.classList.contains('invalid')).toBe(false); - }); + // Assert that the elements have the expected classes + expect(passwordField.classList.contains('invalid')).toBe(false); + expect(passwordLabel.classList.contains('invalid')).toBe(false); + }); - test('validatePassword should update elements correctly for empty fields', () => { + test('validatePassword should update elements correctly for empty fields', () => { // set the password value - passwordField.value = ''; + passwordField.value = ''; - // Trigger the blur event on the password field - passwordField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the password field + passwordField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(passwordField.classList.contains('invalid')).toBe(false); - expect(passwordLabel.classList.contains('invalid')).toBe(false); - }); + // Assert that the elements have the expected classes + expect(passwordField.classList.contains('invalid')).toBe(false); + expect(passwordLabel.classList.contains('invalid')).toBe(false); + }); - test('validatePassword should update elements correctly for passwords over 20 chars', () => { + test('validatePassword should update elements correctly for passwords over 20 chars', () => { // set the password values - passwordField.value = 'password1234567891011'; + passwordField.value = 'password1234567891011'; - // Trigger the blur event on the password fields - passwordField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the password fields + passwordField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(passwordField.classList.contains('invalid')).toBe(true); - expect(passwordLabel.classList.contains('invalid')).toBe(true); - }); + // Assert that the elements have the expected classes + expect(passwordField.classList.contains('invalid')).toBe(true); + expect(passwordLabel.classList.contains('invalid')).toBe(true); + }); - test('validatePassword should update elements correctly for passwords under 3 chars', () => { + test('validatePassword should update elements correctly for passwords under 3 chars', () => { // set the password values - passwordField.value = 'pa'; + passwordField.value = 'pa'; - // Trigger the blur event on the password fields - passwordField.dispatchEvent(new Event('blur')); + // Trigger the blur event on the password fields + passwordField.dispatchEvent(new Event('blur')); - // Assert that the elements have the expected classes - expect(passwordField.classList.contains('invalid')).toBe(true); - expect(passwordLabel.classList.contains('invalid')).toBe(true); - }); + // Assert that the elements have the expected classes + expect(passwordField.classList.contains('invalid')).toBe(true); + expect(passwordLabel.classList.contains('invalid')).toBe(true); + }); }); describe('Print disability tests', () => { - let checkbox, selector; + let checkbox, selector; - beforeEach(() => { - initSignupForm(); + beforeEach(() => { + initSignupForm(); - checkbox = document.querySelector('#pd-request'); - selector = document.querySelector('#pda-selector'); - }); + checkbox = document.querySelector('#pd-request'); + selector = document.querySelector('#pda-selector'); + }); - test('Qualifying authority selector only visible when PD checkbox is checked', () => { - checkbox.checked = false; - checkbox.dispatchEvent(new Event('change', { bubbles: true })); - expect(selector.classList.contains('hidden')).toBe(true); + test('Qualifying authority selector only visible when PD checkbox is checked', () => { + checkbox.checked = false; + checkbox.dispatchEvent(new Event('change', { bubbles: true })); + expect(selector.classList.contains('hidden')).toBe(true); - checkbox.checked = true; - checkbox.dispatchEvent(new Event('change', { bubbles: true })); - expect(selector.classList.contains('hidden')).toBe(false); - }); + checkbox.checked = true; + checkbox.dispatchEvent(new Event('change', { bubbles: true })); + expect(selector.classList.contains('hidden')).toBe(false); + }); }); diff --git a/tests/unit/js/utils.test.js b/tests/unit/js/utils.test.js index 58c14739f67..b35e7eb6995 100644 --- a/tests/unit/js/utils.test.js +++ b/tests/unit/js/utils.test.js @@ -1,69 +1,69 @@ import { removeChildren } from '../../../openlibrary/plugins/openlibrary/js/utils'; import { - childlessElem, - elemWithDescendants, - multiChildElem, + childlessElem, + elemWithDescendants, + multiChildElem, } from './sample-html/utils-test-data'; describe('`removeChildren()` tests', () => { - it('changes nothing if element has no children', () => { - document.body.innerHTML = childlessElem; - const elem = document.querySelector('.remove-tests'); - const clonedElem = elem.cloneNode(true); + it('changes nothing if element has no children', () => { + document.body.innerHTML = childlessElem; + const elem = document.querySelector('.remove-tests'); + const clonedElem = elem.cloneNode(true); - // Initial checks - expect(elem.childElementCount).toBe(0); - expect(elem.isEqualNode(clonedElem)).toBe(true); + // Initial checks + expect(elem.childElementCount).toBe(0); + expect(elem.isEqualNode(clonedElem)).toBe(true); - // Element should be unchanged after function call - removeChildren(elem); - expect(elem.childElementCount).toBe(0); - expect(elem.isEqualNode(clonedElem)).toBe(true); - }); + // Element should be unchanged after function call + removeChildren(elem); + expect(elem.childElementCount).toBe(0); + expect(elem.isEqualNode(clonedElem)).toBe(true); + }); - it("removes all of an element's children", () => { - document.body.innerHTML = multiChildElem; - const elem = document.querySelector('.remove-tests'); - const clonedElem = elem.cloneNode(true); + it('removes all of an element\'s children', () => { + document.body.innerHTML = multiChildElem; + const elem = document.querySelector('.remove-tests'); + const clonedElem = elem.cloneNode(true); - // Initial checks - expect(elem.childElementCount).toBe(2); - expect(elem.isEqualNode(clonedElem)).toBe(true); + // Initial checks + expect(elem.childElementCount).toBe(2); + expect(elem.isEqualNode(clonedElem)).toBe(true); - // After removing children - removeChildren(elem); - expect(elem.childElementCount).toBe(0); - expect(elem.isEqualNode(clonedElem)).toBe(false); - }); + // After removing children + removeChildren(elem); + expect(elem.childElementCount).toBe(0); + expect(elem.isEqualNode(clonedElem)).toBe(false); + }); - it('removes children if they have children of their own', () => { - document.body.innerHTML = elemWithDescendants; - const elem = document.querySelector('.remove-tests'); - const clonedElem = elem.cloneNode(true); + it('removes children if they have children of their own', () => { + document.body.innerHTML = elemWithDescendants; + const elem = document.querySelector('.remove-tests'); + const clonedElem = elem.cloneNode(true); - // Inital checks - expect(elem.childElementCount).toBe(1); - expect(elem.children[0].childElementCount).toBe(1); - expect(elem.isEqualNode(clonedElem)).toBe(true); + // Inital checks + expect(elem.childElementCount).toBe(1); + expect(elem.children[0].childElementCount).toBe(1); + expect(elem.isEqualNode(clonedElem)).toBe(true); - // After removing children - removeChildren(elem); - expect(elem.childElementCount).toBe(0); - expect(elem.isEqualNode(clonedElem)).toBe(false); - }); + // After removing children + removeChildren(elem); + expect(elem.childElementCount).toBe(0); + expect(elem.isEqualNode(clonedElem)).toBe(false); + }); - it('handles multiple parameters correctly', () => { - document.body.innerHTML = elemWithDescendants + multiChildElem; - const elems = document.querySelectorAll('.remove-tests'); + it('handles multiple parameters correctly', () => { + document.body.innerHTML = elemWithDescendants + multiChildElem; + const elems = document.querySelectorAll('.remove-tests'); - // Initial checks: - expect(elems.length).toBe(2); - expect(elems[0].childElementCount).toBe(1); - expect(elems[1].childElementCount).toBe(2); + // Initial checks: + expect(elems.length).toBe(2); + expect(elems[0].childElementCount).toBe(1); + expect(elems[1].childElementCount).toBe(2); - // After removing children: - removeChildren(...elems); - expect(elems[0].childElementCount).toBe(0); - expect(elems[1].childElementCount).toBe(0); - }); + // After removing children: + removeChildren(...elems); + expect(elems[0].childElementCount).toBe(0); + expect(elems[1].childElementCount).toBe(0); + }); }); diff --git a/webpack.config.css.js b/webpack.config.css.js index bb9aa878a89..77a12587a79 100644 --- a/webpack.config.css.js +++ b/webpack.config.css.js @@ -12,89 +12,89 @@ const glob = require('glob'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); const distDir = path.resolve( - __dirname, - process.env.BUILD_DIR || 'static/build/css', + __dirname, + process.env.BUILD_DIR || 'static/build/css', ); // Find all CSS entry files matching static/css/page-*.css const cssFiles = glob.sync('./static/css/page-*.css'); const entries = { - // Design tokens — compiled from static/css/tokens/ into a single file - tokens: './static/css/tokens.css', + // Design tokens — compiled from static/css/tokens/ into a single file + tokens: './static/css/tokens.css', }; cssFiles.forEach((file) => { - const name = path.basename(file, '.css'); - entries[name] = file; + const name = path.basename(file, '.css'); + entries[name] = file; }); module.exports = { - context: __dirname, - entry: entries, - output: { - path: distDir, - // Output only CSS, JS is not needed - filename: '[name].css.js', // dummy, CSS will be extracted - clean: true, - }, - module: { - rules: [ - { - test: /\.css$/, - use: [ - MiniCssExtractPlugin.loader, - { - loader: 'css-loader', - options: { - url: false, - import: true, // Enable @import resolution + context: __dirname, + entry: entries, + output: { + path: distDir, + // Output only CSS, JS is not needed + filename: '[name].css.js', // dummy, CSS will be extracted + clean: true, + }, + module: { + rules: [ + { + test: /\.css$/, + use: [ + MiniCssExtractPlugin.loader, + { + loader: 'css-loader', + options: { + url: false, + import: true, // Enable @import resolution + }, + }, + ], }, - }, ], - }, - ], - }, - plugins: [ - new MiniCssExtractPlugin({ - filename: '[name].css', - }), - // Inline plugin to remove intermediary JS assets - { - apply: (compiler) => { - compiler.hooks.thisCompilation.tap( - 'RemoveJSAssetsPlugin', - (compilation) => { - compilation.hooks.processAssets.tap( - { - name: 'RemoveJSAssetsPlugin', - stage: + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: '[name].css', + }), + // Inline plugin to remove intermediary JS assets + { + apply: (compiler) => { + compiler.hooks.thisCompilation.tap( + 'RemoveJSAssetsPlugin', + (compilation) => { + compilation.hooks.processAssets.tap( + { + name: 'RemoveJSAssetsPlugin', + stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, - }, - (assets) => { - Object.keys(assets) - .filter((asset) => asset.endsWith('.js')) - .forEach((asset) => { - compilation.deleteAsset(asset); - }); - }, - ); - }, - ); - }, + }, + (assets) => { + Object.keys(assets) + .filter((asset) => asset.endsWith('.js')) + .forEach((asset) => { + compilation.deleteAsset(asset); + }); + }, + ); + }, + ); + }, + }, + ], + optimization: { + minimizer: [new CssMinimizerPlugin()], + runtimeChunk: false, + splitChunks: false, }, - ], - optimization: { - minimizer: [new CssMinimizerPlugin()], - runtimeChunk: false, - splitChunks: false, - }, - // Useful for developing in docker/windows, which doesn't support file watchers - watchOptions: + // Useful for developing in docker/windows, which doesn't support file watchers + watchOptions: process.env.FORCE_POLLING === 'true' - ? { - poll: 1000, // Check for changes every second - aggregateTimeout: 300, // Delay before rebuilding - ignored: /node_modules/, + ? { + poll: 1000, // Check for changes every second + aggregateTimeout: 300, // Delay before rebuilding + ignored: /node_modules/, } - : undefined, + : undefined, }; From 9974747789064431b0aa62d5f225dfb2bfe831f6 Mon Sep 17 00:00:00 2001 From: harshgupta2125 <harsh2125gupta@gmail.com> Date: Sat, 11 Apr 2026 13:49:54 +0530 Subject: [PATCH 07/15] Remove unused Biome dependency --- package-lock.json | 164 ---------------------------------------------- package.json | 1 - 2 files changed, 165 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6d82f210f49..a36663e7648 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@babel/core": "^7.24.7", "@babel/eslint-parser": "^7.24.7", "@babel/preset-env": "^7.24.7", - "@biomejs/biome": "^2.4.10", "@ericblade/quagga2": "^1.7.4", "@eslint/js": "^9.39.4", "@vitejs/plugin-legacy": "^8.0.1", @@ -1933,169 +1932,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@biomejs/biome": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.10.tgz", - "integrity": "sha512-xxA3AphFQ1geij4JTHXv4EeSTda1IFn22ye9LdyVPoJU19fNVl0uzfEuhsfQ4Yue/0FaLs2/ccVi4UDiE7R30w==", - "dev": true, - "license": "MIT OR Apache-2.0", - "bin": { - "biome": "bin/biome" - }, - "engines": { - "node": ">=14.21.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/biome" - }, - "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.4.10", - "@biomejs/cli-darwin-x64": "2.4.10", - "@biomejs/cli-linux-arm64": "2.4.10", - "@biomejs/cli-linux-arm64-musl": "2.4.10", - "@biomejs/cli-linux-x64": "2.4.10", - "@biomejs/cli-linux-x64-musl": "2.4.10", - "@biomejs/cli-win32-arm64": "2.4.10", - "@biomejs/cli-win32-x64": "2.4.10" - } - }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.10.tgz", - "integrity": "sha512-vuzzI1cWqDVzOMIkYyHbKqp+AkQq4K7k+UCXWpkYcY/HDn1UxdsbsfgtVpa40shem8Kax4TLDLlx8kMAecgqiw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.10.tgz", - "integrity": "sha512-14fzASRo+BPotwp7nWULy2W5xeUyFnTaq1V13Etrrxkrih+ez/2QfgFm5Ehtf5vSjtgx/IJycMMpn5kPd5ZNaA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.10.tgz", - "integrity": "sha512-7MH1CMW5uuxQ/s7FLST63qF8B3Hgu2HRdZ7tA1X1+mk+St4JOuIrqdhIBnnyqeyWJNI+Bww7Es5QZ0wIc1Cmkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.10.tgz", - "integrity": "sha512-WrJY6UuiSD/Dh+nwK2qOTu8kdMDlLV3dLMmychIghHPAysWFq1/DGC1pVZx8POE3ZkzKR3PUUnVrtZfMfaJjyQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.10.tgz", - "integrity": "sha512-tZLvEEi2u9Xu1zAqRjTcpIDGVtldigVvzug2fTuPG0ME/g8/mXpRPcNgLB22bGn6FvLJpHHnqLnwliOu8xjYrg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.10.tgz", - "integrity": "sha512-kDTi3pI6PBN6CiczsWYOyP2zk0IJI08EWEQyDMQWW221rPaaEz6FvjLhnU07KMzLv8q3qSuoB93ua6inSQ55Tw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.10.tgz", - "integrity": "sha512-umwQU6qPzH+ISTf/eHyJ/QoQnJs3V9Vpjz2OjZXe9MVBZ7prgGafMy7yYeRGnlmDAn87AKTF3Q6weLoMGpeqdQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-x64": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.10.tgz", - "integrity": "sha512-aW/JU5GuyH4uxMrNYpoC2kjaHlyJGLgIa3XkhPEZI0uKhZhJZU8BuEyJmvgzSPQNGozBwWjC972RaNdcJ9KyJg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, "node_modules/@cacheable/memory": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.8.tgz", diff --git a/package.json b/package.json index ed2cd473a7c..24d99ec1988 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "@babel/core": "^7.24.7", "@babel/eslint-parser": "^7.24.7", "@babel/preset-env": "^7.24.7", - "@biomejs/biome": "^2.4.10", "@ericblade/quagga2": "^1.7.4", "@eslint/js": "^9.39.4", "@vitejs/plugin-legacy": "^8.0.1", From 5423218ac87e9c3976ebc8b756759134425fc7df Mon Sep 17 00:00:00 2001 From: harshgupta2125 <harsh2125gupta@gmail.com> Date: Sun, 12 Apr 2026 19:58:38 +0530 Subject: [PATCH 08/15] apply ESLint auto-fixes across all non-locked JS files --- eslint.config.cjs | 130 +++++++++++++++++- openlibrary/components/MergeUI.vue | 58 ++++---- .../components/MergeUI/AuthorRoleTable.vue | 4 +- .../components/MergeUI/EditionSnippet.vue | 18 +-- .../components/MergeUI/ExcerptsTable.vue | 4 +- openlibrary/components/MergeUI/MergeRow.vue | 2 +- .../components/MergeUI/MergeRowField.vue | 2 +- .../components/MergeUI/MergeRowJointField.vue | 2 +- openlibrary/components/MergeUI/TextDiff.vue | 4 +- openlibrary/components/ObservationForm.vue | 34 ++--- .../ObservationForm/components/CardBody.vue | 26 ++-- .../ObservationForm/components/CardHeader.vue | 2 +- .../components/CategorySelector.vue | 20 +-- .../ObservationForm/components/OLChip.vue | 16 +-- .../ObservationForm/components/SavedTags.vue | 32 ++--- .../ObservationForm/components/ValueCard.vue | 6 +- .../plugins/openlibrary/js/book-page-lists.js | 6 +- .../openlibrary/js/breadcrumb_select/index.js | 4 +- .../js/bulk-tagger/BulkTagger/MenuOption.js | 16 +-- .../BulkTagger/SortedMenuOptionContainer.js | 18 +-- .../openlibrary/js/bulk-tagger/index.js | 2 +- .../openlibrary/js/bulk-tagger/models/Tag.js | 12 +- .../plugins/openlibrary/js/clampers.js | 2 +- .../openlibrary/js/compact-title/index.js | 4 +- openlibrary/plugins/openlibrary/js/covers.js | 14 +- .../plugins/openlibrary/js/dropper/Dropper.js | 14 +- .../plugins/openlibrary/js/dropper/index.js | 6 +- .../js/edition-nav-bar/EditionNavBar.js | 68 ++++----- .../openlibrary/js/edition-nav-bar/index.js | 4 +- .../openlibrary/js/editions-table/index.js | 4 +- .../plugins/openlibrary/js/following.js | 2 +- .../js/fulltext-search-suggestion.js | 8 +- .../plugins/openlibrary/js/go-back-links.js | 2 +- .../plugins/openlibrary/js/graphs/index.js | 8 +- .../plugins/openlibrary/js/graphs/plot.js | 16 +-- .../openlibrary/js/ia_thirdparty_logins.js | 4 +- .../plugins/openlibrary/js/idValidation.js | 20 +-- .../plugins/openlibrary/js/ile/utils/ol.js | 4 +- .../plugins/openlibrary/js/interstitial.js | 2 +- .../plugins/openlibrary/js/isbnOverride.js | 6 +- .../plugins/openlibrary/js/jquery.repeat.js | 10 +- openlibrary/plugins/openlibrary/js/jsdef.js | 22 +-- .../plugins/openlibrary/js/lazy-carousel.js | 10 +- .../openlibrary/js/lazy-thing-preview.js | 12 +- .../js/librarian-dashboard/index.js | 18 +-- .../plugins/openlibrary/js/list_books.js | 8 +- .../openlibrary/js/lists/ListService.js | 8 +- .../openlibrary/js/lists/ListViewBody.js | 4 +- .../openlibrary/js/lists/ShowcaseItem.js | 20 +-- .../merge-request-table/MergeRequestTable.js | 4 +- .../MergeRequestTable/TableHeader.js | 14 +- .../MergeRequestTable/TableRow.js | 18 +-- .../js/merge-request-table/index.js | 2 +- .../openlibrary/js/my-books/CreateListForm.js | 10 +- .../openlibrary/js/my-books/MyBooksDropper.js | 18 +-- .../MyBooksDropper/CheckInComponents.js | 86 ++++++------ .../my-books/MyBooksDropper/ReadingLists.js | 22 +-- .../plugins/openlibrary/js/my-books/index.js | 4 +- .../openlibrary/js/my-books/store/index.js | 18 +-- .../openlibrary/js/native-dialog/index.js | 4 +- .../plugins/openlibrary/js/nonjquery_utils.js | 6 +- .../plugins/openlibrary/js/offline-banner.js | 2 +- .../plugins/openlibrary/js/ol.analytics.js | 4 +- openlibrary/plugins/openlibrary/js/ol.js | 16 +-- .../plugins/openlibrary/js/partner_ol_lib.js | 6 +- .../plugins/openlibrary/js/patron_exports.js | 4 +- .../plugins/openlibrary/js/private-button.js | 2 +- openlibrary/plugins/openlibrary/js/python.js | 6 +- .../openlibrary/js/reading-goals/index.js | 18 +-- .../openlibrary/js/readinglog_stats.js | 10 +- .../openlibrary/js/return-form/index.js | 2 +- openlibrary/plugins/openlibrary/js/search.js | 14 +- .../openlibrary/js/service-worker-matchers.js | 12 +- .../openlibrary/js/star-ratings/index.js | 4 +- .../plugins/openlibrary/js/stats/index.js | 4 +- openlibrary/plugins/openlibrary/js/tabs.js | 2 +- openlibrary/plugins/openlibrary/js/team.js | 2 +- .../plugins/openlibrary/js/template.js | 8 +- .../plugins/openlibrary/js/type_changer.js | 4 +- openlibrary/plugins/openlibrary/js/utils.js | 14 +- .../plugins/openlibrary/js/waitlist.js | 2 +- static/bookmarklets/import_webbook.js | 2 +- stories/.storybook/main.js | 2 +- stories/.storybook/preview.js | 2 +- tests/unit/js/SelectionManager.test.js | 4 +- .../unit/js/sample-html/dropper-test-data.js | 2 +- tests/unit/js/sample-html/lists-test-data.js | 20 +-- tests/unit/js/search.test.js | 6 +- tests/unit/js/service-worker-matchers.test.js | 2 +- 89 files changed, 609 insertions(+), 491 deletions(-) diff --git a/eslint.config.cjs b/eslint.config.cjs index 343b0508231..51fac7abd3f 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -28,7 +28,7 @@ module.exports = [ ], }, - // Configuration for build and config files (CommonJS) - MUST come before js.configs.recommended + // Configuration for build and config files (CommonJS) { files: [ "webpack.config.js", @@ -50,7 +50,7 @@ module.exports = [ }, }, - // Configuration for Vite config files (ES modules) - MUST come before js.configs.recommended + // Configuration for Vite config files (ES modules) { files: [ "openlibrary/components/vite.config.mjs", @@ -68,7 +68,7 @@ module.exports = [ }, }, - // Configuration for Storybook preview files (ES modules) - MUST come before js.configs.recommended + // Configuration for Storybook preview files (ES modules) { files: ["stories/.storybook/preview.js"], languageOptions: { @@ -89,7 +89,7 @@ module.exports = [ // Vue plugin configuration ...vuePlugin.configs["flat/recommended"], - // Base configuration for all JS/Vue files (except config and build files) + // Base configuration for all JS/Vue files { files: ["**/*.js", "**/*.vue"], plugins: { @@ -135,6 +135,12 @@ module.exports = [ "quote-props": ["error", "as-needed"], "keyword-spacing": ["error", { before: true, after: true }], "key-spacing": ["error", { mode: "strict" }], + + // GLOBALLY ENFORCED FORMATTING RULES + "semi": ["error", "always"], + "space-before-function-paren": ["error", "always"], + "comma-spacing": ["error", { "before": false, "after": true }], + "vue/no-mutating-props": "off", "vue/multi-word-component-names": [ "error", @@ -142,7 +148,7 @@ module.exports = [ ignores: ["Bookshelf", "Shelf"], }, ], - // jQuery deprecated rules (from plugin:no-jquery/deprecated) + // jQuery deprecated rules "no-jquery/no-box-model": "warn", "no-jquery/no-browser": "warn", "no-jquery/no-live": "warn", @@ -222,4 +228,116 @@ module.exports = [ }, }, }, -]; + + // TEMPORARY EXEMPTIONS: Turn off new formatting rules for files locked in active PRs + { + files: [ + "openlibrary/components/AuthorMap.vue", + "openlibrary/components/AuthorMap/AuthorCard.vue", + "openlibrary/components/AuthorMap/WorldMap.vue", + "openlibrary/components/AuthorMap/WorldMapRaw.vue", + "openlibrary/components/AuthorMap/utils.js", + "openlibrary/components/BarcodeScanner.vue", + "openlibrary/components/BarcodeScanner/components/LazyBookCard.vue", + "openlibrary/components/BarcodeScanner/utils/classes.js", + "openlibrary/components/BulkSearch.vue", + "openlibrary/components/BulkSearch/components/BookCard.vue", + "openlibrary/components/BulkSearch/components/BulkSearchControls.vue", + "openlibrary/components/BulkSearch/components/MatchRow.vue", + "openlibrary/components/BulkSearch/components/MatchTable.vue", + "openlibrary/components/BulkSearch/components/NoBookCard.vue", + "openlibrary/components/BulkSearch/utils/classes.js", + "openlibrary/components/BulkSearch/utils/samples.js", + "openlibrary/components/BulkSearch/utils/searchUtils.js", + "openlibrary/components/HelloWorld.vue", + "openlibrary/components/IdentifiersInput.vue", + "openlibrary/components/IdentifiersInput/utils/utils.js", + "openlibrary/components/LibraryExplorer.vue", + "openlibrary/components/LibraryExplorer/components/BookCover3D.vue", + "openlibrary/components/LibraryExplorer/components/BookRoom.vue", + "openlibrary/components/LibraryExplorer/components/BooksCarousel.vue", + "openlibrary/components/LibraryExplorer/components/CSSBox.vue", + "openlibrary/components/LibraryExplorer/components/ClassSlider.vue", + "openlibrary/components/LibraryExplorer/components/DemoA.vue", + "openlibrary/components/LibraryExplorer/components/FlatBookCover.vue", + "openlibrary/components/LibraryExplorer/components/LibraryToolbar.vue", + "openlibrary/components/LibraryExplorer/components/OLCarousel.vue", + "openlibrary/components/LibraryExplorer/components/Shelf.vue", + "openlibrary/components/LibraryExplorer/components/ShelfIndex.vue", + "openlibrary/components/LibraryExplorer/components/ShelfLabel.vue", + "openlibrary/components/LibraryExplorer/components/ShelfProgressBar.vue", + "openlibrary/components/LibraryExplorer/utils.js", + "openlibrary/components/LibraryExplorer/utils/lcc.js", + "openlibrary/components/MergeUI/MergeTable.vue", + "openlibrary/components/MergeUI/utils.js", + "openlibrary/components/ObservationForm/ObservationService.js", + "openlibrary/components/ObservationForm/Utils.js", + "openlibrary/components/configs.js", + "openlibrary/components/dev/serve-component.js", + "openlibrary/components/dev/vite.config.js", + "openlibrary/components/lit/OLChip.js", + "openlibrary/components/lit/OLChipGroup.js", + "openlibrary/components/lit/OLMarkdownEditor.js", + "openlibrary/components/lit/OLReadMore.js", + "openlibrary/components/lit/OlAutocomplete.js", + "openlibrary/components/lit/OlDrawer.js", + "openlibrary/components/lit/OlLanguageEdit.js", + "openlibrary/components/lit/OlPagination.js", + "openlibrary/components/lit/OlPopover.js", + "openlibrary/components/lit/OlTooltip.js", + "openlibrary/components/lit/editor-core.js", + "openlibrary/components/lit/html-block.js", + "openlibrary/components/lit/index.js", + "openlibrary/components/rollupInputCore.js", + "openlibrary/plugins/openlibrary/js/Browser.js", + "openlibrary/plugins/openlibrary/js/SearchBar.js", + "openlibrary/plugins/openlibrary/js/SearchPage.js", + "openlibrary/plugins/openlibrary/js/SearchUtils.js", + "openlibrary/plugins/openlibrary/js/Toast.js", + "openlibrary/plugins/openlibrary/js/add-book.js", + "openlibrary/plugins/openlibrary/js/add_new_field.js", + "openlibrary/plugins/openlibrary/js/add_provider.js", + "openlibrary/plugins/openlibrary/js/admin.js", + "openlibrary/plugins/openlibrary/js/affiliate-links.js", + "openlibrary/plugins/openlibrary/js/autocomplete.js", + "openlibrary/plugins/openlibrary/js/banner/index.js", + "openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger.js", + "openlibrary/plugins/openlibrary/js/carousel/Carousel.js", + "openlibrary/plugins/openlibrary/js/carousel/index.js", + "openlibrary/plugins/openlibrary/js/dialog.js", + "openlibrary/plugins/openlibrary/js/edit.js", + "openlibrary/plugins/openlibrary/js/goodreads_import.js", + "openlibrary/plugins/openlibrary/js/i18n.js", + "openlibrary/plugins/openlibrary/js/ile/index.js", + "openlibrary/plugins/openlibrary/js/ile/utils/SelectionManager/SelectionManager.js", + "openlibrary/plugins/openlibrary/js/index.js", + "openlibrary/plugins/openlibrary/js/loading-gradient.js", + "openlibrary/plugins/openlibrary/js/markdown-editor/index.js", + "openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestEditPage.js", + "openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestEditPageAuthor.js", + "openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestService.js", + "openlibrary/plugins/openlibrary/js/merge.js", + "openlibrary/plugins/openlibrary/js/modals/index.js", + "openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLogForms.js", + "openlibrary/plugins/openlibrary/js/password-toggle.js", + "openlibrary/plugins/openlibrary/js/pwa-install-prompt.js", + "openlibrary/plugins/openlibrary/js/service-worker-init.js", + "openlibrary/plugins/openlibrary/js/signup.js", + "scripts/build-icons.js", + "static/bookmarklets/bulk-import-ui.js", + "static/bookmarklets/isbn-utils.js", + "static/bookmarklets/list-api.js", + "static/js/preferences-handler.js", + "static/js/preferences.js", + "tests/unit/js/preferences.test.js", + "ui/components/StarRatings/StarRatings.js", + "webpack.config.css.js", + "webpack.config.js" + ], + rules: { + "semi": "off", + "space-before-function-paren": "off", + "comma-spacing": "off" + } + } +]; \ No newline at end of file diff --git a/openlibrary/components/MergeUI.vue b/openlibrary/components/MergeUI.vue index ff56fcd9913..253bff74ae0 100644 --- a/openlibrary/components/MergeUI.vue +++ b/openlibrary/components/MergeUI.vue @@ -52,13 +52,13 @@ </template> <script> -import MergeTable from './MergeUI/MergeTable.vue' +import MergeTable from './MergeUI/MergeTable.vue'; import { do_merge, update_merge_request, createMergeRequest, DEFAULT_EDITION_LIMIT } from './MergeUI/utils.js'; -const DO_MERGE = 'Do Merge' -const REQUEST_MERGE = 'Request Merge' -const LOADING = 'Loading...' -const SAVING = 'Saving...' +const DO_MERGE = 'Do Merge'; +const REQUEST_MERGE = 'Request Merge'; +const LOADING = 'Loading...'; +const SAVING = 'Saving...'; export default { name: 'App', @@ -81,17 +81,17 @@ export default { default: 'true', } }, - data() { + data () { return { url: new URL(location.toString()), mergeStatus: LOADING, mergeOutput: null, show_diffs: false, comment: '' - } + }; }, computed: { - olids() { + olids () { const olidsString = this.url.searchParams.get('records'); if (!olidsString) return []; return olidsString @@ -100,20 +100,20 @@ export default { .filter(Boolean); }, - isSuperLibrarian() { - return this.canmerge === 'true' + isSuperLibrarian () { + return this.canmerge === 'true'; }, - isDisabled() { - return this.mergeStatus !== DO_MERGE && this.mergeStatus !== REQUEST_MERGE + isDisabled () { + return this.mergeStatus !== DO_MERGE && this.mergeStatus !== REQUEST_MERGE; }, - showRejectButton() { - return this.mrid && this.isSuperLibrarian + showRejectButton () { + return this.mrid && this.isSuperLibrarian; } }, - mounted() { - const readyCta = this.isSuperLibrarian ? DO_MERGE : REQUEST_MERGE + mounted () { + const readyCta = this.isSuperLibrarian ? DO_MERGE : REQUEST_MERGE; this.$watch( '$refs.mergeTable.merge', (new_value) => { @@ -122,7 +122,7 @@ export default { ); }, methods: { - async doMerge() { + async doMerge () { if (!this.$refs.mergeTable.merge) return; const { record: master, dupes, editions_to_move, unmergeable_works } = this.$refs.mergeTable.merge; @@ -141,10 +141,10 @@ export default { } this.mergeOutput = await r.json(); if (this.mrid) { - await update_merge_request(this.mrid, 'approve', this.comment) + await update_merge_request(this.mrid, 'approve', this.comment); } else { - const workIds = [master.key].concat(Array.from(dupes, item => item.key)) - await createMergeRequest(workIds) + const workIds = [master.key].concat(Array.from(dupes, item => item.key)); + await createMergeRequest(workIds); } } catch (e) { this.mergeOutput = e.message; @@ -153,25 +153,25 @@ export default { } } else { // Create a new merge request with "pending" status - const workIds = [master.key].concat(Array.from(dupes, item => item.key)) - const splitKey = master.key.split('/') - const primaryRecord = splitKey[splitKey.length - 1] + const workIds = [master.key].concat(Array.from(dupes, item => item.key)); + const splitKey = master.key.split('/'); + const primaryRecord = splitKey[splitKey.length - 1]; await createMergeRequest(workIds, primaryRecord, 'create-pending', this.comment) .then(response => response.json()) .then(data => { if (data.status === 'ok') { // Redirect to merge table on success: - window.location.replace(`/merges#mrid-${data.id}`) + window.location.replace(`/merges#mrid-${data.id}`); } - }) + }); } this.mergeStatus = 'Done'; }, - async rejectMerge() { + async rejectMerge () { try { - await update_merge_request(this.mrid, 'decline', this.comment) - this.mergeOutput = 'Merge request closed' + await update_merge_request(this.mrid, 'decline', this.comment); + this.mergeOutput = 'Merge request closed'; } catch (e) { this.mergeOutput = e.message; throw e; @@ -179,7 +179,7 @@ export default { this.mergeStatus = 'Reject Merge'; } } -} +}; </script> <style> diff --git a/openlibrary/components/MergeUI/AuthorRoleTable.vue b/openlibrary/components/MergeUI/AuthorRoleTable.vue index a057644f017..9efda1993f3 100644 --- a/openlibrary/components/MergeUI/AuthorRoleTable.vue +++ b/openlibrary/components/MergeUI/AuthorRoleTable.vue @@ -54,9 +54,9 @@ export default { roles: Array }, computed: { - fields() { + fields () { return _.uniq(_.flatMap(this.roles, Object.keys)).sort(); } } -} +}; </script> diff --git a/openlibrary/components/MergeUI/EditionSnippet.vue b/openlibrary/components/MergeUI/EditionSnippet.vue index 0de0a239f5c..270bd12b4c7 100644 --- a/openlibrary/components/MergeUI/EditionSnippet.vue +++ b/openlibrary/components/MergeUI/EditionSnippet.vue @@ -61,17 +61,17 @@ export default { edition: Object }, computed: { - publish_year() { + publish_year () { if (!this.edition.publish_date) return ''; const m = this.edition.publish_date.match(/\d{4}/); return m ? m[0] : null; }, - publishers() { + publishers () { return this.edition.publishers || []; }, - number_of_pages() { + number_of_pages () { if (this.edition.number_of_pages) { return this.edition.number_of_pages; } else if (this.edition.pagination) { @@ -82,17 +82,17 @@ export default { return '?'; }, - full_title() { + full_title () { let title = this.edition.title; if (this.edition.subtitle) title += `: ${this.edition.subtitle}`; return title; }, - cover_id() { + cover_id () { return this.edition.covers?.[0] ?? null; }, - cover_url() { + cover_url () { if (this.cover_id) return `https://covers.openlibrary.org/b/id/${this.cover_id}-M.jpg`; const ocaid = this.edition.ocaid; @@ -102,13 +102,13 @@ export default { return ''; }, - languages() { + languages () { if (!this.edition.languages) return '???'; const langs = this.edition.languages.map(lang => lang.key.split('/')[2]); return langs.join(', '); }, - asins() { + asins () { return _.uniq([ ...((this.edition.identifiers && this.edition.identifiers.amazon) || []), this.edition.isbn_10 && ISBN.asIsbn10(this.edition.isbn_10), @@ -118,7 +118,7 @@ export default { }, methods: { - openEnlargedCover() { + openEnlargedCover () { let url = ''; if (this.cover_id) { url = `https://covers.openlibrary.org/b/id/${this.cover_id}.jpg`; diff --git a/openlibrary/components/MergeUI/ExcerptsTable.vue b/openlibrary/components/MergeUI/ExcerptsTable.vue index 013f3ad0f0c..ddfecfd1baf 100644 --- a/openlibrary/components/MergeUI/ExcerptsTable.vue +++ b/openlibrary/components/MergeUI/ExcerptsTable.vue @@ -37,9 +37,9 @@ export default { excerpts: Array }, computed: { - fields() { + fields () { return _.uniq(_.flatMap(this.excerpts, Object.keys)); } } -} +}; </script> diff --git a/openlibrary/components/MergeUI/MergeRow.vue b/openlibrary/components/MergeUI/MergeRow.vue index 01df7bcbf21..6a17706ef11 100644 --- a/openlibrary/components/MergeUI/MergeRow.vue +++ b/openlibrary/components/MergeUI/MergeRow.vue @@ -88,7 +88,7 @@ export default { type: Boolean } }, - data() { + data () { return { master_key: null }; diff --git a/openlibrary/components/MergeUI/MergeRowField.vue b/openlibrary/components/MergeUI/MergeRowField.vue index 2742122f31c..dc9a2c9db13 100644 --- a/openlibrary/components/MergeUI/MergeRowField.vue +++ b/openlibrary/components/MergeUI/MergeRowField.vue @@ -157,7 +157,7 @@ export default { } }, computed: { - title() { + title () { let title = `.${this.field}`; if (this.value instanceof Array) { const length = this.value.length; diff --git a/openlibrary/components/MergeUI/MergeRowJointField.vue b/openlibrary/components/MergeUI/MergeRowJointField.vue index 502e7aa49f5..27e3cb9507e 100644 --- a/openlibrary/components/MergeUI/MergeRowJointField.vue +++ b/openlibrary/components/MergeUI/MergeRowJointField.vue @@ -40,7 +40,7 @@ export default { } }, computed: { - presentFields() { + presentFields () { return this.fields.filter(f => f in this.record); } } diff --git a/openlibrary/components/MergeUI/TextDiff.vue b/openlibrary/components/MergeUI/TextDiff.vue index 91773141e6b..0359a824c17 100644 --- a/openlibrary/components/MergeUI/TextDiff.vue +++ b/openlibrary/components/MergeUI/TextDiff.vue @@ -25,7 +25,7 @@ export default { } }, computed: { - diff() { + diff () { const fn = { char: diffChars, word: diffWordsWithSpace, @@ -33,7 +33,7 @@ export default { return fn[this.resolution](this.left, this.right); } } -} +}; </script> <style scoped> diff --git a/openlibrary/components/ObservationForm.vue b/openlibrary/components/ObservationForm.vue index 30754a9ba6a..f74214eb4c0 100644 --- a/openlibrary/components/ObservationForm.vue +++ b/openlibrary/components/ObservationForm.vue @@ -30,11 +30,11 @@ </template> <script> -import CategorySelector from './ObservationForm/components/CategorySelector.vue' -import SavedTags from './ObservationForm/components/SavedTags.vue' -import ValueCard from './ObservationForm/components/ValueCard.vue' +import CategorySelector from './ObservationForm/components/CategorySelector.vue'; +import SavedTags from './ObservationForm/components/SavedTags.vue'; +import ValueCard from './ObservationForm/components/ValueCard.vue'; -import { decodeAndParseJSON, resizeColorbox } from './ObservationForm/Utils' +import { decodeAndParseJSON, resizeColorbox } from './ObservationForm/Utils'; export default { name: 'ObservationForm', @@ -84,7 +84,7 @@ export default { required: true } }, - data: function() { + data: function () { return { /** * An object representing the currently selected tag type. @@ -113,7 +113,7 @@ export default { * An array containing all book tag types and values. */ observationsArray: null, - } + }; }, computed: { /** @@ -121,28 +121,28 @@ export default { * * @returns {Number|null} The ID of the selected observation, if one exists. */ - getSelectedId: function() { + getSelectedId: function () { if (this.selectedObservation) { return this.selectedObservation.id; } - return null + return null; } }, - created: function() { + created: function () { this.observationsArray = decodeAndParseJSON(this.schema)['observations']; this.allSelectedValues = decodeAndParseJSON(this.observations); this.selectRandomObservation(); }, - mounted: function() { + mounted: function () { this.observer = new ResizeObserver(() => { resizeColorbox(); }); - this.observer.observe(this.$refs.form) + this.observer.observe(this.$refs.form); }, - beforeUnmount: function() { + beforeUnmount: function () { if (this.observer) { - this.observer.disconnect() + this.observer.disconnect(); } }, methods: { @@ -151,18 +151,18 @@ export default { * * @param {Object | null} observation The new selected observation, or `null` if no type is selected. */ - updateSelected: function(observation) { - this.selectedObservation = observation + updateSelected: function (observation) { + this.selectedObservation = observation; }, /** * Randomly sets a selected observation. */ - selectRandomObservation: function() { + selectRandomObservation: function () { const randomNumber = Math.floor(Math.random() * 100000); this.selectedObservation = this.observationsArray[randomNumber % this.observationsArray.length]; } } -} +}; </script> <style scoped> diff --git a/openlibrary/components/ObservationForm/components/CardBody.vue b/openlibrary/components/ObservationForm/components/CardBody.vue index d268713cc92..3ba378e914b 100644 --- a/openlibrary/components/ObservationForm/components/CardBody.vue +++ b/openlibrary/components/ObservationForm/components/CardBody.vue @@ -14,9 +14,9 @@ </template> <script> -import OLChip from './OLChip.vue' +import OLChip from './OLChip.vue'; -import { updateObservation } from '../ObservationService' +import { updateObservation } from '../ObservationService'; export default { name: 'CardBody', @@ -81,8 +81,8 @@ export default { /** * Returns an array of all of this book tag type's currently selected values. */ - selectedValues: function() { - return this.allSelectedValues[this.type]?.length ? this.allSelectedValues[this.type] : [] + selectedValues: function () { + return this.allSelectedValues[this.type]?.length ? this.allSelectedValues[this.type] : []; } }, methods: { @@ -94,19 +94,19 @@ export default { * @param {boolean} isSelected `true` if a chip is selected, `false` otherwise. * @param {String} text The text that the updated chip is displaying. */ - updateSelected: function(isSelected, text) { - let updatedValues = this.allSelectedValues[this.type] ? this.allSelectedValues[this.type] : [] + updateSelected: function (isSelected, text) { + let updatedValues = this.allSelectedValues[this.type] ? this.allSelectedValues[this.type] : []; if (isSelected) { if (this.multiSelect) { - updatedValues.push(text) + updatedValues.push(text); updateObservation('add', this.type, text, this.workKey, this.username) .catch(() => { updatedValues.pop(); }) .finally(() => { this.allSelectedValues[this.type] = updatedValues; - }) + }); } else { if (updatedValues.length) { let deleteSuccessful = false; @@ -118,13 +118,13 @@ export default { if (deleteSuccessful) { updateObservation('add', this.type, text, this.workKey, this.username) .then(() => { - updatedValues = [text] + updatedValues = [text]; }) .finally(() => { this.allSelectedValues[this.type] = updatedValues; - }) + }); } - }) + }); } } } else { @@ -133,11 +133,11 @@ export default { updateObservation('delete', this.type, text, this.workKey, this.username) .catch(() => { updatedValues.push(text); - }) + }); } } } -} +}; </script> <style scoped> diff --git a/openlibrary/components/ObservationForm/components/CardHeader.vue b/openlibrary/components/ObservationForm/components/CardHeader.vue index 4fb3262e8bf..091553b6459 100644 --- a/openlibrary/components/ObservationForm/components/CardHeader.vue +++ b/openlibrary/components/ObservationForm/components/CardHeader.vue @@ -18,7 +18,7 @@ export default { required: true } }, -} +}; </script> <style scoped> diff --git a/openlibrary/components/ObservationForm/components/CategorySelector.vue b/openlibrary/components/ObservationForm/components/CategorySelector.vue index b9e541ac529..b89204a3cc3 100644 --- a/openlibrary/components/ObservationForm/components/CategorySelector.vue +++ b/openlibrary/components/ObservationForm/components/CategorySelector.vue @@ -27,7 +27,7 @@ </template> <script> -import OLChip from './OLChip.vue' +import OLChip from './OLChip.vue'; export default { name: 'CategorySelector', @@ -74,7 +74,7 @@ export default { default: 0 } }, - data: function() { + data: function () { return { /** * The ID of the selected book tag type. @@ -82,7 +82,7 @@ export default { * @type {number | null} */ selectedId: this.initialSelectedId, - } + }; }, methods: { /** @@ -91,20 +91,20 @@ export default { * @param {boolean} isSelected Whether or not a chip is currently selected. * @param {String} text The text displayed by a chip. */ - updateSelected: function(isSelected, text) { + updateSelected: function (isSelected, text) { if (isSelected) { // TODO: This for loop shouldn't be necessary for (let i = 0; i < this.observationsArray.length; ++i) { if (this.observationsArray[i].label === text) { this.selectedId = this.observationsArray[i].id; - this.$emit('update-selected', this.observationsArray[i]) + this.$emit('update-selected', this.observationsArray[i]); } } } else { this.selectedId = null; // Set ObservationForm's selected observation to null - this.$emit('update-selected', null) + this.$emit('update-selected', null); } }, /** @@ -112,8 +112,8 @@ export default { * * @param {number} id A chip's id. */ - isSelected: function(id) { - return this.selectedId === id + isSelected: function (id) { + return this.selectedId === id; }, /** * Returns an HTML code denoting what symbol to display in a book tag type chip. @@ -123,7 +123,7 @@ export default { * * @returns {String} An HTML code representing selections of a type. */ - displaySymbol: function(type) { + displaySymbol: function (type) { if (this.allSelectedValues[type] && this.allSelectedValues[type].length) { // ✔ - Heavy checkmark return '✔'; @@ -131,7 +131,7 @@ export default { return '•'; } } -} +}; </script> <style scoped> diff --git a/openlibrary/components/ObservationForm/components/OLChip.vue b/openlibrary/components/ObservationForm/components/OLChip.vue index e5f58967a2e..45cbfc180b7 100644 --- a/openlibrary/components/ObservationForm/components/OLChip.vue +++ b/openlibrary/components/ObservationForm/components/OLChip.vue @@ -51,7 +51,7 @@ export default { default: '' } }, - data: function() { + data: function () { return { /** * Tracks whether this chip is currently selected. @@ -59,7 +59,7 @@ export default { * @type {boolean} */ isSelected: this.selected - } + }; }, computed: { /** @@ -67,20 +67,20 @@ export default { * * @returns 'click' if this chip can be selected, otherwise `null` */ - canSelect: function() { + canSelect: function () { return this.selectable ? 'click' : null; } }, watch: { selected (newValue) { - this.isSelected = newValue + this.isSelected = newValue; } }, methods: { /** * Toggles the value of `isSelected` and fires an `update-selected` event. */ - onClick: function() { + onClick: function () { this.toggleSelected(); /** * Update selected event. @@ -88,16 +88,16 @@ export default { * @property {boolean} isSelected Selected status of this chip. * @property {String} text Main text displayed by this chip. */ - this.$emit('update-selected', this.isSelected, this.text) + this.$emit('update-selected', this.isSelected, this.text); }, /** * Toggles the state of `isSelected` */ - toggleSelected: function() { + toggleSelected: function () { this.isSelected = !this.isSelected; } } -} +}; </script> <style scoped> diff --git a/openlibrary/components/ObservationForm/components/SavedTags.vue b/openlibrary/components/ObservationForm/components/SavedTags.vue index 0d023c3d1a5..ecf79dba6dd 100644 --- a/openlibrary/components/ObservationForm/components/SavedTags.vue +++ b/openlibrary/components/ObservationForm/components/SavedTags.vue @@ -38,9 +38,9 @@ </template> <script> -import OLChip from './OLChip.vue' +import OLChip from './OLChip.vue'; -import { updateObservation } from '../ObservationService' +import { updateObservation } from '../ObservationService'; export default { @@ -80,7 +80,7 @@ export default { required: true } }, - data: function() { + data: function () { return { /** * Contains class strings for each selected book tag @@ -94,18 +94,18 @@ export default { * @type {Object} */ classLists: {} - } + }; }, computed: { /** * An array of a patron's book tags. */ - selectedValues: function() { + selectedValues: function () { const results = []; for (const type in this.allSelectedValues) { for (const value of this.allSelectedValues[type]) { - results.push(`${type}: ${value}`) + results.push(`${type}: ${value}`); } } @@ -118,8 +118,8 @@ export default { * * @param {String} chipText The text of the selected tag chip, in the form "<type>: <value>" */ - removeItem: function(chipText) { - const [type, value] = chipText.split(': ') + removeItem: function (chipText) { + const [type, value] = chipText.split(': '); const valueIndex = this.allSelectedValues[type].indexOf(value); const valueArr = this.allSelectedValues[type]; @@ -131,9 +131,9 @@ export default { }) .finally(() => { if (valueArr.length === 0) { - delete this.allSelectedValues[type] + delete this.allSelectedValues[type]; } - }) + }); // Remove hover class: this.removeHoverClass(chipText); @@ -143,7 +143,7 @@ export default { * * @param {String} value The chip's key. */ - addHoverClass: function(value) { + addHoverClass: function (value) { this.classLists[value] = 'hover'; }, /** @@ -151,8 +151,8 @@ export default { * * @param {String} value The chip's key. */ - removeHoverClass: function(value) { - this.classLists[value] = '' + removeHoverClass: function (value) { + this.classLists[value] = ''; }, /** * Returns the class list string for the chip with the given key. @@ -160,11 +160,11 @@ export default { * @param {String} value The chip's key * @returns The chip's class list string. */ - getClassList: function(value) { - return this.classLists[value] ? this.classLists[value] : '' + getClassList: function (value) { + return this.classLists[value] ? this.classLists[value] : ''; } } -} +}; </script> <style scoped> diff --git a/openlibrary/components/ObservationForm/components/ValueCard.vue b/openlibrary/components/ObservationForm/components/ValueCard.vue index 3a9ca41a81f..5fdded40018 100644 --- a/openlibrary/components/ObservationForm/components/ValueCard.vue +++ b/openlibrary/components/ObservationForm/components/ValueCard.vue @@ -16,7 +16,7 @@ </template> <script> -import CardBody from './CardBody.vue' +import CardBody from './CardBody.vue'; import CardHeader from './CardHeader.vue'; export default { @@ -54,7 +54,7 @@ export default { values: { type: Array, required: true, - validator: function(arr) { + validator: function (arr) { for (const item of arr) { if (typeof(item) !== 'string') { return false; @@ -94,7 +94,7 @@ export default { required: true } }, -} +}; </script> <style scoped> diff --git a/openlibrary/plugins/openlibrary/js/book-page-lists.js b/openlibrary/plugins/openlibrary/js/book-page-lists.js index 9eae0945579..f7393047512 100644 --- a/openlibrary/plugins/openlibrary/js/book-page-lists.js +++ b/openlibrary/plugins/openlibrary/js/book-page-lists.js @@ -6,7 +6,7 @@ import { buildPartialsUrl } from './utils'; * * @param elem {HTMLElement} Container for book page lists section */ -export function initListsSection(elem) { +export function initListsSection (elem) { // Show loading indicator const loadingIndicator = elem.querySelector('.loadingIndicator'); loadingIndicator.classList.remove('hidden'); @@ -67,7 +67,7 @@ export function initListsSection(elem) { * Initialize private buttons after the lists section has been loaded * @param {HTMLElement} container - The container that now has the loaded content */ -function initPrivateButtonsAfterLoad(container) { +function initPrivateButtonsAfterLoad (container) { const privateButtons = container.querySelectorAll( '.list-follow-card__private-button', ); @@ -80,7 +80,7 @@ function initPrivateButtonsAfterLoad(container) { } } -async function fetchPartials(workId, editionId) { +async function fetchPartials (workId, editionId) { const params = {}; if (workId) { params.workId = workId; diff --git a/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js b/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js index 35333ed4378..2126881bf4a 100644 --- a/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js +++ b/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js @@ -3,12 +3,12 @@ * * @param {NodeList<HTMLElement>} crumbs - NodeList of breadcrumb select elements. */ -export function initBreadcrumbSelect(crumbs) { +export function initBreadcrumbSelect (crumbs) { const allowedKeys = new Set(['Tab', 'Enter', ' ']); const preventedKeys = new Set(['ArrowUp', 'ArrowDown']); // watch crumbs for changes, // ensures it's a full value change, not a user exploring options via keyboard - function handleNavEvents(nav) { + function handleNavEvents (nav) { let ignoreChange = false; nav.addEventListener('change', () => { diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js index 48932b2da62..a3635cf1a35 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js @@ -37,7 +37,7 @@ export class MenuOption { * @param {OptionState} optionState * @param {Number} taggedWorksCount Number of selected works which have the given tag */ - constructor(tag, optionState, taggedWorksCount) { + constructor (tag, optionState, taggedWorksCount) { /** * Reference to the root element of this MenuOption. * @@ -79,7 +79,7 @@ export class MenuOption { * Must be called before an event handler can be attached to * this menu option */ - initialize() { + initialize () { this.createMenuOption(); } @@ -89,7 +89,7 @@ export class MenuOption { * Stores newly created element as `rootElement`. The new element is not * attached to the DOM, and does not yet have any attached event handlers. */ - createMenuOption() { + createMenuOption () { const parentElem = document.createElement('div'); parentElem.classList.add('selected-tag'); @@ -119,7 +119,7 @@ export class MenuOption { /** * Removes this MenuOption from the DOM. */ - remove() { + remove () { this.rootElement.remove(); } @@ -133,7 +133,7 @@ export class MenuOption { * @see {@link MenuOptionState} * @see {initialize} */ - updateMenuOptionState(menuOptionState) { + updateMenuOptionState (menuOptionState) { if (this.rootElement) { // `rootElement` not set until `initialize` is called this.optionState = menuOptionState; @@ -178,7 +178,7 @@ export class MenuOption { * * Fires an `option-hidden` event when this is called. */ - hide() { + hide () { this.rootElement.classList.add('hidden'); this.rootElement.dispatchEvent(new CustomEvent('option-hidden')); } @@ -186,14 +186,14 @@ export class MenuOption { /** * Shows this menu option. */ - show() { + show () { this.rootElement.classList.remove('hidden'); } /** * Stages the selected menu option. */ - stage() { + stage () { this.rootElement.classList.add('selected-tag--staged'); } } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js index 1755625e819..302cf0ed3d8 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js @@ -17,7 +17,7 @@ export class SortedMenuOptionContainer { * * @param {HTMLElement} element The container */ - constructor(element) { + constructor (element) { this.rootElement = element; this.sortedMenuOptions = []; } @@ -27,7 +27,7 @@ export class SortedMenuOptionContainer { * * @param {...MenuOption} menuOptions Menu options to be added to the container. */ - add(...menuOptions) { + add (...menuOptions) { for (const option of menuOptions) { const index = this.findIndex(option); this.sortedMenuOptions.splice(index, 0, option); @@ -41,7 +41,7 @@ export class SortedMenuOptionContainer { * @param {MenuOption} menuOption The option being attached to the DOM. * @param {Number} index The index where the given option will be inserted. */ - updateViewOnAdd(menuOption, index) { + updateViewOnAdd (menuOption, index) { if (index === 0) { this.rootElement.prepend(menuOption.rootElement); } else { @@ -55,7 +55,7 @@ export class SortedMenuOptionContainer { * * @param {...MenuOption} menuOptions Options that are to be removed from this container */ - remove(...menuOptions) { + remove (...menuOptions) { for (const option of menuOptions) { const index = this.findIndex(option); const removed = this.sortedMenuOptions.splice(index, 1); @@ -70,7 +70,7 @@ export class SortedMenuOptionContainer { * @param {MenuOption} menuOption * @returns {Number} Index where the given menu option should be inserted. */ - findIndex(menuOption) { + findIndex (menuOption) { let index = 0; // XXX : Binary search? @@ -106,7 +106,7 @@ export class SortedMenuOptionContainer { * @param {MenuOption} menuOption The object that we are searching for * @returns {boolean} `true` if a matching menu option exists in this container */ - contains(menuOption) { + contains (menuOption) { return this.sortedMenuOptions.some((option) => menuOption.tag.equals(option.tag), ); @@ -118,7 +118,7 @@ export class SortedMenuOptionContainer { * @param {Tag} tag * @returns {boolean} `true` if a menu option which represents the given tag is in this container. */ - containsOptionWithTag(tag) { + containsOptionWithTag (tag) { return this.sortedMenuOptions.some((option) => tag.equals(option.tag)); } @@ -128,14 +128,14 @@ export class SortedMenuOptionContainer { * @param {Tag} tag * @returns {MenuOption|undefined} The first matching menu option, or `undefined` if none were found. */ - findByTag(tag) { + findByTag (tag) { return this.sortedMenuOptions.find((option) => tag.equals(option.tag)); } /** * Removes all menu options from this container. */ - clear() { + clear () { while (this.sortedMenuOptions.length > 0) { this.sortedMenuOptions.pop(); } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js index 4c7ef444a73..bdffeaf38f8 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js @@ -3,7 +3,7 @@ * * @returns HTML for the bulk tagging form */ -export function renderBulkTagger() { +export function renderBulkTagger () { return `<form action="/tags/bulk_tag_works" method="post" class="bulk-tagging-form hidden"> <div class="form-header"> <p>Manage Subjects</p> diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js index 1dadf9d2bc3..397155236d3 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js @@ -33,7 +33,7 @@ export const subjectTypeMapping = { * @returns {Number} * @see {Array.sort} */ -export function compare(tagA, tagB) { +export function compare (tagA, tagB) { const lowerA = createComparableTag(tagA); const lowerB = createComparableTag(tagB); @@ -62,7 +62,7 @@ export function compare(tagA, tagB) { * @returns {Object} Tag-like object that is suitable to use for sorting comparisons. * @see {compare} */ -function createComparableTag(tag) { +function createComparableTag (tag) { return { tagName: tag.tagName.toLowerCase(), tagType: tag.tagType.toLowerCase(), @@ -88,7 +88,7 @@ export class Tag { * * @throws Will throw an error if both `tagType` and `displayType` are falsey */ - constructor(tagName, tagType = null, displayType = null) { + constructor (tagName, tagType = null, displayType = null) { if (!(tagType || displayType)) { throw new Error('Tag must have at least one type'); } @@ -105,7 +105,7 @@ export class Tag { * @returns {String} The corresponding technical tag type * @throws Will throw an error if the given type is unrecognized. */ - convertToType(displayType) { + convertToType (displayType) { const result = subjectTypeMapping[displayType]; if (!result) { throw new Error('Unrecognized `displayType` value'); @@ -121,7 +121,7 @@ export class Tag { * @returns {String} A type string that can be displayed in the UI * @throws Will throw an error if the given type is unrecognized */ - convertToDisplayType(tagType) { + convertToDisplayType (tagType) { const result = displayTypeMapping[tagType]; if (!result) { throw new Error('Unrecognized `tagType` value'); @@ -138,7 +138,7 @@ export class Tag { * @param {Tag} tag * @returns `true` if the given tag is considered equivalent to this tag. */ - equals(tag) { + equals (tag) { const lowerSelf = createComparableTag(this); const lowerTag = createComparableTag(tag); diff --git a/openlibrary/plugins/openlibrary/js/clampers.js b/openlibrary/plugins/openlibrary/js/clampers.js index b47e68f7210..8e8434c056a 100644 --- a/openlibrary/plugins/openlibrary/js/clampers.js +++ b/openlibrary/plugins/openlibrary/js/clampers.js @@ -2,7 +2,7 @@ * @param {NodeListOf<Element>} clampers * */ -export function initClampers(clampers) { +export function initClampers (clampers) { for (const clamper of clampers) { if (clamper.clientHeight === clamper.scrollHeight) { clamper.classList.remove('clamp'); diff --git a/openlibrary/plugins/openlibrary/js/compact-title/index.js b/openlibrary/plugins/openlibrary/js/compact-title/index.js index eb94b86201f..99da228c803 100644 --- a/openlibrary/plugins/openlibrary/js/compact-title/index.js +++ b/openlibrary/plugins/openlibrary/js/compact-title/index.js @@ -25,7 +25,7 @@ let mainTitleElem; * @param {HTMLElement} navbar The book page navbar component * @param {HTMLElement} title The compact title component */ -export function initCompactTitle(navbar, title) { +export function initCompactTitle (navbar, title) { mainTitleElem = document.querySelector( '.work-title-and-author.desktop .work-title', ); @@ -46,7 +46,7 @@ export function initCompactTitle(navbar, title) { * @param {HTMLElement} navbar The book page navbar component * @param {HTMLElement} title The compact title component */ -function onScroll(navbar, title) { +function onScroll (navbar, title) { const compactTitleBounds = title.getBoundingClientRect(); const navbarBounds = navbar.getBoundingClientRect(); const mainTitleBounds = mainTitleElem.getBoundingClientRect(); diff --git a/openlibrary/plugins/openlibrary/js/covers.js b/openlibrary/plugins/openlibrary/js/covers.js index 987af672df8..86e81cf25ea 100644 --- a/openlibrary/plugins/openlibrary/js/covers.js +++ b/openlibrary/plugins/openlibrary/js/covers.js @@ -8,7 +8,7 @@ import 'jquery-ui-touch-punch'; // this makes drag-to-reorder work on touch devi import { closePopup } from './utils'; //cover/change.html -export function initCoversChange() { +export function initCoversChange () { // Pull data from data-config of class "manageCovers" in covers/manage.html const data_config_json = $('.manageCovers').data('config'); const doc_type_key = data_config_json['key']; @@ -40,7 +40,7 @@ export function initCoversChange() { }); } -function add_iframe(selector, src) { +function add_iframe (selector, src) { $(selector) .append( '<iframe frameborder="0" height="580" width="580" marginheight="0" marginwidth="0" scrolling="auto"></iframe>', @@ -49,7 +49,7 @@ function add_iframe(selector, src) { .attr('src', src); } -function showLoadingIndicator() { +function showLoadingIndicator () { const loadingIndicator = document.querySelector('.loadingIndicator'); const formDivs = document.querySelectorAll('.ol-cover-form, .imageIntro'); @@ -60,7 +60,7 @@ function showLoadingIndicator() { } // covers/manage.html and covers/add.html -export function initCoversAddManage() { +export function initCoversAddManage () { $('.ol-cover-form').on('submit', () => { showLoadingIndicator(); }); @@ -77,7 +77,7 @@ export function initCoversAddManage() { // covers/saved.html // Uses parent.$ in place of $ where elements lie outside of the "saved" window -export function initCoversSaved() { +export function initCoversSaved () { // Save the new image // Pull data from data-config of class "imageSaved" in covers/saved.html const data_config_json = parent.$('.manageCovers').data('config'); @@ -124,7 +124,7 @@ export function initCoversSaved() { } // This function will be triggered when the user clicks the "Paste" button -async function pasteImage() { +async function pasteImage () { let formData = null; try { const clipboardItems = await navigator.clipboard.read(); @@ -180,7 +180,7 @@ async function pasteImage() { } } -export function initPasteForm(coverForm) { +export function initPasteForm (coverForm) { const pasteButton = coverForm.querySelector('#pasteButton'); let formData = null; diff --git a/openlibrary/plugins/openlibrary/js/dropper/Dropper.js b/openlibrary/plugins/openlibrary/js/dropper/Dropper.js index 02388936c2e..961f9bb4c90 100644 --- a/openlibrary/plugins/openlibrary/js/dropper/Dropper.js +++ b/openlibrary/plugins/openlibrary/js/dropper/Dropper.js @@ -29,7 +29,7 @@ export class Dropper { * * @param {HTMLElement} dropper Reference to the dropper's root element */ - constructor(dropper) { + constructor (dropper) { /** * References the root element of the dropper. * @@ -80,7 +80,7 @@ export class Dropper { /** * Adds click listener to dropper's toggle arrow. */ - initialize() { + initialize () { this.dropClick.addEventListener('click', () => { this.toggleDropper(); }); @@ -92,7 +92,7 @@ export class Dropper { * Subclasses of `Dropper` may override this to add * functionality that should occur on dropper open. */ - onOpen() {} + onOpen () {} /** * Function that is called after a dropper has closed. @@ -100,7 +100,7 @@ export class Dropper { * Subclasses of `Dropper` may override this to add * functionality that should occur on dropper close. */ - onClose() {} + onClose () {} /** * Function that is called when the drop-click affordance of @@ -108,7 +108,7 @@ export class Dropper { * * Subclasses of `Dropper` may override this as needed. */ - onDisabledClick() {} + onDisabledClick () {} /** * Closes dropper if opened; opens dropper if closed. @@ -119,7 +119,7 @@ export class Dropper { * Calls either `onOpen()` or `onClose()` after the dropper * has been toggled. */ - toggleDropper() { + toggleDropper () { if (this.isDropperDisabled) { this.onDisabledClick(); } else { @@ -144,7 +144,7 @@ export class Dropper { * Calls `onDisabledClick()` if this dropper is disabled. * Otherwise, closes dropper and calls `onClose()`. */ - closeDropper() { + closeDropper () { if (this.isDropperDisabled) { this.onDisabledClick(); } else { diff --git a/openlibrary/plugins/openlibrary/js/dropper/index.js b/openlibrary/plugins/openlibrary/js/dropper/index.js index 41421cc5968..5c4aec9886c 100644 --- a/openlibrary/plugins/openlibrary/js/dropper/index.js +++ b/openlibrary/plugins/openlibrary/js/dropper/index.js @@ -11,7 +11,7 @@ const droppers = []; * * @param {HTMLCollection<HTMLElement>} dropperElements */ -export function initDroppers(dropperElements) { +export function initDroppers (dropperElements) { for (const dropper of dropperElements) { droppers.push(dropper); @@ -56,7 +56,7 @@ export function initDroppers(dropperElements) { * close an open dropdown in a given container * @param {jQuery.Object} $container */ -function closeDropper($container) { +function closeDropper ($container) { $container.find('.dropdown').slideUp(25); // Legacy droppers $container.find('.generic-dropper__dropdown').slideUp(25); // New generic droppers $container.find('.arrow').removeClass('up'); @@ -72,7 +72,7 @@ function closeDropper($container) { * * @param {NodeList<HTMLElement>} dropperElements */ -export function initGenericDroppers(dropperElements) { +export function initGenericDroppers (dropperElements) { const genericDroppers = Array.from(dropperElements); // Close any open dropdown if the user clicks outside of component: diff --git a/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js b/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js index f83406c7cf2..89f135e3772 100644 --- a/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js +++ b/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js @@ -8,63 +8,63 @@ export default class EdtionNavBar { * * @param {HTMLElement} navbarWrapper */ - constructor(navbarWrapper) { + constructor (navbarWrapper) { /** * Reference to the parent element of the navbar. * @type {HTMLElement} */ - this.navbarWrapper = navbarWrapper + this.navbarWrapper = navbarWrapper; /** * The navbar * @type {HTMLElement} */ - this.navbarElem = navbarWrapper.querySelector('.work-menu') + this.navbarElem = navbarWrapper.querySelector('.work-menu'); /** * The mobile-only navigation arrow. Not guaranteed to exist. * @type {HTMLElement|null} */ - this.navArrowLeft = navbarWrapper.querySelector('.nav-arrow-left') + this.navArrowLeft = navbarWrapper.querySelector('.nav-arrow-left'); /** * The mobile-only navigation arrow. Not guaranteed to exist. * @type {HTMLElement|null} */ - this.navArrowRight = navbarWrapper.querySelector('.nav-arrow-right') + this.navArrowRight = navbarWrapper.querySelector('.nav-arrow-right'); /** * References each nav item in this navbar. * @type {Array<HTMLLIElement>} */ - this.navItems = Array.from(this.navbarElem.querySelectorAll('li')) + this.navItems = Array.from(this.navbarElem.querySelectorAll('li')); /** * Index of the currently selected nav item. * @type {number} */ - this.selectedIndex = 0 + this.selectedIndex = 0; /** * The nav items' target anchor elements. * @type {HTMLAnchorElement} */ - this.targetAnchors = [] + this.targetAnchors = []; - this.initialize() + this.initialize(); } /** * Adds the necessary event handlers to the navbar. */ - initialize() { + initialize () { // Add click listeners to navbar items: for (let i = 0; i < this.navItems.length; ++i) { this.navItems[i].addEventListener('click', () => { - this.selectedIndex = i - this.selectElement(this.navItems[i]) - }) + this.selectedIndex = i; + this.selectElement(this.navItems[i]); + }); // Add this nav item's target anchor to array: - this.targetAnchors.push(document.getElementById(this.navItems[i].children[0].hash.substring(1))) + this.targetAnchors.push(document.getElementById(this.navItems[i].children[0].hash.substring(1))); // Set selectedIndex to the correct value: if (this.navItems[i].classList.contains('selected')) { - this.selectedIndex = i + this.selectedIndex = i; } } @@ -72,43 +72,43 @@ export default class EdtionNavBar { if (this.navArrowLeft) { this.navArrowLeft.addEventListener('click', () => { if (this.selectedIndex > 0) { - --this.selectedIndex - this.navItems[this.selectedIndex].children[0].click() + --this.selectedIndex; + this.navItems[this.selectedIndex].children[0].click(); } - }) + }); } if (this.navArrowRight) { this.navArrowRight.addEventListener('click', () => { if (this.selectedIndex < this.navItems.length - 1) { // Simulate click on the next nav item: - ++this.selectedIndex - this.navItems[this.selectedIndex].children[0].click() + ++this.selectedIndex; + this.navItems[this.selectedIndex].children[0].click(); } - }) + }); } // Add scroll listener for position-aware nav item selection document.addEventListener('scroll', () => { - this.updateSelected() - }) + this.updateSelected(); + }); } /** * Determines this navbar's position on the page and updates the selected * nav item. */ - updateSelected() { - const navbarHeight = this.navbarWrapper.getBoundingClientRect().height + updateSelected () { + const navbarHeight = this.navbarWrapper.getBoundingClientRect().height; if (navbarHeight > 0) { - let i = this.navItems.length + let i = this.navItems.length; // 10 is for a little bit of padding while (--i > 0 && this.navbarWrapper.offsetTop + navbarHeight < (this.targetAnchors[i].offsetTop - 10)) { // Do nothing } if (i !== this.selectedIndex) { - this.selectedIndex = i - this.selectElement(this.navItems[i]) + this.selectedIndex = i; + this.selectElement(this.navItems[i]); } } } @@ -118,7 +118,7 @@ export default class EdtionNavBar { * * @param {HTMLElement} selectedItem Newly selected nav item */ - scrollNavbar(selectedItem) { + scrollNavbar (selectedItem) { // Note: We don't use the browser native scrollIntoView method because // that method scrolls _recursively_, so it also tries to scroll the // body to center the element on the screen, causing weird jitters. @@ -126,7 +126,7 @@ export default class EdtionNavBar { this.navbarElem.scrollTo({ left: selectedItem.offsetLeft - (this.navbarElem.clientWidth - selectedItem.offsetWidth) / 2, behavior: 'instant' - }) + }); } /** @@ -137,11 +137,11 @@ export default class EdtionNavBar { * * @param {HTMLLIElement} selectedElem Element corresponding to the 'selected' navbar item. */ - selectElement(selectedElem) { + selectElement (selectedElem) { for (const li of this.navItems) { - li.classList.remove('selected') + li.classList.remove('selected'); } - selectedElem.classList.add('selected') - this.scrollNavbar(selectedElem) + selectedElem.classList.add('selected'); + this.scrollNavbar(selectedElem); } } diff --git a/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js b/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js index 1342027e6d2..8b36a5766c1 100644 --- a/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js +++ b/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js @@ -11,7 +11,7 @@ const navbars = []; * * @param {HTMLCollection<HTMLElement>} navbarWrappers Each navbar found on the book page */ -export function initNavbars(navbarWrappers) { +export function initNavbars (navbarWrappers) { for (const wrapper of navbarWrappers) { const navbar = new EdtionNavBar(wrapper); navbars.push(navbar); @@ -26,7 +26,7 @@ export function initNavbars(navbarWrappers) { * something other then a scroll event (e.g. when * stickied to a new position). */ -export function updateSelectedNavItem() { +export function updateSelectedNavItem () { for (const navbar of navbars) { navbar.updateSelected(); } diff --git a/openlibrary/plugins/openlibrary/js/editions-table/index.js b/openlibrary/plugins/openlibrary/js/editions-table/index.js index 422d7d524e7..52e3dbe29be 100755 --- a/openlibrary/plugins/openlibrary/js/editions-table/index.js +++ b/openlibrary/plugins/openlibrary/js/editions-table/index.js @@ -4,7 +4,7 @@ import '../../../../../static/css/legacy-datatables.css'; const DEFAULT_LENGTH = 3; const LS_RESULTS_LENGTH_KEY = 'editions-table.resultsLength'; -export function initEditionsTable() { +export function initEditionsTable () { var rowCount; let currentLength; // Prevent reinitialization of the editions datatable @@ -30,7 +30,7 @@ export function initEditionsTable() { } }); - function toggleSorting(e) { + function toggleSorting (e) { $('#editions th span').html(''); $(e).find('span').html(' ↑'); if ($(e).hasClass('sorting_asc')) { diff --git a/openlibrary/plugins/openlibrary/js/following.js b/openlibrary/plugins/openlibrary/js/following.js index 2cfe1b24aaf..af99bd43bea 100644 --- a/openlibrary/plugins/openlibrary/js/following.js +++ b/openlibrary/plugins/openlibrary/js/following.js @@ -1,6 +1,6 @@ import { PersistentToast } from './Toast'; -export async function initAsyncFollowing(followForms) { +export async function initAsyncFollowing (followForms) { followForms.forEach((form) => { form.addEventListener('submit', async (e) => { e.preventDefault(); diff --git a/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js b/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js index e3315ea3a45..bf915cd8aa9 100644 --- a/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js +++ b/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js @@ -1,6 +1,6 @@ import { buildPartialsUrl } from './utils'; -export function initFulltextSearchSuggestion(fulltextSearchSuggestion) { +export function initFulltextSearchSuggestion (fulltextSearchSuggestion) { const isLoading = showLoadingIndicators(fulltextSearchSuggestion); if (isLoading) { const query = fulltextSearchSuggestion.dataset.query; @@ -8,7 +8,7 @@ export function initFulltextSearchSuggestion(fulltextSearchSuggestion) { } } -function showLoadingIndicators(fulltextSearchSuggestion) { +function showLoadingIndicators (fulltextSearchSuggestion) { let isLoading = false; const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator'); @@ -18,7 +18,7 @@ function showLoadingIndicators(fulltextSearchSuggestion) { } return isLoading; } -async function getPartials(fulltextSearchSuggestion, query) { +async function getPartials (fulltextSearchSuggestion, query) { return fetch(buildPartialsUrl('FulltextSearchSuggestion', { data: query })) .then((resp) => { if (resp.status !== 200) { @@ -68,6 +68,6 @@ async function getPartials(fulltextSearchSuggestion, query) { * * @returns {string} HTML for a retry link. */ -function renderRetryLink() { +function renderRetryLink () { return '<span class="fulltext-suggestions__retry">Failed to fetch fulltext search suggestions. <button type="button" style="border:none;background:none;color:blue;text-decoration:underline;cursor:pointer;">Retry?</button></span>'; } diff --git a/openlibrary/plugins/openlibrary/js/go-back-links.js b/openlibrary/plugins/openlibrary/js/go-back-links.js index 8028ae7fc3a..35930459b81 100644 --- a/openlibrary/plugins/openlibrary/js/go-back-links.js +++ b/openlibrary/plugins/openlibrary/js/go-back-links.js @@ -4,7 +4,7 @@ * * @param {NodeList<HTMLElement>} goBackLinks */ -export function initGoBackLinks(goBackLinks) { +export function initGoBackLinks (goBackLinks) { for (const link of goBackLinks) { link.addEventListener('click', () => { if (history.length > 2) { diff --git a/openlibrary/plugins/openlibrary/js/graphs/index.js b/openlibrary/plugins/openlibrary/js/graphs/index.js index 2245763067f..1db0fc43206 100644 --- a/openlibrary/plugins/openlibrary/js/graphs/index.js +++ b/openlibrary/plugins/openlibrary/js/graphs/index.js @@ -1,7 +1,7 @@ import options from './options.js'; import { loadEditionsGraph, loadGraphIfExists } from './plot'; -export function plotAdminGraphs() { +export function plotAdminGraphs () { loadGraphIfExists('editgraph', {}, 'edit(s) on'); loadGraphIfExists('membergraph', {}, 'new members(s) on'); loadGraphIfExists('works_minigraph', {}, ' works on '); @@ -13,7 +13,7 @@ export function plotAdminGraphs() { loadGraphIfExists('books-added-per-day', options.booksAdded); } -export function initHomepageGraphs() { +export function initHomepageGraphs () { loadGraphIfExists('visitors-graph', {}, 'unique visitors on', '#e44028'); loadGraphIfExists('members-graph', {}, 'new members on', '#748d36'); loadGraphIfExists('edits-graph', {}, 'catalog edits on', '#00636a'); @@ -21,13 +21,13 @@ export function initHomepageGraphs() { loadGraphIfExists('ebooks-graph', {}, 'ebooks borrowed on', '#35672e'); } -export function initPublishersGraph() { +export function initPublishersGraph () { if (document.getElementById('chartPubHistory')) { loadEditionsGraph('chartPubHistory', {}, 'editions in'); } } -export function init() { +export function init () { plotAdminGraphs(); initHomepageGraphs(); initPublishersGraph(); diff --git a/openlibrary/plugins/openlibrary/js/graphs/plot.js b/openlibrary/plugins/openlibrary/js/graphs/plot.js index b9e302f7869..a7d3fe356fe 100644 --- a/openlibrary/plugins/openlibrary/js/graphs/plot.js +++ b/openlibrary/plugins/openlibrary/js/graphs/plot.js @@ -15,7 +15,7 @@ import 'flot/jquery.flot.time.js'; * - http://localhost:8080/subjects/fantasy#sort=date_published&ebooks=true * - http://localhost:8080/publishers/Barnes_&_Noble */ -export function loadEditionsGraph() { +export function loadEditionsGraph () { var data, options, placeholder, plot, dateFrom, dateTo, previousPoint; data = [ { @@ -57,7 +57,7 @@ export function loadEditionsGraph() { }; placeholder = $('#chartPubHistory'); - function showTooltip(x, y, contents) { + function showTooltip (x, y, contents) { $(`<div id="chartLabel">${contents}</div>`) .css({ position: 'absolute', @@ -125,7 +125,7 @@ export function loadEditionsGraph() { applyDateFilter(yearFrom, yearTo); }); - function applyDateFilter( + function applyDateFilter ( yearFrom, yearTo, hideSelector = '.chartUnzoom', @@ -161,7 +161,7 @@ export function loadEditionsGraph() { } } -export function plot_minigraph(node, data) { +export function plot_minigraph (node, data) { var options = { series: { lines: { @@ -182,7 +182,7 @@ export function plot_minigraph(node, data) { $.plot(node, [data], options); } -export function plot_tooltip_graph( +export function plot_tooltip_graph ( node, data, tooltip_message, @@ -223,7 +223,7 @@ export function plot_tooltip_graph( graph = $.plot(node, [data], options); - function showTooltip(x, y, contents) { + function showTooltip (x, y, contents) { $(`<div id="chartLabelA">${contents}</div>`) .css({ position: 'absolute', @@ -268,7 +268,7 @@ export function plot_tooltip_graph( * @param {string} [color] in hexidecimal to apply to the bars of a tooltip graph. * Ignored if options and no tooltip_message is passed. */ -export function loadGraph( +export function loadGraph ( id, options = {}, tooltip_message = '', @@ -307,7 +307,7 @@ export function loadGraph( * @param {string} [color] in hexidecimal to apply to the bars of a tooltip graph. * Ignored if options and no tooltip_message is passed. */ -export function loadGraphIfExists(id, options, tooltip_message, color) { +export function loadGraphIfExists (id, options, tooltip_message, color) { if ($(`#${id}`).length) { loadGraph(id, options, tooltip_message, color); } diff --git a/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js b/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js index 258df735694..4d2f8eba2d4 100644 --- a/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js +++ b/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js @@ -3,13 +3,13 @@ * * @param {*} element - The element to be modified by the handleMessageEvent function. */ -export function initMessageEventListener(element) { +export function initMessageEventListener (element) { /** * Handles messages from archive.org and performs actions based on the message type. * * @param {MessageEvent} e - The message event. */ - function handleMessageEvent(e) { + function handleMessageEvent (e) { if (!/[./]archive\.org$$/.test(e.origin)) return; if (e.data.type === 'resize') { diff --git a/openlibrary/plugins/openlibrary/js/idValidation.js b/openlibrary/plugins/openlibrary/js/idValidation.js index 090d58b3495..982bf84ab4f 100644 --- a/openlibrary/plugins/openlibrary/js/idValidation.js +++ b/openlibrary/plugins/openlibrary/js/idValidation.js @@ -3,7 +3,7 @@ * @param {String} isbn ISBN string for parsing * @returns {String} parsed isbn string */ -export function parseIsbn(isbn) { +export function parseIsbn (isbn) { return isbn.replace(/[ -]/g, ''); } @@ -13,7 +13,7 @@ export function parseIsbn(isbn) { * @param {String} isbn ISBN string to check * @returns {boolean} true if the isbn has a valid format */ -export function isFormatValidIsbn10(isbn) { +export function isFormatValidIsbn10 (isbn) { const regex = /^[0-9]{9}[0-9X]$/; return regex.test(isbn); } @@ -24,7 +24,7 @@ export function isFormatValidIsbn10(isbn) { * @param {String} isbn ISBN string for validating * @returns {boolean} true if ISBN string is a valid ISBN 10 */ -export function isChecksumValidIsbn10(isbn) { +export function isChecksumValidIsbn10 (isbn) { const chars = isbn.replace('X', 'A').split(''); chars.reverse(); @@ -42,7 +42,7 @@ export function isChecksumValidIsbn10(isbn) { * @param {String} isbn ISBN string to check * @returns {boolean} true if the isbn has a valid format */ -export function isFormatValidIsbn13(isbn) { +export function isFormatValidIsbn13 (isbn) { const regex = /^[0-9]{13}$/; return regex.test(isbn); } @@ -53,7 +53,7 @@ export function isFormatValidIsbn13(isbn) { * @param {String} isbn ISBN string for validating * @returns {Boolean} true if ISBN string is a valid ISBN 13 */ -export function isChecksumValidIsbn13(isbn) { +export function isChecksumValidIsbn13 (isbn) { const chars = isbn.split(''); const sum = chars .map((char, idx) => ((idx % 2) * 2 + 1) * parseInt(char, 10)) @@ -69,7 +69,7 @@ export function isChecksumValidIsbn13(isbn) { * @param {String} lccn LCCN string for parsing * @returns {String} parsed LCCN string */ -export function parseLccn(lccn) { +export function parseLccn (lccn) { // cleaning initial lccn entry const parsed = lccn // any alpha characters need to be lowercase @@ -98,7 +98,7 @@ export function parseLccn(lccn) { * @param {String} lccn LCCN string to test for valid syntax * @returns {boolean} true if given LCCN is valid syntax, false otherwise */ -export function isValidLccn(lccn) { +export function isValidLccn (lccn) { // matching parsed entry to regex representing valid lccn // regex taken from /openlibrary/utils/lccn.py const regex = /^([a-z]|[a-z]?([a-z]{2}|[0-9]{2})|[a-z]{2}[0-9]{2})?[0-9]{8}$/; @@ -110,7 +110,7 @@ export function isValidLccn(lccn) { * @param {String} oclc OCLC string for parsing * @returns {String} parsed OCLC string */ -export function parseOclc(oclc) { +export function parseOclc (oclc) { // cleaning initial oclc entry return ( oclc @@ -130,7 +130,7 @@ export function parseOclc(oclc) { * @param {String} oclc OCLC string to test for valid syntax * @returns {boolean} true if given OCLC is valid syntax, false otherwise */ -export function isValidOclc(oclc) { +export function isValidOclc (oclc) { // matching parsed entry to regex representing valid oclc const regex = /^[1-9][0-9]*$/; return regex.test(oclc); @@ -145,7 +145,7 @@ export function isValidOclc(oclc) { * @param {String} newId New identifier entry to be checked * @returns {boolean} true if the new identifier has already been entered */ -export function isIdDupe(idEntries, newId) { +export function isIdDupe (idEntries, newId) { // check each current entry value against new identifier return Array.from(idEntries).some((entry) => entry['value'] === newId); } diff --git a/openlibrary/plugins/openlibrary/js/ile/utils/ol.js b/openlibrary/plugins/openlibrary/js/ile/utils/ol.js index 872e593723d..374ff134517 100644 --- a/openlibrary/plugins/openlibrary/js/ile/utils/ol.js +++ b/openlibrary/plugins/openlibrary/js/ile/utils/ol.js @@ -11,7 +11,7 @@ import uniqBy from 'lodash/uniqBy'; * @param {WorkOLID} old_work * @param {WorkOLID} new_work */ -export async function move_to_work(edition_ids, old_work, new_work) { +export async function move_to_work (edition_ids, old_work, new_work) { for (const olid of edition_ids) { const url = `/books/${olid}.json`; const record = await fetch(url).then((r) => r.json()); @@ -30,7 +30,7 @@ export async function move_to_work(edition_ids, old_work, new_work) { * @param {AuthorOLID} old_author * @param {AuthorOLID} new_author */ -export async function move_to_author(work_ids, old_author, new_author) { +export async function move_to_author (work_ids, old_author, new_author) { for (const olid of work_ids) { const url = `/works/${olid}.json`; const record = await fetch(url).then((r) => r.json()); diff --git a/openlibrary/plugins/openlibrary/js/interstitial.js b/openlibrary/plugins/openlibrary/js/interstitial.js index a7fbc577c9c..4d0dcfd03fb 100644 --- a/openlibrary/plugins/openlibrary/js/interstitial.js +++ b/openlibrary/plugins/openlibrary/js/interstitial.js @@ -1,4 +1,4 @@ -export function initInterstitial(elem) { +export function initInterstitial (elem) { let seconds = elem.dataset.wait; const url = elem.dataset.url; const timerElement = elem.querySelector('#timer'); diff --git a/openlibrary/plugins/openlibrary/js/isbnOverride.js b/openlibrary/plugins/openlibrary/js/isbnOverride.js index 4cd039cb5d1..18f4adb4259 100644 --- a/openlibrary/plugins/openlibrary/js/isbnOverride.js +++ b/openlibrary/plugins/openlibrary/js/isbnOverride.js @@ -9,13 +9,13 @@ */ export const isbnOverride = { data: null, - set(isbnData) { + set (isbnData) { this.data = isbnData; }, - get() { + get () { return this.data; }, - clear() { + clear () { this.data = null; }, }; diff --git a/openlibrary/plugins/openlibrary/js/jquery.repeat.js b/openlibrary/plugins/openlibrary/js/jquery.repeat.js index 36f9642bf31..04b1690cb3a 100644 --- a/openlibrary/plugins/openlibrary/js/jquery.repeat.js +++ b/openlibrary/plugins/openlibrary/js/jquery.repeat.js @@ -6,7 +6,7 @@ import Template from './template'; * * Used in addbook process. */ -export function init() { +export function init () { // used in books/edit/exercpt, books/edit/web and books/edit/edition $.fn.repeat = function (options) { var addSelector, removeSelector, id, elems, t, code, nextRowId; @@ -21,7 +21,7 @@ export function init() { template: $(`${id}-template`), }; - function createTemplate(selector) { + function createTemplate (selector) { code = $(selector) .html() .replace(/%7B%7B/gi, '<%=') @@ -38,7 +38,7 @@ export function init() { * object representing. * @return {object} data mapping names to values */ - function formdata() { + function formdata () { var data = {}; $(':input', elems.form).each(function () { var $e = $(this), @@ -60,7 +60,7 @@ export function init() { * Creates a removable `repeat-item`. * @param {jQuery.Event} event */ - function onAdd(event) { + function onAdd (event) { var data, newid; const isbnOverrideData = isbnOverride.get(); event.preventDefault(); @@ -100,7 +100,7 @@ export function init() { elems._this.trigger('repeat-add'); } - function onRemove(event) { + function onRemove (event) { event.preventDefault(); $(this).parents('.repeat-item').eq(0).remove(); elems._this.trigger('repeat-remove'); diff --git a/openlibrary/plugins/openlibrary/js/jsdef.js b/openlibrary/plugins/openlibrary/js/jsdef.js index e31b72bac4e..43e144d3038 100644 --- a/openlibrary/plugins/openlibrary/js/jsdef.js +++ b/openlibrary/plugins/openlibrary/js/jsdef.js @@ -21,7 +21,7 @@ import { cond, truncate } from './utils'; */ //used in templates/lib/pagination.html -export function range(begin, end, step) { +export function range (begin, end, step) { var r, i; step = step || 1; if (end === undefined) { @@ -42,7 +42,7 @@ export function range(begin, end, step) { * > " - ".join(["a", "b", "c"]) * a - b - c */ -export function join(items) { +export function join (items) { return items.join(this); } @@ -51,12 +51,12 @@ export function join(items) { */ // used in templates/admin/loans.html -export function len(array) { +export function len (array) { return array.length; } // used in templates/type/permission/edit.html -export function enumerate(a) { +export function enumerate (a) { var b = new Array(a.length); var i; for (i in a) { @@ -65,7 +65,7 @@ export function enumerate(a) { return b; } -export function ForLoop(parent, seq) { +export function ForLoop (parent, seq) { this.parent = parent; this.seq = seq; @@ -91,7 +91,7 @@ ForLoop.prototype.next = function () { }; // used in plugins/upstream/jsdef.py -export function foreach(seq, parent_loop, callback) { +export function foreach (seq, parent_loop, callback) { var loop = new ForLoop(parent_loop, seq); var i, args, j; @@ -113,7 +113,7 @@ export function foreach(seq, parent_loop, callback) { } // used in templates/lists/widget.html -export function websafe(value) { +export function websafe (value) { // Safari 6 is failing with weird javascript error in this function. // Added try-catch to avoid it. try { @@ -132,7 +132,7 @@ export function websafe(value) { * Quote a string * @param {string|number} text to quote */ -export function htmlquote(text) { +export function htmlquote (text) { // This code exists for compatibility with template.js text = String(text); text = text.replace(/&/g, '&'); // Must be done first! @@ -143,7 +143,7 @@ export function htmlquote(text) { return text; } -export function is_jsdef() { +export function is_jsdef () { return true; } @@ -156,11 +156,11 @@ export function is_jsdef() { * @param {string} key - the key to get from the object * @param {any} def - the default value to return if the key isn't found */ -export function jsdef_get(obj, key, def = null) { +export function jsdef_get (obj, key, def = null) { return key in obj ? obj[key] : def; } -export function exposeGlobally() { +export function exposeGlobally () { // Extend existing prototypes String.prototype.join = join; diff --git a/openlibrary/plugins/openlibrary/js/lazy-carousel.js b/openlibrary/plugins/openlibrary/js/lazy-carousel.js index aba66337370..a17ac40038c 100644 --- a/openlibrary/plugins/openlibrary/js/lazy-carousel.js +++ b/openlibrary/plugins/openlibrary/js/lazy-carousel.js @@ -7,7 +7,7 @@ import { buildPartialsUrl } from './utils'; * * @param elems {NodeList<HTMLElement>} Collection of placeholder carousel elements */ -export function initLazyCarousel(elems) { +export function initLazyCarousel (elems) { // Create intersection observer const intersectionObserver = new IntersectionObserver(intersectionCallback, { root: null, @@ -34,7 +34,7 @@ export function initLazyCarousel(elems) { * @param data {object} * @returns {Promise<Response>} */ -async function fetchPartials(data) { +async function fetchPartials (data) { return fetch(buildPartialsUrl('LazyCarousel', { ...data })); } @@ -49,7 +49,7 @@ async function fetchPartials(data) { * * @param target {HTMLElement} A placeholder element for a carousel */ -function doFetchAndUpdate(target) { +function doFetchAndUpdate (target) { const config = JSON.parse(target.dataset.config); const loadingIndicator = target.querySelector('.loadingIndicator'); @@ -99,7 +99,7 @@ function doFetchAndUpdate(target) { * * @param target {Element} */ -function handleRetry(target) { +function handleRetry (target) { target.querySelector('.loadingIndicator').classList.remove('hidden'); target.querySelector('.lazy-carousel-retry').classList.add('hidden'); const carouselFallbackElem = target.querySelector('.lazy-carousel-fallback'); @@ -117,7 +117,7 @@ function handleRetry(target) { * @param entries {Array<IntersectionObserverEntry>} * @param observer {IntersectionObserver} */ -function intersectionCallback(entries, observer) { +function intersectionCallback (entries, observer) { entries.forEach((entry) => { if (entry.isIntersecting) { const target = entry.target; diff --git a/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js b/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js index 132306d0d10..d0819198494 100644 --- a/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js +++ b/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js @@ -19,7 +19,7 @@ import debounce from 'lodash/debounce'; * Currently only works with works, editions, and authors. */ export class LazyThingPreview { - constructor() { + constructor () { /** @type {Array<{key: string, render_fn: Function}>} */ this.queue = []; /** @type {Object<string, object>} */ @@ -28,7 +28,7 @@ export class LazyThingPreview { this.renderDebounced = debounce(this.render.bind(this), 100); } - init() { + init () { $('.lazy-thing-preview').each((i, el) => { this.push({ key: el.dataset.key, @@ -40,7 +40,7 @@ export class LazyThingPreview { /** * @param {{key: string, render_fn_name: string}} arg0 */ - push({ key, render_fn_name }) { + push ({ key, render_fn_name }) { const render_fn = window[render_fn_name]; if (this.cache[key]) { this.renderKey(key, render_fn, this.cache[key]); @@ -55,7 +55,7 @@ export class LazyThingPreview { * @param {Function} render_fn * @param {object} book */ - renderKey(key, render_fn, book) { + renderKey (key, render_fn, book) { const $el = $(`.lazy-thing-preview[data-key="${key}"]`); $el.html(render_fn(book)); } @@ -64,7 +64,7 @@ export class LazyThingPreview { * @param {string[]} keys * @returns {AsyncGenerator<object[]>} */ - async *getThings(keys) { + async *getThings (keys) { const workKeys = keys.filter((key) => key.startsWith('/works/')); const editionKeys = keys.filter((key) => key.startsWith('/books/')); const authorKeys = keys.filter((key) => key.startsWith('/authors/')); @@ -108,7 +108,7 @@ export class LazyThingPreview { } } - async render() { + async render () { const keys = this.queue.map(({ key }) => key); const render_fn_map = Object.fromEntries( this.queue.map(({ key, render_fn }) => [key, render_fn]), diff --git a/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js b/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js index ffa0d6287ea..4a036122388 100644 --- a/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js +++ b/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js @@ -8,7 +8,7 @@ let i18nStrings; * * @param {HTMLDetailsElement} rootElement */ -export function initLibrarianDashboard(rootElement) { +export function initLibrarianDashboard (rootElement) { i18nStrings = JSON.parse(rootElement.dataset.i18n); const table = rootElement.querySelector('.dq-table'); rootElement.addEventListener( @@ -26,7 +26,7 @@ export function initLibrarianDashboard(rootElement) { * @param {HTMLTableElement} table * @returns {Promise<void>} */ -async function populateTable(table) { +async function populateTable (table) { const bookCount = Number(table.dataset.totalBooks); const rows = table.querySelectorAll('.dq-table__row'); @@ -40,7 +40,7 @@ async function populateTable(table) { * @param {number} totalCount Total number of search results * @returns {Promise<void>} */ -async function updateRow(row, totalCount) { +async function updateRow (row, totalCount) { const queryFragment = row.dataset.queryFragment; const apiUrl = buildUrl(queryFragment, false); const searchPageUrl = buildUrl(queryFragment); @@ -87,7 +87,7 @@ async function updateRow(row, totalCount) { * @param {string} queryFragment * @param {boolean} forUi */ -function buildUrl(queryFragment, forUi = true) { +function buildUrl (queryFragment, forUi = true) { const match = window.location.pathname.match(/authors\/(OL\d+A)/); const queryParamString = match ? `?q=author_key:${match[1]}` @@ -104,7 +104,7 @@ function buildUrl(queryFragment, forUi = true) { * @param {HTMLTableRowElement} row * @param {string} newCellMarkup Markup for the new status cells */ -function replaceStatusCells(row, newCellMarkup) { +function replaceStatusCells (row, newCellMarkup) { const statusCells = row.querySelectorAll('td:not(.dq-table__criterion-cell)'); for (const cell of statusCells) { cell.remove(); @@ -124,7 +124,7 @@ function replaceStatusCells(row, newCellMarkup) { * * @returns {string} HTML string */ -function renderResultsCells(results, totalCount, failingHref) { +function renderResultsCells (results, totalCount, failingHref) { const numFound = results.numFound; const percentage = Math.floor(((totalCount - numFound) / totalCount) * 100); @@ -142,7 +142,7 @@ function renderResultsCells(results, totalCount, failingHref) { * * @returns {string} HTML string */ -function renderRetryCell() { +function renderRetryCell () { return `<td> <button class="dqs-run-again"> ${i18nStrings['reload']} @@ -156,7 +156,7 @@ function renderRetryCell() { * @param {string} href Link to search page for failing query * @returns {string} */ -function renderErrorCell(href) { +function renderErrorCell (href) { return `<td colspan="2"> <a href="${href}">${i18nStrings['error']}</a> </td>`; @@ -167,6 +167,6 @@ function renderErrorCell(href) { * * @returns {string} */ -function renderPendingCell() { +function renderPendingCell () { return `<td colspan="3">${i18nStrings['loading']}</td>`; } diff --git a/openlibrary/plugins/openlibrary/js/list_books.js b/openlibrary/plugins/openlibrary/js/list_books.js index 2455fec6099..db740cb2b01 100644 --- a/openlibrary/plugins/openlibrary/js/list_books.js +++ b/openlibrary/plugins/openlibrary/js/list_books.js @@ -3,21 +3,21 @@ export class ListBooks { * @param {HTMLElement} listBooks * @param {HTMLElement} layoutToolbar **/ - constructor(listBooks, layoutToolbar) { + constructor (listBooks, layoutToolbar) { this.listBooks = listBooks; this.layoutToolbar = layoutToolbar; this.activeLayout = this.layoutToolbar.querySelector('a.active'); } - attach() { + attach () { $(this.layoutToolbar).on('click', 'a', this.updateLayout.bind(this)); } /** * @param {MouseEvent} event */ - updateLayout(event) { + updateLayout (event) { event.preventDefault(); const layoutAnchor = event.target; this.layoutToolbar.querySelector('a.active').classList.remove('active'); @@ -27,7 +27,7 @@ export class ListBooks { document.cookie = `LBL=${layout}; path=/; max-age=31536000`; } - static init() { + static init () { // Assume only one list-books/layout per page new ListBooks( document.querySelector('.list-books'), diff --git a/openlibrary/plugins/openlibrary/js/lists/ListService.js b/openlibrary/plugins/openlibrary/js/lists/ListService.js index 7f501321c2c..7311c6e24f1 100644 --- a/openlibrary/plugins/openlibrary/js/lists/ListService.js +++ b/openlibrary/plugins/openlibrary/js/lists/ListService.js @@ -12,7 +12,7 @@ import { buildPartialsUrl } from '../utils'; * @param {object} data Object containing the new list's name, description, and seeds. * @returns {Promise<Response>} The results of the POST request */ -export async function createList(userKey, data) { +export async function createList (userKey, data) { return await fetch(`${userKey}/lists.json`, { method: 'post', headers: { @@ -30,7 +30,7 @@ export async function createList(userKey, data) { * @param {object} seed Object containing the new list's name, description, and seeds. * @returns {Promise<Response>} The result of the POST request */ -export async function addItem(listKey, seed) { +export async function addItem (listKey, seed) { const body = { add: [seed] }; return await fetch(`${listKey}/seeds.json`, { method: 'post', @@ -49,7 +49,7 @@ export async function addItem(listKey, seed) { * @param {string|{ key: string }} seed The item being removed from the list. * @returns {Promise<Response>} The POST response */ -export async function removeItem(listKey, seed) { +export async function removeItem (listKey, seed) { const body = { remove: [seed] }; return await fetch(`${listKey}/seeds.json`, { method: 'post', @@ -62,7 +62,7 @@ export async function removeItem(listKey, seed) { } // XXX : jsdoc -export async function getListPartials() { +export async function getListPartials () { return await fetch(buildPartialsUrl('MyBooksDropperLists'), { headers: { 'Content-Type': 'application/json', diff --git a/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js b/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js index fea3670f917..8af93fb5f9f 100644 --- a/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js +++ b/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js @@ -38,7 +38,7 @@ if (itemsWithDeleteSeed.length) { * @param {string} seed - path to seed book being removed, ex: /books/OL23269118M * @param {function} success - click function */ -function remove_seed(list_key, seed, success) { +function remove_seed (list_key, seed, success) { if (seed[0] === '/') { seed = { key: seed }; } @@ -63,7 +63,7 @@ function remove_seed(list_key, seed, success) { /** * @returns {number} count of number of seed books in a list */ -function get_seed_count() { +function get_seed_count () { return $('ul#listResults').children().length; } diff --git a/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js b/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js index 09cbe3cdc7e..ae71782188e 100644 --- a/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js +++ b/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js @@ -33,7 +33,7 @@ export class ShowcaseItem { * * @param {HTMLElement} showcaseElem */ - constructor(showcaseElem) { + constructor (showcaseElem) { /** * Reference to the root element of this component. * @member {HTMLElement} @@ -100,7 +100,7 @@ export class ShowcaseItem { * Attaches click listeners to the showcase item's "Remove from list" * affordance. */ - initialize() { + initialize () { this.removeFromListAffordance.addEventListener('click', (event) => { event.preventDefault(); this.removeShowcaseItem(); @@ -113,7 +113,7 @@ export class ShowcaseItem { * Removes any affiliated showcase items from the DOM, and updates all * dropper list affordances. */ - async removeShowcaseItem() { + async removeShowcaseItem () { await removeItem(this.listKey, this.seed) .then((response) => response.json()) .then(() => { @@ -147,7 +147,7 @@ export class ShowcaseItem { * Removes self from the myBooksStore's showcase array * upon success. */ - removeSelf() { + removeSelf () { const showcases = myBooksStore.getShowcases(); const thisIndex = showcases.indexOf(this); if (thisIndex >= 0) { @@ -167,7 +167,7 @@ export class ShowcaseItem { * * @param {boolean} showWorks `true` if only active showcase items related to works should be displayed */ - toggleVisibility(showWorks) { + toggleVisibility (showWorks) { if (this.isActiveShowcase) { if (showWorks) { if (this.isWork) { @@ -192,7 +192,7 @@ export class ShowcaseItem { * @param {string} seedKey * @return {boolean} `true` if the given keys match this item's keys */ - isShowcaseForListAndSeed(listKey, seedKey) { + isShowcaseForListAndSeed (listKey, seedKey) { return this.listKey === listKey && this.seedKey === seedKey; } } @@ -212,7 +212,7 @@ const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; * @param {string} seed * @returns {string} Type of the given seed key. */ -function getSeedType(seed) { +function getSeedType (seed) { // XXX : validate input? if (seed[0] !== '/') { return 'subject'; @@ -240,7 +240,7 @@ function getSeedType(seed) { * @param {string} [coverUrl] * @returns {HTMLLIElement} */ -export function createActiveShowcaseItem( +export function createActiveShowcaseItem ( listKey, seedKey, listTitle, @@ -287,7 +287,7 @@ export function createActiveShowcaseItem( * * @param {boolean} showWorksOnly */ -export function toggleActiveShowcaseItems(showWorksOnly) { +export function toggleActiveShowcaseItems (showWorksOnly) { for (const item of myBooksStore.getShowcases()) { item.toggleVisibility(showWorksOnly); } @@ -308,7 +308,7 @@ export function toggleActiveShowcaseItems(showWorksOnly) { * @param {string} listTitle * @param {string} [coverUrl] */ -export function attachNewActiveShowcaseItem( +export function attachNewActiveShowcaseItem ( listKey, seedKey, listTitle, diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js index 4142778d4d9..2bb68c4228e 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js @@ -19,7 +19,7 @@ export default class MergeRequestTable { * * @param {HTMLElement} mergeRequestTable */ - constructor(mergeRequestTable) { + constructor (mergeRequestTable) { /** * The `username` of the authenticated patron, or '' if logged out. * @@ -54,7 +54,7 @@ export default class MergeRequestTable { /** * Hydrates the librarian request table. */ - initialize() { + initialize () { this.tableHeader.initialize(); document.addEventListener('click', (event) => this.tableHeader.closeMenusIfClickOutside(event), diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js index 081f1eccbe8..256c5cabf60 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js @@ -19,7 +19,7 @@ export default class TableHeader { * * @param {HTMLElement} tableHeader */ - constructor(tableHeader) { + constructor (tableHeader) { /** * References to each select menu. These are always visible * in the header bar, and, when clicked, display a drop-down @@ -52,7 +52,7 @@ export default class TableHeader { /** * Hydrates the table header affordances. */ - initialize() { + initialize () { this.initFilters(); } @@ -62,7 +62,7 @@ export default class TableHeader { * @param {Event} event * @param {string} menuButtonId */ - toggleAMenuWhileClosingOthers(event, menuButtonId) { + toggleAMenuWhileClosingOthers (event, menuButtonId) { // prevent closing of menu on bubbling unless click menuButton itself if (event.target.id === menuButtonId) { // close other open menus, then toggle selected menu @@ -76,7 +76,7 @@ export default class TableHeader { * * @param {string} menuButtonId */ - closeOtherMenus(menuButtonId) { + closeOtherMenus (menuButtonId) { this.dropMenuButtons.forEach((menuButton) => { if (menuButton.id !== menuButtonId) { menuButton.firstElementChild.classList.add('hidden'); @@ -89,7 +89,7 @@ export default class TableHeader { * * @param {Event} event */ - filterMenuItems(event) { + filterMenuItems (event) { const input = document.getElementById(event.target.id); const filter = input.value.toUpperCase(); const menu = input.closest('.mr-dropdown-menu'); @@ -110,7 +110,7 @@ export default class TableHeader { * * @param {Event} event */ - closeMenusIfClickOutside(event) { + closeMenusIfClickOutside (event) { const menusClicked = Array.from(this.dropMenuButtons).filter( (menuButton) => { return menuButton.contains(event.target); @@ -126,7 +126,7 @@ export default class TableHeader { * Initialize events for merge queue filter dropdown menu functionality. * */ - initFilters() { + initFilters () { this.dropMenuButtons.forEach((menuButton) => { menuButton.addEventListener('click', (event) => { this.toggleAMenuWhileClosingOthers(event, menuButton.id); diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js index 79b27901463..4ba90586c99 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js @@ -14,7 +14,7 @@ import { let i18nStrings; -export function setI18nStrings(localizedStrings) { +export function setI18nStrings (localizedStrings) { i18nStrings = localizedStrings; } @@ -39,7 +39,7 @@ export class TableRow { * @param {HTMLElement} row Root element of a table row * @param {string} username `username` of logged-in patron. Empty if unauthenticated. */ - constructor(row, username) { + constructor (row, username) { /** * Reference to this row. * @@ -155,7 +155,7 @@ export class TableRow { /** * Hydrates interactive elements in this row. */ - initialize() { + initialize () { this.toggleCommentButton.addEventListener('click', () => this.toggleComments(), ); @@ -182,7 +182,7 @@ export class TableRow { * the full comments panel is hidden. This function toggles * each element's visibility. */ - toggleComments() { + toggleComments () { this.commentPreview.classList.toggle('hidden'); this.fullCommentsPanel.classList.toggle('hidden'); @@ -196,7 +196,7 @@ export class TableRow { * Closes the request linked to this row, and removes this * row from the DOM. */ - async closeRequest() { + async closeRequest () { const comment = prompt(i18nStrings['close_request_comment_prompt']); if (comment !== null) { // Comment will be `null` if "Cancel" button pressed @@ -219,7 +219,7 @@ export class TableRow { * * Updates the view on success. */ - async addComment() { + async addComment () { const comment = this.commentReplyInput.value.trim(); if (comment) { await commentOnRequest(this.mrid, comment) @@ -249,7 +249,7 @@ export class TableRow { * * @param {string} comment The newly added comment. */ - updateCommentViews(comment) { + updateCommentViews (comment) { const escapedComment = document.createTextNode(comment); // Update preview: @@ -274,7 +274,7 @@ export class TableRow { * * Hides the review button, and shows the assignee display. */ - async claimRequest() { + async claimRequest () { await claimRequest(this.mrid) .then((result) => result.json()) .then((data) => { @@ -291,7 +291,7 @@ export class TableRow { * * Hides the assignee display and shows the review button on success. */ - async unassignReviewer() { + async unassignReviewer () { await unassignRequest(this.mrid) .then((result) => result.json()) .then((data) => { diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/index.js b/openlibrary/plugins/openlibrary/js/merge-request-table/index.js index 0b710fb9fc4..113dfdf5daf 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/index.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/index.js @@ -5,7 +5,7 @@ import MergeRequestTable from './MergeRequestTable'; * * @param {HTMLElement} elem Reference to the queue's root element. */ -export function initLibrarianQueue(elem) { +export function initLibrarianQueue (elem) { const librarianQueue = new MergeRequestTable(elem); librarianQueue.initialize(); } diff --git a/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js b/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js index 877e7843792..ce3cfb95e9e 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js +++ b/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js @@ -27,7 +27,7 @@ export class CreateListForm { * * @param {HTMLElement} form */ - constructor(form) { + constructor (form) { /** * References this form's "Create List" button. * @@ -56,7 +56,7 @@ export class CreateListForm { /** * Attaches click listener to the "Create List" button. */ - initialize() { + initialize () { this.createListButton.addEventListener('click', (event) => { event.preventDefault(); this.createNewList(); @@ -78,7 +78,7 @@ export class CreateListForm { * * @async */ - async createNewList() { + async createNewList () { // Construct seed object for first list item: const listTitle = websafe(this.listTitleInput.value); const listDescription = websafe(this.listDescriptionInput.value); @@ -117,7 +117,7 @@ export class CreateListForm { * @param {string} listKey Key of the newly created list * @param {string} listTitle Title of the new list */ - updateDroppersOnListCreation(listKey, listTitle, coverUrl) { + updateDroppersOnListCreation (listKey, listTitle, coverUrl) { const droppers = myBooksStore.getDroppers(); const openDropper = myBooksStore.getOpenDropper(); @@ -135,7 +135,7 @@ export class CreateListForm { /** * Clears the list title and desciption fields in the form. */ - resetForm() { + resetForm () { this.listTitleInput.value = ''; this.listDescriptionInput.value = ''; } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js index ee87c8ed538..74b20b0016e 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js @@ -36,7 +36,7 @@ export class MyBooksDropper extends Dropper { * * @param {HTMLElement} dropper */ - constructor(dropper) { + constructor (dropper) { super(dropper); const dropperActionCallbacks = { @@ -97,7 +97,7 @@ export class MyBooksDropper extends Dropper { /** * Hydrates dropper contents and loads patron's lists. */ - initialize() { + initialize () { super.initialize(); this.readingLogForms.initialize(); @@ -117,7 +117,7 @@ export class MyBooksDropper extends Dropper { * @param {HTMLElement} loadingIndicator * @returns {NodeJS.Timer} */ - initLoadingAnimation(loadingIndicator) { + initLoadingAnimation (loadingIndicator) { let count = 0; const intervalId = setInterval(() => { let ellipsis = ''; @@ -137,7 +137,7 @@ export class MyBooksDropper extends Dropper { * * @param {string} partialHtml */ - updateReadingLists(partialHtml) { + updateReadingLists (partialHtml) { clearInterval(this.loadingAnimationId); this.replaceLoadingIndicators(this.loadingIndicator, partialHtml); } @@ -151,7 +151,7 @@ export class MyBooksDropper extends Dropper { * * @returns {Array<string>} */ - getSeedKeys() { + getSeedKeys () { const results = [this.readingLists.seedKey]; if (this.readingLists.workKey) { results.push(this.readingLists.workKey); @@ -172,7 +172,7 @@ export class MyBooksDropper extends Dropper { * @param {HTMLElement} dropperListsPlaceholder Loading indicator found inside of the dropdown content * @param {ListPartials} partials */ - replaceLoadingIndicators(dropperListsPlaceholder, partialHTML) { + replaceLoadingIndicators (dropperListsPlaceholder, partialHTML) { const dropperParent = dropperListsPlaceholder ? dropperListsPlaceholder.parentElement : null; @@ -195,7 +195,7 @@ export class MyBooksDropper extends Dropper { * * @param shelf {ReadingLogShelf} */ - updateShelfDisplay(shelf) { + updateShelfDisplay (shelf) { this.readingLogForms.updateActivatedStatus(true); this.readingLogForms.updatePrimaryBookshelfId(Number(shelf)); this.readingLogForms.updatePrimaryButtonText( @@ -220,7 +220,7 @@ export class MyBooksDropper extends Dropper { * * @override */ - onOpen() { + onOpen () { myBooksStore.setOpenDropper(this); } @@ -231,7 +231,7 @@ export class MyBooksDropper extends Dropper { * * @override */ - onDisabledClick() { + onDisabledClick () { window.location = `/account/login?redirect=${location.pathname}`; } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js index 90db2c813bb..a6a4aaf63d0 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js @@ -20,7 +20,7 @@ const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; * @param {Number} year * @returns `true` if the given year is a leap year. */ -function isLeapYear(year) { +function isLeapYear (year) { return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); } @@ -40,7 +40,7 @@ export class CheckInComponents { /** * @param checkInContainer */ - constructor(checkInContainer) { + constructor (checkInContainer) { // HTML for the check-in components is not rendered if // the patron is unauthenticated, or if the dropper // is for an orphaned edition. @@ -88,7 +88,7 @@ export class CheckInComponents { this.checkInForm = undefined; } - initialize() { + initialize () { this.checkInPrompt.initialize(); this.checkInPrompt .getRootElement() @@ -199,7 +199,7 @@ export class CheckInComponents { * * @returns {HTMLElement} */ - createModalContentFromTemplate() { + createModalContentFromTemplate () { const templateElem = document.createElement('template'); const modalContentTemplate = document.querySelector('#check-in-form-modal'); templateElem.innerHTML = modalContentTemplate.outerHTML; @@ -216,7 +216,7 @@ export class CheckInComponents { * @param {number|null} month * @param {number|null} day */ - updateDateAndShowDisplay(year, month = null, day = null) { + updateDateAndShowDisplay (year, month = null, day = null) { // Update last read date display let dateString = String(year); if (month) { @@ -252,7 +252,7 @@ export class CheckInComponents { * @param {string} url * @returns {Promise<Response>} */ - postCheckIn(eventData, url) { + postCheckIn (eventData, url) { return fetch(url, { method: 'POST', headers: { @@ -269,7 +269,7 @@ export class CheckInComponents { * @param {string} eventId * @returns {Promise<Response>} */ - async deleteCheckIn(eventId) { + async deleteCheckIn (eventId) { return fetch(`/check-ins/${eventId}`, { method: 'DELETE', }); @@ -283,7 +283,7 @@ export class CheckInComponents { * @param {number|null} day * @returns {CheckInEventPostRequestData} */ - prepareEventRequest(year, month = null, day = null) { + prepareEventRequest (year, month = null, day = null) { // Get event id const eventId = this.checkInForm.getEventId(); @@ -311,49 +311,49 @@ export class CheckInComponents { * * @returns {boolean} */ - hasReadDate() { + hasReadDate () { return !this.checkInDisplay.getRootElement().classList.contains('hidden'); } /** * Resets the check-in form. */ - resetForm() { + resetForm () { this.checkInForm.resetForm(); } /** * Show the check-in display. */ - showCheckInDisplay() { + showCheckInDisplay () { this.checkInDisplay.show(); } /** * Hide the check-in display. */ - hideCheckInDisplay() { + hideCheckInDisplay () { this.checkInDisplay.hide(); } /** * Show the check-in prompt. */ - showCheckInPrompt() { + showCheckInPrompt () { this.checkInPrompt.show(); } /** * Hide the check-in prompt. */ - hideCheckInPrompt() { + hideCheckInPrompt () { this.checkInPrompt.hide(); } /** * Closes the opened `colorbox` modal. */ - closeModal() { + closeModal () { $.colorbox.close(); } } @@ -368,11 +368,11 @@ class CheckInPrompt { /** * @param {HTMLElement} checkInPrompt */ - constructor(checkInPrompt) { + constructor (checkInPrompt) { this.rootElem = checkInPrompt; } - initialize() { + initialize () { const yearLink = this.rootElem.querySelector('.prompt-current-year'); yearLink.addEventListener('click', () => { // Get the current year @@ -400,7 +400,7 @@ class CheckInPrompt { * @param {number|null} month * @param {number|null} day */ - dispatchCheckInSubmission(year, month = null, day = null) { + dispatchCheckInSubmission (year, month = null, day = null) { const submitEvent = new CustomEvent('submit-check-in', { detail: { year: year, @@ -414,14 +414,14 @@ class CheckInPrompt { /** * Hides this check-in prompt. */ - hide() { + hide () { this.rootElem.classList.add('hidden'); } /** * Shows this check-in prompt. */ - show() { + show () { this.rootElem.classList.remove('hidden'); } @@ -429,7 +429,7 @@ class CheckInPrompt { * Returns reference to the root element of this check-in prompt. * @returns {HTMLElement} */ - getRootElement() { + getRootElement () { return this.rootElem; } } @@ -443,7 +443,7 @@ class CheckInDisplay { /** * @param {HTMLElement} checkInDisplay */ - constructor(checkInDisplay) { + constructor (checkInDisplay) { this.rootElem = checkInDisplay; this.dateDisplayElem = this.rootElem.querySelector('.check-in-date'); } @@ -453,28 +453,28 @@ class CheckInDisplay { * * @param {string} date */ - updateDateDisplay(date) { + updateDateDisplay (date) { this.dateDisplayElem.textContent = date; } /** * Hides this date display. */ - hide() { + hide () { this.rootElem.classList.add('hidden'); } /** * Shows this date display. */ - show() { + show () { this.rootElem.classList.remove('hidden'); } /** * @returns {HTMLElement} */ - getRootElement() { + getRootElement () { return this.rootElem; } } @@ -497,7 +497,7 @@ export class CheckInForm { * @param {string|null} lastReadDate * @param {number|null} eventId */ - constructor( + constructor ( formElem, workOlid, editionKey = null, @@ -568,7 +568,7 @@ export class CheckInForm { this.deleteButton = this.rootElem.querySelector('.check-in__delete-btn'); } - initialize() { + initialize () { // Set form's action this.rootElem.action = `/works/${this.workOlid}/check-ins.json`; // Set form's event ID @@ -654,7 +654,7 @@ export class CheckInForm { /** * Gets currently selected date, then updates the form. */ - onDateSelectionChange() { + onDateSelectionChange () { const year = this.yearSelect.selectedIndex ? Number(this.yearSelect.value) : null; @@ -672,7 +672,7 @@ export class CheckInForm { * @param {number|null} month * @param {number|null} day */ - updateSelectedDate(year = null, month = null, day = null) { + updateSelectedDate (year = null, month = null, day = null) { if (!month) { day = null; } @@ -717,7 +717,7 @@ export class CheckInForm { * * @param {number} daysInMonth */ - updateDayOptions(daysInMonth) { + updateDayOptions (daysInMonth) { for (let i = 0; i < this.daySelect.options.length; ++i) { if (i <= daysInMonth) { this.daySelect.options[i].classList.remove('hidden'); @@ -733,7 +733,7 @@ export class CheckInForm { * Unsets the `event_id` input value, hides the delete button, and * resets the date select elements to their default values. */ - resetForm() { + resetForm () { this.setEventId(''); this.updateSelectedDate(); this.hideDeleteButton(); @@ -742,14 +742,14 @@ export class CheckInForm { /** * Shows this form's delete button. */ - showDeleteButton() { + showDeleteButton () { this.deleteButton.classList.remove('invisible'); } /** * Hides this form's delete button. */ - hideDeleteButton() { + hideDeleteButton () { this.deleteButton.classList.add('invisible'); } @@ -758,7 +758,7 @@ export class CheckInForm { * * @returns {number|null} The selected year, or `null` if none selected */ - getSelectedYear() { + getSelectedYear () { return this.yearSelect.selectedIndex ? Number(this.yearSelect.value) : null; } @@ -767,7 +767,7 @@ export class CheckInForm { * * @returns {number|null} The selected month, or `null` if none selected */ - getSelectedMonth() { + getSelectedMonth () { return this.monthSelect.selectedIndex || null; } @@ -776,7 +776,7 @@ export class CheckInForm { * * @returns {number|null} The selected day, or `null` if none selected */ - getSelectedDay() { + getSelectedDay () { return this.daySelect.selectedIndex || null; } @@ -785,7 +785,7 @@ export class CheckInForm { * * @returns {string} */ - getEventId() { + getEventId () { return this.eventIdInput.value; } @@ -794,7 +794,7 @@ export class CheckInForm { * * @param value */ - setEventId(value) { + setEventId (value) { this.eventIdInput.value = value; } @@ -803,7 +803,7 @@ export class CheckInForm { * * @returns {string} */ - getEventType() { + getEventType () { return this.eventTypeInput.value; } @@ -812,7 +812,7 @@ export class CheckInForm { * * @returns {string} */ - getEditionKey() { + getEditionKey () { return this.editionKeyInput.value; } @@ -821,7 +821,7 @@ export class CheckInForm { * * @returns {string} */ - getFormAction() { + getFormAction () { return this.rootElem.action; } @@ -830,7 +830,7 @@ export class CheckInForm { * * @returns {HTMLFormElement} */ - getRootElement() { + getRootElement () { return this.rootElem; } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js index 778867eac80..7fef061ee1f 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js @@ -25,7 +25,7 @@ export class ReadingLists { * Adds functionality to the given dropper's list affordances. * @param {HTMLElement} dropper */ - constructor(dropper) { + constructor (dropper) { /** * References the given My Books Dropper root element. * @@ -95,7 +95,7 @@ export class ReadingLists { /** * Adds functionality to all of the dropper's list affordances. */ - initialize() { + initialize () { this.initModifyListAffordances( this.dropper.querySelectorAll('.modify-list'), ); @@ -117,7 +117,7 @@ export class ReadingLists { /** * Updates dropdown list affordances when an update occurs. */ - updateListDisplays() { + updateListDisplays () { const isWorkSelected = this.workCheckBox && this.workCheckBox.checked; for (const key of Object.keys(this.patronLists)) { const listData = this.patronLists[key]; @@ -140,7 +140,7 @@ export class ReadingLists { * @param {boolean} isListMember True if the item is on the list * @param {string} listKey Unique identifier for a list */ - toggleDisplayedType(isListMember, listKey) { + toggleDisplayedType (isListMember, listKey) { const listData = this.patronLists[listKey]; if (isListMember) { @@ -158,7 +158,7 @@ export class ReadingLists { * * @param {NodeList<HTMLElement>} modifyListElements */ - initModifyListAffordances(modifyListElements) { + initModifyListAffordances (modifyListElements) { for (const elem of modifyListElements) { const listItemKeys = elem.dataset.listItems; const listKey = elem.dataset.listKey; @@ -212,7 +212,7 @@ export class ReadingLists { * @param {string} listKey Unique key for list * @param {boolean} isAddingItem `true` if an item is being added to a list */ - async modifyList(listKey, isAddingItem) { + async modifyList (listKey, isAddingItem) { let seed; const isWork = this.workCheckBox && this.workCheckBox.checked; @@ -292,7 +292,7 @@ export class ReadingLists { * @param {boolean} isWork `true` if a work was added or removed * @param {boolean} wasItemAdded `true` if item was added to list */ - updateViewAfterModifyingList(listKey, isWork, wasItemAdded) { + updateViewAfterModifyingList (listKey, isWork, wasItemAdded) { if (isWork) { this.patronLists[listKey].workOnList = wasItemAdded; } else { @@ -310,7 +310,7 @@ export class ReadingLists { * * @param {HTMLElement} openListModalButton */ - addOpenListModalClickListener(openListModalButton) { + addOpenListModalClickListener (openListModalButton) { openListModalButton.addEventListener('click', (event) => { event.preventDefault(); @@ -332,7 +332,7 @@ export class ReadingLists { * @param {boolean} isActive `True` if this dropper's seed is on the list * @param {string} coverUrl URL for the list's cover image */ - onListCreationSuccess(listKey, listTitle, isActive, coverUrl) { + onListCreationSuccess (listKey, listTitle, isActive, coverUrl) { const dropperListAffordance = this.createDropdownListAffordance( listKey, listTitle, @@ -364,7 +364,7 @@ export class ReadingLists { * @param {boolean} isActive `true` if the seed is on this list * @returns {HTMLElement} Reference to the newly created element */ - createDropdownListAffordance(listKey, listTitle, isActive) { + createDropdownListAffordance (listKey, listTitle, isActive) { const itemMarkUp = `<span class="list__status-indicator"></span> <a href="${listKey}" class="modify-list dropper__close" data-list-cover-url="${listKey}" data-list-key="${listKey}">${listTitle}</a> `; @@ -394,7 +394,7 @@ export class ReadingLists { * * @returns {string} The seed key */ - getSeed() { + getSeed () { if (this.workCheckBox && this.workCheckBox.checked) { // seed is the work key: return this.workKey; diff --git a/openlibrary/plugins/openlibrary/js/my-books/index.js b/openlibrary/plugins/openlibrary/js/my-books/index.js index 25ea487319a..40328a09c6e 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/index.js +++ b/openlibrary/plugins/openlibrary/js/my-books/index.js @@ -11,7 +11,7 @@ import myBooksStore from './store'; // XXX : jsdoc // XXX : decompose -export function initMyBooksAffordances(dropperElements, showcaseElements) { +export function initMyBooksAffordances (dropperElements, showcaseElements) { const showcases = []; for (const elem of showcaseElements) { const showcase = new ShowcaseItem(elem); @@ -102,7 +102,7 @@ export function initMyBooksAffordances(dropperElements, showcaseElements) { * @param workKey {string} * @returns {MyBooksDropper|undefined} */ -export function findDropperForWork(workKey) { +export function findDropperForWork (workKey) { return myBooksStore.getDroppers().find((dropper) => { return workKey === dropper.workKey; }); diff --git a/openlibrary/plugins/openlibrary/js/my-books/store/index.js b/openlibrary/plugins/openlibrary/js/my-books/store/index.js index 78c6cb73e97..7aeb6258ccb 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/store/index.js +++ b/openlibrary/plugins/openlibrary/js/my-books/store/index.js @@ -11,7 +11,7 @@ class MyBooksStore { /** * Initializes the store. */ - constructor() { + constructor () { this._store = { droppers: [], showcases: [], @@ -23,56 +23,56 @@ class MyBooksStore { /** * @returns {Array<MyBooksDropper>} */ - getDroppers() { + getDroppers () { return this._store.droppers; } /** * @param {Array<MyBooksDropper>} droppers */ - setDroppers(droppers) { + setDroppers (droppers) { this._store.droppers = droppers; } /** * @returns {Array<ShowcaseItem>} */ - getShowcases() { + getShowcases () { return this._store.showcases; } /** * @param {Array<ShowcaseItem>} showcases */ - setShowcases(showcases) { + setShowcases (showcases) { this._store.showcases = showcases; } /** * @returns {string} */ - getUserKey() { + getUserKey () { return this._store.userKey; } /** * @param {string} userKey */ - setUserKey(userKey) { + setUserKey (userKey) { this._store.userKey = userKey; } /** * @returns {MyBooksDropper} */ - getOpenDropper() { + getOpenDropper () { return this._store.openDropper; } /** * @param {MyBooksDropper} dropper */ - setOpenDropper(dropper) { + setOpenDropper (dropper) { this._store.openDropper = dropper; } } diff --git a/openlibrary/plugins/openlibrary/js/native-dialog/index.js b/openlibrary/plugins/openlibrary/js/native-dialog/index.js index 37d3f80ecf1..743c290a68c 100644 --- a/openlibrary/plugins/openlibrary/js/native-dialog/index.js +++ b/openlibrary/plugins/openlibrary/js/native-dialog/index.js @@ -6,7 +6,7 @@ * 2. The dialog receives a `close-dialog` event. * @param {HTMLCollection<HTMLDialogElement>} elems */ -export function initDialogs(elems) { +export function initDialogs (elems) { for (const elem of elems) { elem.addEventListener('click', (event) => { // Event target exclusions needed for FireFox, which sets mouse positions to zero on @@ -36,7 +36,7 @@ export function initDialogs(elems) { * @param {HTMLDialogElement} dialog * @returns `true` if the click was out of bounds. */ -function isOutOfBounds(event, dialog) { +function isOutOfBounds (event, dialog) { const rect = dialog.getBoundingClientRect(); return ( event.clientX < rect.left || diff --git a/openlibrary/plugins/openlibrary/js/nonjquery_utils.js b/openlibrary/plugins/openlibrary/js/nonjquery_utils.js index 3b2c05bd4a3..350fc0ded8d 100644 --- a/openlibrary/plugins/openlibrary/js/nonjquery_utils.js +++ b/openlibrary/plugins/openlibrary/js/nonjquery_utils.js @@ -12,12 +12,12 @@ * @param {Boolean} [execAsap] * @returns {Function} */ -export function debounce(func, threshold = 100, execAsap = false) { +export function debounce (func, threshold = 100, execAsap = false) { let timeout; - return function debounced() { + return function debounced () { const obj = this, args = arguments; - function delayed() { + function delayed () { if (!execAsap) func.apply(obj, args); timeout = null; } diff --git a/openlibrary/plugins/openlibrary/js/offline-banner.js b/openlibrary/plugins/openlibrary/js/offline-banner.js index 6083652f731..e9dec3bf7b4 100644 --- a/openlibrary/plugins/openlibrary/js/offline-banner.js +++ b/openlibrary/plugins/openlibrary/js/offline-banner.js @@ -1,4 +1,4 @@ -export function initOfflineBanner() { +export function initOfflineBanner () { window.addEventListener('offline', () => { $('#offline-info').slideDown(); $('#offline-info').fadeTo(5000, 1).slideUp(); diff --git a/openlibrary/plugins/openlibrary/js/ol.analytics.js b/openlibrary/plugins/openlibrary/js/ol.analytics.js index 67fce11f4de..f022deac8bc 100644 --- a/openlibrary/plugins/openlibrary/js/ol.analytics.js +++ b/openlibrary/plugins/openlibrary/js/ol.analytics.js @@ -5,7 +5,7 @@ * */ -export default function initAnalytics() { +export default function initAnalytics () { var vs, i; var startTime = new Date(); if (window.archive_analytics) { @@ -52,7 +52,7 @@ export default function initAnalytics() { // NOTE: This might cause issues if this script is made async #4474 window.addEventListener( 'DOMContentLoaded', - function send_analytics_pageview() { + function send_analytics_pageview () { window.archive_analytics.send_pageview({}); }, ); diff --git a/openlibrary/plugins/openlibrary/js/ol.js b/openlibrary/plugins/openlibrary/js/ol.js index 92b42878f22..e9be0e3ee45 100644 --- a/openlibrary/plugins/openlibrary/js/ol.js +++ b/openlibrary/plugins/openlibrary/js/ol.js @@ -6,11 +6,11 @@ import { SearchModeSelector, mode as searchMode } from './SearchUtils'; /* Sets the key in the website cookie to the specified value */ -function setValueInCookie(key, value) { +function setValueInCookie (key, value) { document.cookie = `${key}=${value};path=/`; } -export default function init() { +export default function init () { const urlParams = getJsonFromUrl(location.search); if (urlParams.mode) { searchMode.write(urlParams.mode); @@ -26,17 +26,17 @@ export default function init() { initWebsiteTranslationOptions(); } -export function initBorrowAndReadLinks() { +export function initBorrowAndReadLinks () { // LOADING ONCLICK FUNCTIONS FOR BORROW AND READ LINKS // used in openlibrary/macros/AvailabilityButton.html and openlibrary/macros/LoanStatus.html - $(function(){ - $('.cta-btn--ia.cta-btn--borrow,.cta-btn--ia.cta-btn--read').on('click', function(){ + $(function (){ + $('.cta-btn--ia.cta-btn--borrow,.cta-btn--ia.cta-btn--read').on('click', function (){ $(this).removeClass('cta-btn cta-btn--available').addClass('cta-btn cta-btn--available--load'); }); }); - $(function(){ - $('#waitlist_ebook').on('click', function(){ + $(function (){ + $('#waitlist_ebook').on('click', function (){ $(this).removeClass('cta-btn cta-btn--unavailable').addClass('cta-btn cta-btn--unavailable--load'); }); }); @@ -44,7 +44,7 @@ export function initBorrowAndReadLinks() { } -export function initWebsiteTranslationOptions() { +export function initWebsiteTranslationOptions () { $('.locale-options li a').on('click', function (event) { event.preventDefault(); const locale = $(this).data('lang-id'); diff --git a/openlibrary/plugins/openlibrary/js/partner_ol_lib.js b/openlibrary/plugins/openlibrary/js/partner_ol_lib.js index 7f7cc55628c..d7aa2dac34c 100644 --- a/openlibrary/plugins/openlibrary/js/partner_ol_lib.js +++ b/openlibrary/plugins/openlibrary/js/partner_ol_lib.js @@ -1,7 +1,7 @@ /** * @param {string} container */ -function getIsbnToElementMap(container) { +function getIsbnToElementMap (container) { const reISBN = /(978)?[0-9]{9}[0-9X]/i; const elements = Array.from(document.querySelectorAll(container)); const isbnElementMap = {}; @@ -18,7 +18,7 @@ function getIsbnToElementMap(container) { * @param {string[]} isbnList * @returns {Promise<Array>} */ -async function getAvailabilityDataFromOpenLibrary(isbnList) { +async function getAvailabilityDataFromOpenLibrary (isbnList) { const apiBaseUrl = 'https://openlibrary.org/search.json'; const apiUrl = `${apiBaseUrl}?fields=*,availability&q=isbn:${isbnList.join('+OR+')}`; const response = await fetch(apiUrl); @@ -47,7 +47,7 @@ async function getAvailabilityDataFromOpenLibrary(isbnList) { * textOnBtn: "Open Library!" * }); */ -async function addOpenLibraryButtons(options) { +async function addOpenLibraryButtons (options) { const { bookContainer, selectorToPlaceBtnIn, textOnBtn } = options; if (bookContainer === undefined) { throw Error( diff --git a/openlibrary/plugins/openlibrary/js/patron_exports.js b/openlibrary/plugins/openlibrary/js/patron_exports.js index 7d9638ff88f..a4c41c20f8d 100644 --- a/openlibrary/plugins/openlibrary/js/patron_exports.js +++ b/openlibrary/plugins/openlibrary/js/patron_exports.js @@ -3,7 +3,7 @@ * * @param {HTMLElement} buttonElement */ -function disableButton(buttonElement) { +function disableButton (buttonElement) { buttonElement.setAttribute('disabled', 'true'); buttonElement.setAttribute('aria-disabled', 'true'); } @@ -16,7 +16,7 @@ function disableButton(buttonElement) { * * @param {NodeList<HTMLFormElement>} elems */ -export function initPatronExportForms(elems) { +export function initPatronExportForms (elems) { elems.forEach((form) => { const submitButton = form.querySelector('input[type=submit]'); form.addEventListener('submit', () => { diff --git a/openlibrary/plugins/openlibrary/js/private-button.js b/openlibrary/plugins/openlibrary/js/private-button.js index ce9000db51a..98d5fcc6d47 100644 --- a/openlibrary/plugins/openlibrary/js/private-button.js +++ b/openlibrary/plugins/openlibrary/js/private-button.js @@ -1,6 +1,6 @@ import { FadingToast } from './Toast'; -export function initPrivateButtons(buttons) { +export function initPrivateButtons (buttons) { buttons.forEach((button) => { button.addEventListener('click', (event) => { event.preventDefault(); diff --git a/openlibrary/plugins/openlibrary/js/python.js b/openlibrary/plugins/openlibrary/js/python.js index 2398f8d1d5a..cda5e3c9d76 100644 --- a/openlibrary/plugins/openlibrary/js/python.js +++ b/openlibrary/plugins/openlibrary/js/python.js @@ -7,7 +7,7 @@ * @param {mixed} n * @return {string} */ -export function commify(n) { +export function commify (n) { var text = n.toString(); var re = /(\d+)(\d{3})/; @@ -19,7 +19,7 @@ export function commify(n) { } // Implementation of Python urllib.urlencode in Javascript. -export function urlencode(query) { +export function urlencode (query) { var parts = []; var k; for (k in query) { @@ -28,7 +28,7 @@ export function urlencode(query) { return parts.join('&'); } -export function slice(array, begin, end) { +export function slice (array, begin, end) { var a = []; var i; for (i = begin; i < Math.min(array.length, end); i++) { diff --git a/openlibrary/plugins/openlibrary/js/reading-goals/index.js b/openlibrary/plugins/openlibrary/js/reading-goals/index.js index 1c8481e4a62..9e65ec67842 100644 --- a/openlibrary/plugins/openlibrary/js/reading-goals/index.js +++ b/openlibrary/plugins/openlibrary/js/reading-goals/index.js @@ -6,7 +6,7 @@ import { buildPartialsUrl } from '../utils'; * * @param {HTMLCollection<HTMLElement>} links Prompts for adding a reading goal */ -export function initYearlyGoalPrompt(links) { +export function initYearlyGoalPrompt (links) { for (const link of links) { if (!link.classList.contains('goal-set')) { link.addEventListener('click', onYearlyGoalClick); @@ -17,7 +17,7 @@ export function initYearlyGoalPrompt(links) { /** * Finds and shows the yearly goal modal. */ -function onYearlyGoalClick() { +function onYearlyGoalClick () { const yearlyGoalModal = document.querySelector('#yearly-goal-modal'); yearlyGoalModal.showModal(); } @@ -33,7 +33,7 @@ function onYearlyGoalClick() { * * @param {HTMLCollection<HTMLElement>} elems ELements which display only the current year */ -export function displayLocalYear(elems) { +export function displayLocalYear (elems) { const localYear = new Date().getFullYear(); for (const elem of elems) { const serverYear = Number(elem.dataset.serverYear); @@ -48,7 +48,7 @@ export function displayLocalYear(elems) { * * @param {HTMLCollection<HTMLElement>} editLinks Edit goal links */ -export function initGoalEditLinks(editLinks) { +export function initGoalEditLinks (editLinks) { for (const link of editLinks) { const parent = link.closest('.reading-goal-progress'); const modal = parent.querySelector('dialog'); @@ -64,7 +64,7 @@ export function initGoalEditLinks(editLinks) { * @param {HTMLElement} editLink An edit goal link * @param {HTMLDialogElement} modal The modal that will be shown */ -function addGoalEditClickListener(editLink, modal) { +function addGoalEditClickListener (editLink, modal) { editLink.addEventListener('click', () => { modal.showModal(); }); @@ -76,7 +76,7 @@ function addGoalEditClickListener(editLink, modal) { * * @param {HTMLCollection<HTMLElement>} submitButtons Submit goal buttons */ -export function initGoalSubmitButtons(submitButtons) { +export function initGoalSubmitButtons (submitButtons) { for (const button of submitButtons) { addGoalSubmissionListener(button); } @@ -89,7 +89,7 @@ export function initGoalSubmitButtons(submitButtons) { * the action set a new goal, or updated an existing goal. * @param {HTMLELement} submitButton Reading goal form submit button */ -function addGoalSubmissionListener(submitButton) { +function addGoalSubmissionListener (submitButton) { submitButton.addEventListener('click', (event) => { event.preventDefault(); @@ -170,7 +170,7 @@ function addGoalSubmissionListener(submitButton) { * @param {HTMLElement} elem A reading goal progress component * @param {Number} goal The new reading goal */ -function updateProgressComponent(elem, goal) { +function updateProgressComponent (elem, goal) { // Calculate new percentage: const booksReadSpan = elem.querySelector( '.reading-goal-progress__books-read', @@ -194,7 +194,7 @@ function updateProgressComponent(elem, goal) { * @param {NodeList} yearlyGoalElems Containers for progress components and reading goal links. * @param {string} goalYear Year that the goal is set for. */ -function fetchProgressAndUpdateViews(yearlyGoalElems, goalYear) { +function fetchProgressAndUpdateViews (yearlyGoalElems, goalYear) { fetch(buildPartialsUrl('ReadingGoalProgress', { year: goalYear })) .then((response) => { if (!response.ok) { diff --git a/openlibrary/plugins/openlibrary/js/readinglog_stats.js b/openlibrary/plugins/openlibrary/js/readinglog_stats.js index 84d95c0ee72..493d7239989 100644 --- a/openlibrary/plugins/openlibrary/js/readinglog_stats.js +++ b/openlibrary/plugins/openlibrary/js/readinglog_stats.js @@ -40,7 +40,7 @@ import 'chartjs-plugin-datalabels'; /** * @param {Config} config */ -export function init(config) { +export function init (config) { Chart.scaleService.updateScaleDefaults('linear', { ticks: { beginAtZero: true, stepSize: 1 }, }); @@ -53,7 +53,7 @@ export function init(config) { * @param {Element} container * @param {HTMLCanvasElement} canvas */ - function createWorkChart(config, chartConfig, container, canvas) { + function createWorkChart (config, chartConfig, container, canvas) { /** @type {{[key: string]: Work[]}} */ const grouped = {}; /** @type {Work[]} */ @@ -153,7 +153,7 @@ export function init(config) { }, ]; - function buildSparql(authors) { + function buildSparql (authors) { return ` SELECT DISTINCT ?x ?xLabel ?olid ${SPARQL_FIELDS.map( @@ -246,13 +246,13 @@ export function init(config) { * @param {string} key * @return {any} */ -function getPath(obj, key) { +function getPath (obj, key) { /** * @param {object} obj * @param {string[]} param1 * @return {any} */ - function main(obj, [head, ...rest]) { + function main (obj, [head, ...rest]) { if (typeof obj === 'undefined') return undefined; if (!head) return obj; if (head.endsWith('[]')) diff --git a/openlibrary/plugins/openlibrary/js/return-form/index.js b/openlibrary/plugins/openlibrary/js/return-form/index.js index ade5d8a1a45..c6b08b6d00f 100644 --- a/openlibrary/plugins/openlibrary/js/return-form/index.js +++ b/openlibrary/plugins/openlibrary/js/return-form/index.js @@ -4,7 +4,7 @@ * * @param {NodeList<HTMLElement>} returnForms */ -export function initReturnForms(returnForms) { +export function initReturnForms (returnForms) { for (const form of returnForms) { const i18nStrings = JSON.parse(form.dataset.i18n); form.addEventListener('submit', (event) => { diff --git a/openlibrary/plugins/openlibrary/js/search.js b/openlibrary/plugins/openlibrary/js/search.js index ca641c0507c..f99b2cecf81 100644 --- a/openlibrary/plugins/openlibrary/js/search.js +++ b/openlibrary/plugins/openlibrary/js/search.js @@ -11,7 +11,7 @@ import { buildPartialsUrl } from './utils'; * @param {Number} start_facet_count initial number of displayed facets * @param {Number} facet_inc number of hidden facets to be displayed */ -export function more(header, start_facet_count, facet_inc) { +export function more (header, start_facet_count, facet_inc) { const facetEntry = `div.${header} div.facetEntry`; const shown = $(`${facetEntry}:not(:hidden)`).length; const total = $(facetEntry).length; @@ -33,7 +33,7 @@ export function more(header, start_facet_count, facet_inc) { * @param {Number} start_facet_count initial number of displayed facets * @param {Number} facet_inc number of displayed facets to be hidden */ -export function less(header, start_facet_count, facet_inc) { +export function less (header, start_facet_count, facet_inc) { const facetEntry = `div.${header} div.facetEntry`; const shown = $(`${facetEntry}:not(:hidden)`).length; const total = $(facetEntry).length; @@ -66,7 +66,7 @@ export function less(header, start_facet_count, facet_inc) { * * @param {HTMLElement} facetsElem Root element of the search facets sidebar component */ -export async function initSearchFacets(facetsElem) { +export async function initSearchFacets (facetsElem) { const asyncLoad = facetsElem.dataset.asyncLoad; if (asyncLoad) { @@ -99,7 +99,7 @@ export async function initSearchFacets(facetsElem) { /** * Adds click listeners to the "show more" and "show less" facet affordances. */ -function hydrateFacets() { +function hydrateFacets () { const data_config_json = $('#searchFacets').data('config'); const start_facet_count = data_config_json['start_facet_count']; const facet_inc = data_config_json['facet_inc']; @@ -130,7 +130,7 @@ function hydrateFacets() { * * @throws Error when `/partials` response is not in 200-299 range. */ -function fetchPartials(param) { +function fetchPartials (param) { const data = { param: param, path: location.pathname, @@ -156,7 +156,7 @@ function fetchPartials(param) { * @param {string} markup HTML markup for a single element * @returns {HTMLElement} */ -function createElementFromMarkup(markup) { +function createElementFromMarkup (markup) { const template = document.createElement('template'); template.innerHTML = markup; return template.content.children[0]; @@ -169,7 +169,7 @@ function createElementFromMarkup(markup) { * @param {IntersectionObserverInit} options * @returns {Promise<void>} */ -async function whenVisible(elem, options = {}) { +async function whenVisible (elem, options = {}) { return new Promise((resolve) => { const intersectionObserver = new IntersectionObserver( (entries, observer) => { diff --git a/openlibrary/plugins/openlibrary/js/service-worker-matchers.js b/openlibrary/plugins/openlibrary/js/service-worker-matchers.js index ba1f41f99ad..fb19165e299 100644 --- a/openlibrary/plugins/openlibrary/js/service-worker-matchers.js +++ b/openlibrary/plugins/openlibrary/js/service-worker-matchers.js @@ -6,7 +6,7 @@ It is in a is a separate file to avoid this error when writing tests: > 1 | import { ExpirationPlugin } from 'workbox-expiration'; */ -export function matchMiscFiles({ url }) { +export function matchMiscFiles ({ url }) { const miscFiles = [ '/favicon.ico', '/static/manifest.json', @@ -16,28 +16,28 @@ export function matchMiscFiles({ url }) { return miscFiles.includes(url.pathname); } -export function matchSmallMediumCovers({ url }) { +export function matchSmallMediumCovers ({ url }) { const regex = /-[SM].jpg$/; return regex.test(url.pathname); } -export function matchLargeCovers({ url }) { +export function matchLargeCovers ({ url }) { const regex = /-L.jpg$/; return regex.test(url.pathname); } -export function matchStaticImages({ url }) { +export function matchStaticImages ({ url }) { const regex = /^\/images\/|^\/static\/images\//; return regex.test(url.pathname); } -export function matchStaticBuild({ url }) { +export function matchStaticBuild ({ url }) { const regex = /^\/static\/build\/.*(\.js|\.css)/; const localhost = url.origin.includes('localhost'); return !localhost && regex.test(url.pathname); } -export function matchArchiveOrgImage({ url }) { +export function matchArchiveOrgImage ({ url }) { // most importantly, to cache your profile picture from loading every time // also caches some covers return url.href.startsWith('https://archive.org/services/img/'); diff --git a/openlibrary/plugins/openlibrary/js/star-ratings/index.js b/openlibrary/plugins/openlibrary/js/star-ratings/index.js index 94e1f72053c..a57a2ade1f4 100644 --- a/openlibrary/plugins/openlibrary/js/star-ratings/index.js +++ b/openlibrary/plugins/openlibrary/js/star-ratings/index.js @@ -2,7 +2,7 @@ import { findDropperForWork } from '../my-books'; import { ReadingLogShelves } from '../my-books/MyBooksDropper/ReadingLogForms'; import { FadingToast } from '../Toast.js'; -export function initRatingHandlers(ratingForms) { +export function initRatingHandlers (ratingForms) { for (const form of ratingForms) { form.addEventListener('submit', (e) => { handleRatingSubmission(e, form); @@ -10,7 +10,7 @@ export function initRatingHandlers(ratingForms) { } } -function handleRatingSubmission(event, form) { +function handleRatingSubmission (event, form) { event.preventDefault(); // Continue only if selected star is different from previous rating if (!event.submitter.classList.contains('star-selected')) { diff --git a/openlibrary/plugins/openlibrary/js/stats/index.js b/openlibrary/plugins/openlibrary/js/stats/index.js index 8c0b30d6d72..4c6adac6c00 100644 --- a/openlibrary/plugins/openlibrary/js/stats/index.js +++ b/openlibrary/plugins/openlibrary/js/stats/index.js @@ -5,7 +5,7 @@ * @returns {Promise<void>} * @see /openlibrary/templates/admin/index.html */ -export async function initUniqueLoginCounts(containerElem) { +export async function initUniqueLoginCounts (containerElem) { const loadingIndicator = containerElem.querySelector('.loadingIndicator'); const i18nStrings = JSON.parse(containerElem.dataset.i18n); @@ -29,6 +29,6 @@ export async function initUniqueLoginCounts(containerElem) { * @returns {Promise<Response>} * @see `monthly_logins` class in /openlibrary/plugins/openlibrary/api.py */ -async function fetchCounts() { +async function fetchCounts () { return fetch('/api/monthly_logins.json'); } diff --git a/openlibrary/plugins/openlibrary/js/tabs.js b/openlibrary/plugins/openlibrary/js/tabs.js index 47c3565451d..74f9bfba4ec 100644 --- a/openlibrary/plugins/openlibrary/js/tabs.js +++ b/openlibrary/plugins/openlibrary/js/tabs.js @@ -1,7 +1,7 @@ const TABS_OPTIONS = { fx: { opacity: 'toggle' } }; import 'jquery-ui/ui/widgets/tabs'; -export function initTabs($node) { +export function initTabs ($node) { $node.tabs(TABS_OPTIONS); $node.filter('.autohash').on('tabsselect', (event, ui) => { document.location.hash = ui.panel.id; diff --git a/openlibrary/plugins/openlibrary/js/team.js b/openlibrary/plugins/openlibrary/js/team.js index 84ddf9dff9a..315601e4415 100644 --- a/openlibrary/plugins/openlibrary/js/team.js +++ b/openlibrary/plugins/openlibrary/js/team.js @@ -1,6 +1,6 @@ import team from '../../../templates/about/team.json'; import { updateURLParameters } from './utils'; -export function initTeamFilter() { +export function initTeamFilter () { const currentYear = new Date().getFullYear().toString(); // Photos const default_profile_image = diff --git a/openlibrary/plugins/openlibrary/js/template.js b/openlibrary/plugins/openlibrary/js/template.js index 371b0546ce8..d195edbdf54 100644 --- a/openlibrary/plugins/openlibrary/js/template.js +++ b/openlibrary/plugins/openlibrary/js/template.js @@ -2,18 +2,18 @@ // // Inspired by http://ejohn.org/blog/javascript-micro-templating/ -export default function Template(tmpl_text) { +export default function Template (tmpl_text) { var s = []; var js = ['var _p=[];', 'with(env) {']; var tokens, i, t, f, g; - function addCode(text) { + function addCode (text) { js.push(text); } - function addExpr(text) { + function addExpr (text) { js.push(`_p.push(htmlquote(${text}));`); } - function addText(text) { + function addText (text) { js.push(`_p.push(__s[${s.length}]);`); s.push(text); } diff --git a/openlibrary/plugins/openlibrary/js/type_changer.js b/openlibrary/plugins/openlibrary/js/type_changer.js index 234125460f1..b46d88aa6bd 100644 --- a/openlibrary/plugins/openlibrary/js/type_changer.js +++ b/openlibrary/plugins/openlibrary/js/type_changer.js @@ -2,10 +2,10 @@ * Functionality for TypeChanger.html */ -export function initTypeChanger(elem) { +export function initTypeChanger (elem) { // /about?m=edit - where this code is run - function changeTemplate() { + function changeTemplate () { // Change the template of the page based on the selected value const searchParams = new URLSearchParams(window.location.search); const t = elem.value; diff --git a/openlibrary/plugins/openlibrary/js/utils.js b/openlibrary/plugins/openlibrary/js/utils.js index dae8342575b..fa0d0d6d144 100644 --- a/openlibrary/plugins/openlibrary/js/utils.js +++ b/openlibrary/plugins/openlibrary/js/utils.js @@ -5,13 +5,13 @@ See: https://github.com/internetarchive/openlibrary/pull/9180#issuecomment-21079 */ // closes active popup -export function closePopup() { +export function closePopup () { // Note we don't import colorbox here, since it's on the parent parent.jQuery.fn.colorbox.close(); } // used in templates/admin/imports.html -export function truncate(text, limit) { +export function truncate (text, limit) { if (text.length > limit) { return `${text.substr(0, limit)}...`; } else { @@ -20,7 +20,7 @@ export function truncate(text, limit) { } // used in openlibrary/templates/books/edit/excerpts.html -export function cond(predicate, true_value, false_value) { +export function cond (predicate, true_value, false_value) { if (predicate) { return true_value; } else { @@ -33,7 +33,7 @@ export function cond(predicate, true_value, false_value) { * * @param {...HTMLElement} elements */ -export function removeChildren(...elements) { +export function removeChildren (...elements) { for (const elem of elements) { if (elem) { while (elem.firstChild) { @@ -44,7 +44,7 @@ export function removeChildren(...elements) { } // Function to add or update multiple query parameters -export function updateURLParameters(params) { +export function updateURLParameters (params) { // Get the current URL const url = new URL(window.location.href); @@ -63,7 +63,7 @@ export function updateURLParameters(params) { * Remove leading/trailing empty space on field deselect. * @param string a value for document.querySelectorAll() */ -export function trimInputValues(param) { +export function trimInputValues (param) { const inputs = document.querySelectorAll(param); inputs.forEach((input) => { input.addEventListener('blur', function () { @@ -72,7 +72,7 @@ export function trimInputValues(param) { }); } -export function buildPartialsUrl(component, params = {}) { +export function buildPartialsUrl (component, params = {}) { const curUrl = new URL(window.location.href); const url = new URL(`${location.origin}/partials/${component}.json`); diff --git a/openlibrary/plugins/openlibrary/js/waitlist.js b/openlibrary/plugins/openlibrary/js/waitlist.js index 29d223ad35a..5b298f4c0e6 100644 --- a/openlibrary/plugins/openlibrary/js/waitlist.js +++ b/openlibrary/plugins/openlibrary/js/waitlist.js @@ -5,7 +5,7 @@ import 'jquery-ui/ui/widgets/dialog'; * * @param {NodeList<HTMLElement>} leaveWaitlistLinks - NodeList of leave waitlist links */ -export function initLeaveWaitlist(leaveWaitlistLinks) { +export function initLeaveWaitlist (leaveWaitlistLinks) { for (const link of leaveWaitlistLinks) { link.addEventListener('click', () => { const $link = $(link); diff --git a/static/bookmarklets/import_webbook.js b/static/bookmarklets/import_webbook.js index e1103664991..a698c978e07 100644 --- a/static/bookmarklets/import_webbook.js +++ b/static/bookmarklets/import_webbook.js @@ -1,5 +1,5 @@ /* eslint-disable no-unused-labels */ -javascript:(async()=> { +javascript:(async ()=> { const url = prompt('Enter the book URL you want to import:'); if (!url) return; const promptText = `You are an expert book metadata librarian and research assistant. Your role is to search the web and collect accurate data to produce the most useful, factual, complete, and patron-oriented book page possible for the URL: diff --git a/stories/.storybook/main.js b/stories/.storybook/main.js index 36475e47e22..87d9d43833a 100644 --- a/stories/.storybook/main.js +++ b/stories/.storybook/main.js @@ -21,4 +21,4 @@ module.exports = { addons: [ '@storybook/addon-essentials' ], -} +}; diff --git a/stories/.storybook/preview.js b/stories/.storybook/preview.js index 2eaf63f831e..85cd3d12a8e 100644 --- a/stories/.storybook/preview.js +++ b/stories/.storybook/preview.js @@ -1,4 +1,4 @@ export const parameters = { actions: { argTypesRegex: '^on[A-Z].*' }, -} +}; diff --git a/tests/unit/js/SelectionManager.test.js b/tests/unit/js/SelectionManager.test.js index 5bd56ea470c..d224e12b7a0 100644 --- a/tests/unit/js/SelectionManager.test.js +++ b/tests/unit/js/SelectionManager.test.js @@ -1,6 +1,6 @@ import SelectionManager from '../../../openlibrary/plugins/openlibrary/js/ile/utils/SelectionManager/SelectionManager.js'; -function createTestElementsForProcessClick() { +function createTestElementsForProcessClick () { const listItem = document.createElement('li'); listItem.classList.add('searchResultItem', 'ile-selectable'); @@ -18,7 +18,7 @@ function createTestElementsForProcessClick() { return { listItem, link }; } -function setupSelectionManager() { +function setupSelectionManager () { const sm = new SelectionManager(null, '/search'); sm.ile = { $statusImages: { append: jest.fn() } }; sm.selectedItems = { work: [] }; diff --git a/tests/unit/js/sample-html/dropper-test-data.js b/tests/unit/js/sample-html/dropper-test-data.js index 23ded486df3..080dc8dbefa 100644 --- a/tests/unit/js/sample-html/dropper-test-data.js +++ b/tests/unit/js/sample-html/dropper-test-data.js @@ -13,7 +13,7 @@ export const closedDropperMarkup = generateDropperMarkup(false); export const disabledDropperMarkup = generateDropperMarkup(false, true); -function generateDropperMarkup(isDropperOpen, isDropperDisabled = false) { +function generateDropperMarkup (isDropperOpen, isDropperDisabled = false) { let wrapperClasses = 'generic-dropper-wrapper'; let arrowClasses = 'arrow'; diff --git a/tests/unit/js/sample-html/lists-test-data.js b/tests/unit/js/sample-html/lists-test-data.js index 4c3404dac89..37eff9d5690 100644 --- a/tests/unit/js/sample-html/lists-test-data.js +++ b/tests/unit/js/sample-html/lists-test-data.js @@ -1,4 +1,4 @@ -function createListFormMarkup(isFilled) { +function createListFormMarkup (isFilled) { const listName = isFilled ? 'My New List' : ''; const listDescription = isFilled ? 'A list for all of my books' : ''; @@ -52,7 +52,7 @@ const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; * @param {boolean} isActiveShowcase * @param {Array<ShowcaseDetails>} showcaseData */ -function createShowcaseMarkup(isActiveShowcase, showcaseData) { +function createShowcaseMarkup (isActiveShowcase, showcaseData) { const listId = isActiveShowcase ? 'already-lists' : 'list-lists'; const listClasses = 'listLists'.concat( isActiveShowcase ? ' already-lists' : '', @@ -122,12 +122,12 @@ export const showcaseDetailsData = [ listOwner: '/people/openlibrary', seedType: 'subject', }, -] +]; -export const multipleShowcasesOnPage = createShowcaseMarkup(true, [showcaseDetailsData[0], showcaseDetailsData[2]]) + createShowcaseMarkup(false, [showcaseDetailsData[0], showcaseDetailsData[1]]) -export const activeListShowcase = createShowcaseMarkup(true, [showcaseDetailsData[0]]) -export const listsSectionShowcase = createShowcaseMarkup(false, [showcaseDetailsData[0]]) -export const subjectShowcase = createShowcaseMarkup(false, [showcaseDetailsData[4]]) -export const authorShowcase = createShowcaseMarkup(false, [showcaseDetailsData[3]]) -export const workShowcase = createShowcaseMarkup(false, [showcaseDetailsData[0]]) -export const editionShowcase = createShowcaseMarkup(false, [showcaseDetailsData[1]]) +export const multipleShowcasesOnPage = createShowcaseMarkup(true, [showcaseDetailsData[0], showcaseDetailsData[2]]) + createShowcaseMarkup(false, [showcaseDetailsData[0], showcaseDetailsData[1]]); +export const activeListShowcase = createShowcaseMarkup(true, [showcaseDetailsData[0]]); +export const listsSectionShowcase = createShowcaseMarkup(false, [showcaseDetailsData[0]]); +export const subjectShowcase = createShowcaseMarkup(false, [showcaseDetailsData[4]]); +export const authorShowcase = createShowcaseMarkup(false, [showcaseDetailsData[3]]); +export const workShowcase = createShowcaseMarkup(false, [showcaseDetailsData[0]]); +export const editionShowcase = createShowcaseMarkup(false, [showcaseDetailsData[1]]); diff --git a/tests/unit/js/search.test.js b/tests/unit/js/search.test.js index 77ac8a7faac..ee97f4b4d76 100644 --- a/tests/unit/js/search.test.js +++ b/tests/unit/js/search.test.js @@ -11,7 +11,7 @@ import { * @param {Number} minVisibleFacet minimum number of visible facet * @return {String} HTML search facets section */ -function createSearchFacets( +function createSearchFacets ( totalFacet = 2, visibleFacet = 2, minVisibleFacet = 2, @@ -66,7 +66,7 @@ function createSearchFacets( * @param {Number} totalFacet total number of facet * @param {Number} expectedVisibleFacet expected number of visible facet */ -function checkFacetVisibility(totalFacet, expectedVisibleFacet) { +function checkFacetVisibility (totalFacet, expectedVisibleFacet) { const facetEntryList = document.getElementsByClassName('facetEntry'); test('facetEntry element number', () => { @@ -96,7 +96,7 @@ function checkFacetVisibility(totalFacet, expectedVisibleFacet) { * @param {Number} minVisibleFacet minimum visible facet number * @param {Number} expectedVisibleFacet expected number of visible facet */ -function checkFacetMoreLessVisibility( +function checkFacetMoreLessVisibility ( totalFacet, minVisibleFacet, expectedVisibleFacet, diff --git a/tests/unit/js/service-worker-matchers.test.js b/tests/unit/js/service-worker-matchers.test.js index 49af0942536..a2638c17472 100644 --- a/tests/unit/js/service-worker-matchers.test.js +++ b/tests/unit/js/service-worker-matchers.test.js @@ -8,7 +8,7 @@ import { } from '../../../openlibrary/plugins/openlibrary/js/service-worker-matchers'; // Helper function to create a URL object -function _u(url) { +function _u (url) { return { url: new URL(url) }; } // Group related tests together From c114797428fc74f6c069a25ead115901936889df Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 14:29:40 +0000 Subject: [PATCH 09/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- eslint.config.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eslint.config.cjs b/eslint.config.cjs index 51fac7abd3f..b0bcba51f9e 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -135,7 +135,7 @@ module.exports = [ "quote-props": ["error", "as-needed"], "keyword-spacing": ["error", { before: true, after: true }], "key-spacing": ["error", { mode: "strict" }], - + // GLOBALLY ENFORCED FORMATTING RULES "semi": ["error", "always"], "space-before-function-paren": ["error", "always"], @@ -340,4 +340,4 @@ module.exports = [ "comma-spacing": "off" } } -]; \ No newline at end of file +]; From e2176c0789577e2fd8745fe1634a71187af3add7 Mon Sep 17 00:00:00 2001 From: RayBB <RayBB@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:04:08 -0700 Subject: [PATCH 10/15] remove extra entries to exclude file --- eslint.config.cjs | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/eslint.config.cjs b/eslint.config.cjs index b0bcba51f9e..351315a26a2 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -232,11 +232,6 @@ module.exports = [ // TEMPORARY EXEMPTIONS: Turn off new formatting rules for files locked in active PRs { files: [ - "openlibrary/components/AuthorMap.vue", - "openlibrary/components/AuthorMap/AuthorCard.vue", - "openlibrary/components/AuthorMap/WorldMap.vue", - "openlibrary/components/AuthorMap/WorldMapRaw.vue", - "openlibrary/components/AuthorMap/utils.js", "openlibrary/components/BarcodeScanner.vue", "openlibrary/components/BarcodeScanner/components/LazyBookCard.vue", "openlibrary/components/BarcodeScanner/utils/classes.js", @@ -247,7 +242,6 @@ module.exports = [ "openlibrary/components/BulkSearch/components/MatchTable.vue", "openlibrary/components/BulkSearch/components/NoBookCard.vue", "openlibrary/components/BulkSearch/utils/classes.js", - "openlibrary/components/BulkSearch/utils/samples.js", "openlibrary/components/BulkSearch/utils/searchUtils.js", "openlibrary/components/HelloWorld.vue", "openlibrary/components/IdentifiersInput.vue", @@ -272,22 +266,11 @@ module.exports = [ "openlibrary/components/MergeUI/utils.js", "openlibrary/components/ObservationForm/ObservationService.js", "openlibrary/components/ObservationForm/Utils.js", - "openlibrary/components/configs.js", - "openlibrary/components/dev/serve-component.js", - "openlibrary/components/dev/vite.config.js", "openlibrary/components/lit/OLChip.js", "openlibrary/components/lit/OLChipGroup.js", - "openlibrary/components/lit/OLMarkdownEditor.js", "openlibrary/components/lit/OLReadMore.js", - "openlibrary/components/lit/OlAutocomplete.js", - "openlibrary/components/lit/OlDrawer.js", - "openlibrary/components/lit/OlLanguageEdit.js", "openlibrary/components/lit/OlPagination.js", "openlibrary/components/lit/OlPopover.js", - "openlibrary/components/lit/OlTooltip.js", - "openlibrary/components/lit/editor-core.js", - "openlibrary/components/lit/html-block.js", - "openlibrary/components/lit/index.js", "openlibrary/components/rollupInputCore.js", "openlibrary/plugins/openlibrary/js/Browser.js", "openlibrary/plugins/openlibrary/js/SearchBar.js", @@ -295,7 +278,6 @@ module.exports = [ "openlibrary/plugins/openlibrary/js/SearchUtils.js", "openlibrary/plugins/openlibrary/js/Toast.js", "openlibrary/plugins/openlibrary/js/add-book.js", - "openlibrary/plugins/openlibrary/js/add_new_field.js", "openlibrary/plugins/openlibrary/js/add_provider.js", "openlibrary/plugins/openlibrary/js/admin.js", "openlibrary/plugins/openlibrary/js/affiliate-links.js", @@ -310,29 +292,13 @@ module.exports = [ "openlibrary/plugins/openlibrary/js/i18n.js", "openlibrary/plugins/openlibrary/js/ile/index.js", "openlibrary/plugins/openlibrary/js/ile/utils/SelectionManager/SelectionManager.js", - "openlibrary/plugins/openlibrary/js/index.js", - "openlibrary/plugins/openlibrary/js/loading-gradient.js", "openlibrary/plugins/openlibrary/js/markdown-editor/index.js", - "openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestEditPage.js", - "openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestEditPageAuthor.js", "openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestService.js", "openlibrary/plugins/openlibrary/js/merge.js", "openlibrary/plugins/openlibrary/js/modals/index.js", "openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLogForms.js", "openlibrary/plugins/openlibrary/js/password-toggle.js", - "openlibrary/plugins/openlibrary/js/pwa-install-prompt.js", - "openlibrary/plugins/openlibrary/js/service-worker-init.js", - "openlibrary/plugins/openlibrary/js/signup.js", - "scripts/build-icons.js", - "static/bookmarklets/bulk-import-ui.js", - "static/bookmarklets/isbn-utils.js", - "static/bookmarklets/list-api.js", - "static/js/preferences-handler.js", - "static/js/preferences.js", - "tests/unit/js/preferences.test.js", - "ui/components/StarRatings/StarRatings.js", - "webpack.config.css.js", - "webpack.config.js" + "openlibrary/plugins/openlibrary/js/service-worker-init.js" ], rules: { "semi": "off", From ce4a336c5b7b99613fdfcff968f69802a69c508e Mon Sep 17 00:00:00 2001 From: RayBB <RayBB@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:12:46 -0700 Subject: [PATCH 11/15] undo all changes since master to test pre-commit fixing --- .../BarcodeScanner/utils/classes.js | 44 +- .../components/BulkSearch/utils/classes.js | 290 +++-- .../components/BulkSearch/utils/samples.js | 9 +- .../BulkSearch/utils/searchUtils.js | 31 +- .../IdentifiersInput/utils/utils.js | 39 +- .../components/LibraryExplorer/utils.js | 30 +- .../components/LibraryExplorer/utils/lcc.js | 21 +- openlibrary/components/MergeUI.vue | 58 +- .../components/MergeUI/AuthorRoleTable.vue | 4 +- .../components/MergeUI/EditionSnippet.vue | 18 +- .../components/MergeUI/ExcerptsTable.vue | 4 +- openlibrary/components/MergeUI/MergeRow.vue | 2 +- .../components/MergeUI/MergeRowField.vue | 2 +- .../components/MergeUI/MergeRowJointField.vue | 2 +- openlibrary/components/MergeUI/TextDiff.vue | 4 +- openlibrary/components/ObservationForm.vue | 34 +- .../ObservationForm/ObservationService.js | 20 +- .../components/ObservationForm/Utils.js | 6 +- .../ObservationForm/components/CardBody.vue | 26 +- .../ObservationForm/components/CardHeader.vue | 2 +- .../components/CategorySelector.vue | 20 +- .../ObservationForm/components/OLChip.vue | 16 +- .../ObservationForm/components/SavedTags.vue | 32 +- .../ObservationForm/components/ValueCard.vue | 6 +- openlibrary/components/configs.js | 13 +- openlibrary/components/dev/vite.config.js | 7 +- openlibrary/components/lit/OLChip.js | 14 +- openlibrary/components/lit/OLChipGroup.js | 2 +- openlibrary/components/lit/OLReadMore.js | 7 +- openlibrary/components/lit/OlPagination.js | 143 +-- openlibrary/components/lit/OlPopover.js | 140 +-- openlibrary/components/lit/index.js | 4 +- openlibrary/components/rollupInputCore.js | 2 +- openlibrary/components/vite-lit.config.mjs | 60 +- openlibrary/plugins/openlibrary/js/Browser.js | 2 +- .../plugins/openlibrary/js/SearchBar.js | 224 ++-- .../plugins/openlibrary/js/SearchPage.js | 8 +- .../plugins/openlibrary/js/SearchUtils.js | 65 +- openlibrary/plugins/openlibrary/js/Toast.js | 49 +- .../plugins/openlibrary/js/add-book.js | 65 +- .../plugins/openlibrary/js/add_provider.js | 60 +- openlibrary/plugins/openlibrary/js/admin.js | 14 +- .../plugins/openlibrary/js/affiliate-links.js | 70 +- .../plugins/openlibrary/js/autocomplete.js | 251 ++-- .../plugins/openlibrary/js/banner/index.js | 25 +- .../plugins/openlibrary/js/book-page-lists.js | 116 +- .../openlibrary/js/breadcrumb_select/index.js | 4 +- .../openlibrary/js/bulk-tagger/BulkTagger.js | 835 ++++++------- .../js/bulk-tagger/BulkTagger/MenuOption.js | 219 ++-- .../BulkTagger/SortedMenuOptionContainer.js | 173 ++- .../openlibrary/js/bulk-tagger/index.js | 4 +- .../openlibrary/js/bulk-tagger/models/Tag.js | 137 ++- .../openlibrary/js/carousel/Carousel.js | 74 +- .../plugins/openlibrary/js/carousel/index.js | 18 +- .../plugins/openlibrary/js/clampers.js | 15 +- .../openlibrary/js/compact-title/index.js | 58 +- openlibrary/plugins/openlibrary/js/covers.js | 106 +- openlibrary/plugins/openlibrary/js/dialog.js | 94 +- .../plugins/openlibrary/js/dropper/Dropper.js | 180 ++- .../plugins/openlibrary/js/dropper/index.js | 58 +- openlibrary/plugins/openlibrary/js/edit.js | 414 +++---- .../js/edition-nav-bar/EditionNavBar.js | 68 +- .../openlibrary/js/edition-nav-bar/index.js | 12 +- .../openlibrary/js/editions-table/index.js | 61 +- .../plugins/openlibrary/js/following.js | 12 +- .../js/fulltext-search-suggestion.js | 73 +- .../plugins/openlibrary/js/go-back-links.js | 10 +- .../openlibrary/js/goodreads_import.js | 161 ++- .../plugins/openlibrary/js/graphs/index.js | 10 +- .../plugins/openlibrary/js/graphs/options.js | 20 +- .../plugins/openlibrary/js/graphs/plot.js | 189 ++- openlibrary/plugins/openlibrary/js/i18n.js | 10 +- .../openlibrary/js/ia_thirdparty_logins.js | 29 +- .../plugins/openlibrary/js/idValidation.js | 61 +- .../plugins/openlibrary/js/ile/index.js | 71 +- .../plugins/openlibrary/js/ile/utils/ol.js | 34 +- openlibrary/plugins/openlibrary/js/index.js | 701 +++++------ .../plugins/openlibrary/js/interstitial.js | 20 +- .../plugins/openlibrary/js/isbnOverride.js | 14 +- .../plugins/openlibrary/js/jquery.repeat.js | 46 +- openlibrary/plugins/openlibrary/js/jsdef.js | 60 +- .../plugins/openlibrary/js/lazy-carousel.js | 90 +- .../openlibrary/js/lazy-thing-preview.js | 100 +- .../js/librarian-dashboard/index.js | 106 +- .../plugins/openlibrary/js/list_books.js | 20 +- .../openlibrary/js/lists/ListService.js | 8 +- .../openlibrary/js/lists/ListViewBody.js | 61 +- .../openlibrary/js/lists/ShowcaseItem.js | 273 ++--- .../openlibrary/js/markdown-editor/index.js | 26 +- .../MergeRequestService.js | 49 +- .../merge-request-table/MergeRequestTable.js | 67 +- .../MergeRequestTable/TableHeader.js | 167 ++- .../MergeRequestTable/TableRow.js | 376 +++--- .../js/merge-request-table/index.js | 6 +- openlibrary/plugins/openlibrary/js/merge.js | 50 +- .../plugins/openlibrary/js/modals/index.js | 189 ++- .../openlibrary/js/my-books/CreateListForm.js | 158 ++- .../openlibrary/js/my-books/MyBooksDropper.js | 283 +++-- .../MyBooksDropper/CheckInComponents.js | 1030 ++++++++--------- .../my-books/MyBooksDropper/ReadingLists.js | 444 ++++--- .../MyBooksDropper/ReadingLogForms.js | 291 +++-- .../plugins/openlibrary/js/my-books/index.js | 103 +- .../openlibrary/js/my-books/store/index.js | 82 +- .../openlibrary/js/native-dialog/index.js | 39 +- .../plugins/openlibrary/js/nonjquery_utils.js | 12 +- .../plugins/openlibrary/js/offline-banner.js | 3 +- .../plugins/openlibrary/js/ol.analytics.js | 43 +- openlibrary/plugins/openlibrary/js/ol.js | 16 +- .../plugins/openlibrary/js/partner_ol_lib.js | 36 +- .../plugins/openlibrary/js/password-toggle.js | 13 +- .../plugins/openlibrary/js/patron_exports.js | 10 +- .../plugins/openlibrary/js/private-button.js | 13 +- openlibrary/plugins/openlibrary/js/python.js | 8 +- .../openlibrary/js/reading-goals/index.js | 211 ++-- .../openlibrary/js/readinglog_stats.js | 163 ++- .../openlibrary/js/return-form/index.js | 6 +- openlibrary/plugins/openlibrary/js/search.js | 116 +- .../openlibrary/js/service-worker-init.js | 10 +- .../openlibrary/js/service-worker-matchers.js | 23 +- .../plugins/openlibrary/js/service-worker.js | 52 +- openlibrary/plugins/openlibrary/js/signup.js | 164 +-- .../openlibrary/js/star-ratings/index.js | 41 +- .../plugins/openlibrary/js/stats/index.js | 33 +- openlibrary/plugins/openlibrary/js/tabs.js | 4 +- openlibrary/plugins/openlibrary/js/team.js | 48 +- .../plugins/openlibrary/js/template.js | 21 +- .../plugins/openlibrary/js/type_changer.js | 6 +- openlibrary/plugins/openlibrary/js/utils.js | 23 +- .../plugins/openlibrary/js/waitlist.js | 10 +- scripts/gh_scripts/new_pr_labeler.mjs | 162 ++- scripts/solr_restarter/index.js | 217 ++-- static/bookmarklets/import_webbook.js | 2 +- stories/.storybook/main.js | 2 +- stories/.storybook/preview.js | 2 +- stories/Button.stories.js | 63 +- tests/unit/js/Browser.test.js | 31 +- tests/unit/js/SearchBar.test.js | 88 +- tests/unit/js/SearchUtils.test.js | 8 +- tests/unit/js/SelectionManager.test.js | 9 +- tests/unit/js/autocomplete.test.js | 51 +- tests/unit/js/droppers.test.js | 405 +++---- .../js/editionEditPageClassification.test.js | 34 +- tests/unit/js/editionsEditPage.test.js | 20 +- tests/unit/js/html-test-data.js | 4 +- tests/unit/js/idValidation.test.js | 20 +- tests/unit/js/jquery.repeat.test.js | 14 +- tests/unit/js/jsdef.test.js | 19 +- tests/unit/js/lists.test.js | 362 +++--- tests/unit/js/my-books.test.js | 202 ++-- tests/unit/js/python.test.js | 12 +- .../unit/js/sample-html/checkIns-test-data.js | 2 +- .../unit/js/sample-html/dropper-test-data.js | 22 +- tests/unit/js/sample-html/lists-test-data.js | 57 +- tests/unit/js/sample-html/utils-test-data.js | 6 +- tests/unit/js/search.test.js | 84 +- tests/unit/js/service-worker-matchers.test.js | 140 +-- tests/unit/js/setup.js | 1 - tests/unit/js/signup.test.js | 57 +- tests/unit/js/utils.test.js | 82 +- vue.config.js | 4 +- webpack.config.css.js | 74 +- 161 files changed, 6038 insertions(+), 7433 deletions(-) diff --git a/openlibrary/components/BarcodeScanner/utils/classes.js b/openlibrary/components/BarcodeScanner/utils/classes.js index b9de19c49fd..2428734ef3e 100644 --- a/openlibrary/components/BarcodeScanner/utils/classes.js +++ b/openlibrary/components/BarcodeScanner/utils/classes.js @@ -1,6 +1,6 @@ // @ts-check /* eslint-disable no-console */ -import { createScheduler, createWorker } from 'tesseract.js'; +import { createWorker, createScheduler } from 'tesseract.js'; export class OCRScanner { constructor() { @@ -10,8 +10,8 @@ export class OCRScanner { this.listeners = { /** @type {Array<(isbn: string) => void>} */ - onISBNDetected: [], - }; + onISBNDetected: [] + } } /** @param {(isbn: string) => void} callback */ @@ -39,28 +39,20 @@ export class OCRScanner { } /** - * @param {HTMLCanvasElement} canvas - */ + * @param {HTMLCanvasElement} canvas + */ async doOCR(canvas) { - const { - data: { lines }, - } = await this.scheduler.addJob('recognize', canvas); - const textLines = lines.map((l) => l.text.trim()).filter((line) => line); + const { data: { lines } } = await this.scheduler.addJob('recognize', canvas); + const textLines = lines.map(l => l.text.trim()).filter(line => line); console.log(textLines.join('\n')); for (const line of textLines) { const sanitizedLine = line.replace(/[\s-'.–—]/g, ''); if (!/\d{2}/.test(sanitizedLine)) continue; console.log(sanitizedLine); - if ( - sanitizedLine.includes('isbn') || - /97[0-9]{10}[0-9x]/i.test(sanitizedLine) || - /[0-9]{9}[0-9x]/i.test(sanitizedLine) - ) { - const isbn = sanitizedLine.match( - /(97[0-9]{10}[0-9x]|[0-9]{9}[0-9x])/i, - )[0]; + if (sanitizedLine.includes('isbn') || /97[0-9]{10}[0-9x]/i.test(sanitizedLine) || /[0-9]{9}[0-9x]/i.test(sanitizedLine)) { + const isbn = sanitizedLine.match(/(97[0-9]{10}[0-9x]|[0-9]{9}[0-9x])/i)[0]; console.log(`ISBN detected: ${isbn}`); - this.listeners.onISBNDetected.forEach((callback) => callback(isbn)); + this.listeners.onISBNDetected.forEach(callback => callback(isbn)); } } } @@ -71,12 +63,12 @@ export class OCRScanner { */ export class ThrottleGrouping { /** - * @param {object} param0 - * @param {TFunc} param0.func - * @param {function(Parameters<TFunc>[]): Parameters<TFunc>} param0.reducer - * @param {number} param0.wait - */ - constructor({ func, reducer, wait = 100 }) { + * @param {object} param0 + * @param {TFunc} param0.func + * @param {function(Parameters<TFunc>[]): Parameters<TFunc>} param0.reducer + * @param {number} param0.wait + */ + constructor({func, reducer, wait=100}) { this.func = func; this.reducer = reducer; this.wait = wait; @@ -92,8 +84,8 @@ export class ThrottleGrouping { } /** - * @param {Parameters<TFunc>} args - */ + * @param {Parameters<TFunc>} args + */ takeNext(...args) { this.curGroup.push(args); if (!this.timeout) { diff --git a/openlibrary/components/BulkSearch/utils/classes.js b/openlibrary/components/BulkSearch/utils/classes.js index 964ceab5be8..2cb7faf81d2 100644 --- a/openlibrary/components/BulkSearch/utils/classes.js +++ b/openlibrary/components/BulkSearch/utils/classes.js @@ -2,7 +2,7 @@ export class ExtractedBook { constructor(title = '', author = '', isbn = '') { - /** @type {string} */ + /** @type {string} */ this.title = title; /**@type {string} */ this.author = author; @@ -12,84 +12,77 @@ export class ExtractedBook { } class AbstractExtractor { + /** - * @param {string} label - */ + * @param {string} label + */ constructor(label) { - /** @type {string} */ - this.label = label; + /** @type {string} */ + this.label = label } /** - * @param {ExtractionOptions} _extractOptions - * @param {string} _text - * @returns {Promise<BookMatch[]>} - */ - // eslint-disable-next-line no-unused-vars - async run(_extractOptions, _text) { - - throw new Error('Not Implemented Error'); + * @param {ExtractionOptions} _extractOptions + * @param {string} _text + * @returns {Promise<BookMatch[]>} + */ + async run(_extractOptions, _text) { //eslint-disable-line no-unused-vars + throw new Error('Not Implemented Error') } } export class RegexExtractor extends AbstractExtractor { - name = 'regex_extractor'; + + name = 'regex_extractor' /** - * - * @param {string} label - * @param {string} pattern - */ - constructor(label, pattern) { - super(label); + * + * @param {string} label + * @param {string} pattern + */ + constructor(label, pattern){ + super(label) /** @type {RegExp} */ this.pattern = new RegExp(pattern, 'gmu'); } /** - * @param {ExtractionOptions} _extractOptions - * @param {string} text - * @returns {Promise<BookMatch[]>} - */ + * @param {ExtractionOptions} _extractOptions + * @param {string} text + * @returns {Promise<BookMatch[]>} + */ async run(_extractOptions, text) { - const data = [...text.matchAll(this.pattern)]; - const extractedBooks = data.map( - (entry) => - new ExtractedBook( - entry.groups?.title, - entry.groups?.author, - entry.groups?.isbn, - ), - ); - const matchedBooks = extractedBooks.map( - (entry) => new BookMatch(entry, []), - ); - return matchedBooks; + const data = [...text.matchAll(this.pattern)] + const extractedBooks = data.map((entry) => new ExtractedBook(entry.groups?.title, entry.groups?.author, entry.groups?.isbn)) + const matchedBooks = extractedBooks.map((entry) => new BookMatch(entry, [])) + return matchedBooks } } -export class AiExtractor extends AbstractExtractor { - name = 'ai_extractor'; +export class AiExtractor extends AbstractExtractor{ + + name = 'ai_extractor' /** - * @param {string} label - * @param {string} model - */ + * @param {string} label + * @param {string} model + */ constructor(label, model) { - super(label); + super(label) /** @type {string} */ - this.model = model; + this.model = model } /** - * - * @param {ExtractionOptions} extractOptions - * @param {string} text - * @returns {Promise<BookMatch[]>} - */ + * + * @param {ExtractionOptions} extractOptions + * @param {string} text + * @returns {Promise<BookMatch[]>} + */ async run(extractOptions, text) { const request = { + method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${extractOptions.openaiApiKey}`, + Authorization: `Bearer ${extractOptions.openaiApiKey}` }, body: JSON.stringify({ model: this.model, @@ -97,178 +90,157 @@ export class AiExtractor extends AbstractExtractor { messages: [ { role: 'system', - content: - 'You are a book extraction system. You will be given a free form passage of text containing references to books, and you will need to extract the book titles, author, and optionally ISBN in a JSON array.', + content: 'You are a book extraction system. You will be given a free form passage of text containing references to books, and you will need to extract the book titles, author, and optionally ISBN in a JSON array.' }, { role: 'user', content: `Please extract the books from the following text:\n\n${text}`, - }, + } ], - }), - }; + }) + + } try { - const resp = await fetch( - 'https://api.openai.com/v1/chat/completions', - request, - ); + const resp = await fetch('https://api.openai.com/v1/chat/completions', request) if (!resp.ok) { - const status = resp.status; - let errorMessage = 'Network response was not okay.'; + const status = resp.status + let errorMessage = 'Network response was not okay.' if (status === 401) { - errorMessage = `${errorMessage} Error: Incorrect Authorization key.`; + + errorMessage = `${errorMessage} Error: Incorrect Authorization key.` } - throw new Error(errorMessage); + throw new Error(errorMessage) } - const data = await resp.json(); - return JSON.parse(data.choices[0].message.content)['books'].map( - (entry) => - new BookMatch( - new ExtractedBook(entry?.title, entry?.author, entry?.isbn), - {}, - ), - ); - } catch (error) { - return []; + const data = await resp.json() + return JSON.parse(data.choices[0].message.content)['books'] + .map((entry) => + new BookMatch(new ExtractedBook(entry?.title, entry?.author, entry?.isbn), {}) + ) + } + catch (error) { + return [] } + + } } -export class TableExtractor extends AbstractExtractor { - name = 'table_extractor'; +export class TableExtractor extends AbstractExtractor{ + + name = 'table_extractor' /** - * - * @param {string} label - */ + * + * @param {string} label + */ constructor(label) { - super(label); + super(label) /** @type {string} */ - this.authorColumn = 'author'; + this.authorColumn = 'author' /** @type {string} */ - this.titleColumn = 'title'; + this.titleColumn = 'title' } /** - * @param {ExtractionOptions} extractionOptions - * @param {string} text - * @return {Promise<BookMatch[]>} - */ - async run(extractionOptions, text) { - /** @type {string[]} */ - const lines = text.split('\n'); + * @param {ExtractionOptions} extractionOptions + * @param {string} text + * @return {Promise<BookMatch[]>} + */ + async run(extractionOptions, text){ + + /** @type {string[]} */ + const lines = text.split('\n') /** @type {string[][]} */ - const cells = lines.map((line) => line.split('\t')); + const cells = lines.map(line => line.split('\t')) /** @type {{columns: String[], rows: {columnName: string}[]}} */ const tableData = { columns: cells[0], - rows: [], - }; - for (let i = 1; i < cells.length; i++) { - const row = {}; - for (let j = 0; j < tableData.columns.length; j++) { - row[tableData.columns[j].trim().toLowerCase()] = cells[i][j]; + rows: [] + } + for (let i=1; i< cells.length; i++){ + const row = {} + for (let j = 0; j < tableData.columns.length; j++){ + row[tableData.columns[j].trim().toLowerCase()] = cells[i][j] } - // @ts-expect-error - tableData.rows.push(row); + // @ts-ignore + tableData.rows.push(row) } return tableData.rows.map( - (row) => - new BookMatch( - new ExtractedBook( - row[this.titleColumn] || '', - row[this.authorColumn] || '', - row['isbn'] || '', - ), - {}, - ), - ); + row => new BookMatch( + new ExtractedBook( + row[this.titleColumn] || '', row[this.authorColumn] || '', row['isbn'] || ''), + {}) + ) } } class ExtractionOptions { constructor() { - /** @type {string} */ - this.openaiApiKey = ''; + /** @type {string} */ + this.openaiApiKey = '' } } -class MatchOptions { - constructor() { - /** @type {boolean} */ +class MatchOptions { + constructor (){ + /** @type {boolean} */ this.includeAuthor = true; } } export class BookMatch { + /** - * - * @param {ExtractedBook} extractedBook - * @param {*} solrDocs - */ - constructor(extractedBook, solrDocs) { - /** @type {ExtractedBook} */ + * + * @param {ExtractedBook} extractedBook + * @param {*} solrDocs + */ + constructor(extractedBook, solrDocs){ + /** @type {ExtractedBook} */ this.extractedBook = extractedBook; - this.solrDocs = solrDocs; + this.solrDocs = solrDocs } } -const BASE_LIST_URL = '/account/lists/add?seeds='; -export class BulkSearchState { - constructor() { - /** @type {string} */ - this.inputText = ''; +const BASE_LIST_URL = '/account/lists/add?seeds=' + +export class BulkSearchState{ + constructor(){ + /** @type {string} */ + this.inputText= ''; /** @type {BookMatch[]} */ this.matchedBooks = []; /** @type {MatchOptions} */ - this.matchOptions = new MatchOptions(); + this.matchOptions = new MatchOptions() /** @type {ExtractionOptions} */ this.extractionOptions = new ExtractionOptions(); /** @type {AbstractExtractor[]} */ - this.extractors = [ - new RegexExtractor( - 'Pattern: Title by Author', - '(^|>)(?<title>[A-Za-z][\\p{L}0-9\\- ,]{1,250})\\s+(by|[-\u2013\u2014\\t])\\s+(?<author>[\\p{L}][\\p{L}\\.\\- ]{3,70})( \\(.*)?($|<\\/)', - ), - new RegexExtractor( - 'Pattern: Author - Title', - '(^|>)(?<author>[A-Za-z][\\p{L}0-9\\- ,]{1,250})\\s+[,-\u2013\u2014\\t]\\s+(?<title>[\\p{L}][\\p{L}\\.\\- ]{3,70})( \\(.*)?($|<\\/)', - ), - new RegexExtractor( - 'Pattern: Title - Author', - '(^|>)(?<title>[A-Za-z][\\p{L}0-9\\- ,]{1,250})\\s+[,-\u2013\u2014\\t]\\s+(?<author>[\\p{L}][\\p{L}\\.\\- ]{3,70})( \\(.*)?($|<\\/)', - ), - new RegexExtractor( - 'Pattern: Title (Author)', - '^(?<title>[\\p{L}].{1,250})\\s\\(?<author>(.{3,70})\\)$$', - ), - new RegexExtractor( - 'Wikipedia Citation Pattern: (e.g. Baum, Frank L. (1994). The Wizard of Oz)', - '^(?<author>[^.()]+).*?\\)\\. (?<title>[^.]+)', - ), + this.extractors = [ + new RegexExtractor('Pattern: Title by Author', '(^|>)(?<title>[A-Za-z][\\p{L}0-9\\- ,]{1,250})\\s+(by|[-\u2013\u2014\\t])\\s+(?<author>[\\p{L}][\\p{L}\\.\\- ]{3,70})( \\(.*)?($|<\\/)'), + new RegexExtractor('Pattern: Author - Title', '(^|>)(?<author>[A-Za-z][\\p{L}0-9\\- ,]{1,250})\\s+[,-\u2013\u2014\\t]\\s+(?<title>[\\p{L}][\\p{L}\\.\\- ]{3,70})( \\(.*)?($|<\\/)'), + new RegexExtractor('Pattern: Title - Author', '(^|>)(?<title>[A-Za-z][\\p{L}0-9\\- ,]{1,250})\\s+[,-\u2013\u2014\\t]\\s+(?<author>[\\p{L}][\\p{L}\\.\\- ]{3,70})( \\(.*)?($|<\\/)'), + new RegexExtractor('Pattern: Title (Author)', '^(?<title>[\\p{L}].{1,250})\\s\\(?<author>(.{3,70})\\)$$'), + new RegexExtractor('Wikipedia Citation Pattern: (e.g. Baum, Frank L. (1994). The Wizard of Oz)', '^(?<author>[^.()]+).*?\\)\\. (?<title>[^.]+)'), new AiExtractor('✨ AI Extraction (Beta)', 'gpt-4o-mini'), - new TableExtractor('Extract from a Table/Spreadsheet'), - ]; + new TableExtractor('Extract from a Table/Spreadsheet') + ] /** @type {Number} */ - this._activeExtractorIndex = 0; + this._activeExtractorIndex = 0 } /**@type {AbstractExtractor} */ get activeExtractor() { - return this.extractors[this._activeExtractorIndex]; + return this.extractors[this._activeExtractorIndex] } /**@type {String} */ get listUrl() { - return ( - BASE_LIST_URL + - this.matchedBooks - .map((bm) => bm.solrDocs?.docs?.[0]?.key.split('/')[2]) - .filter((key) => key) - ); + return BASE_LIST_URL + this.matchedBooks + .map(bm => bm.solrDocs?.docs?.[0]?.key.split('/')[2]) + .filter(key => key); } /**@type {String} */ - get listString() { + get listString(){ return `${this.matchedBooks - .map((bm) => bm.solrDocs?.docs?.[0]?.key.split('/')[2]) - .filter((key) => key)}`; + .map(bm => bm.solrDocs?.docs?.[0]?.key.split('/')[2]) + .filter(key => key)}`; } } diff --git a/openlibrary/components/BulkSearch/utils/samples.js b/openlibrary/components/BulkSearch/utils/samples.js index bddd30802ea..e9c08adddf5 100644 --- a/openlibrary/components/BulkSearch/utils/samples.js +++ b/openlibrary/components/BulkSearch/utils/samples.js @@ -16,8 +16,7 @@ export const sampleData = [ }, { name: 'Holocaust Wikipedia citations', - source: - 'https://en.wikipedia.org/wiki/Bibliography_of_the_Holocaust#Historical_studies', - text: 'Bauer, Yehuda (1994). Jews for Sale? Nazi-Jewish Negotiations 1933-1945. Yale University Press. ISBN 0-300-06852-2.\nBerenbaum, Michael (1990). A Mosaic of Victims: Non-Jews Persecuted and Murdered by the Nazis.\nBergen, Doris (2009). War and Genocide: Concise History of the Holocaust.\nBlack, Edwin (2010). The Farhud: The Arab-Nazi Alliance in the Holocaust. Dialog Press. ISBN 0-914153-14-5.\nBraham, Randolph (1994) [1981]. The Politics of Genocide: The Holocaust in Hungary. Columbia University Press. ISBN 0-88033-247-6.\nBraham, Randolph (2011). The Auschwitz Reports and the Holocaust in Hungary. Columbia University Press.\nChalmers, Beverley (2015). Birth, Sex and Abuse: Women\'s Voices Under Nazi Rule. Grosvenor House Publishing Ltd. ISBN 1781483531.\nDavies, Norman; Lukas, Richard C. (2001) [1996]. Forgotten Holocaust: The Poles Under German Occupation.\nDean, Martin (2008). Robbing the Jews: The Confiscation of Jewish Property in the Holocaust. Cambridge University Press.\nEvans, Suzanne (2004). Forgotten Crimes: The Holocaust and People with Disabilities.\nFriedländer, Saul (1998). The Years of Persecution: Nazi Germany and the Jews, 1933-1939. Vol. 1.\nGrau, Gunter; Shoppmann, Claudia (1995). The Hidden Holocaust?: Gay and Lesbian Persecution in Germany 1933-45.\nHedgepeth, Sonja; Saidel, Rochelle (2010). Sexual Violence against Jewish Women during the Holocaust.\nPeukert, Detlev (1994). "The Genesis of the \'Final Solution\' from the Spirit of Science". In Thomas Childers; Jane Caplan (eds.). Reevaluating the Third Reich. New York: Holmes & Meier. pp. 234–252. ISBN 0-8419-1178-9.\nRees, Laurence (2017). The Holocaust: A New History. London: Viking Press. ISBN 978-1610398442.\nAméry, Jean (1980). At the Mind\'s Limits: Contemplations by a Survivor on Auschwitz and its Realities.\nBlitz Konig, Nanette (2018). Holocaust Memoirs of a Bergen-Belsen Survivor and Classmate of Anne Frank. Amsterdam Publishers. ISBN 9789492371614.\nDittman, Anita (2005). Trapped in Hitler\'s Hell. ISBN 0-9721512-8-1.\nGerrard, Mady. Full Circle. KLPM. ISBN 978-0955865008.\nKogon, Eugen (1974). Der SS-Staat. Das System der deutschen Konzentrationslager (in German).\nRittner, Carol; Roth, John K. (1998). Different Voices: Women and the Holocaust.\nStojka, Ceija (1988). We Live in Seclusion: The Memories of a Romni.\nSteinberg, Manny (2014). Outcry: Holocaust Memoirs. Amsterdam Publishers. ISBN 978-9-082103137.\nWetzler, Alfréd (2007). Escape from Hell: The True Story of the Auschwitz Protocol. Berghahn Books.\nWinter, Walter (2004). Winter Time: Memoirs of a German Sinto who Survived Auschwitz.\nCzech, Danuta (1999). Auschwitz Chronicle: 1939-1945.\nDean, Martin (1999). Collaboration in the Holocaust: Crimes of the Local Police in Belorussia and Ukraine.\nFings, Karola; Kenrick, Donald, eds. (1999). The Gypsies During the Second World War.\nHogan, David J.; Aretha, David, eds. (2000). The Holocaust Chronicle: A History in Words and Pictures. Lincolnwood, IL: Publications International.\nPressac, Jean-Claude (1989). Auschwitz: Technique and operation of the gas chambers.\nAgamben, Giorgio (1999). Remnants of Auschwitz: The Witness and the Archive.\nBloxham, Donald (2009). The Final Solution: A Genocide.\nLawson, Tom (2010). Debates on the Holocaust. University of Manchester Press.\nLeff, Laurel (2005). Buried By The Times: The Holocaust And America\'s Most Important Newspaper. Cambridge University Press. ISBN 0-521-81287-9.\nMason, Timothy. "Intention and Explanation: A Current Controversy about the Interpretation of National Socialism". In Marrus, Michael R. (ed.). The Nazi Holocaust Part 3, The "Final Solution": The Implementation of Mass Murder. Vol. 1. Westpoint, CT: Mecler. pp. 3–20.\nNiewyk, Donald L. (1992). Holocaust: Problems & Perspective of Interpretation.', - }, -]; + source: 'https://en.wikipedia.org/wiki/Bibliography_of_the_Holocaust#Historical_studies', + text: 'Bauer, Yehuda (1994). Jews for Sale? Nazi-Jewish Negotiations 1933-1945. Yale University Press. ISBN 0-300-06852-2.\nBerenbaum, Michael (1990). A Mosaic of Victims: Non-Jews Persecuted and Murdered by the Nazis.\nBergen, Doris (2009). War and Genocide: Concise History of the Holocaust.\nBlack, Edwin (2010). The Farhud: The Arab-Nazi Alliance in the Holocaust. Dialog Press. ISBN 0-914153-14-5.\nBraham, Randolph (1994) [1981]. The Politics of Genocide: The Holocaust in Hungary. Columbia University Press. ISBN 0-88033-247-6.\nBraham, Randolph (2011). The Auschwitz Reports and the Holocaust in Hungary. Columbia University Press.\nChalmers, Beverley (2015). Birth, Sex and Abuse: Women\'s Voices Under Nazi Rule. Grosvenor House Publishing Ltd. ISBN 1781483531.\nDavies, Norman; Lukas, Richard C. (2001) [1996]. Forgotten Holocaust: The Poles Under German Occupation.\nDean, Martin (2008). Robbing the Jews: The Confiscation of Jewish Property in the Holocaust. Cambridge University Press.\nEvans, Suzanne (2004). Forgotten Crimes: The Holocaust and People with Disabilities.\nFriedländer, Saul (1998). The Years of Persecution: Nazi Germany and the Jews, 1933-1939. Vol. 1.\nGrau, Gunter; Shoppmann, Claudia (1995). The Hidden Holocaust?: Gay and Lesbian Persecution in Germany 1933-45.\nHedgepeth, Sonja; Saidel, Rochelle (2010). Sexual Violence against Jewish Women during the Holocaust.\nPeukert, Detlev (1994). "The Genesis of the \'Final Solution\' from the Spirit of Science". In Thomas Childers; Jane Caplan (eds.). Reevaluating the Third Reich. New York: Holmes & Meier. pp. 234–252. ISBN 0-8419-1178-9.\nRees, Laurence (2017). The Holocaust: A New History. London: Viking Press. ISBN 978-1610398442.\nAméry, Jean (1980). At the Mind\'s Limits: Contemplations by a Survivor on Auschwitz and its Realities.\nBlitz Konig, Nanette (2018). Holocaust Memoirs of a Bergen-Belsen Survivor and Classmate of Anne Frank. Amsterdam Publishers. ISBN 9789492371614.\nDittman, Anita (2005). Trapped in Hitler\'s Hell. ISBN 0-9721512-8-1.\nGerrard, Mady. Full Circle. KLPM. ISBN 978-0955865008.\nKogon, Eugen (1974). Der SS-Staat. Das System der deutschen Konzentrationslager (in German).\nRittner, Carol; Roth, John K. (1998). Different Voices: Women and the Holocaust.\nStojka, Ceija (1988). We Live in Seclusion: The Memories of a Romni.\nSteinberg, Manny (2014). Outcry: Holocaust Memoirs. Amsterdam Publishers. ISBN 978-9-082103137.\nWetzler, Alfréd (2007). Escape from Hell: The True Story of the Auschwitz Protocol. Berghahn Books.\nWinter, Walter (2004). Winter Time: Memoirs of a German Sinto who Survived Auschwitz.\nCzech, Danuta (1999). Auschwitz Chronicle: 1939-1945.\nDean, Martin (1999). Collaboration in the Holocaust: Crimes of the Local Police in Belorussia and Ukraine.\nFings, Karola; Kenrick, Donald, eds. (1999). The Gypsies During the Second World War.\nHogan, David J.; Aretha, David, eds. (2000). The Holocaust Chronicle: A History in Words and Pictures. Lincolnwood, IL: Publications International.\nPressac, Jean-Claude (1989). Auschwitz: Technique and operation of the gas chambers.\nAgamben, Giorgio (1999). Remnants of Auschwitz: The Witness and the Archive.\nBloxham, Donald (2009). The Final Solution: A Genocide.\nLawson, Tom (2010). Debates on the Holocaust. University of Manchester Press.\nLeff, Laurel (2005). Buried By The Times: The Holocaust And America\'s Most Important Newspaper. Cambridge University Press. ISBN 0-521-81287-9.\nMason, Timothy. "Intention and Explanation: A Current Controversy about the Interpretation of National Socialism". In Marrus, Michael R. (ed.). The Nazi Holocaust Part 3, The "Final Solution": The Implementation of Mass Murder. Vol. 1. Westpoint, CT: Mecler. pp. 3–20.\nNiewyk, Donald L. (1992). Holocaust: Problems & Perspective of Interpretation.' + } +] diff --git a/openlibrary/components/BulkSearch/utils/searchUtils.js b/openlibrary/components/BulkSearch/utils/searchUtils.js index b95d98f4f5e..49b00098012 100644 --- a/openlibrary/components/BulkSearch/utils/searchUtils.js +++ b/openlibrary/components/BulkSearch/utils/searchUtils.js @@ -1,8 +1,9 @@ + /** @typedef {import('./classes.js').ExtractedBook} ExtractedBook */ /** @typedef {import('./classes.js').MatchOptions} MatchOptions */ /** @typedef {import('./classes.js').BookMatch} BookMatch */ -const OL_SEARCH_BASE = 'openlibrary.org'; +const OL_SEARCH_BASE = 'openlibrary.org' /** * @param {ExtractedBook} extractedBook @@ -10,34 +11,19 @@ const OL_SEARCH_BASE = 'openlibrary.org'; */ export function buildSearchUrl(extractedBook, matchOptions, json = true) { let title = extractedBook.title?.split(/[:(?]/)[0].replace(/’/g, '\''); - const author = extractedBook.author; + const author = extractedBook.author // Remove leading articles from title; these can sometimes be missing from OL records, // and will hence cause a failed match. // Taken from https://github.com/internetarchive/openlibrary/blob/4d880c1bf3e2391dd001c7818052fd639d38ff58/conf/solr/conf/managed-schema.xml#L526 - title = title - .replace( - /^(an? |the |l[aeo]s? |l'|de la |el |il |un[ae]? |du |de[imrst]? |das |ein |eine[mnrs]? |bir )/i, - '', - ) - .trim(); + title = title.replace(/^(an? |the |l[aeo]s? |l'|de la |el |il |un[ae]? |du |de[imrst]? |das |ein |eine[mnrs]? |bir )/i, '').trim(); const query = []; if (title) { query.push(`title:"${title}"`); } - if ( - matchOptions.includeAuthor && - author && - author.toLowerCase() !== 'null' && - author.toLowerCase() !== 'unknown' - ) { - const authorParts = author - .replace(/^\S+\./, '') - .trim() - .split(/\s/); - const authorLastName = author.includes(',') - ? author.replace(/,.*/, '') - : authorParts[authorParts.length - 1]; + if (matchOptions.includeAuthor && author && author.toLowerCase() !== 'null' && author.toLowerCase() !== 'unknown') { + const authorParts = author.replace(/^\S+\./, '').trim().split(/\s/); + const authorLastName = author.includes(',') ? author.replace(/,.*/, '') : authorParts[authorParts.length - 1]; query.push(`author:${authorLastName}`); } @@ -50,8 +36,7 @@ export function buildSearchUrl(extractedBook, matchOptions, json = true) { const url = `${path}?${new URLSearchParams({ q: query.join(' '), mode: 'everything', - fields: - 'key,title,author_name,cover_i,first_publish_year,edition_count,ebook_access', + fields: 'key,title,author_name,cover_i,first_publish_year,edition_count,ebook_access', })}`; return url; } diff --git a/openlibrary/components/IdentifiersInput/utils/utils.js b/openlibrary/components/IdentifiersInput/utils/utils.js index bc2fe044e5b..fcb2fd796ad 100644 --- a/openlibrary/components/IdentifiersInput/utils/utils.js +++ b/openlibrary/components/IdentifiersInput/utils/utils.js @@ -1,21 +1,21 @@ import { + parseIsbn, + parseLccn, isChecksumValidIsbn10, isChecksumValidIsbn13, isFormatValidIsbn10, isFormatValidIsbn13, isValidLccn, - parseIsbn, - parseLccn, } from '../../../plugins/openlibrary/js/idValidation.js'; export function errorDisplay(message, error_output) { let errorSelector; if (error_output === '#hiddenAuthorIdentifiers') { - errorSelector = document.querySelector('#id-errors-author'); + errorSelector = document.querySelector('#id-errors-author') } else if (error_output === '#hiddenWorkIdentifiers') { - errorSelector = document.querySelector('#id-errors-work'); + errorSelector = document.querySelector('#id-errors-work') } else if (error_output === '#hiddenEditionIdentifiers') { - errorSelector = document.querySelector('#id-errors-edition'); + errorSelector = document.querySelector('#id-errors-edition') } if (message) { errorSelector.style.display = ''; @@ -24,24 +24,18 @@ export function errorDisplay(message, error_output) { errorSelector.style.display = 'none'; errorSelector.innerHTML = ''; } + } function validateIsbn10(value) { const isbn10_value = parseIsbn(value); if (!isFormatValidIsbn10(isbn10_value)) { - errorDisplay( - 'ID must be exactly 10 characters [0-9] or X.', - '#hiddenEditionIdentifiers', - ); + errorDisplay('ID must be exactly 10 characters [0-9] or X.', '#hiddenEditionIdentifiers'); return false; } else if ( - isFormatValidIsbn10(isbn10_value) && - !isChecksumValidIsbn10(isbn10_value) + isFormatValidIsbn10(isbn10_value) && !isChecksumValidIsbn10(isbn10_value) ) { - errorDisplay( - `ISBN ${isbn10_value} may be invalid. Please confirm if you'd like to add it before saving all changes`, - '#hiddenEditionIdentifiers', - ); + errorDisplay(`ISBN ${isbn10_value} may be invalid. Please confirm if you'd like to add it before saving all changes`, '#hiddenEditionIdentifiers'); } return true; } @@ -50,19 +44,12 @@ function validateIsbn13(value) { const isbn13_value = parseIsbn(value); if (!isFormatValidIsbn13(isbn13_value)) { - errorDisplay( - 'ID must be exactly 13 digits [0-9]. For example: 978-1-56619-909-4', - '#hiddenEditionIdentifiers', - ); + errorDisplay('ID must be exactly 13 digits [0-9]. For example: 978-1-56619-909-4', '#hiddenEditionIdentifiers'); return false; } else if ( - isFormatValidIsbn13(isbn13_value) && - !isChecksumValidIsbn13(isbn13_value) + isFormatValidIsbn13(isbn13_value) && !isChecksumValidIsbn13(isbn13_value) ) { - errorDisplay( - `ISBN ${isbn13_value} may be invalid. Please confirm if you'd like to add it before saving all changes`, - '#hiddenEditionIdentifiers', - ); + errorDisplay(`ISBN ${isbn13_value} may be invalid. Please confirm if you'd like to add it before saving all changes`, '#hiddenEditionIdentifiers'); } return true; } @@ -92,7 +79,7 @@ export function validateIdentifiers(name, value, entries, error_output) { } else if (name === 'lccn') { validId = validateLccn(value); } - if (Array.from(entries).some((entry) => entry === value) === true) { + if (Array.from(entries).some(entry => entry === value) === true) { validId = false; errorDisplay('That ID already exists for an identifier.', error_output); } diff --git a/openlibrary/components/LibraryExplorer/utils.js b/openlibrary/components/LibraryExplorer/utils.js index a87c0c6c8be..404d76b3558 100644 --- a/openlibrary/components/LibraryExplorer/utils.js +++ b/openlibrary/components/LibraryExplorer/utils.js @@ -36,7 +36,7 @@ export function hashCode(str) { */ export function hierarchyFind(node, predicate) { if (!predicate(node)) return []; - for (const child of node.children || []) { + for (const child of (node.children || [])) { const childResult = hierarchyFind(child, predicate); if (childResult.length) return [node, ...childResult]; } @@ -66,28 +66,18 @@ export function testLuceneSyntax(pattern, string) { * while keeping it solr-query safe. * @param {string} string */ -export function decrementStringSolr( - string, - caseSensitive = true, - numeric = false, -) { - const lastChar = caseSensitive - ? string[string.length - 1] - : string[string.length - 1].toUpperCase(); +export function decrementStringSolr(string, caseSensitive=true, numeric=false) { + const lastChar = caseSensitive ? string[string.length - 1] : string[string.length - 1].toUpperCase(); // Anything < '.' will likely cause query issues, so assume it's // the end of the that prefix. // Also append Z; this is the equivalent of going back one, and then expanding (e.g. 0.123 decremented is not 0.122, it's 0.12999999) const maxTail = (numeric ? '9' : 'z').repeat(5); - const newLastChar = - lastChar === '.' - ? '' - : lastChar === '0' - ? `.${maxTail}` - : lastChar === 'A' - ? `9${maxTail}` - : lastChar === 'a' - ? `Z${maxTail}` - : `${String.fromCharCode(lastChar.charCodeAt(0) - 1)}${maxTail}`; + const newLastChar = ( + lastChar === '.' ? '' : + lastChar === '0' ? `.${maxTail}` : + lastChar === 'A' ? `9${maxTail}` : + lastChar === 'a' ? `Z${maxTail}` : + `${String.fromCharCode(lastChar.charCodeAt(0) - 1)}${maxTail}`); return string.slice(0, -1) + newLastChar; } @@ -104,7 +94,7 @@ export async function pollUntilTruthy(fn, { timeout = 1000, step = 100 } = {}) { while (Date.now() - start <= timeout) { const val = fn(); if (val) return val; - await new Promise((resolve) => setTimeout(resolve, step)); + await new Promise(resolve => setTimeout(resolve, step)); } return undefined; } diff --git a/openlibrary/components/LibraryExplorer/utils/lcc.js b/openlibrary/components/LibraryExplorer/utils/lcc.js index 1043ff32ac0..06c9bed4538 100644 --- a/openlibrary/components/LibraryExplorer/utils/lcc.js +++ b/openlibrary/components/LibraryExplorer/utils/lcc.js @@ -12,18 +12,15 @@ const LCC_PARTS_RE = new RegExp( (?<cutter1>\s*\.\s*[^\d\s\[]{1,3}\d*\S*)? (?<rest>\s.*)? $`.replace(/\s/g, ''), - 'i', -); + 'i'); export function short_lcc_to_sortable_lcc(lcc) { const m = clean_raw_lcc(lcc).match(LCC_PARTS_RE); - if (!m) return null; + if (!m) return null const letters = m.groups.letters.toUpperCase().padEnd(3, '-'); const number = parseFloat(m.groups.number || 0); - const cutter1 = m.groups.cutter1 - ? `.${m.groups.cutter1.replace(/^[ .]+/, '')}` - : ''; + const cutter1 = m.groups.cutter1 ? `.${m.groups.cutter1.replace(/^[ .]+/, '')}` : ''; const rest = m.groups.rest ? ` ${m.groups.rest}` : ''; // There will often be a CPB Box No (whatever that is) in the LCC field; @@ -44,11 +41,12 @@ export function sortable_lcc_to_short_lcc(lcc) { letters: m.groups.letters.replace(/-+/, ''), number: parseFloat(m.groups.number), cutter1: m.groups.cutter1 ? m.groups.cutter1.trim() : '', - rest: m.groups.rest ? ` ${m.groups.rest}` : '', - }; + rest: m.groups.rest ? ` ${m.groups.rest}` : '' + } return `${parts.letters}${parts.number}${parts.cutter1}${parts.rest}`; } + /** * Remove noise in lcc before matching to LCC_PARTS_RE * @param {string} raw_lcc @@ -56,11 +54,8 @@ export function sortable_lcc_to_short_lcc(lcc) { */ export function clean_raw_lcc(raw_lcc) { let lcc = raw_lcc.replace(/\\/g, ' ').trim(); - if ( - (lcc.startsWith('[') && lcc.endsWith(']')) || - (lcc.startsWith('(') && lcc.endsWith(')')) - ) { + if ((lcc.startsWith('[') && lcc.endsWith(']')) || (lcc.startsWith('(') && lcc.endsWith(')'))) { lcc = lcc.slice(1, -1); } - return lcc; + return lcc } diff --git a/openlibrary/components/MergeUI.vue b/openlibrary/components/MergeUI.vue index 253bff74ae0..ff56fcd9913 100644 --- a/openlibrary/components/MergeUI.vue +++ b/openlibrary/components/MergeUI.vue @@ -52,13 +52,13 @@ </template> <script> -import MergeTable from './MergeUI/MergeTable.vue'; +import MergeTable from './MergeUI/MergeTable.vue' import { do_merge, update_merge_request, createMergeRequest, DEFAULT_EDITION_LIMIT } from './MergeUI/utils.js'; -const DO_MERGE = 'Do Merge'; -const REQUEST_MERGE = 'Request Merge'; -const LOADING = 'Loading...'; -const SAVING = 'Saving...'; +const DO_MERGE = 'Do Merge' +const REQUEST_MERGE = 'Request Merge' +const LOADING = 'Loading...' +const SAVING = 'Saving...' export default { name: 'App', @@ -81,17 +81,17 @@ export default { default: 'true', } }, - data () { + data() { return { url: new URL(location.toString()), mergeStatus: LOADING, mergeOutput: null, show_diffs: false, comment: '' - }; + } }, computed: { - olids () { + olids() { const olidsString = this.url.searchParams.get('records'); if (!olidsString) return []; return olidsString @@ -100,20 +100,20 @@ export default { .filter(Boolean); }, - isSuperLibrarian () { - return this.canmerge === 'true'; + isSuperLibrarian() { + return this.canmerge === 'true' }, - isDisabled () { - return this.mergeStatus !== DO_MERGE && this.mergeStatus !== REQUEST_MERGE; + isDisabled() { + return this.mergeStatus !== DO_MERGE && this.mergeStatus !== REQUEST_MERGE }, - showRejectButton () { - return this.mrid && this.isSuperLibrarian; + showRejectButton() { + return this.mrid && this.isSuperLibrarian } }, - mounted () { - const readyCta = this.isSuperLibrarian ? DO_MERGE : REQUEST_MERGE; + mounted() { + const readyCta = this.isSuperLibrarian ? DO_MERGE : REQUEST_MERGE this.$watch( '$refs.mergeTable.merge', (new_value) => { @@ -122,7 +122,7 @@ export default { ); }, methods: { - async doMerge () { + async doMerge() { if (!this.$refs.mergeTable.merge) return; const { record: master, dupes, editions_to_move, unmergeable_works } = this.$refs.mergeTable.merge; @@ -141,10 +141,10 @@ export default { } this.mergeOutput = await r.json(); if (this.mrid) { - await update_merge_request(this.mrid, 'approve', this.comment); + await update_merge_request(this.mrid, 'approve', this.comment) } else { - const workIds = [master.key].concat(Array.from(dupes, item => item.key)); - await createMergeRequest(workIds); + const workIds = [master.key].concat(Array.from(dupes, item => item.key)) + await createMergeRequest(workIds) } } catch (e) { this.mergeOutput = e.message; @@ -153,25 +153,25 @@ export default { } } else { // Create a new merge request with "pending" status - const workIds = [master.key].concat(Array.from(dupes, item => item.key)); - const splitKey = master.key.split('/'); - const primaryRecord = splitKey[splitKey.length - 1]; + const workIds = [master.key].concat(Array.from(dupes, item => item.key)) + const splitKey = master.key.split('/') + const primaryRecord = splitKey[splitKey.length - 1] await createMergeRequest(workIds, primaryRecord, 'create-pending', this.comment) .then(response => response.json()) .then(data => { if (data.status === 'ok') { // Redirect to merge table on success: - window.location.replace(`/merges#mrid-${data.id}`); + window.location.replace(`/merges#mrid-${data.id}`) } - }); + }) } this.mergeStatus = 'Done'; }, - async rejectMerge () { + async rejectMerge() { try { - await update_merge_request(this.mrid, 'decline', this.comment); - this.mergeOutput = 'Merge request closed'; + await update_merge_request(this.mrid, 'decline', this.comment) + this.mergeOutput = 'Merge request closed' } catch (e) { this.mergeOutput = e.message; throw e; @@ -179,7 +179,7 @@ export default { this.mergeStatus = 'Reject Merge'; } } -}; +} </script> <style> diff --git a/openlibrary/components/MergeUI/AuthorRoleTable.vue b/openlibrary/components/MergeUI/AuthorRoleTable.vue index 9efda1993f3..a057644f017 100644 --- a/openlibrary/components/MergeUI/AuthorRoleTable.vue +++ b/openlibrary/components/MergeUI/AuthorRoleTable.vue @@ -54,9 +54,9 @@ export default { roles: Array }, computed: { - fields () { + fields() { return _.uniq(_.flatMap(this.roles, Object.keys)).sort(); } } -}; +} </script> diff --git a/openlibrary/components/MergeUI/EditionSnippet.vue b/openlibrary/components/MergeUI/EditionSnippet.vue index 270bd12b4c7..0de0a239f5c 100644 --- a/openlibrary/components/MergeUI/EditionSnippet.vue +++ b/openlibrary/components/MergeUI/EditionSnippet.vue @@ -61,17 +61,17 @@ export default { edition: Object }, computed: { - publish_year () { + publish_year() { if (!this.edition.publish_date) return ''; const m = this.edition.publish_date.match(/\d{4}/); return m ? m[0] : null; }, - publishers () { + publishers() { return this.edition.publishers || []; }, - number_of_pages () { + number_of_pages() { if (this.edition.number_of_pages) { return this.edition.number_of_pages; } else if (this.edition.pagination) { @@ -82,17 +82,17 @@ export default { return '?'; }, - full_title () { + full_title() { let title = this.edition.title; if (this.edition.subtitle) title += `: ${this.edition.subtitle}`; return title; }, - cover_id () { + cover_id() { return this.edition.covers?.[0] ?? null; }, - cover_url () { + cover_url() { if (this.cover_id) return `https://covers.openlibrary.org/b/id/${this.cover_id}-M.jpg`; const ocaid = this.edition.ocaid; @@ -102,13 +102,13 @@ export default { return ''; }, - languages () { + languages() { if (!this.edition.languages) return '???'; const langs = this.edition.languages.map(lang => lang.key.split('/')[2]); return langs.join(', '); }, - asins () { + asins() { return _.uniq([ ...((this.edition.identifiers && this.edition.identifiers.amazon) || []), this.edition.isbn_10 && ISBN.asIsbn10(this.edition.isbn_10), @@ -118,7 +118,7 @@ export default { }, methods: { - openEnlargedCover () { + openEnlargedCover() { let url = ''; if (this.cover_id) { url = `https://covers.openlibrary.org/b/id/${this.cover_id}.jpg`; diff --git a/openlibrary/components/MergeUI/ExcerptsTable.vue b/openlibrary/components/MergeUI/ExcerptsTable.vue index ddfecfd1baf..013f3ad0f0c 100644 --- a/openlibrary/components/MergeUI/ExcerptsTable.vue +++ b/openlibrary/components/MergeUI/ExcerptsTable.vue @@ -37,9 +37,9 @@ export default { excerpts: Array }, computed: { - fields () { + fields() { return _.uniq(_.flatMap(this.excerpts, Object.keys)); } } -}; +} </script> diff --git a/openlibrary/components/MergeUI/MergeRow.vue b/openlibrary/components/MergeUI/MergeRow.vue index 6a17706ef11..01df7bcbf21 100644 --- a/openlibrary/components/MergeUI/MergeRow.vue +++ b/openlibrary/components/MergeUI/MergeRow.vue @@ -88,7 +88,7 @@ export default { type: Boolean } }, - data () { + data() { return { master_key: null }; diff --git a/openlibrary/components/MergeUI/MergeRowField.vue b/openlibrary/components/MergeUI/MergeRowField.vue index dc9a2c9db13..2742122f31c 100644 --- a/openlibrary/components/MergeUI/MergeRowField.vue +++ b/openlibrary/components/MergeUI/MergeRowField.vue @@ -157,7 +157,7 @@ export default { } }, computed: { - title () { + title() { let title = `.${this.field}`; if (this.value instanceof Array) { const length = this.value.length; diff --git a/openlibrary/components/MergeUI/MergeRowJointField.vue b/openlibrary/components/MergeUI/MergeRowJointField.vue index 27e3cb9507e..502e7aa49f5 100644 --- a/openlibrary/components/MergeUI/MergeRowJointField.vue +++ b/openlibrary/components/MergeUI/MergeRowJointField.vue @@ -40,7 +40,7 @@ export default { } }, computed: { - presentFields () { + presentFields() { return this.fields.filter(f => f in this.record); } } diff --git a/openlibrary/components/MergeUI/TextDiff.vue b/openlibrary/components/MergeUI/TextDiff.vue index 0359a824c17..91773141e6b 100644 --- a/openlibrary/components/MergeUI/TextDiff.vue +++ b/openlibrary/components/MergeUI/TextDiff.vue @@ -25,7 +25,7 @@ export default { } }, computed: { - diff () { + diff() { const fn = { char: diffChars, word: diffWordsWithSpace, @@ -33,7 +33,7 @@ export default { return fn[this.resolution](this.left, this.right); } } -}; +} </script> <style scoped> diff --git a/openlibrary/components/ObservationForm.vue b/openlibrary/components/ObservationForm.vue index f74214eb4c0..30754a9ba6a 100644 --- a/openlibrary/components/ObservationForm.vue +++ b/openlibrary/components/ObservationForm.vue @@ -30,11 +30,11 @@ </template> <script> -import CategorySelector from './ObservationForm/components/CategorySelector.vue'; -import SavedTags from './ObservationForm/components/SavedTags.vue'; -import ValueCard from './ObservationForm/components/ValueCard.vue'; +import CategorySelector from './ObservationForm/components/CategorySelector.vue' +import SavedTags from './ObservationForm/components/SavedTags.vue' +import ValueCard from './ObservationForm/components/ValueCard.vue' -import { decodeAndParseJSON, resizeColorbox } from './ObservationForm/Utils'; +import { decodeAndParseJSON, resizeColorbox } from './ObservationForm/Utils' export default { name: 'ObservationForm', @@ -84,7 +84,7 @@ export default { required: true } }, - data: function () { + data: function() { return { /** * An object representing the currently selected tag type. @@ -113,7 +113,7 @@ export default { * An array containing all book tag types and values. */ observationsArray: null, - }; + } }, computed: { /** @@ -121,28 +121,28 @@ export default { * * @returns {Number|null} The ID of the selected observation, if one exists. */ - getSelectedId: function () { + getSelectedId: function() { if (this.selectedObservation) { return this.selectedObservation.id; } - return null; + return null } }, - created: function () { + created: function() { this.observationsArray = decodeAndParseJSON(this.schema)['observations']; this.allSelectedValues = decodeAndParseJSON(this.observations); this.selectRandomObservation(); }, - mounted: function () { + mounted: function() { this.observer = new ResizeObserver(() => { resizeColorbox(); }); - this.observer.observe(this.$refs.form); + this.observer.observe(this.$refs.form) }, - beforeUnmount: function () { + beforeUnmount: function() { if (this.observer) { - this.observer.disconnect(); + this.observer.disconnect() } }, methods: { @@ -151,18 +151,18 @@ export default { * * @param {Object | null} observation The new selected observation, or `null` if no type is selected. */ - updateSelected: function (observation) { - this.selectedObservation = observation; + updateSelected: function(observation) { + this.selectedObservation = observation }, /** * Randomly sets a selected observation. */ - selectRandomObservation: function () { + selectRandomObservation: function() { const randomNumber = Math.floor(Math.random() * 100000); this.selectedObservation = this.observationsArray[randomNumber % this.observationsArray.length]; } } -}; +} </script> <style scoped> diff --git a/openlibrary/components/ObservationForm/ObservationService.js b/openlibrary/components/ObservationForm/ObservationService.js index c9fbcc97f53..8b00a3c59c9 100644 --- a/openlibrary/components/ObservationForm/ObservationService.js +++ b/openlibrary/components/ObservationForm/ObservationService.js @@ -10,23 +10,23 @@ * @returns A Promise representing the state of the POST request. */ export function updateObservation(action, type, value, workKey, username) { - const data = constructDataObject(type, value, username, action); + const data = constructDataObject(type, value, username, action) return fetch(`${workKey}/observations`, { method: 'POST', headers: { - 'Content-Type': 'application/json', + 'Content-Type': 'application/json' }, - body: JSON.stringify(data), + body: JSON.stringify(data) }) - .then((response) => { + .then(response => { if (!response.ok) { - throw new Error('Server response was not ok'); + throw new Error('Server response was not ok') } }) - .catch((error) => { - throw error; - }); + .catch(error => { + throw error + }) } /** @@ -51,8 +51,8 @@ function constructDataObject(type, value, username, action) { const data = { username: username, action: action, - observation: {}, - }; + observation: {} + } data.observation[type] = value; diff --git a/openlibrary/components/ObservationForm/Utils.js b/openlibrary/components/ObservationForm/Utils.js index 5f56344a50c..e0c3a32c1cd 100644 --- a/openlibrary/components/ObservationForm/Utils.js +++ b/openlibrary/components/ObservationForm/Utils.js @@ -13,11 +13,7 @@ export function decodeAndParseJSON(str) { window.$.colorbox is a jQuery plugin */ export function resizeColorbox() { - if ( - window.$ && - window.$.colorbox && - typeof window.$.colorbox.resize === 'function' - ) { + if (window.$ && window.$.colorbox && typeof window.$.colorbox.resize === 'function') { window.$.colorbox.resize(); } } diff --git a/openlibrary/components/ObservationForm/components/CardBody.vue b/openlibrary/components/ObservationForm/components/CardBody.vue index 3ba378e914b..d268713cc92 100644 --- a/openlibrary/components/ObservationForm/components/CardBody.vue +++ b/openlibrary/components/ObservationForm/components/CardBody.vue @@ -14,9 +14,9 @@ </template> <script> -import OLChip from './OLChip.vue'; +import OLChip from './OLChip.vue' -import { updateObservation } from '../ObservationService'; +import { updateObservation } from '../ObservationService' export default { name: 'CardBody', @@ -81,8 +81,8 @@ export default { /** * Returns an array of all of this book tag type's currently selected values. */ - selectedValues: function () { - return this.allSelectedValues[this.type]?.length ? this.allSelectedValues[this.type] : []; + selectedValues: function() { + return this.allSelectedValues[this.type]?.length ? this.allSelectedValues[this.type] : [] } }, methods: { @@ -94,19 +94,19 @@ export default { * @param {boolean} isSelected `true` if a chip is selected, `false` otherwise. * @param {String} text The text that the updated chip is displaying. */ - updateSelected: function (isSelected, text) { - let updatedValues = this.allSelectedValues[this.type] ? this.allSelectedValues[this.type] : []; + updateSelected: function(isSelected, text) { + let updatedValues = this.allSelectedValues[this.type] ? this.allSelectedValues[this.type] : [] if (isSelected) { if (this.multiSelect) { - updatedValues.push(text); + updatedValues.push(text) updateObservation('add', this.type, text, this.workKey, this.username) .catch(() => { updatedValues.pop(); }) .finally(() => { this.allSelectedValues[this.type] = updatedValues; - }); + }) } else { if (updatedValues.length) { let deleteSuccessful = false; @@ -118,13 +118,13 @@ export default { if (deleteSuccessful) { updateObservation('add', this.type, text, this.workKey, this.username) .then(() => { - updatedValues = [text]; + updatedValues = [text] }) .finally(() => { this.allSelectedValues[this.type] = updatedValues; - }); + }) } - }); + }) } } } else { @@ -133,11 +133,11 @@ export default { updateObservation('delete', this.type, text, this.workKey, this.username) .catch(() => { updatedValues.push(text); - }); + }) } } } -}; +} </script> <style scoped> diff --git a/openlibrary/components/ObservationForm/components/CardHeader.vue b/openlibrary/components/ObservationForm/components/CardHeader.vue index 091553b6459..4fb3262e8bf 100644 --- a/openlibrary/components/ObservationForm/components/CardHeader.vue +++ b/openlibrary/components/ObservationForm/components/CardHeader.vue @@ -18,7 +18,7 @@ export default { required: true } }, -}; +} </script> <style scoped> diff --git a/openlibrary/components/ObservationForm/components/CategorySelector.vue b/openlibrary/components/ObservationForm/components/CategorySelector.vue index b89204a3cc3..b9e541ac529 100644 --- a/openlibrary/components/ObservationForm/components/CategorySelector.vue +++ b/openlibrary/components/ObservationForm/components/CategorySelector.vue @@ -27,7 +27,7 @@ </template> <script> -import OLChip from './OLChip.vue'; +import OLChip from './OLChip.vue' export default { name: 'CategorySelector', @@ -74,7 +74,7 @@ export default { default: 0 } }, - data: function () { + data: function() { return { /** * The ID of the selected book tag type. @@ -82,7 +82,7 @@ export default { * @type {number | null} */ selectedId: this.initialSelectedId, - }; + } }, methods: { /** @@ -91,20 +91,20 @@ export default { * @param {boolean} isSelected Whether or not a chip is currently selected. * @param {String} text The text displayed by a chip. */ - updateSelected: function (isSelected, text) { + updateSelected: function(isSelected, text) { if (isSelected) { // TODO: This for loop shouldn't be necessary for (let i = 0; i < this.observationsArray.length; ++i) { if (this.observationsArray[i].label === text) { this.selectedId = this.observationsArray[i].id; - this.$emit('update-selected', this.observationsArray[i]); + this.$emit('update-selected', this.observationsArray[i]) } } } else { this.selectedId = null; // Set ObservationForm's selected observation to null - this.$emit('update-selected', null); + this.$emit('update-selected', null) } }, /** @@ -112,8 +112,8 @@ export default { * * @param {number} id A chip's id. */ - isSelected: function (id) { - return this.selectedId === id; + isSelected: function(id) { + return this.selectedId === id }, /** * Returns an HTML code denoting what symbol to display in a book tag type chip. @@ -123,7 +123,7 @@ export default { * * @returns {String} An HTML code representing selections of a type. */ - displaySymbol: function (type) { + displaySymbol: function(type) { if (this.allSelectedValues[type] && this.allSelectedValues[type].length) { // ✔ - Heavy checkmark return '✔'; @@ -131,7 +131,7 @@ export default { return '•'; } } -}; +} </script> <style scoped> diff --git a/openlibrary/components/ObservationForm/components/OLChip.vue b/openlibrary/components/ObservationForm/components/OLChip.vue index 45cbfc180b7..e5f58967a2e 100644 --- a/openlibrary/components/ObservationForm/components/OLChip.vue +++ b/openlibrary/components/ObservationForm/components/OLChip.vue @@ -51,7 +51,7 @@ export default { default: '' } }, - data: function () { + data: function() { return { /** * Tracks whether this chip is currently selected. @@ -59,7 +59,7 @@ export default { * @type {boolean} */ isSelected: this.selected - }; + } }, computed: { /** @@ -67,20 +67,20 @@ export default { * * @returns 'click' if this chip can be selected, otherwise `null` */ - canSelect: function () { + canSelect: function() { return this.selectable ? 'click' : null; } }, watch: { selected (newValue) { - this.isSelected = newValue; + this.isSelected = newValue } }, methods: { /** * Toggles the value of `isSelected` and fires an `update-selected` event. */ - onClick: function () { + onClick: function() { this.toggleSelected(); /** * Update selected event. @@ -88,16 +88,16 @@ export default { * @property {boolean} isSelected Selected status of this chip. * @property {String} text Main text displayed by this chip. */ - this.$emit('update-selected', this.isSelected, this.text); + this.$emit('update-selected', this.isSelected, this.text) }, /** * Toggles the state of `isSelected` */ - toggleSelected: function () { + toggleSelected: function() { this.isSelected = !this.isSelected; } } -}; +} </script> <style scoped> diff --git a/openlibrary/components/ObservationForm/components/SavedTags.vue b/openlibrary/components/ObservationForm/components/SavedTags.vue index ecf79dba6dd..0d023c3d1a5 100644 --- a/openlibrary/components/ObservationForm/components/SavedTags.vue +++ b/openlibrary/components/ObservationForm/components/SavedTags.vue @@ -38,9 +38,9 @@ </template> <script> -import OLChip from './OLChip.vue'; +import OLChip from './OLChip.vue' -import { updateObservation } from '../ObservationService'; +import { updateObservation } from '../ObservationService' export default { @@ -80,7 +80,7 @@ export default { required: true } }, - data: function () { + data: function() { return { /** * Contains class strings for each selected book tag @@ -94,18 +94,18 @@ export default { * @type {Object} */ classLists: {} - }; + } }, computed: { /** * An array of a patron's book tags. */ - selectedValues: function () { + selectedValues: function() { const results = []; for (const type in this.allSelectedValues) { for (const value of this.allSelectedValues[type]) { - results.push(`${type}: ${value}`); + results.push(`${type}: ${value}`) } } @@ -118,8 +118,8 @@ export default { * * @param {String} chipText The text of the selected tag chip, in the form "<type>: <value>" */ - removeItem: function (chipText) { - const [type, value] = chipText.split(': '); + removeItem: function(chipText) { + const [type, value] = chipText.split(': ') const valueIndex = this.allSelectedValues[type].indexOf(value); const valueArr = this.allSelectedValues[type]; @@ -131,9 +131,9 @@ export default { }) .finally(() => { if (valueArr.length === 0) { - delete this.allSelectedValues[type]; + delete this.allSelectedValues[type] } - }); + }) // Remove hover class: this.removeHoverClass(chipText); @@ -143,7 +143,7 @@ export default { * * @param {String} value The chip's key. */ - addHoverClass: function (value) { + addHoverClass: function(value) { this.classLists[value] = 'hover'; }, /** @@ -151,8 +151,8 @@ export default { * * @param {String} value The chip's key. */ - removeHoverClass: function (value) { - this.classLists[value] = ''; + removeHoverClass: function(value) { + this.classLists[value] = '' }, /** * Returns the class list string for the chip with the given key. @@ -160,11 +160,11 @@ export default { * @param {String} value The chip's key * @returns The chip's class list string. */ - getClassList: function (value) { - return this.classLists[value] ? this.classLists[value] : ''; + getClassList: function(value) { + return this.classLists[value] ? this.classLists[value] : '' } } -}; +} </script> <style scoped> diff --git a/openlibrary/components/ObservationForm/components/ValueCard.vue b/openlibrary/components/ObservationForm/components/ValueCard.vue index 5fdded40018..3a9ca41a81f 100644 --- a/openlibrary/components/ObservationForm/components/ValueCard.vue +++ b/openlibrary/components/ObservationForm/components/ValueCard.vue @@ -16,7 +16,7 @@ </template> <script> -import CardBody from './CardBody.vue'; +import CardBody from './CardBody.vue' import CardHeader from './CardHeader.vue'; export default { @@ -54,7 +54,7 @@ export default { values: { type: Array, required: true, - validator: function (arr) { + validator: function(arr) { for (const item of arr) { if (typeof(item) !== 'string') { return false; @@ -94,7 +94,7 @@ export default { required: true } }, -}; +} </script> <style scoped> diff --git a/openlibrary/components/configs.js b/openlibrary/components/configs.js index 4fe49f387ac..486802064d6 100644 --- a/openlibrary/components/configs.js +++ b/openlibrary/components/configs.js @@ -3,9 +3,8 @@ const urlParams = new URLSearchParams(location.search); -const IS_VUE_APP = document.title === 'Vue App'; -const OL_BASE_DEFAULT = - urlParams.get('ol_base') || (IS_VUE_APP ? 'openlibrary.org' : ''); +const IS_VUE_APP = document.title === 'Vue App'; +const OL_BASE_DEFAULT = urlParams.get('ol_base') || (IS_VUE_APP ? 'openlibrary.org' : ''); const CONFIGS = { OL_BASE_COVERS: urlParams.get('ol_base_covers') || 'covers.openlibrary.org', @@ -21,13 +20,7 @@ const CONFIGS = { LANG: urlParams.get('lang'), }; -for (const key of [ - 'OL_BASE_COVERS', - 'OL_BASE_SEARCH', - 'OL_BASE_BOOKS', - 'OL_BASE_LANGS', - 'OL_BASE_SAVES', -]) { +for (const key of ['OL_BASE_COVERS', 'OL_BASE_SEARCH', 'OL_BASE_BOOKS', 'OL_BASE_LANGS', 'OL_BASE_SAVES']) { if (CONFIGS[key] && !CONFIGS[key].startsWith('http')) { CONFIGS[key] = `https://${CONFIGS[key]}`; } diff --git a/openlibrary/components/dev/vite.config.js b/openlibrary/components/dev/vite.config.js index 872d385d734..ea39d477e95 100644 --- a/openlibrary/components/dev/vite.config.js +++ b/openlibrary/components/dev/vite.config.js @@ -2,11 +2,10 @@ This is the config used for the dev server ala `npm run serve` This does not effect production builds */ - -import vue from '@vitejs/plugin-vue'; -import { defineConfig } from 'vite'; +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' // https://vite.dev/config/ export default defineConfig({ plugins: [vue()], -}); +}) diff --git a/openlibrary/components/lit/OLChip.js b/openlibrary/components/lit/OLChip.js index 613bfda0b6a..7fa2e5c56d9 100644 --- a/openlibrary/components/lit/OLChip.js +++ b/openlibrary/components/lit/OLChip.js @@ -1,4 +1,4 @@ -import { css, html, LitElement, nothing } from 'lit'; +import { LitElement, html, css, nothing } from 'lit'; /** * OLChip - A pill-shaped interactive chip web component @@ -137,13 +137,11 @@ export class OLChip extends LitElement { } _handleClick() { - this.dispatchEvent( - new CustomEvent('ol-chip-select', { - bubbles: true, - composed: true, - detail: { selected: !this.selected }, - }), - ); + this.dispatchEvent(new CustomEvent('ol-chip-select', { + bubbles: true, + composed: true, + detail: { selected: !this.selected }, + })); } _renderIcons() { diff --git a/openlibrary/components/lit/OLChipGroup.js b/openlibrary/components/lit/OLChipGroup.js index 78bf876dfa3..10ca82828b8 100644 --- a/openlibrary/components/lit/OLChipGroup.js +++ b/openlibrary/components/lit/OLChipGroup.js @@ -1,4 +1,4 @@ -import { css, html, LitElement } from 'lit'; +import { LitElement, html, css } from 'lit'; /** * OLChipGroup - A flex-wrap container for ol-chip components diff --git a/openlibrary/components/lit/OLReadMore.js b/openlibrary/components/lit/OLReadMore.js index b2d7df97577..33cc01de31e 100644 --- a/openlibrary/components/lit/OLReadMore.js +++ b/openlibrary/components/lit/OLReadMore.js @@ -1,4 +1,4 @@ -import { css, html, LitElement } from 'lit'; +import { LitElement, html, css } from 'lit'; /** * OLReadMore - A web component for expandable/collapsible content @@ -142,10 +142,7 @@ export class OLReadMore extends LitElement { _updateBackgroundColor() { if (this.backgroundColor) { - this.style.setProperty( - '--ol-readmore-gradient-color', - this.backgroundColor, - ); + this.style.setProperty('--ol-readmore-gradient-color', this.backgroundColor); } } diff --git a/openlibrary/components/lit/OlPagination.js b/openlibrary/components/lit/OlPagination.js index e092cf6727c..298152caa33 100644 --- a/openlibrary/components/lit/OlPagination.js +++ b/openlibrary/components/lit/OlPagination.js @@ -1,4 +1,4 @@ -import { css, html, LitElement } from 'lit'; +import { LitElement, html, css } from 'lit'; /** * A pagination component that displays page numbers with navigation controls. @@ -67,7 +67,7 @@ export class OlPagination extends LitElement { labelGoToPage: { type: String, attribute: 'label-go-to-page' }, labelCurrentPage: { type: String, attribute: 'label-current-page' }, labelPagination: { type: String, attribute: 'label-pagination' }, - _focusedIndex: { type: Number, state: true }, + _focusedIndex: { type: Number, state: true } }; static styles = css` @@ -142,12 +142,10 @@ export class OlPagination extends LitElement { `; /** Left chevron arrow icon */ - static _leftArrowIcon = - html`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>`; + static _leftArrowIcon = html`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>`; /** Right chevron arrow icon */ - static _rightArrowIcon = - html`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>`; + static _rightArrowIcon = html`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>`; constructor() { super(); @@ -167,22 +165,22 @@ export class OlPagination extends LitElement { } /** - * Interpolate a label template by replacing {key} placeholders with values. - * @param {String} template - The label template (e.g., "Go to page {page}") - * @param {Object} values - Key-value pairs to substitute (e.g., { page: 5 }) - * @returns {String} The interpolated string - */ + * Interpolate a label template by replacing {key} placeholders with values. + * @param {String} template - The label template (e.g., "Go to page {page}") + * @param {Object} values - Key-value pairs to substitute (e.g., { page: 5 }) + * @returns {String} The interpolated string + */ _interpolateLabel(template, values) { return template.replace(/\{(\w+)\}/g, (_, key) => values[key] ?? ''); } /** - * Build URL for a specific page number. - * Uses baseUrl if provided, otherwise falls back to the current window location. - * This preserves all existing query parameters (like changequery() does). - * @param {Number} page - The page number - * @returns {String|null} The URL for the page - */ + * Build URL for a specific page number. + * Uses baseUrl if provided, otherwise falls back to the current window location. + * This preserves all existing query parameters (like changequery() does). + * @param {Number} page - The page number + * @returns {String|null} The URL for the page + */ _getPageUrl(page) { try { const base = this.baseUrl || window.location.href; @@ -199,49 +197,38 @@ export class OlPagination extends LitElement { } /** - * Calculate which page numbers to display based on current page and total pages. - * Always shows exactly 5 page numbers max, adjusting position based on current page: - * - Near start: 1, 2, 3, 4 ... last (5 total) - * - Middle: 1 ... current-1, current, current+1 ... last (5 total) - * - Near end: 1 ... last-3, last-2, last-1, last (5 total) - * @returns {Array} Array of page numbers and 'ellipsis' markers - */ + * Calculate which page numbers to display based on current page and total pages. + * Always shows exactly 5 page numbers max, adjusting position based on current page: + * - Near start: 1, 2, 3, 4 ... last (5 total) + * - Middle: 1 ... current-1, current, current+1 ... last (5 total) + * - Near end: 1 ... last-3, last-2, last-1, last (5 total) + * @returns {Array} Array of page numbers and 'ellipsis' markers + */ _getVisiblePages() { const total = this.totalPages; const current = this.currentPage; if (total <= 5) return [...Array(total)].map((_, i) => i + 1); if (current <= 3) return [1, 2, 3, 4, 'ellipsis-right', total]; - if (current >= total - 2) - return [1, 'ellipsis-left', total - 3, total - 2, total - 1, total]; - - return [ - 1, - 'ellipsis-left', - current - 1, - current, - current + 1, - 'ellipsis-right', - total, - ]; + if (current >= total - 2) return [1, 'ellipsis-left', total - 3, total - 2, total - 1, total]; + + return [1, 'ellipsis-left', current - 1, current, current + 1, 'ellipsis-right', total]; } /** - * Get all focusable elements in the pagination - * @returns {Array} Array of focusable elements (buttons or anchors) - */ + * Get all focusable elements in the pagination + * @returns {Array} Array of focusable elements (buttons or anchors) + */ _getFocusableElements() { return Array.from( - this.shadowRoot.querySelectorAll( - '.pagination-item:not([aria-disabled="true"])', - ), + this.shadowRoot.querySelectorAll('.pagination-item:not([aria-disabled="true"])') ); } /** - * Handle keyboard navigation within the pagination - * @param {KeyboardEvent} e - */ + * Handle keyboard navigation within the pagination + * @param {KeyboardEvent} e + */ _handleKeyDown(e) { const focusable = this._getFocusableElements(); const currentIndex = focusable.indexOf(this.shadowRoot.activeElement); @@ -271,9 +258,9 @@ export class OlPagination extends LitElement { } /** - * Navigate to a specific page - * @param {Number} page - The page number to navigate to - */ + * Navigate to a specific page + * @param {Number} page - The page number to navigate to + */ _goToPage(page) { const maxPage = this.mode === 'arrows' ? Infinity : this.totalPages; if (page < 1 || page > maxPage || page === this.currentPage) { @@ -293,12 +280,12 @@ export class OlPagination extends LitElement { } /** - * Handle click on anchor-based page links. - * Dispatches the ol-pagination-change event to allow interception. - * If the event is cancelled via preventDefault(), anchor navigation is also prevented. - * @param {Event} e - Click event - * @param {Number} page - The page number - */ + * Handle click on anchor-based page links. + * Dispatches the ol-pagination-change event to allow interception. + * If the event is cancelled via preventDefault(), anchor navigation is also prevented. + * @param {Event} e - Click event + * @param {Number} page - The page number + */ _handlePageClick(e, page) { const event = new CustomEvent('ol-pagination-change', { detail: { page }, @@ -314,14 +301,14 @@ export class OlPagination extends LitElement { } /** - * Render a pagination item (button or anchor based on URL mode) - * @param {Object} options - Render options - * @param {Number} options.page - Target page number - * @param {String} options.label - Aria label for the item - * @param {String} options.className - Additional CSS class - * @param {TemplateResult} options.content - Content to render inside the item - * @returns {TemplateResult} Lit template for the button or anchor - */ + * Render a pagination item (button or anchor based on URL mode) + * @param {Object} options - Render options + * @param {Number} options.page - Target page number + * @param {String} options.label - Aria label for the item + * @param {String} options.className - Additional CSS class + * @param {TemplateResult} options.content - Content to render inside the item + * @returns {TemplateResult} Lit template for the button or anchor + */ _renderPaginationItem({ page, label, className = '', content }) { const url = this._getPageUrl(page); const isCurrent = page === this.currentPage; @@ -350,10 +337,10 @@ export class OlPagination extends LitElement { } /** - * Render a single page button/link or ellipsis - * @param {Number|String} page - Page number or 'ellipsis-left'/'ellipsis-right' - * @returns {TemplateResult} Lit template for the button or anchor - */ + * Render a single page button/link or ellipsis + * @param {Number|String} page - Page number or 'ellipsis-left'/'ellipsis-right' + * @returns {TemplateResult} Lit template for the button or anchor + */ _renderPageButton(page) { if (typeof page === 'string' && page.startsWith('ellipsis')) { return html`<span class="ellipsis" aria-hidden="true">•••</span>`; @@ -368,25 +355,21 @@ export class OlPagination extends LitElement { } /** - * Render a navigation arrow (previous or next) - * @param {String} direction - 'prev' or 'next' - * @returns {TemplateResult} Lit template for the arrow - */ + * Render a navigation arrow (previous or next) + * @param {String} direction - 'prev' or 'next' + * @returns {TemplateResult} Lit template for the arrow + */ _renderNavArrow(direction) { const isPrev = direction === 'prev'; const isDisabled = isPrev ? this.currentPage === 1 - : this.mode === 'arrows' - ? !this.hasNextPage - : this.currentPage === this.totalPages; + : this.mode === 'arrows' ? !this.hasNextPage : this.currentPage === this.totalPages; if (isDisabled && this.mode !== 'arrows') return html``; if (isDisabled) { const label = isPrev ? this.labelPreviousPage : this.labelNextPage; - const icon = isPrev - ? OlPagination._leftArrowIcon - : OlPagination._rightArrowIcon; + const icon = isPrev ? OlPagination._leftArrowIcon : OlPagination._rightArrowIcon; return html` <span class="pagination-item pagination-arrow" @@ -398,15 +381,13 @@ export class OlPagination extends LitElement { const page = isPrev ? this.currentPage - 1 : this.currentPage + 1; const label = isPrev ? this.labelPreviousPage : this.labelNextPage; - const icon = isPrev - ? OlPagination._leftArrowIcon - : OlPagination._rightArrowIcon; + const icon = isPrev ? OlPagination._leftArrowIcon : OlPagination._rightArrowIcon; return this._renderPaginationItem({ page, label, className: 'pagination-arrow', - content: icon, + content: icon }); } @@ -422,7 +403,7 @@ export class OlPagination extends LitElement { @keydown=${this._handleKeyDown} > ${this._renderNavArrow('prev')} - ${visiblePages.map((page) => this._renderPageButton(page))} + ${visiblePages.map(page => this._renderPageButton(page))} ${this._renderNavArrow('next')} </nav> `; diff --git a/openlibrary/components/lit/OlPopover.js b/openlibrary/components/lit/OlPopover.js index 0dc0737c0eb..be7e1f05fcf 100644 --- a/openlibrary/components/lit/OlPopover.js +++ b/openlibrary/components/lit/OlPopover.js @@ -1,10 +1,9 @@ -import { css, html, LitElement, nothing } from 'lit'; +import { LitElement, html, css, nothing } from 'lit'; import { ifDefined } from 'lit/directives/if-defined.js'; let _idCounter = 0; -const FOCUSABLE = - 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; +const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; /** * A reusable popover component that anchors to a trigger element. @@ -273,20 +272,14 @@ export class OlPopover extends LitElement { const showPanel = this._animState !== 'closed'; return html` <slot name="trigger"></slot> - ${ - showPanel - ? html` - ${ - this._mobile - ? html` + ${showPanel ? html` + ${this._mobile ? html` <div class="backdrop" data-state="${this._animState}" @click="${this._onBackdropClick}" ></div> - ` - : nothing -} + ` : nothing} <div id="${this._panelId}" class="panel ${this._mobile ? 'tray' : ''}" @@ -295,15 +288,11 @@ export class OlPopover extends LitElement { aria-modal="true" aria-label="${ifDefined(this.accessibleLabel || undefined)}" tabindex="-1" - style="${ - this._mobile - ? '' - : ` + style="${this._mobile ? '' : ` top: ${this._position.top}px; left: ${this._position.left}px; transform-origin: ${this._transformOrigin}; - ` -}" + `}" @transitionend="${this._onTransitionEnd}" > <span @@ -313,15 +302,11 @@ export class OlPopover extends LitElement { data-edge="start" @focus="${this._onSentinelFocus}" ></span> - ${ - this._mobile - ? html` + ${this._mobile ? html` <div class="tray-handle" aria-hidden="true"> <div class="tray-handle-bar"></div> </div> - ` - : nothing -} + ` : nothing} <slot></slot> <span class="focus-sentinel" @@ -331,9 +316,7 @@ export class OlPopover extends LitElement { @focus="${this._onSentinelFocus}" ></span> </div> - ` - : nothing -} + ` : nothing} `; } @@ -362,9 +345,7 @@ export class OlPopover extends LitElement { document.addEventListener('keydown', this._onKeydownGlobal); this._mobile = window.matchMedia('(max-width: 767px)').matches; - const reducedMotion = window.matchMedia( - '(prefers-reduced-motion: reduce)', - ).matches; + const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (this._mobile) { this._lockBodyScroll(); @@ -394,12 +375,8 @@ export class OlPopover extends LitElement { // Add touch listeners for swipe-to-dismiss (mobile) if (this._mobile) { - panel.addEventListener('touchstart', this._onTouchStart, { - passive: true, - }); - panel.addEventListener('touchmove', this._onTouchMove, { - passive: false, - }); + panel.addEventListener('touchstart', this._onTouchStart, { passive: true }); + panel.addEventListener('touchmove', this._onTouchMove, { passive: false }); panel.addEventListener('touchend', this._onTouchEnd, { passive: true }); } @@ -407,13 +384,10 @@ export class OlPopover extends LitElement { panel.focus({ preventScroll: true }); if (reducedMotion) { - this.dispatchEvent( - new CustomEvent('ol-popover-open', { - bubbles: true, - composed: true, - detail: { placement: this.placement }, - }), - ); + this.dispatchEvent(new CustomEvent('ol-popover-open', { + bubbles: true, composed: true, + detail: { placement: this.placement }, + })); return; } @@ -421,22 +395,17 @@ export class OlPopover extends LitElement { panel.getBoundingClientRect(); this._animState = 'entering'; - this.dispatchEvent( - new CustomEvent('ol-popover-open', { - bubbles: true, - composed: true, - detail: { placement: this.placement }, - }), - ); + this.dispatchEvent(new CustomEvent('ol-popover-open', { + bubbles: true, composed: true, + detail: { placement: this.placement }, + })); }); } _hide() { if (this._animState === 'closed') return; - const reducedMotion = window.matchMedia( - '(prefers-reduced-motion: reduce)', - ).matches; + const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (reducedMotion) { this._animState = 'closed'; this._cleanup(); @@ -458,9 +427,9 @@ export class OlPopover extends LitElement { } /** - * Central cleanup called when the popover finishes closing. - * Removes all global listeners, unlocks scroll, and restores focus. - */ + * Central cleanup called when the popover finishes closing. + * Removes all global listeners, unlocks scroll, and restores focus. + */ _cleanup() { this._removeListeners(); this._unlockBodyScroll(); @@ -519,9 +488,9 @@ export class OlPopover extends LitElement { // ── Positioning ───────────────────────────────────────────── /** - * Compute the final position of the popover panel, flipping and shifting - * as needed to keep it within the viewport. - */ + * Compute the final position of the popover panel, flipping and shifting + * as needed to keep it within the viewport. + */ _computePosition(panelW, panelH) { const trigger = this._triggerEl; if (!trigger) return; @@ -542,11 +511,7 @@ export class OlPopover extends LitElement { if (side === 'bottom' && panelH > spaceBelow && spaceAbove > spaceBelow) { side = 'top'; - } else if ( - side === 'top' && - panelH > spaceAbove && - spaceBelow > spaceAbove - ) { + } else if (side === 'top' && panelH > spaceAbove && spaceBelow > spaceAbove) { side = 'bottom'; } @@ -606,9 +571,7 @@ export class OlPopover extends LitElement { _parsePlacement(placement) { const parts = (placement || 'bottom-center').split('-'); const side = parts[0] === 'top' ? 'top' : 'bottom'; - const align = ['start', 'center', 'end'].includes(parts[1]) - ? parts[1] - : 'center'; + const align = ['start', 'center', 'end'].includes(parts[1]) ? parts[1] : 'center'; return [side, align]; } @@ -620,17 +583,12 @@ export class OlPopover extends LitElement { // ── Scroll / resize repositioning ─────────────────────────── _addScrollResizeListeners() { - window.addEventListener('scroll', this._onScrollResize, { - capture: true, - passive: true, - }); + window.addEventListener('scroll', this._onScrollResize, { capture: true, passive: true }); window.addEventListener('resize', this._onScrollResize, { passive: true }); } _removeScrollResizeListeners() { - window.removeEventListener('scroll', this._onScrollResize, { - capture: true, - }); + window.removeEventListener('scroll', this._onScrollResize, { capture: true }); window.removeEventListener('resize', this._onScrollResize); if (this._rafId) { cancelAnimationFrame(this._rafId); @@ -676,13 +634,10 @@ export class OlPopover extends LitElement { } _requestClose(reason) { - this.dispatchEvent( - new CustomEvent('ol-popover-close', { - bubbles: true, - composed: true, - detail: { reason }, - }), - ); + this.dispatchEvent(new CustomEvent('ol-popover-close', { + bubbles: true, composed: true, + detail: { reason }, + })); } // ── Mobile touch / swipe-to-dismiss ───────────────────────── @@ -750,13 +705,11 @@ export class OlPopover extends LitElement { if (dragY > DISMISS_THRESHOLD || velocity > VELOCITY_THRESHOLD) { // Swipe dismiss — animate to off-screen, then close if (panel) { - panel.style.transition = - 'transform 200ms cubic-bezier(0.23, 1, 0.32, 1)'; + panel.style.transition = 'transform 200ms cubic-bezier(0.23, 1, 0.32, 1)'; panel.style.transform = 'translateY(100%)'; } if (backdrop) { - backdrop.style.transition = - 'opacity 200ms cubic-bezier(0.23, 1, 0.32, 1)'; + backdrop.style.transition = 'opacity 200ms cubic-bezier(0.23, 1, 0.32, 1)'; backdrop.style.opacity = '0'; } @@ -765,13 +718,10 @@ export class OlPopover extends LitElement { this._clearDragStyles(); this._animState = 'closed'; this._cleanup(); - this.dispatchEvent( - new CustomEvent('ol-popover-close', { - bubbles: true, - composed: true, - detail: { reason: 'swipe' }, - }), - ); + this.dispatchEvent(new CustomEvent('ol-popover-close', { + bubbles: true, composed: true, + detail: { reason: 'swipe' }, + })); }; if (panel) { @@ -782,13 +732,11 @@ export class OlPopover extends LitElement { } else { // Snap back to open position if (panel) { - panel.style.transition = - 'transform 200ms cubic-bezier(0.23, 1, 0.32, 1)'; + panel.style.transition = 'transform 200ms cubic-bezier(0.23, 1, 0.32, 1)'; panel.style.transform = ''; } if (backdrop) { - backdrop.style.transition = - 'opacity 200ms cubic-bezier(0.23, 1, 0.32, 1)'; + backdrop.style.transition = 'opacity 200ms cubic-bezier(0.23, 1, 0.32, 1)'; backdrop.style.opacity = ''; } diff --git a/openlibrary/components/lit/index.js b/openlibrary/components/lit/index.js index 079ca16b678..cca455a22f8 100644 --- a/openlibrary/components/lit/index.js +++ b/openlibrary/components/lit/index.js @@ -5,9 +5,9 @@ * Components are bundled together via Vite for production use. */ -export { OLChip } from './OLChip.js'; -export { OLChipGroup } from './OLChipGroup.js'; // Export components (importing also registers them as custom elements) export { OLReadMore } from './OLReadMore.js'; export { OlPagination } from './OlPagination.js'; export { OlPopover } from './OlPopover.js'; +export { OLChip } from './OLChip.js'; +export { OLChipGroup } from './OLChipGroup.js'; diff --git a/openlibrary/components/rollupInputCore.js b/openlibrary/components/rollupInputCore.js index 77a9664d9c0..169a3481bd4 100644 --- a/openlibrary/components/rollupInputCore.js +++ b/openlibrary/components/rollupInputCore.js @@ -1,6 +1,6 @@ -import { kebabCase } from 'lodash'; import { defineCustomElement } from 'vue'; import AsyncComputed from 'vue-async-computed'; +import { kebabCase } from 'lodash'; export const createWebComponentSimple = (rootComponent, name) => { // This is the name we use in the DOM like: <ol-barcode-scanner></ol-barcode-scanner> diff --git a/openlibrary/components/vite-lit.config.mjs b/openlibrary/components/vite-lit.config.mjs index 66a04200d9e..9ac6c52382d 100644 --- a/openlibrary/components/vite-lit.config.mjs +++ b/openlibrary/components/vite-lit.config.mjs @@ -9,44 +9,44 @@ * - ol-components-legacy.js (transpiled for older browsers) */ +import { defineConfig } from 'vite'; import legacy from '@vitejs/plugin-legacy'; import { join } from 'path'; -import { defineConfig } from 'vite'; const BUILD_DIR = process.env.BUILD_DIR || 'static/build/components'; export default defineConfig({ - plugins: [ - // Provides legacy browser support - // Creates both modern and legacy builds - legacy({ - targets: ['defaults', 'not IE 11'], - // Generate polyfills for older browsers - modernPolyfills: true, - }), - ], - build: { - // Output directory for built files - outDir: join(BUILD_DIR, '/production'), + plugins: [ + // Provides legacy browser support + // Creates both modern and legacy builds + legacy({ + targets: ['defaults', 'not IE 11'], + // Generate polyfills for older browsers + modernPolyfills: true + }) + ], + build: { + // Output directory for built files + outDir: join(BUILD_DIR, '/production'), - // Rollup-specific options - rollupOptions: { - input: { - 'ol-components': 'openlibrary/components/lit/index.js', - }, - output: { - // Output filename pattern - entryFileNames: '[name].js', + // Rollup-specific options + rollupOptions: { + input: { + 'ol-components': 'openlibrary/components/lit/index.js' + }, + output: { + // Output filename pattern + entryFileNames: '[name].js', - // Ensure we're building for browsers, not Node.js - format: 'es', - }, - }, + // Ensure we're building for browsers, not Node.js + format: 'es' + } + }, - // Minify the output - minify: 'terser', + // Minify the output + minify: 'terser', - // Generate source maps for debugging - sourcemap: true, - }, + // Generate source maps for debugging + sourcemap: true + } }); diff --git a/openlibrary/plugins/openlibrary/js/Browser.js b/openlibrary/plugins/openlibrary/js/Browser.js index 62086529777..5827d28af9c 100644 --- a/openlibrary/plugins/openlibrary/js/Browser.js +++ b/openlibrary/plugins/openlibrary/js/Browser.js @@ -9,7 +9,7 @@ export function getJsonFromUrl(urlSearch) { const query = urlSearch.substr(1); const result = {}; if (query) { - query.split('&').forEach((part) => { + query.split('&').forEach(part => { const item = part.split('='); result[item[0]] = decodeURIComponent(item[1]); }); diff --git a/openlibrary/plugins/openlibrary/js/SearchBar.js b/openlibrary/plugins/openlibrary/js/SearchBar.js index 200fb02a8ae..cc32927ef19 100644 --- a/openlibrary/plugins/openlibrary/js/SearchBar.js +++ b/openlibrary/plugins/openlibrary/js/SearchBar.js @@ -1,8 +1,8 @@ -import $ from 'jquery'; -import { websafe } from './jsdef'; import { debounce } from './nonjquery_utils.js'; import * as SearchUtils from './SearchUtils'; import { PersistentValue } from './SearchUtils'; +import $ from 'jquery'; +import { websafe } from './jsdef' /** Mapping of search bar facets to search endpoints */ const FACET_TO_ENDPOINT = { @@ -62,26 +62,26 @@ const RENDER_AUTOCOMPLETE_RESULT = { <span class="author-desc"><div class="author-name">${websafe(author.name)}</div></span> </a> </li>`; - }, -}; + } +} /** * Manages the interactions associated with the search bar in the header */ export class SearchBar { /** - * @param {JQuery} $component - * @param {Object?} urlParams - */ - constructor($component, urlParams = {}) { - /** UI Elements */ + * @param {JQuery} $component + * @param {Object?} urlParams + */ + constructor($component, urlParams={}) { + /** UI Elements */ this.$component = $component; this.$form = this.$component.find('form.search-bar-input'); this.$input = this.$form.find('input[type="text"]'); this.$results = this.$component.find('ul.search-results'); this.$facetSelect = this.$component.find('.search-facet-selector select'); this.$barcodeScanner = this.$component.find('#barcode_scanner_link'); - this.$searchSubmit = this.$component.find('.search-bar-submit'); + this.$searchSubmit = this.$component.find('.search-bar-submit') /** State */ /** Whether the bar is in collapsible mode */ @@ -91,9 +91,7 @@ export class SearchBar { /** Selected facet (persisted) */ this.facet = new PersistentValue('facet', { default: DEFAULT_FACET, - initValidation(val) { - return val in FACET_TO_ENDPOINT; - }, + initValidation(val) { return val in FACET_TO_ENDPOINT; } }); this.initFromUrlParams(urlParams); @@ -112,7 +110,7 @@ export class SearchBar { if (e.key === 'Tab' && e.shiftKey) { this.clearAutocompletionResults(); } - }); + }) this.$input.on('keydown', (e) => { if (e.key === 'ArrowUp') { @@ -124,19 +122,18 @@ export class SearchBar { } else if (e.key === 'Escape') { this.clearAutocompletionResults(); } - }); + }) this.$barcodeScanner.on('keydown', (e) => { if (e.key === 'Tab') { this.clearAutocompletionResults(); } - }); + }) this.$results.on('keydown', (e) => { if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { // On arrow keys focus on the next item unless there is none, then focus on input - const direction = - e.key === 'ArrowUp' ? 'previousElementSibling' : 'nextElementSibling'; + const direction = e.key === 'ArrowUp' ? 'previousElementSibling' : 'nextElementSibling'; if (!e.target[direction]) { this.$input.trigger('focus'); return false; @@ -161,7 +158,7 @@ export class SearchBar { this.escapeInput = true; this.clearAutocompletionResults(); } - }); + }) this.$form.on('keydown', (e) => { if (e.key === 'Tab') { @@ -178,9 +175,9 @@ export class SearchBar { } /** - * Update internal state from url parameters - * @param {Object} urlParams - */ + * Update internal state from url parameters + * @param {Object} urlParams + */ initFromUrlParams(urlParams) { if (urlParams.facet in FACET_TO_ENDPOINT) { this.facet.write(urlParams.facet); @@ -203,22 +200,16 @@ export class SearchBar { const q = this.$input.val(); this.$input.val(SearchBar.marshalBookSearchQuery(q)); } - this.$form.attr( - 'action', - SearchBar.composeSearchUrl(this.facetEndpoint, this.$input.val()), - ); + this.$form.attr('action', SearchBar.composeSearchUrl(this.facetEndpoint, this.$input.val())); SearchUtils.addModeInputsToForm(this.$form, SearchUtils.mode.read()); } /** Initialize event handlers that allow the form to collapse for small screens */ initCollapsibleMode() { this.toggleCollapsibleModeForSmallScreens($(window).width()); - $(window).on( - 'resize', - debounce(() => { - this.toggleCollapsibleModeForSmallScreens($(window).width()); - }, 50), - ); + $(window).on('resize', debounce(() => { + this.toggleCollapsibleModeForSmallScreens($(window).width()); + }, 50)); const expandAndFocusSearch = (event) => { if (this.inCollapsibleMode && this.collapsed) { @@ -226,15 +217,13 @@ export class SearchBar { this.toggleCollapse(); this.$input.trigger('focus'); } - }; + } const expandSelectors = ['.search-component', 'a[href="/search"]']; // When clicking on the search bar or a link to /search, expand search if it isn't already. // If clicking elsewhere, collapse search. - $(document).on('submit', '.in-collapsible-mode', (event) => - expandAndFocusSearch(event), - ); - $(document).on('click', (event) => { + $(document).on('submit', '.in-collapsible-mode', event => expandAndFocusSearch(event)); + $(document).on('click', event => { const shouldExpand = (item) => $(event.target).closest(item).length === 1; if (expandSelectors.some(shouldExpand)) { expandAndFocusSearch(event); @@ -245,9 +234,9 @@ export class SearchBar { } /** - * Enables/disables CollapsibleMode depending on screen size - * @param {Number} windowWidth - */ + * Enables/disables CollapsibleMode depending on screen size + * @param {Number} windowWidth + */ toggleCollapsibleModeForSmallScreens(windowWidth) { if (windowWidth < 568) { if (!this.inCollapsibleMode) { @@ -295,20 +284,14 @@ export class SearchBar { } /** - * Converts an already processed query into a search url - * @param {String} facetEndpoint - * @param {String} q query that's ready to get passed to the search endpoint - * @param {Boolean} [json] whether to hit the JSON endpoint - * @param {Number} [limit] how many items to get - * @param {String[]} [fields] the Solr fields to fetch (if using JSON) - */ - static composeSearchUrl( - facetEndpoint, - q, - json = false, - limit = null, - fields = null, - ) { + * Converts an already processed query into a search url + * @param {String} facetEndpoint + * @param {String} q query that's ready to get passed to the search endpoint + * @param {Boolean} [json] whether to hit the JSON endpoint + * @param {Number} [limit] how many items to get + * @param {String[]} [fields] the Solr fields to fetch (if using JSON) + */ + static composeSearchUrl(facetEndpoint, q, json=false, limit=null, fields=null) { let url = facetEndpoint; if (json) { url += `.json?q=${q}&_spellcheck_count=0`; @@ -323,10 +306,10 @@ export class SearchBar { } /** - * Prepare an unprocessed query for book searching - * @param {String} q - * @return {String} - */ + * Prepare an unprocessed query for book searching + * @param {String} q + * @return {String} + */ static marshalBookSearchQuery(q) { if (q && q.indexOf(':') === -1 && q.indexOf('"') === -1) { q = `title: "${q}"`; @@ -336,56 +319,38 @@ export class SearchBar { /** Setup event listeners for autocompletion */ initAutocompletionLogic() { - // searches should be cancelled if you click anywhere in the page + // searches should be cancelled if you click anywhere in the page $(document.body).on('click', this.clearAutocompletionResults.bind(this)); // but clicking search input should not empty search results. this.$input.on('click', false); - this.$input.on( - 'keyup', - debounce( - (event) => { - // ignore directional keys, enter, escape, and shift for callback - if (![13, 16, 27, 37, 38, 39, 40].includes(event.keyCode)) { - this.renderAutocompletionResults(); - } - }, - 500, - false, - ), - ); - - this.$input.on( - 'focus', - debounce( - (event) => { - event.stopPropagation(); - // don't render on focus if there are already results showing, avoid flashing - const resultsAreRendered = this.$results.children().length > 0; - if (this.escapeInput || resultsAreRendered) { - return; - } - this.renderAutocompletionResults(); - }, - 300, - false, - ), - ); + this.$input.on('keyup', debounce(event => { + // ignore directional keys, enter, escape, and shift for callback + if (![13,16,27,37,38,39,40].includes(event.keyCode)) { + this.renderAutocompletionResults(); + } + }, 500, false)); + + this.$input.on('focus', debounce(event => { + event.stopPropagation(); + // don't render on focus if there are already results showing, avoid flashing + const resultsAreRendered = this.$results.children().length > 0; + if (this.escapeInput || resultsAreRendered) { + return; + } + this.renderAutocompletionResults(); + }, 300, false)); } /** - * @async - * Awkwardly fetches the the results as well as renders them :/ - * Cleans up and performs the query, then update the autocomplete results - * @returns {JQuery.jqXHR} - **/ + * @async + * Awkwardly fetches the the results as well as renders them :/ + * Cleans up and performs the query, then update the autocomplete results + * @returns {JQuery.jqXHR} + **/ renderAutocompletionResults() { let q = this.$input.val().trim(); - if ( - q.length < 3 || - q.toLowerCase() === 'the' || - !(this.facetEndpoint in RENDER_AUTOCOMPLETE_RESULT) - ) { + if (q.length < 3 || q.toLowerCase() === 'the' || !(this.facetEndpoint in RENDER_AUTOCOMPLETE_RESULT)) { return; } if (this.facet.read() === 'title') { @@ -393,23 +358,14 @@ export class SearchBar { } this.$results.css('opacity', 0.5); - return $.getJSON( - SearchBar.composeSearchUrl( - this.facetEndpoint, - q, - true, - 10, - DEFAULT_JSON_FIELDS, - ), - (data) => { - const renderer = RENDER_AUTOCOMPLETE_RESULT[this.facetEndpoint]; - this.$results.css('opacity', 1); - this.clearAutocompletionResults(); - for (const d in data.docs) { - this.$results.append(renderer(data.docs[d])); - } - }, - ); + return $.getJSON(SearchBar.composeSearchUrl(this.facetEndpoint, q, true, 10, DEFAULT_JSON_FIELDS), data => { + const renderer = RENDER_AUTOCOMPLETE_RESULT[this.facetEndpoint]; + this.$results.css('opacity', 1); + this.clearAutocompletionResults(); + for (const d in data.docs) { + this.$results.append(renderer(data.docs[d])); + } + }); } clearAutocompletionResults() { @@ -417,11 +373,11 @@ export class SearchBar { } /** - * Updates the UI to match after the facet is changed - * @param {String} newFacet - */ + * Updates the UI to match after the facet is changed + * @param {String} newFacet + */ handleFacetValueChange(newFacet) { - // update the UI + // update the UI this.$facetSelect.val(newFacet); const text = this.$facetSelect.find('option:selected').text(); $('header#header-bar .search-facet-value').html(text); @@ -433,9 +389,9 @@ export class SearchBar { } /** - * Handles changes to the facet from the UI - * @param {JQuery.Event} event - */ + * Handles changes to the facet from the UI + * @param {JQuery.Event} event + */ handleFacetSelectChange(event) { const newFacet = event.target.value; // We don't want to persist advanced becaues it behaves like a button @@ -448,27 +404,27 @@ export class SearchBar { } /** - * For testing purposes, wraps window.location - * @returns {URL} The current URL - */ + * For testing purposes, wraps window.location + * @returns {URL} The current URL + */ getCurUrl() { return window.location; } /** - * Just so we can stub/test this - * @param {String} path - */ + * Just so we can stub/test this + * @param {String} path + */ navigateTo(path) { window.location.assign(path); } /** - * Makes changes to the UI after a change occurs to the mode - * Parts of this might be dead code; I don't really understand why - * this is necessary, so opting to leave it alone for now. - * @param {String} newMode - */ + * Makes changes to the UI after a change occurs to the mode + * Parts of this might be dead code; I don't really understand why + * this is necessary, so opting to leave it alone for now. + * @param {String} newMode + */ handleSearchModeChange(newMode) { $('.instantsearch-mode').val(newMode); $(`input[name=mode][value=${newMode}]`).prop('checked', true); diff --git a/openlibrary/plugins/openlibrary/js/SearchPage.js b/openlibrary/plugins/openlibrary/js/SearchPage.js index 4bc5355268f..98abc635308 100644 --- a/openlibrary/plugins/openlibrary/js/SearchPage.js +++ b/openlibrary/plugins/openlibrary/js/SearchPage.js @@ -1,14 +1,14 @@ -import $ from 'jquery'; import { addModeInputsToForm, mode as searchMode } from './SearchUtils'; +import $ from 'jquery'; /** @typedef {import('./SearchUtils').SearchModeSelector} SearchModeSelector */ /** Manages some (PROBABLY VERY FEW) of the interactions on the search page */ export class SearchPage { /** - * @param {HTMLFormElement|JQuery} form the .olform search form - * @param {SearchModeSelector} searchModeSelector - */ + * @param {HTMLFormElement|JQuery} form the .olform search form + * @param {SearchModeSelector} searchModeSelector + */ constructor(form, searchModeSelector) { this.$form = $(form); searchMode.sync(this.updateModeInputs.bind(this)); diff --git a/openlibrary/plugins/openlibrary/js/SearchUtils.js b/openlibrary/plugins/openlibrary/js/SearchUtils.js index 7606b887541..18da91076be 100644 --- a/openlibrary/plugins/openlibrary/js/SearchUtils.js +++ b/openlibrary/plugins/openlibrary/js/SearchUtils.js @@ -1,5 +1,5 @@ -import $ from 'jquery'; import { removeURLParameter } from './Browser'; +import $ from 'jquery'; /** * Adds hidden input elements/modifes the action of the form to match the given search mode @@ -25,6 +25,7 @@ export function addModeInputsToForm($form, searchMode) { } } + /** * @typedef {Object} PersistentValue.Options * @property {String?} [default] @@ -36,34 +37,33 @@ export function addModeInputsToForm($form, searchMode) { /** String value that's persisted to localstorage */ export class PersistentValue { /** - * @param {String} key - * @param {PersistentValue.Options} options - */ - constructor(key, options = {}) { + * @param {String} key + * @param {PersistentValue.Options} options + */ + constructor(key, options={}) { this.key = key; this.options = Object.assign({}, PersistentValue.DEFAULT_OPTIONS, options); this._listeners = []; const noValue = this.read() === null; - const isValid = () => - !this.options.initValidation || this.options.initValidation(this.read()); + const isValid = () => !this.options.initValidation || this.options.initValidation(this.read()); if (noValue || !isValid()) { this.write(this.options.default); } } /** - * Read the stored value - * @return {String} - */ + * Read the stored value + * @return {String} + */ read() { return localStorage.getItem(this.key); } /** - * Update the stored value - * @param {String} newValue - */ + * Update the stored value + * @param {String} newValue + */ write(newValue) { const oldValue = this.read(); let toWrite = newValue; @@ -83,22 +83,22 @@ export class PersistentValue { } /** - * Listen to updates to this value - * @param {Function} listener - * @param {Boolean} callAtStart whether to call the listener right now with the current value - */ - sync(listener, callAtStart = true) { + * Listen to updates to this value + * @param {Function} listener + * @param {Boolean} callAtStart whether to call the listener right now with the current value + */ + sync(listener, callAtStart=true) { this._listeners.push(listener); if (callAtStart) listener(this.read()); } /** - * @private - * Notify listeners of an update - * @param {String} newValue - */ + * @private + * Notify listeners of an update + * @param {String} newValue + */ _emit(newValue) { - this._listeners.forEach((listener) => listener(newValue)); + this._listeners.forEach(listener => listener(newValue)); } } @@ -109,34 +109,35 @@ PersistentValue.DEFAULT_OPTIONS = { writeTransformation: null, }; + const MODES = ['everything', 'ebooks']; const DEFAULT_MODE = 'everything'; /** Search mode; {@see MODES} */ export const mode = new PersistentValue('mode', { default: DEFAULT_MODE, - initValidation: (mode) => MODES.indexOf(mode) !== -1, + initValidation: mode => MODES.indexOf(mode) !== -1, writeTransformation(newValue, oldValue) { const mode = (newValue && newValue.toLowerCase()) || oldValue; const isValidMode = MODES.indexOf(mode) !== -1; return isValidMode ? mode : DEFAULT_MODE; - }, + } }); /** Manages interactions of the search mode radio buttons */ export class SearchModeSelector { /** - * @param {JQuery} radioButtons - */ + * @param {JQuery} radioButtons + */ constructor(radioButtons) { this.$radioButtons = radioButtons; - this.change((newMode) => mode.write(newMode)); + this.change(newMode => mode.write(newMode)); } /** - * Listen for changes - * @param {Function} handler - */ + * Listen for changes + * @param {Function} handler + */ change(handler) { - this.$radioButtons.on('change', (event) => handler($(event.target).val())); + this.$radioButtons.on('change', event => handler($(event.target).val())); } } diff --git a/openlibrary/plugins/openlibrary/js/Toast.js b/openlibrary/plugins/openlibrary/js/Toast.js index 41f4431f484..5b81a53f462 100644 --- a/openlibrary/plugins/openlibrary/js/Toast.js +++ b/openlibrary/plugins/openlibrary/js/Toast.js @@ -9,13 +9,13 @@ const DEFAULT_TIMEOUT = 2500; export class Toast { /** - * @param {JQuery} $toast The element containing the appropriate parts - * @param {JQuery|HTMLElement} containerParent where to add the toast bar - */ - constructor($toast, containerParent = document.body) { + * @param {JQuery} $toast The element containing the appropriate parts + * @param {JQuery|HTMLElement} containerParent where to add the toast bar + */ + constructor($toast, containerParent=document.body) { const $parent = $(containerParent); if (!$parent.has('.toast-container').length) { - $parent.prepend('<div class="toast-container"></div>'); + $parent.prepend('<div class="toast-container"></div>') } if ($toast.data('toast-trigger')) { $($toast.data('toast-trigger')).on('click', () => this.show()); @@ -27,8 +27,11 @@ export class Toast { /** Displays the toast component on the page. */ show() { - this.$toast.appendTo(this.$container).fadeIn(); - this.$toast.find('.toast__close').one('click', () => this.close()); + this.$toast + .appendTo(this.$container) + .fadeIn(); + this.$toast.find('.toast__close') + .one('click', () => this.close()); } /** Hides the toast component and removes it from the DOM. */ @@ -42,19 +45,19 @@ export class Toast { */ export class FadingToast extends Toast { /** - * Creates a new toast component, adds a close listener to the component, and adds the component - * as the first child of the given parent element. - * - * @param {string} message Message that will be displayed in the toast component - * @param {JQuery} [$parent] Designates where the toast component will be attached - * @param {number} [timeout] Amount of time, in milliseconds, that the component will be visible - */ - constructor(message, $parent = null, timeout = DEFAULT_TIMEOUT) { - // TODO(i18n-js) + * Creates a new toast component, adds a close listener to the component, and adds the component + * as the first child of the given parent element. + * + * @param {string} message Message that will be displayed in the toast component + * @param {JQuery} [$parent] Designates where the toast component will be attached + * @param {number} [timeout] Amount of time, in milliseconds, that the component will be visible + */ + constructor(message, $parent=null, timeout=DEFAULT_TIMEOUT) { + // TODO(i18n-js) const $toast = $(`<div class="toast"> <span class="toast__body">${message}</span> <a class="toast__close">×<span class="shift">Close</span></a> - </div>`); + </div>`) // Prevent sending null parent: if ($parent) { @@ -80,14 +83,14 @@ export class FadingToast extends Toast { */ export class PersistentToast extends Toast { /** - * @param {string} message String that will be displayed within the toast component - * @param {string} classes Additional classes to add to the toast component - */ - constructor(message, classes = '') { + * @param {string} message String that will be displayed within the toast component + * @param {string} classes Additional classes to add to the toast component + */ + constructor(message, classes='') { const $toast = $(`<div class="toast ${classes}"> <span class="toast__body">${message}</span> <a class="toast__close">×<span class="shift">Close</span> - </div>`); - super($toast); + </div>`) + super($toast) } } diff --git a/openlibrary/plugins/openlibrary/js/add-book.js b/openlibrary/plugins/openlibrary/js/add-book.js index 18d36453d60..bf92fb27713 100644 --- a/openlibrary/plugins/openlibrary/js/add-book.js +++ b/openlibrary/plugins/openlibrary/js/add-book.js @@ -1,14 +1,14 @@ import { + parseIsbn, + parseLccn, + parseOclc, isChecksumValidIsbn10, isChecksumValidIsbn13, isFormatValidIsbn10, isFormatValidIsbn13, isValidLccn, - isValidOclc, - parseIsbn, - parseLccn, - parseOclc, -} from './idValidation.js'; + isValidOclc +} from './idValidation.js' import { trimInputValues } from './utils.js'; let invalidChecksum; @@ -18,18 +18,16 @@ let invalidLccn; let invalidOclc; let emptyId; -const i18nStrings = JSON.parse( - document.querySelector('form[name=edit]').dataset.i18n, -); +const i18nStrings = JSON.parse(document.querySelector('form[name=edit]').dataset.i18n); const addBookForm = $('form#addbook'); -export function initAddBookImport() { - $('.list-books a').on('click', function () { +export function initAddBookImport () { + $('.list-books a').on('click', function() { var li = $(this).parents('li').first(); $('input#work').val(`/works/${li.attr('id')}`); addBookForm.trigger('submit'); }); - $('#bookAddCont').on('click', () => { + $('#bookAddCont').on('click', function() { $('input#work').val('none-of-these'); addBookForm.trigger('submit'); }); @@ -41,21 +39,21 @@ export function initAddBookImport() { invalidOclc = i18nStrings.invalid_oclc; emptyId = i18nStrings.empty_id; - $('#id_value').on('change', autoCompleteIdName); + $('#id_value').on('change',autoCompleteIdName); $('#addbook').on('submit', parseAndValidateId); $('#id_value').on('input', clearErrors); $('#id_name').on('change', clearErrors); $('#publish_date').on('blur', validatePublishDate); - trimInputValues('input'); + trimInputValues('input') // Prevents submission if the publish date is > 1 year in the future - addBookForm.on('submit', () => { + addBookForm.on('submit', function() { if ($('#publish-date-errors').hasClass('hidden')) { return true; } else return false; - }); + }) } // a flag to make raiseIsbnError perform differently upon subsequent calls @@ -70,14 +68,12 @@ function displayIsbnError(event, errorMessage) { const confirm = document.getElementById('confirm-add'); confirm.classList.remove('hidden'); const isbnInput = document.getElementById('id_value'); - isbnInput.focus({ focusVisible: true }); + isbnInput.focus({focusVisible: true}); event.preventDefault(); return; } // parsing potentially invalid ISBN - document.getElementById('id_value').value = parseIsbn( - document.getElementById('id_value').value, - ); + document.getElementById('id_value').value = parseIsbn(document.getElementById('id_value').value); } function displayIdentifierError(event, errorMessage) { @@ -102,13 +98,17 @@ function parseAndValidateId(event) { if (fieldName === 'isbn_10') { parseAndValidateIsbn10(event, idValue); - } else if (fieldName === 'isbn_13') { + } + else if (fieldName === 'isbn_13') { parseAndValidateIsbn13(event, idValue); - } else if (fieldName === 'lccn') { + } + else if (fieldName === 'lccn') { parseAndValidateLccn(event, idValue); - } else if (fieldName === 'oclc_numbers') { + } + else if (fieldName === 'oclc_numbers') { parseAndValidateOclc(event, idValue); - } else if (!fieldName || !isEmptyId(event, idValue)) { + } + else if (!fieldName || !isEmptyId(event, idValue)) { document.getElementById('id_value').value = idValue.trim(); } } @@ -163,21 +163,24 @@ function parseAndValidateOclc(event, idValue) { document.getElementById('id_value').value = idValue; } -function autoCompleteIdName() { +function autoCompleteIdName(){ const idValue = document.querySelector('input#id_value').value.trim(); const idValueIsbn = parseIsbn(idValue); const currentSelection = document.getElementById('id_name').value; - if (isFormatValidIsbn10(idValueIsbn) && isChecksumValidIsbn10(idValueIsbn)) { + if (isFormatValidIsbn10(idValueIsbn) && isChecksumValidIsbn10(idValueIsbn)){ document.getElementById('id_name').value = 'isbn_10'; - } else if ( - isFormatValidIsbn13(idValueIsbn) && - isChecksumValidIsbn13(idValueIsbn) - ) { + } + + else if (isFormatValidIsbn13(idValueIsbn) && isChecksumValidIsbn13(idValueIsbn)){ document.getElementById('id_name').value = 'isbn_13'; - } else if (isValidLccn(parseLccn(idValue))) { + } + + else if ((isValidLccn(parseLccn(idValue)))){ document.getElementById('id_name').value = 'lccn'; - } else { + } + + else { document.getElementById('id_name').value = currentSelection || ''; } } diff --git a/openlibrary/plugins/openlibrary/js/add_provider.js b/openlibrary/plugins/openlibrary/js/add_provider.js index 3bbbee6393c..11cd5c13cc5 100644 --- a/openlibrary/plugins/openlibrary/js/add_provider.js +++ b/openlibrary/plugins/openlibrary/js/add_provider.js @@ -1,60 +1,60 @@ export function initAddProviderRowLink(elem) { - elem.addEventListener('click', function () { - let index = Number(elem.dataset.index); - const tbody = document.querySelector('#provider-table-body'); - tbody.appendChild(createProviderRow(index)); + elem.addEventListener('click', function() { + let index = Number(elem.dataset.index) + const tbody = document.querySelector('#provider-table-body') + tbody.appendChild(createProviderRow(index)) if (index === 0) { - document.querySelector('#provider-table').classList.remove('hidden'); + document.querySelector('#provider-table').classList.remove('hidden') } - this.dataset.index = ++index; - }); + this.dataset.index = ++index + }) } function createProviderRow(index) { - const tr = document.createElement('tr'); + const tr = document.createElement('tr') const innerHtml = `${createTextInputDataCell(index, 'url')} ${createSelectDataCell(index, 'access', accessTypeValues)} ${createSelectDataCell(index, 'format', formatValues)} - ${createTextInputDataCell(index, 'provider_name')}`; + ${createTextInputDataCell(index, 'provider_name')}` - tr.innerHTML = innerHtml; - return tr; + tr.innerHTML = innerHtml + return tr } function createTextInputDataCell(index, type) { - const id = `edition--providers--${index}--${type}`; - return `<td><input name="${id}" id="${id}" ${type === 'url' ? 'type="url"' : ''}></td>`; + const id = `edition--providers--${index}--${type}` + return `<td><input name="${id}" id="${id}" ${type === 'url' ? 'type="url"' : ''}></td>` } function createSelectDataCell(index, type, values) { - const id = `edition--providers--${index}--${type}`; + const id = `edition--providers--${index}--${type}` return `<td> <select name="${id}" id="${id}"> ${createSelectOptions(values)} </select> - </td>`; + </td>` } const accessTypeValues = [ - { value: '', text: '' }, - { value: 'read', text: 'Read' }, - { value: 'listen', text: 'Listen' }, - { value: 'buy', text: 'Buy' }, - { value: 'borrow', text: 'Borrow' }, - { value: 'preview', text: 'Preview' }, -]; + {value: '', text: ''}, + {value: 'read', text: 'Read'}, + {value: 'listen', text: 'Listen'}, + {value: 'buy', text: 'Buy'}, + {value: 'borrow', text: 'Borrow'}, + {value: 'preview', text: 'Preview'} +] const formatValues = [ - { value: '', text: '' }, - { value: 'web', text: 'Web' }, - { value: 'epub', text: 'ePub' }, - { value: 'pdf', text: 'PDF' }, -]; + {value: '', text: ''}, + {value: 'web', text: 'Web'}, + {value: 'epub', text: 'ePub'}, + {value: 'pdf', text: 'PDF'} +] function createSelectOptions(values) { - let html = ''; + let html = '' for (const value of values) { - html += `<option value="${value.value}">${value.text}</option>\n`; + html += `<option value="${value.value}">${value.text}</option>\n` } - return html; + return html } diff --git a/openlibrary/plugins/openlibrary/js/admin.js b/openlibrary/plugins/openlibrary/js/admin.js index e62857e8f51..93e9cfef789 100644 --- a/openlibrary/plugins/openlibrary/js/admin.js +++ b/openlibrary/plugins/openlibrary/js/admin.js @@ -9,11 +9,11 @@ export function initAdmin() { var tag; $(this).toggleClass('active'); - action = $(this).hasClass('active') ? 'add_tag' : 'remove_tag'; + action = $(this).hasClass('active') ? 'add_tag': 'remove_tag'; tag = $(this).text(); $.post(window.location.href, { action: action, - tag: tag, + tag: tag }); }); @@ -26,11 +26,11 @@ export function initAdmin() { export function initAnonymizationButton(button) { const displayName = button.dataset.displayName; const confirmMessage = `Really anonymize ${displayName}'s account? This will delete ${displayName}'s profile page and booknotes, and anonymize ${displayName}'s reading log, reviews, star ratings, and merge request submissions.`; - button.addEventListener('click', (event) => { + button.addEventListener('click', function(event) { if (!confirm(confirmMessage)) { event.preventDefault(); } - }); + }) } /** @@ -40,12 +40,12 @@ export function initAnonymizationButton(button) { * @param {NodeList<HTMLButtonElement>} buttons */ export function initConfirmationButtons(buttons) { - const confirmMessage = 'Are you sure?'; + const confirmMessage = 'Are you sure?' for (const button of buttons) { - button.addEventListener('click', (event) => { + button.addEventListener('click', function(event) { if (!confirm(confirmMessage)) { event.preventDefault(); } - }); + }) } } diff --git a/openlibrary/plugins/openlibrary/js/affiliate-links.js b/openlibrary/plugins/openlibrary/js/affiliate-links.js index 267341c4910..c31e7247428 100644 --- a/openlibrary/plugins/openlibrary/js/affiliate-links.js +++ b/openlibrary/plugins/openlibrary/js/affiliate-links.js @@ -1,4 +1,4 @@ -import { buildPartialsUrl } from './utils'; +import { buildPartialsUrl } from './utils' /** * Adds functionality to fetch affialite links asyncronously. @@ -10,16 +10,16 @@ import { buildPartialsUrl } from './utils'; * @param {NodeList<HTMLElement>} affiliateLinksSections Collection of each affiliate links section that is on the page */ export function initAffiliateLinks(affiliateLinksSections) { - const isLoading = showLoadingIndicators(affiliateLinksSections); + const isLoading = showLoadingIndicators(affiliateLinksSections) if (isLoading) { - // Replace loading indicators with fetched partials + // Replace loading indicators with fetched partials - const title = affiliateLinksSections[0].dataset.title; - const opts = JSON.parse(affiliateLinksSections[0].dataset.opts); - const args = [title, opts]; - const d = { args: args }; + const title = affiliateLinksSections[0].dataset.title + const opts = JSON.parse(affiliateLinksSections[0].dataset.opts) + const args = [title, opts] + const d = {args: args} - getPartials(d, affiliateLinksSections); + getPartials(d, affiliateLinksSections) } } @@ -31,15 +31,15 @@ export function initAffiliateLinks(affiliateLinksSections) { * @returns {boolean} `true` if a loading indicator is displayed on the screen */ function showLoadingIndicators(linkSections) { - let isLoading = false; + let isLoading = false for (const section of linkSections) { - const loadingIndicator = section.querySelector('.loadingIndicator'); + const loadingIndicator = section.querySelector('.loadingIndicator') if (loadingIndicator) { - isLoading = true; - loadingIndicator.classList.remove('hidden'); + isLoading = true + loadingIndicator.classList.remove('hidden') } } - return isLoading; + return isLoading } /** @@ -50,50 +50,44 @@ function showLoadingIndicators(linkSections) { * @returns {Promise} */ async function getPartials(data, affiliateLinksSections) { - const dataString = JSON.stringify(data); + const dataString = JSON.stringify(data) - return fetch(buildPartialsUrl('AffiliateLinks', { data: dataString })) + return fetch(buildPartialsUrl('AffiliateLinks', {data: dataString})) .then((resp) => { if (resp.status !== 200) { - throw new Error( - `Failed to fetch partials. Status code: ${resp.status}`, - ); + throw new Error(`Failed to fetch partials. Status code: ${resp.status}`) } - return resp.json(); + return resp.json() }) .then((data) => { - const span = document.createElement('span'); - span.innerHTML = data['partials']; - const links = span.firstElementChild; + const span = document.createElement('span') + span.innerHTML = data['partials'] + const links = span.firstElementChild for (const section of affiliateLinksSections) { - section.replaceWith(links.cloneNode(true)); + section.replaceWith(links.cloneNode(true)) } }) .catch(() => { // XXX : Handle errors sensibly for (const section of affiliateLinksSections) { - const loadingIndicator = section.querySelector('.loadingIndicator'); + const loadingIndicator = section.querySelector('.loadingIndicator') if (loadingIndicator) { - loadingIndicator.classList.add('hidden'); + loadingIndicator.classList.add('hidden') } - const existingRetryAffordance = section.querySelector( - '.affiliate-links-section__retry', - ); + const existingRetryAffordance = section.querySelector('.affiliate-links-section__retry') if (existingRetryAffordance) { - existingRetryAffordance.classList.remove('hidden'); + existingRetryAffordance.classList.remove('hidden') } else { - section.insertAdjacentHTML('afterbegin', renderRetryLink()); - const retryAffordance = section.querySelector( - '.affiliate-links-section__retry', - ); + section.insertAdjacentHTML('afterbegin', renderRetryLink()) + const retryAffordance = section.querySelector('.affiliate-links-section__retry') retryAffordance.addEventListener('click', () => { - retryAffordance.classList.add('hidden'); - getPartials(data, affiliateLinksSections); - }); + retryAffordance.classList.add('hidden') + getPartials(data, affiliateLinksSections) + }) } } - }); + }) } /** @@ -102,5 +96,5 @@ async function getPartials(data, affiliateLinksSections) { * @returns {string} HTML for a retry link. */ function renderRetryLink() { - return '<span class="affiliate-links-section__retry">Failed to fetch affiliate links. <button type="button" style="border:none;background:none;color:blue;text-decoration:underline;cursor:pointer;">Retry?</button></span>'; + return '<span class="affiliate-links-section__retry">Failed to fetch affiliate links. <a href="javascript:;">Retry?</a></span>' } diff --git a/openlibrary/plugins/openlibrary/js/autocomplete.js b/openlibrary/plugins/openlibrary/js/autocomplete.js index 9464a6544a6..e2677599fd8 100644 --- a/openlibrary/plugins/openlibrary/js/autocomplete.js +++ b/openlibrary/plugins/openlibrary/js/autocomplete.js @@ -13,11 +13,8 @@ import 'jquery-ui-touch-punch'; // this makes drag-to-reorder work on touch devi */ export function highlight(value, term) { return value.replace( - new RegExp( - `(?![^&;]+;)(?!<[^<>]*)(${term.replace(/([\^$()[]\{\}\*\.\+\?\|\\])/gi, '$1')})(?![^<>]*>)(?![^&;]+;)`, - 'gi', - ), - '<strong>$1</strong>', + new RegExp(`(?![^&;]+;)(?!<[^<>]*)(${term.replace(/([\^$()[]\{\}\*\.\+\?\|\\])/gi, '$1')})(?![^<>]*>)(?![^&;]+;)`, 'gi'), + '<strong>$1</strong>' ); } @@ -30,69 +27,64 @@ export function highlight(value, term) { * creating a new entry will be added * @return {array} of modified results that are compatible with the jquery autocomplete search suggestions */ -export const mapApiResultsToAutocompleteSuggestions = ( - results, - labelFormatter, - addNewFieldTerm, -) => { +export const mapApiResultsToAutocompleteSuggestions = (results, labelFormatter, addNewFieldTerm) => { const mapAPIResultToSuggestedItem = (r) => ({ key: r.key, label: labelFormatter(r), - value: r.name, + value: r.name }); // When no results if callback is defined, append a create new entry if (addNewFieldTerm) { - results.push({ - name: addNewFieldTerm, - key: '__new__', - value: addNewFieldTerm, - }); + results.push( + { + name: addNewFieldTerm, + key: '__new__', + value: addNewFieldTerm + } + ); } return results.map(mapAPIResultToSuggestedItem); }; export function init() { /** - * Some extra options for when creating an autocomplete input field - * @typedef {Object} OpenLibraryAutocompleteOptions - * @property {string} endpoint - url to hit for autocomplete results - * @property{(boolean|Function)} [addnew] - when (or whether) to display a "Create new record" - * element in the autocomplete list. The function takes the query and should return a boolean. - * a boolean. - * @property{string} [new_name] - name to display when __new__ selected. Defaults to the query - * @property {boolean} [allow_empty] - whether to allow empty list. Only applies to multi-select - * @property {boolean} [sortable=false] - */ + * Some extra options for when creating an autocomplete input field + * @typedef {Object} OpenLibraryAutocompleteOptions + * @property {string} endpoint - url to hit for autocomplete results + * @property{(boolean|Function)} [addnew] - when (or whether) to display a "Create new record" + * element in the autocomplete list. The function takes the query and should return a boolean. + * a boolean. + * @property{string} [new_name] - name to display when __new__ selected. Defaults to the query + * @property {boolean} [allow_empty] - whether to allow empty list. Only applies to multi-select + * @property {boolean} [sortable=false] + */ /** - * @private - * @param{HTMLInputElement} _this - input element that will become autocompleting. - * @param{OpenLibraryAutocompleteOptions} ol_ac_opts - * @param{Object} ac_opts - options passed to $.autocomplete; see that. - * @param {Function} ac_opts.formatItem - optional item formatter. Returns a string of HTML for rendering as an item. - * @param {Function} ac_opts.termPreprocessor - optional hook for processing the search term before doing the search - */ + * @private + * @param{HTMLInputElement} _this - input element that will become autocompleting. + * @param{OpenLibraryAutocompleteOptions} ol_ac_opts + * @param{Object} ac_opts - options passed to $.autocomplete; see that. + * @param {Function} ac_opts.formatItem - optional item formatter. Returns a string of HTML for rendering as an item. + * @param {Function} ac_opts.termPreprocessor - optional hook for processing the search term before doing the search + */ function setup_autocomplete(_this, ol_ac_opts, ac_opts) { var default_ac_opts = { minChars: 2, autoFill: true, - formatItem: (item) => item.name, + formatItem: item => item.name, /** - * Adds the ac_over class to the selected autocomplete item - * - * @param {Event} _event (unused) - * @param {Object} ui containing item key - */ - focus: (_event, ui) => { + * Adds the ac_over class to the selected autocomplete item + * + * @param {Event} _event (unused) + * @param {Object} ui containing item key + */ + focus: function (_event, ui) { const $list = $(_this).data('list'); if ($list) { - $list - .find('li') + $list.find('li') .removeClass('ac_over') - .filter( - (_, el) => $(el).data('ui-autocomplete-item').key === ui.item.key, - ) + .filter((_, el) => $(el).data('ui-autocomplete-item').key === ui.item.key) .addClass('ac_over'); } return ac_opts.autoFill; @@ -105,13 +97,13 @@ export function init() { if ($preview.length) { $preview.html(item.label); } - setTimeout(() => { + setTimeout(function() { $this.addClass('accept'); }, 0); }, mustMatch: true, - formatMatch: (item) => item.name, - termPreprocessor: (term) => term, + formatMatch: function(item) { return item.name; }, + termPreprocessor: function(term) { return term; } }; $.widget('custom.autocompleteHTML', $.ui.autocomplete, { @@ -126,14 +118,14 @@ export function init() { }); // store list so we can add ac_over hover effect in `focus` event $(_this).data('list', $ul); - }, + } }); const options = $.extend(default_ac_opts, ac_opts); - options.source = (q, response) => { + options.source = function (q, response) { const term = options.termPreprocessor(q.term); const params = { q: term, - limit: options.max, + limit: options.max }; if (location.search.indexOf('lang=') !== -1) { params.lang = new URLSearchParams(location.search).get('lang'); @@ -141,53 +133,47 @@ export function init() { if (params.q.length < options.minChars) return; return $.ajax({ url: ol_ac_opts.endpoint, - data: params, + data: params }).then((results) => { response( mapApiResultsToAutocompleteSuggestions( results, (r) => highlight(options.formatItem(r), term), ol_ac_opts.addnew === true || - (ol_ac_opts.addnew && ol_ac_opts.addnew(term)) - ? ol_ac_opts.new_name || term - : null, - ), + (ol_ac_opts.addnew && ol_ac_opts.addnew(term)) ? (ol_ac_opts.new_name || term) : null + ) ); }); }; $(_this) .autocompleteHTML(options) - .on('keypress', function () { + .on('keypress', function() { $(this).removeClass('accept').removeClass('reject'); }); } /** - * @this HTMLElement - the element that contains the different inputs. - * Expects an html structure like: - * <div class="multi-input-autocomplete"> - * <div class="ac-input mia__input"> - * <div class="mia__reorder">≡</div> - * <input class="ac-input__visible" type="text" name="fake_name--0" value="Author 1" /> - * <input class="ac-input__value" type="hidden" name="author--0" value="/authors/OL1234A" /> - * <a class="mia__remove" href="javascript:;">[x]</a> - * </div> - * ... - * < /div> - * @param {Function} input_renderer - ((index, item) -> html_string) render the ith .input. - * @param {OpenLibraryAutocompleteOptions} ol_ac_opts - * @param {Object} ac_opts - options given to override defaults of $.autocomplete; see that. - */ - $.fn.setup_multi_input_autocomplete = function ( - input_renderer, - ol_ac_opts, - ac_opts, - ) { - /** @type {JQuery<HTMLElement>} */ + * @this HTMLElement - the element that contains the different inputs. + * Expects an html structure like: + * <div class="multi-input-autocomplete"> + * <div class="ac-input mia__input"> + * <div class="mia__reorder">≡</div> + * <input class="ac-input__visible" type="text" name="fake_name--0" value="Author 1" /> + * <input class="ac-input__value" type="hidden" name="author--0" value="/authors/OL1234A" /> + * <a class="mia__remove" href="javascript:;">[x]</a> + * </div> + * ... + * < /div> + * @param {Function} input_renderer - ((index, item) -> html_string) render the ith .input. + * @param {OpenLibraryAutocompleteOptions} ol_ac_opts + * @param {Object} ac_opts - options given to override defaults of $.autocomplete; see that. + */ + $.fn.setup_multi_input_autocomplete = function(input_renderer, ol_ac_opts, ac_opts) { + /** @type {JQuery<HTMLElement>} */ var container = $(this); // first let's init any pre-existing inputs - container.find('.ac-input__visible').each(function () { + container.find('.ac-input__visible').each(function() { setup_autocomplete(this, ol_ac_opts, ac_opts); }); const allow_empty = ol_ac_opts.allow_empty; @@ -195,34 +181,27 @@ export function init() { function update_visible() { if (allow_empty || container.find('.mia__input').length > 1) { container.find('.mia__remove').show(); - } else { + } + else { container.find('.mia__remove').hide(); } } function update_indices() { - container.find('.mia__input').each(function (index) { - $(this) - .find('.mia__index') - .each(function () { - $(this).text( - $(this) - .text() - .replace(/\d+/, index + 1), - ); - }); - $(this) - .find('[name]') - .each(function () { - // this won't behave nicely with nested numeric things, if that ever happens - if ($(this).attr('name').match(/\d+/)?.length > 1) { - throw new Error('nested numeric names not supported'); - } - $(this).attr('name', $(this).attr('name').replace(/\d+/, index)); - if ($(this).attr('id')) { - $(this).attr('id', $(this).attr('id').replace(/\d+/, index)); - } - }); + container.find('.mia__input').each(function(index) { + $(this).find('.mia__index').each(function () { + $(this).text($(this).text().replace(/\d+/, index + 1)); + }); + $(this).find('[name]').each(function() { + // this won't behave nicely with nested numeric things, if that ever happens + if ($(this).attr('name').match(/\d+/)?.length > 1) { + throw new Error('nested numeric names not supported'); + } + $(this).attr('name', $(this).attr('name').replace(/\d+/, index)); + if ($(this).attr('id')) { + $(this).attr('id', $(this).attr('id').replace(/\d+/, index)); + } + }); }); } @@ -233,11 +212,11 @@ export function init() { handle: '.mia__reorder', items: '.mia__input', update: update_indices, - cancel: '.mia__move, .mia__remove', + cancel: '.mia__move, .mia__remove' }); } - container.on('click', '.mia__remove', function () { + container.on('click', '.mia__remove', function() { if (allow_empty || container.find('.mia__input').length > 1) { $(this).closest('.mia__input').remove(); update_visible(); @@ -246,7 +225,7 @@ export function init() { }); // Add move button functionality - container.on('click', '.mia__move', function (event) { + container.on('click', '.mia__move', function(event) { event.preventDefault(); const $currentItem = $(this).closest('.mia__input'); const $allItems = container.find('.mia__input'); @@ -291,56 +270,53 @@ export function init() { update_indices(); }); - container.on('click', '.mia__add', (event) => { + container.on('click', '.mia__add', function(event) { var next_index, new_input; event.preventDefault(); next_index = container.find('.mia__input').length; - new_input = $(input_renderer(next_index, { key: '', name: '' })); + new_input = $(input_renderer(next_index, {key: '', name: ''})); new_input.insertBefore(container.find('.mia__add')); setup_autocomplete( new_input.find('.ac-input__visible')[0], ol_ac_opts, - ac_opts, - ); + ac_opts); update_visible(); }); }; /** - * @this HTMLElement - the element that contains the input. - * @param {string} autocomplete_selector - selector to find the input element use for autocomplete. - * @param {OpenLibraryAutocompleteOptions} ol_ac_opts - * @param {Object} ac_opts - options given to override defaults of $.autocomplete; see that. - */ - $.fn.setup_csv_autocomplete = function ( - autocomplete_selector, - ol_ac_opts, - ac_opts, - ) { + * @this HTMLElement - the element that contains the input. + * @param {string} autocomplete_selector - selector to find the input element use for autocomplete. + * @param {OpenLibraryAutocompleteOptions} ol_ac_opts + * @param {Object} ac_opts - options given to override defaults of $.autocomplete; see that. + */ + $.fn.setup_csv_autocomplete = function(autocomplete_selector, ol_ac_opts, ac_opts) { const container = $(this); const dataConfig = JSON.parse(container[0].dataset.config); /** - * Converts a csv string to an array of strings - * - * Eg - * - "a, b, c" -> ["a", "b", "c"] - * - 'a, "b, b", c' -> ["a", "b, b", "c"] - * @param {string} val - * @returns {string[]} - */ + * Converts a csv string to an array of strings + * + * Eg + * - "a, b, c" -> ["a", "b", "c"] + * - 'a, "b, b", c' -> ["a", "b, b", "c"] + * @param {string} val + * @returns {string[]} + */ function splitField(val) { const m = val.match(/("[^"]+"|[^,"]+)/g); if (!m) { throw new Error('Invalid CSV'); } - return m.map((s) => s.trim().replace(/^"(.*)"$/, '$1')).filter((s) => s); + return m + .map(s => s.trim().replace(/^"(.*)"$/, '$1')) + .filter(s => s); } function joinField(vals) { - const escaped = vals.map((val) => (val.includes(',') ? `"${val}"` : val)); + const escaped = vals.map(val => (val.includes(',')) ? `"${val}"` : val); return escaped.join(', '); } @@ -350,7 +326,7 @@ export function init() { matchSubset: false, autoFill: false, position: { my: 'right top', at: 'right bottom' }, - termPreprocessor: (subject_string) => { + termPreprocessor: function(subject_string) { const terms = splitField(subject_string); if (terms.length !== dataConfig.data.length) { return terms.pop(); @@ -359,7 +335,7 @@ export function init() { return ''; } }, - select: function (event, ui) { + select: function(event, ui) { const terms = splitField(this.value); terms.splice(terms.length - 1, 1, ui.item.value); this.value = `${joinField(terms)}, `; @@ -368,18 +344,15 @@ export function init() { $(this).trigger('input'); return false; }, - response: function (event, ui) { + response: function(event, ui) { /* Remove any entries already on the list */ const terms = splitField(this.value); - ui.content.splice( - 0, - ui.content.length, - ...ui.content.filter((record) => !terms.includes(record.value)), - ); + ui.content.splice(0, ui.content.length, + ...ui.content.filter(record => !terms.includes(record.value))); }, - }; + } - container.find(autocomplete_selector).each(function () { + container.find(autocomplete_selector).each(function() { const options = $.extend(default_ac_opts, ac_opts); setup_autocomplete(this, ol_ac_opts, options); }); diff --git a/openlibrary/plugins/openlibrary/js/banner/index.js b/openlibrary/plugins/openlibrary/js/banner/index.js index 67301490ee7..1a2d63b213e 100644 --- a/openlibrary/plugins/openlibrary/js/banner/index.js +++ b/openlibrary/plugins/openlibrary/js/banner/index.js @@ -11,18 +11,15 @@ function setBannerCookie(cookieName, cookieDurationDays, successCallback) { $.ajax({ type: 'POST', url: '/hide_banner', - data: JSON.stringify({ - 'cookie-name': cookieName, - 'cookie-duration-days': cookieDurationDays, - }), + data: JSON.stringify({'cookie-name': cookieName, 'cookie-duration-days': cookieDurationDays}), contentType: 'application/json', dataType: 'json', - beforeSend: (xhr) => { + beforeSend: function(xhr) { xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Accept', 'application/json'); }, - success: successCallback, + success: successCallback }); } @@ -33,17 +30,15 @@ function setBannerCookie(cookieName, cookieDurationDays, successCallback) { */ export function initDismissibleBanners(banners) { for (const banner of banners) { - const cookieName = banner.dataset.cookieName; - const cookieDurationDays = banner.dataset.cookieDurationDays; + const cookieName = banner.dataset.cookieName + const cookieDurationDays = banner.dataset.cookieDurationDays - const dismissButton = banner.querySelector( - '.page-banner--dismissable-close', - ); + const dismissButton = banner.querySelector('.page-banner--dismissable-close') dismissButton.addEventListener('click', () => { const successCallback = () => { - banner.remove(); - }; - setBannerCookie(cookieName, cookieDurationDays, successCallback); - }); + banner.remove() + } + setBannerCookie(cookieName, cookieDurationDays, successCallback) + }) } } diff --git a/openlibrary/plugins/openlibrary/js/book-page-lists.js b/openlibrary/plugins/openlibrary/js/book-page-lists.js index f7393047512..eb16d7c0bf8 100644 --- a/openlibrary/plugins/openlibrary/js/book-page-lists.js +++ b/openlibrary/plugins/openlibrary/js/book-page-lists.js @@ -1,92 +1,86 @@ -import { initAsyncFollowing } from './following'; -import { buildPartialsUrl } from './utils'; +import { buildPartialsUrl } from './utils' +import { initAsyncFollowing } from './following' /** * Initializes lazy-loading the "Lists" section of Open Library book pages. * * @param elem {HTMLElement} Container for book page lists section */ -export function initListsSection (elem) { +export function initListsSection(elem) { // Show loading indicator - const loadingIndicator = elem.querySelector('.loadingIndicator'); - loadingIndicator.classList.remove('hidden'); + const loadingIndicator = elem.querySelector('.loadingIndicator') + loadingIndicator.classList.remove('hidden') - const ids = JSON.parse(elem.dataset.ids); + const ids = JSON.parse(elem.dataset.ids) - const intersectionObserver = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - // Unregister intersection listener - intersectionObserver.unobserve(entries[0].target); - fetchPartials(ids.work, ids.edition) - .then((resp) => { - // Check response code, continue if not 4XX or 5XX - return resp.json(); - }) - .then((data) => { - // Replace loading indicator with partials - const listSection = loadingIndicator.parentElement; - const fragment = document.createDocumentFragment(); + const intersectionObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + // Unregister intersection listener + intersectionObserver.unobserve(entries[0].target) + fetchPartials(ids.work, ids.edition) + .then((resp) => { + // Check response code, continue if not 4XX or 5XX + return resp.json() + }) + .then((data) => { + // Replace loading indicator with partials + const listSection = loadingIndicator.parentElement + const fragment = document.createDocumentFragment() - for (const htmlString of data.partials) { - const template = document.createElement('template'); - template.innerHTML = htmlString; - fragment.append(...template.content.childNodes); - } + for (const htmlString of data.partials) { + const template = document.createElement('template') + template.innerHTML = htmlString + fragment.append(...template.content.childNodes) + } - listSection.replaceChildren(fragment); + listSection.replaceChildren(fragment) - // Show "See All" link - if (data.hasLists) { - const showAllLink = elem.querySelector('.lists-heading a'); - if (showAllLink) { - showAllLink.classList.remove('hidden'); - } + // Show "See All" link + if (data.hasLists) { + const showAllLink = elem.querySelector('.lists-heading a') + if (showAllLink) { + showAllLink.classList.remove('hidden') } - // Initialize private buttons after content is loaded - initPrivateButtonsAfterLoad(listSection); + } + // Initialize private buttons after content is loaded + initPrivateButtonsAfterLoad(listSection) - const followForms = listSection.querySelectorAll('.follow-form'); - initAsyncFollowing(followForms); - }); - } - }); - }, - { - root: null, - rootMargin: '200px', - threshold: 0, - }, - ); + const followForms = listSection.querySelectorAll('.follow-form'); + initAsyncFollowing(followForms) + }) + } + }) + }, { + root: null, + rootMargin: '200px', + threshold: 0 + }) - intersectionObserver.observe(elem); + intersectionObserver.observe(elem) } /** * Initialize private buttons after the lists section has been loaded * @param {HTMLElement} container - The container that now has the loaded content */ -function initPrivateButtonsAfterLoad (container) { - const privateButtons = container.querySelectorAll( - '.list-follow-card__private-button', - ); +function initPrivateButtonsAfterLoad(container) { + const privateButtons = container.querySelectorAll('.list-follow-card__private-button') if (privateButtons.length > 0) { - import(/* webpackChunkName: "private-buttons" */ './private-button').then( - (module) => { - module.initPrivateButtons(privateButtons); - }, - ); + import(/* webpackChunkName: "private-buttons" */ './private-button') + .then(module => { + module.initPrivateButtons(privateButtons) + }) } } -async function fetchPartials (workId, editionId) { - const params = {}; +async function fetchPartials(workId, editionId) { + const params = {} if (workId) { - params.workId = workId; + params.workId = workId } if (editionId) { - params.editionId = editionId; + params.editionId = editionId } return fetch(buildPartialsUrl('BPListsSection', params)); diff --git a/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js b/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js index 2126881bf4a..35333ed4378 100644 --- a/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js +++ b/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js @@ -3,12 +3,12 @@ * * @param {NodeList<HTMLElement>} crumbs - NodeList of breadcrumb select elements. */ -export function initBreadcrumbSelect (crumbs) { +export function initBreadcrumbSelect(crumbs) { const allowedKeys = new Set(['Tab', 'Enter', ' ']); const preventedKeys = new Set(['ArrowUp', 'ArrowDown']); // watch crumbs for changes, // ensures it's a full value change, not a user exploring options via keyboard - function handleNavEvents (nav) { + function handleNavEvents(nav) { let ignoreChange = false; nav.addEventListener('change', () => { diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger.js index ca51c155af5..a2f3697a598 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger.js @@ -2,11 +2,12 @@ * Defines functionality related to the ILE's Bulk Tagger tool. * @module ile/BulkTagger */ -import debounce from 'lodash/debounce'; -import { FadingToast } from '../Toast'; +import debounce from 'lodash/debounce' + import { MenuOption, MenuOptionState } from './BulkTagger/MenuOption'; import { SortedMenuOptionContainer } from './BulkTagger/SortedMenuOptionContainer'; -import { Tag } from './models/Tag'; +import { Tag } from './models/Tag' +import { FadingToast } from '../Toast' /** * Maximum amount of search result to be returned by subject @@ -30,704 +31,616 @@ const COLLECTION_PREFIX = 'collection:'; */ export class BulkTagger { /** - * Sets references to key Bulk Tagger affordances. - * - * @param {HTMLElement} bulkTagger Reference to root element of the Bulk Tagger - */ - constructor(bulkTagger) { - /** - * Reference to root Bulk Tagger element. - * @member {HTMLFormElement} + * Sets references to key Bulk Tagger affordances. + * + * @param {HTMLElement} bulkTagger Reference to root element of the Bulk Tagger */ - this.rootElement = bulkTagger; + constructor(bulkTagger) { + /** + * Reference to root Bulk Tagger element. + * @member {HTMLFormElement} + */ + this.rootElement = bulkTagger /** - * Reference to the Bulk Tagger's subject search box. - * @member {HTMLInputElement} - */ - this.searchInput = bulkTagger.querySelector('.subjects-search-input'); + * Reference to the Bulk Tagger's subject search box. + * @member {HTMLInputElement} + */ + this.searchInput = bulkTagger.querySelector('.subjects-search-input') /** - * Menu option container that holds options for staged tags and tags - * that already exist on one or more selected works. - * - * @member {SortedMenuOptionContainer} - */ - this.selectedOptionsContainer; + * Menu option container that holds options for staged tags and tags + * that already exist on one or more selected works. + * + * @member {SortedMenuOptionContainer} + */ + this.selectedOptionsContainer /** - * Menu option container that holds options representing search results. - * - * @member {SortedMenuOptionContainer} - */ - this.searchResultsOptionsContainer; + * Menu option container that holds options representing search results. + * + * @member {SortedMenuOptionContainer} + */ + this.searchResultsOptionsContainer /** - * Reference to the element which contains the affordance that creates new subjects. - * @member {HTMLElement} - */ - this.createSubjectElem = bulkTagger.querySelector( - '.search-subject-row-name', - ); + * Reference to the element which contains the affordance that creates new subjects. + * @member {HTMLElement} + */ + this.createSubjectElem = bulkTagger.querySelector('.search-subject-row-name') /** - * Element which displays the subject name within the "create new tag" affordance. - * @member {HTMLElement} - */ - this.subjectNameElem = - this.createSubjectElem.querySelector('.subject-name'); + * Element which displays the subject name within the "create new tag" affordance. + * @member {HTMLElement} + */ + this.subjectNameElem = this.createSubjectElem.querySelector('.subject-name') /** - * Reference to input which holds the subjects to be batch added. - * @member {HTMLInputElement} - */ - this.addSubjectsInput = bulkTagger.querySelector('input[name=tags_to_add]'); + * Reference to input which holds the subjects to be batch added. + * @member {HTMLInputElement} + */ + this.addSubjectsInput = bulkTagger.querySelector('input[name=tags_to_add]') /** - * Input which contains the subjects to be batch removed. - * @member {HTMLInputElement} - */ - this.removeSubjectsInput = bulkTagger.querySelector( - 'input[name=tags_to_remove]', - ); + * Input which contains the subjects to be batch removed. + * @member {HTMLInputElement} + */ + this.removeSubjectsInput = bulkTagger.querySelector('input[name=tags_to_remove]') /** - * Reference to hidden input which holds a comma-separated list of work OLIDs - * @member {HTMLInputElement} - */ - this.selectedWorksInput = bulkTagger.querySelector('input[name=work_ids]'); + * Reference to hidden input which holds a comma-separated list of work OLIDs + * @member {HTMLInputElement} + */ + this.selectedWorksInput = bulkTagger.querySelector('input[name=work_ids]') /** - * Reference to the bulk tagger form's submit button. - * - * @member {HTMLButtonElement} - */ - this.submitButton = this.rootElement.querySelector('.bulk-tagging-submit'); + * Reference to the bulk tagger form's submit button. + * + * @member {HTMLButtonElement} + */ + this.submitButton = this.rootElement.querySelector('.bulk-tagging-submit') /** - * Stores works' subjects that have been fetched from the server. - * - * Keys to the map are work IDs. - * @member {Map<String, Array<Tag>>} - */ - this.existingSubjects = new Map(); + * Stores works' subjects that have been fetched from the server. + * + * Keys to the map are work IDs. + * @member {Map<String, Array<Tag>>} + */ + this.existingSubjects = new Map() /** - * Array containing OLIDs of each selected work. - * - * @member {Array<String>} - */ - this.selectedWorks = []; + * Array containing OLIDs of each selected work. + * + * @member {Array<String>} + */ + this.selectedWorks = [] /** - * Tags staged for adding to all selected works. - * - * @member {Array<Tag>} - */ - this.tagsToAdd = []; + * Tags staged for adding to all selected works. + * + * @member {Array<Tag>} + */ + this.tagsToAdd = [] /** - * Tags staged for removal from all selected works. - * - * @member {Array<Tag>} - */ - this.tagsToRemove = []; + * Tags staged for removal from all selected works. + * + * @member {Array<Tag>} + */ + this.tagsToRemove = [] /** - * `true` if the bulk tagger appears on a book page. - * - * @type {boolean} - */ - this.isBookPageEdit = false; + * `true` if the bulk tagger appears on a book page. + * + * @type {boolean} + */ + this.isBookPageEdit = false } /** - * Initialized the menu option containers, and adds event listeners to the Bulk Tagger. - */ + * Initialized the menu option containers, and adds event listeners to the Bulk Tagger. + */ initialize() { - // Create sorted menu option containers: - this.selectedOptionsContainer = new SortedMenuOptionContainer( - this.rootElement.querySelector('.selected-tag-subjects'), - ); - this.searchResultsOptionsContainer = new SortedMenuOptionContainer( - this.rootElement.querySelector('.subjects-search-results'), - ); + // Create sorted menu option containers: + this.selectedOptionsContainer = new SortedMenuOptionContainer(this.rootElement.querySelector('.selected-tag-subjects')) + this.searchResultsOptionsContainer = new SortedMenuOptionContainer(this.rootElement.querySelector('.subjects-search-results')) // Add "hide menu" functionality: - const closeFormButton = this.rootElement.querySelector( - '.close-bulk-tagging-form', - ); + const closeFormButton = this.rootElement.querySelector('.close-bulk-tagging-form') closeFormButton.addEventListener('click', () => { - this.hideTaggingMenu(); - }); + this.hideTaggingMenu() + }) // Add input listener to subject search box: - const debouncedInputChangeHandler = debounce( - this.onSearchInputChange.bind(this), - 500, - ); + const debouncedInputChangeHandler = debounce(this.onSearchInputChange.bind(this), 500) this.searchInput.addEventListener('input', () => { const searchTerm = this.searchInput.value.trim(); - debouncedInputChangeHandler(searchTerm); + debouncedInputChangeHandler(searchTerm) }); // Prevent redirect on batch subject submission: this.submitButton.addEventListener('click', (event) => { - event.preventDefault(); - this.submitBatch(); - }); + event.preventDefault() + this.submitBatch() + }) // Add click listeners to "create subject" options: - const createSubjectButtons = this.rootElement.querySelectorAll( - '.subject-type-option', - ); + const createSubjectButtons = this.rootElement.querySelectorAll('.subject-type-option') for (const elem of createSubjectButtons) { - elem.addEventListener('click', () => - this.onCreateTag(new Tag(this.searchInput.value, elem.dataset.tagType)), - ); + elem.addEventListener('click', () => this.onCreateTag(new Tag(this.searchInput.value, elem.dataset.tagType))) } } /** - * Hides the Bulk Tagger. - */ + * Hides the Bulk Tagger. + */ hideTaggingMenu() { - this.rootElement.classList.add('hidden'); - this.rootElement.dispatchEvent(new CustomEvent('option-hidden')); + this.rootElement.classList.add('hidden') + this.rootElement.dispatchEvent(new CustomEvent('option-hidden')) } /** - * Displays the Bulk Tagger. - */ + * Displays the Bulk Tagger. + */ showTaggingMenu() { - this.rootElement.classList.remove('hidden'); + this.rootElement.classList.remove('hidden') } /** - * Updates the BulkTagger when works are selected. - * - * Stores given array in `selectedWorks`, fetches the - * existing tags for each given work, and updates the view with - * the existing tags. - * - * @param {Array<String>} workIds - */ + * Updates the BulkTagger when works are selected. + * + * Stores given array in `selectedWorks`, fetches the + * existing tags for each given work, and updates the view with + * the existing tags. + * + * @param {Array<String>} workIds + */ async updateWorks(workIds) { - this.showLoadingIndicator(); + this.showLoadingIndicator() - this.selectedWorks = workIds; + this.selectedWorks = workIds - await this.fetchSubjectsForWorks(workIds); - this.updateMenuOptions(); + await this.fetchSubjectsForWorks(workIds) + this.updateMenuOptions() - this.hideLoadingIndicator(); + this.hideLoadingIndicator() } /** - * Hides all menu options and shows a loading indicator. - */ + * Hides all menu options and shows a loading indicator. + */ showLoadingIndicator() { - const menuOptionContainer = this.rootElement.querySelector( - '.selection-container', - ); - menuOptionContainer.classList.add('hidden'); - const loadingIndicator = - this.rootElement.querySelector('.loading-indicator'); - loadingIndicator.classList.remove('hidden'); + const menuOptionContainer = this.rootElement.querySelector('.selection-container') + menuOptionContainer.classList.add('hidden') + const loadingIndicator = this.rootElement.querySelector('.loading-indicator') + loadingIndicator.classList.remove('hidden') } /** - * Hides the loading indicator and shows all menu options. - */ + * Hides the loading indicator and shows all menu options. + */ hideLoadingIndicator() { - const loadingIndicator = - this.rootElement.querySelector('.loading-indicator'); - loadingIndicator.classList.add('hidden'); - const menuOptionContainer = this.rootElement.querySelector( - '.selection-container', - ); - menuOptionContainer.classList.remove('hidden'); + const loadingIndicator = this.rootElement.querySelector('.loading-indicator') + loadingIndicator.classList.add('hidden') + const menuOptionContainer = this.rootElement.querySelector('.selection-container') + menuOptionContainer.classList.remove('hidden') } /** - * Fetches and stores subject information for the given work OLIDs. - * - * If we already have fetched the data for a work ID, we do not fetch it - * again. - * @param {Array<String>} workIds - */ + * Fetches and stores subject information for the given work OLIDs. + * + * If we already have fetched the data for a work ID, we do not fetch it + * again. + * @param {Array<String>} workIds + */ async fetchSubjectsForWorks(workIds) { - const worksWithMissingSubjects = workIds.filter( - (id) => !this.existingSubjects.has(id), - ); - - await Promise.all( - worksWithMissingSubjects.map(async (id) => { - // XXX : Too many network requests --- use bulk search if/when it is available - await this.fetchWork(id) + const worksWithMissingSubjects = workIds.filter(id => !this.existingSubjects.has(id)) + + await Promise.all(worksWithMissingSubjects.map(async (id) => { + // XXX : Too many network requests --- use bulk search if/when it is available + await this.fetchWork(id) // XXX : Handle failures - .then((response) => response.json()) - .then((data) => { - const entry = { - subjects: data.subjects || [], - subject_people: data.subject_people || [], - subject_places: data.subject_places || [], - subject_times: data.subject_times || [], - }; - // Move collection labels from `subjects` to `collections` - entry.collections = entry.subjects.filter((label) => - label.startsWith(COLLECTION_PREFIX), - ); - entry.subjects = entry.subjects.filter( - (label) => !entry.collections.includes(label), - ); - for (let i = 0; i < entry.collections.length; ++i) { - // Remove collection prefix from label - entry.collections[i] = entry.collections[i].substring( - COLLECTION_PREFIX.length, - ); - } - if (!this.existingSubjects.has(id)) { - this.existingSubjects.set(id, []); - } - // `key` is the type, `value` is the array of tag names - for (const [key, value] of Object.entries(entry)) { - for (const tagName of value) { - this.existingSubjects.get(id).push(new Tag(tagName, key)); - } + .then(response => response.json()) + .then(data => { + const entry = { + subjects: data.subjects || [], + subject_people: data.subject_people || [], + subject_places: data.subject_places || [], + subject_times: data.subject_times || [] + } + // Move collection labels from `subjects` to `collections` + entry.collections = entry.subjects.filter((label) => label.startsWith(COLLECTION_PREFIX)) + entry.subjects = entry.subjects.filter((label) => !entry.collections.includes(label)) + for (let i = 0; i < entry.collections.length; ++i) { + // Remove collection prefix from label + entry.collections[i] = entry.collections[i].substring(COLLECTION_PREFIX.length) + } + if (!this.existingSubjects.has(id)) { + this.existingSubjects.set(id, []) + } + // `key` is the type, `value` is the array of tag names + for (const [key, value] of Object.entries(entry)) { + for (const tagName of value) { + this.existingSubjects.get(id).push(new Tag(tagName, key)) } - }); - }), - ); + } + }) + })) } /** - * Creates `MenuOption` affordances for all staged tags, and each existing tag that - * was fetched from the server. - */ + * Creates `MenuOption` affordances for all staged tags, and each existing tag that + * was fetched from the server. + */ updateMenuOptions() { - this.selectedOptionsContainer.clear(); + this.selectedOptionsContainer.clear() // Add staged tags first, then add all other missing subjects. // This order prevents unnecessary state mangement steps. // Create menu options for each staged tag: this.tagsToAdd.forEach((tag) => { - const menuOption = new MenuOption( - tag, - MenuOptionState.ALL_TAGGED, - this.selectedWorks.length, - ); - menuOption.initialize(); - this.selectedOptionsContainer.add(menuOption); - }); + const menuOption = new MenuOption(tag, MenuOptionState.ALL_TAGGED, this.selectedWorks.length) + menuOption.initialize() + this.selectedOptionsContainer.add(menuOption) + }) this.tagsToRemove.forEach((tag) => { - const menuOption = new MenuOption(tag, MenuOptionState.NONE_TAGGED, 0); - menuOption.initialize(); - this.selectedOptionsContainer.add(menuOption); - }); + const menuOption = new MenuOption(tag, MenuOptionState.NONE_TAGGED, 0) + menuOption.initialize() + this.selectedOptionsContainer.add(menuOption) + }) // Create menu options for each existing tag: - const stagedMenuOptions = []; + const stagedMenuOptions = [] for (const workOlid of this.selectedWorks) { - const existingTagsForWork = this.existingSubjects.get(workOlid); + const existingTagsForWork = this.existingSubjects.get(workOlid) for (const tag of existingTagsForWork) { + // Does an option for this tag already exist in the container? if (!this.selectedOptionsContainer.containsOptionWithTag(tag)) { + // Have we already created and staged a menu option for this tag? - const stagedOption = stagedMenuOptions.find((option) => - option.tag.equals(tag), - ); + const stagedOption = stagedMenuOptions.find((option) => option.tag.equals(tag)) if (stagedOption) { - stagedOption.taggedWorksCount++; + stagedOption.taggedWorksCount++ if (stagedOption.taggedWorksCount === this.selectedWorks.length) { - stagedOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED); + stagedOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED) } } else { - const state = - this.selectedWorks.length === 1 - ? MenuOptionState.ALL_TAGGED - : MenuOptionState.SOME_TAGGED; - const newOption = new MenuOption(tag, state, 1); - newOption.initialize(); - stagedMenuOptions.push(newOption); + const state = this.selectedWorks.length === 1 ? MenuOptionState.ALL_TAGGED : MenuOptionState.SOME_TAGGED + const newOption = new MenuOption(tag, state, 1) + newOption.initialize() + stagedMenuOptions.push(newOption) } } } } - stagedMenuOptions.forEach((option) => - option.rootElement.addEventListener('click', () => - this.onMenuOptionClick(option), - ), - ); - this.selectedOptionsContainer.add(...stagedMenuOptions); + stagedMenuOptions.forEach((option) => option.rootElement.addEventListener('click', () => this.onMenuOptionClick(option))) + this.selectedOptionsContainer.add(...stagedMenuOptions) } /** - * Click handler for menu options. - * - * Changes the menu option's state, and stages the option's tag - * for addition or removal. - * - * @param {MenuOption} menuOption The clicked menu option - */ + * Click handler for menu options. + * + * Changes the menu option's state, and stages the option's tag + * for addition or removal. + * + * @param {MenuOption} menuOption The clicked menu option + */ onMenuOptionClick(menuOption) { - let stagedTagIndex; + let stagedTagIndex switch (menuOption.optionState) { case MenuOptionState.NONE_TAGGED: - stagedTagIndex = this.tagsToRemove.findIndex( - (tag) => - tag.tagName === menuOption.tag.tagName && - tag.tagType === menuOption.tag.tagType, - ); + stagedTagIndex = this.tagsToRemove.findIndex((tag) => (tag.tagName === menuOption.tag.tagName && tag.tagType === menuOption.tag.tagType)) if (stagedTagIndex > -1) { - this.tagsToRemove.splice(stagedTagIndex, 1); + this.tagsToRemove.splice(stagedTagIndex, 1) } - this.tagsToAdd.push(menuOption.tag); - menuOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED); - break; + this.tagsToAdd.push(menuOption.tag) + menuOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED) + break case MenuOptionState.SOME_TAGGED: - this.tagsToAdd.push(menuOption.tag); - menuOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED); - break; + this.tagsToAdd.push(menuOption.tag) + menuOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED) + break case MenuOptionState.ALL_TAGGED: - stagedTagIndex = this.tagsToAdd.findIndex( - (tag) => - tag.tagName === menuOption.tag.tagName && - tag.tagType === menuOption.tag.tagType, - ); + stagedTagIndex = this.tagsToAdd.findIndex((tag) => (tag.tagName === menuOption.tag.tagName && tag.tagType === menuOption.tag.tagType)) if (stagedTagIndex > -1) { - this.tagsToAdd.splice(stagedTagIndex, 1); + this.tagsToAdd.splice(stagedTagIndex, 1) } - this.tagsToRemove.push(menuOption.tag); - menuOption.updateMenuOptionState(MenuOptionState.NONE_TAGGED); - break; + this.tagsToRemove.push(menuOption.tag) + menuOption.updateMenuOptionState(MenuOptionState.NONE_TAGGED) + break } - menuOption.stage(); - this.updateSubmitButtonState(); + menuOption.stage() + this.updateSubmitButtonState() } /** - * Disables or enables form submission button. - * - * Button is enabled if there are any tags staged for submission. - * Otherwise, the button will be disabled. - */ + * Disables or enables form submission button. + * + * Button is enabled if there are any tags staged for submission. + * Otherwise, the button will be disabled. + */ updateSubmitButtonState() { - const stagedTagCount = this.tagsToAdd.length + this.tagsToRemove.length; + const stagedTagCount = this.tagsToAdd.length + this.tagsToRemove.length if (stagedTagCount > 0) { - this.submitButton.removeAttribute('disabled'); + this.submitButton.removeAttribute('disabled') } else { - this.submitButton.setAttribute('disabled', 'true'); + this.submitButton.setAttribute('disabled', 'true') } } /** - * Fetches a work from OL. - * - * @param {String} workOlid - */ + * Fetches a work from OL. + * + * @param {String} workOlid + */ async fetchWork(workOlid) { - return fetch(`/works/${workOlid}.json`); + return fetch(`/works/${workOlid}.json`) } /** - * Performs a subject search for the given search term, and updates - * the Bulk Tagger with the results. - * - * If the given search term, when trimmed, is an empty string, this - * instead hides the "create subject" affordance. - * - * @param {String} searchTerm - */ + * Performs a subject search for the given search term, and updates + * the Bulk Tagger with the results. + * + * If the given search term, when trimmed, is an empty string, this + * instead hides the "create subject" affordance. + * + * @param {String} searchTerm + */ onSearchInputChange(searchTerm) { - // Remove search results that are not selected: - const resultsToRemove = - this.searchResultsOptionsContainer.sortedMenuOptions.filter( - (option) => option.optionState !== MenuOptionState.ALL_TAGGED, - ); - this.searchResultsOptionsContainer.remove(...resultsToRemove); + // Remove search results that are not selected: + const resultsToRemove = this.searchResultsOptionsContainer.sortedMenuOptions.filter((option) => option.optionState !== MenuOptionState.ALL_TAGGED) + this.searchResultsOptionsContainer.remove(...resultsToRemove) // Hide menu options that do not begin with the search term (case-insensitive) - const trimmedSearchTerm = searchTerm.trim(); + const trimmedSearchTerm = searchTerm.trim() - const allOptions = this.selectedOptionsContainer.sortedMenuOptions.concat( - this.searchResultsOptionsContainer.sortedMenuOptions, - ); + const allOptions = this.selectedOptionsContainer.sortedMenuOptions.concat(this.searchResultsOptionsContainer.sortedMenuOptions) allOptions.forEach((option) => { - if ( - option.tag.tagName - .toLowerCase() - .startsWith(trimmedSearchTerm.toLowerCase()) - ) { - option.show(); + if (option.tag.tagName.toLowerCase().startsWith(trimmedSearchTerm.toLowerCase())) { + option.show() } else { - option.hide(); + option.hide() } - }); + }) - if (trimmedSearchTerm !== '') { - // Perform search: + if (trimmedSearchTerm !== '') { // Perform search: fetch(`/search/subjects.json?q=${searchTerm}&limit=${maxDisplayResults}`) .then((response) => response.json()) .then((data) => { if (data['docs'].length !== 0) { for (const obj of data['docs']) { - const tag = new Tag(obj.name, null, obj['subject_type']); - - if ( - !this.selectedOptionsContainer.containsOptionWithTag(tag) && - !this.searchResultsOptionsContainer.containsOptionWithTag(tag) - ) { - const menuOption = this.createSearchMenuOption(tag); - this.searchResultsOptionsContainer.add(menuOption); + const tag = new Tag(obj.name, null, obj['subject_type']) + + if (!this.selectedOptionsContainer.containsOptionWithTag(tag) && !this.searchResultsOptionsContainer.containsOptionWithTag(tag)) { + const menuOption = this.createSearchMenuOption(tag) + this.searchResultsOptionsContainer.add(menuOption) } } } // Update and show create subject affordance - this.updateAndShowNewSubjectAffordance(trimmedSearchTerm); + this.updateAndShowNewSubjectAffordance(trimmedSearchTerm) }); } else { // Hide create subject affordance - this.createSubjectElem.classList.add('hidden'); + this.createSubjectElem.classList.add('hidden') } } /** - * Updates the "create subject" affordance with the given subject name, - * and shows the affordance if it is hidden. - * - * @param {String} subjectName The name of the subject - */ + * Updates the "create subject" affordance with the given subject name, + * and shows the affordance if it is hidden. + * + * @param {String} subjectName The name of the subject + */ updateAndShowNewSubjectAffordance(subjectName) { - this.subjectNameElem.innerText = subjectName; - this.createSubjectElem.classList.remove('hidden'); + this.subjectNameElem.innerText = subjectName + this.createSubjectElem.classList.remove('hidden') } /** - * Creates, hydrates, and returns a new menu option based on a search result. - * - * In addition to the usual click listener, the newly created element will have an - * `option-hidden` event handler, which will move any selected menu option to the - * selected options container whenever the menu option is hidden. This is done to - * maintain the correct menu option ordering when search results are updated. - * - * Precondition: Menu option representing the given tag is not attached to the DOM. - * - * @param {Tag} tag - * @returns {MenuOption} A menu option representing the given tag - */ + * Creates, hydrates, and returns a new menu option based on a search result. + * + * In addition to the usual click listener, the newly created element will have an + * `option-hidden` event handler, which will move any selected menu option to the + * selected options container whenever the menu option is hidden. This is done to + * maintain the correct menu option ordering when search results are updated. + * + * Precondition: Menu option representing the given tag is not attached to the DOM. + * + * @param {Tag} tag + * @returns {MenuOption} A menu option representing the given tag + */ createSearchMenuOption(tag) { - const menuOption = new MenuOption(tag, MenuOptionState.NONE_TAGGED, 0); - menuOption.initialize(); - menuOption.rootElement.addEventListener('click', () => - this.onMenuOptionClick(menuOption), - ); + const menuOption = new MenuOption(tag, MenuOptionState.NONE_TAGGED, 0) + menuOption.initialize() + menuOption.rootElement.addEventListener('click', () => this.onMenuOptionClick(menuOption)) menuOption.rootElement.addEventListener('option-hidden', () => { // Move to selected menu options container if selected and hidden if (menuOption.optionState === MenuOptionState.ALL_TAGGED) { - if ( - menuOption.rootElement.parentElement === - this.searchResultsOptionsContainer.rootElement - ) { - this.searchResultsOptionsContainer.remove(menuOption); - this.selectedOptionsContainer.add(menuOption); + if (menuOption.rootElement.parentElement === this.searchResultsOptionsContainer.rootElement) { + this.searchResultsOptionsContainer.remove(menuOption) + this.selectedOptionsContainer.add(menuOption) } } - }); + }) - return menuOption; + return menuOption } /** - * Adds a menu option representing the given tag to the selected options container. - * - * If the container already has a menu option for the given tag, this method returns - * without making any changes. - * - * If a corresponding menu option is found in the search results container, that menu - * option is added to the selected options container. Otherwise, a new menu option is - * created, hydrated, and added to the container. - * - * @param {Tag} tag - */ + * Adds a menu option representing the given tag to the selected options container. + * + * If the container already has a menu option for the given tag, this method returns + * without making any changes. + * + * If a corresponding menu option is found in the search results container, that menu + * option is added to the selected options container. Otherwise, a new menu option is + * created, hydrated, and added to the container. + * + * @param {Tag} tag + */ onCreateTag(tag) { - // Return if menu option already exists in selected options: + // Return if menu option already exists in selected options: if (this.selectedOptionsContainer.containsOptionWithTag(tag)) { - return; + return } // Stage tag for addition: - this.tagsToAdd.push(tag); + this.tagsToAdd.push(tag) // If tag is represented by a search result object, update existing object // instead of creating a new one: - const existingOption = this.searchResultsOptionsContainer.findByTag(tag); + const existingOption = this.searchResultsOptionsContainer.findByTag(tag) if (existingOption) { - this.searchResultsOptionsContainer.remove(existingOption); - this.selectedOptionsContainer.add(existingOption); - existingOption.taggedWorksCount = this.selectedWorks.length; - existingOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED); + this.searchResultsOptionsContainer.remove(existingOption) + this.selectedOptionsContainer.add(existingOption) + existingOption.taggedWorksCount = this.selectedWorks.length + existingOption.updateMenuOptionState(MenuOptionState.ALL_TAGGED) } else { - const menuOption = new MenuOption( - tag, - MenuOptionState.ALL_TAGGED, - this.selectedWorks.length, - ); - menuOption.initialize(); - menuOption.rootElement.addEventListener('click', () => - this.onMenuOptionClick(menuOption), - ); - this.selectedOptionsContainer.add(menuOption); + const menuOption = new MenuOption(tag, MenuOptionState.ALL_TAGGED, this.selectedWorks.length) + menuOption.initialize() + menuOption.rootElement.addEventListener('click', () => this.onMenuOptionClick(menuOption)) + this.selectedOptionsContainer.add(menuOption) } - this.updateSubmitButtonState(); + this.updateSubmitButtonState() } /** - * Submits the bulk tagging form and updates the view. - */ + * Submits the bulk tagging form and updates the view. + */ submitBatch() { - // Disable button + + + // Disable button this.submitButton.disabled = true; this.submitButton.textContent = 'Submitting...'; - const url = this.rootElement.action; - this.prepareFormForSubmission(); - const formData = new FormData(this.rootElement); + const url = this.rootElement.action + this.prepareFormForSubmission() + const formData = new FormData(this.rootElement) if (this.isBookPageEdit) { - formData.append('book_page_edit', true); + formData.append('book_page_edit', true) } fetch(url, { method: 'post', - body: formData, - }).then((response) => { - if (!response.ok) { - this.submitButton.disabled = false; - this.submitButton.textContent = 'Submit'; - new FadingToast( - 'Batch subject update failed. Please try again in a few minutes.', - ).show(); - } else { - this.hideTaggingMenu(); - new FadingToast('Subjects successfully updated.').show(); - this.submitButton.textContent = 'Submit'; - this.updateFetchedSubjects(); - this.resetTaggingMenu(); - if (this.isBookPageEdit) { - window.ILE.clearAndReset(); - window.location.reload(); + body: formData + }) + .then(response => { + if (!response.ok) { + this.submitButton.disabled = false; + this.submitButton.textContent = 'Submit'; + new FadingToast('Batch subject update failed. Please try again in a few minutes.').show(); + } else { + this.hideTaggingMenu(); + new FadingToast('Subjects successfully updated.').show() + this.submitButton.textContent = 'Submit'; + this.updateFetchedSubjects(); + this.resetTaggingMenu(); + if (this.isBookPageEdit) { + window.ILE.clearAndReset() + window.location.reload() + } } - } - }); + }) } /** - * Populates the form's hidden inputs. - * - * Expected to be called just before the form is submitted. - */ + * Populates the form's hidden inputs. + * + * Expected to be called just before the form is submitted. + */ prepareFormForSubmission() { - this.selectedWorksInput.value = this.selectedWorks.join(','); + this.selectedWorksInput.value = this.selectedWorks.join(',') const addSubjectsValue = { subjects: this.findMatches(this.tagsToAdd, 'subjects'), subject_people: this.findMatches(this.tagsToAdd, 'subject_people'), subject_places: this.findMatches(this.tagsToAdd, 'subject_places'), - subject_times: this.findMatches(this.tagsToAdd, 'subject_times'), - }; - const collectionsToAdd = this.findMatches(this.tagsToAdd, 'collections'); - collectionsToAdd.forEach((label) => - addSubjectsValue.subjects.push(`${COLLECTION_PREFIX}${label}`), - ); - this.addSubjectsInput.value = JSON.stringify(addSubjectsValue); + subject_times: this.findMatches(this.tagsToAdd, 'subject_times') + } + const collectionsToAdd = this.findMatches(this.tagsToAdd, 'collections') + collectionsToAdd.forEach(label => addSubjectsValue.subjects.push(`${COLLECTION_PREFIX}${label}`)) + this.addSubjectsInput.value = JSON.stringify(addSubjectsValue) const removeSubjectsValue = { subjects: this.findMatches(this.tagsToRemove, 'subjects'), subject_people: this.findMatches(this.tagsToRemove, 'subject_people'), subject_places: this.findMatches(this.tagsToRemove, 'subject_places'), - subject_times: this.findMatches(this.tagsToRemove, 'subject_times'), - }; - const collectionsToRemove = this.findMatches( - this.tagsToRemove, - 'collections', - ); - collectionsToRemove.forEach((label) => - removeSubjectsValue.subjects.push(`${COLLECTION_PREFIX}${label}`), - ); - this.removeSubjectsInput.value = JSON.stringify(removeSubjectsValue); + subject_times: this.findMatches(this.tagsToRemove, 'subject_times') + } + const collectionsToRemove = this.findMatches(this.tagsToRemove, 'collections') + collectionsToRemove.forEach(label => removeSubjectsValue.subjects.push(`${COLLECTION_PREFIX}${label}`)) + this.removeSubjectsInput.value = JSON.stringify(removeSubjectsValue) } /** - * Filters tags that match the given type, and returns the names of - * each filtered tag. - * - * @param {Array<Tag>} tags Tags to be filtered - * @param {String} type Snake-cased tag type - * @returns {Array<String>} The names of the filtered tags - */ + * Filters tags that match the given type, and returns the names of + * each filtered tag. + * + * @param {Array<Tag>} tags Tags to be filtered + * @param {String} type Snake-cased tag type + * @returns {Array<String>} The names of the filtered tags + */ findMatches(tags, type) { - const results = []; + const results = [] tags.reduce((_acc, tag) => { if (tag.tagType === type) { - results.push(tag.tagName); + results.push(tag.tagName) } - }, []); - return results; + }, []) + return results } /** - * Updates the data structure which contains the fetched works' subjects. - * - * Meant to be called after the form has been submitted, but before the - * `resetTaggingMenu` call is made. - */ + * Updates the data structure which contains the fetched works' subjects. + * + * Meant to be called after the form has been submitted, but before the + * `resetTaggingMenu` call is made. + */ updateFetchedSubjects() { for (const tag of this.tagsToAdd) { this.existingSubjects.forEach((tags) => { - const tagExists = - tags.findIndex( - (t) => t.tagName === tag.tagName && t.tagType === tag.tagType, - ) > -1; + const tagExists = tags.findIndex((t) => t.tagName === tag.tagName && t.tagType === tag.tagType) > -1 if (!tagExists) { - tags.push(tag); + tags.push(tag) } - }); + }) } for (const tag of this.tagsToRemove) { this.existingSubjects.forEach((tags) => { - const tagIndex = tags.findIndex( - (t) => t.tagName === tag.tagName && t.tagType === tag.tagType, - ); - const tagExists = tagIndex > -1; + const tagIndex = tags.findIndex((t) => t.tagName === tag.tagName && t.tagType === tag.tagType) + const tagExists = tagIndex > -1 if (tagExists) { - tags.splice(tagIndex, 1); + tags.splice(tagIndex, 1) } - }); + }) } } /** - * Clears the bulk tagger form. - */ + * Clears the bulk tagger form. + */ resetTaggingMenu() { - this.searchInput.value = ''; - this.addSubjectsInput.value = ''; - this.removeSubjectsInput.value = ''; - this.selectedOptionsContainer.clear(); - this.searchResultsOptionsContainer.clear(); + this.searchInput.value = '' + this.addSubjectsInput.value = '' + this.removeSubjectsInput.value = '' + this.selectedOptionsContainer.clear() + this.searchResultsOptionsContainer.clear() - this.createSubjectElem.classList.add('hidden'); + this.createSubjectElem.classList.add('hidden') - this.tagsToAdd = []; - this.tagsToRemove = []; + this.tagsToAdd = [] + this.tagsToRemove = [] } } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js index a3635cf1a35..a2280150267 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js @@ -6,8 +6,8 @@ const classTypeSuffixes = { subject_people: '--person', subject_places: '--place', subject_times: '--time', - collections: '--collection', -}; + collections: '--collection' +} /** * @typedef OptionState @@ -25,175 +25,162 @@ export const MenuOptionState = { NONE_TAGGED: 0, SOME_TAGGED: 1, ALL_TAGGED: 2, -}; +} export class MenuOption { + /** - * Creates a new MenuOption that represents the given tag. - * - * `rootElement` of this object is not set until `initialize` is called. - * - * @param {Tag} tag - * @param {OptionState} optionState - * @param {Number} taggedWorksCount Number of selected works which have the given tag - */ - constructor (tag, optionState, taggedWorksCount) { - /** - * Reference to the root element of this MenuOption. + * Creates a new MenuOption that represents the given tag. * - * This is not set until `initialize` is called. - * @member {HTMLElement} - * @see {initialize} + * `rootElement` of this object is not set until `initialize` is called. + * + * @param {Tag} tag + * @param {OptionState} optionState + * @param {Number} taggedWorksCount Number of selected works which have the given tag */ - this.rootElement; + constructor(tag, optionState, taggedWorksCount) { + /** + * Reference to the root element of this MenuOption. + * + * This is not set until `initialize` is called. + * @member {HTMLElement} + * @see {initialize} + */ + this.rootElement /** - * Copy of the tag which is represented by this menu option. - * - * @member {Tag} - * @readonly - */ - this.tag = tag; + * Copy of the tag which is represented by this menu option. + * + * @member {Tag} + * @readonly + */ + this.tag = tag /** - * Represents the amount of selected works that share this tag. - * - * Not meant to be updated directly. Use `updateMenuOptionState()`, - * which also updates the UI, to set this value. - * - * @member {OptionState} - */ - this.optionState = optionState; + * Represents the amount of selected works that share this tag. + * + * Not meant to be updated directly. Use `updateMenuOptionState()`, + * which also updates the UI, to set this value. + * + * @member {OptionState} + */ + this.optionState = optionState /** - * Tracks number of selected works which have this tag. - * - * @member {Number} - */ - this.taggedWorksCount = taggedWorksCount; + * Tracks number of selected works which have this tag. + * + * @member {Number} + */ + this.taggedWorksCount = taggedWorksCount } /** - * Creates a new menu option. - * - * Must be called before an event handler can be attached to - * this menu option - */ - initialize () { - this.createMenuOption(); + * Creates a new menu option. + * + * Must be called before an event handler can be attached to + * this menu option + */ + initialize() { + this.createMenuOption() } /** - * Creates a new menu option affordance based on the current menu option state. - * - * Stores newly created element as `rootElement`. The new element is not - * attached to the DOM, and does not yet have any attached event handlers. - */ - createMenuOption () { - const parentElem = document.createElement('div'); - parentElem.classList.add('selected-tag'); - - let bemSuffix = ''; + * Creates a new menu option affordance based on the current menu option state. + * + * Stores newly created element as `rootElement`. The new element is not + * attached to the DOM, and does not yet have any attached event handlers. + */ + createMenuOption() { + const parentElem = document.createElement('div') + parentElem.classList.add('selected-tag') + + let bemSuffix = '' switch (this.optionState) { case MenuOptionState.NONE_TAGGED: - bemSuffix = 'none-tagged'; - break; + bemSuffix = 'none-tagged' + break case MenuOptionState.SOME_TAGGED: - bemSuffix = 'some-tagged'; - break; + bemSuffix = 'some-tagged' + break case MenuOptionState.ALL_TAGGED: - bemSuffix = 'all-tagged'; - break; + bemSuffix = 'all-tagged' + break } const markup = `<span class="selected-tag__status selected-tag__status--${bemSuffix}"></span> <span class="selected-tag__name">${this.tag.tagName}</span> <span class="selected-tag__type-container"> <span class="selected-tag__type selected-tag__type${classTypeSuffixes[this.tag.tagType]}">${this.tag.displayType}</span> - </span>`; + </span>` - parentElem.innerHTML = markup; - this.rootElement = parentElem; + parentElem.innerHTML = markup + this.rootElement = parentElem } /** - * Removes this MenuOption from the DOM. - */ - remove () { - this.rootElement.remove(); + * Removes this MenuOption from the DOM. + */ + remove() { + this.rootElement.remove() } /** - * Sets the value of `optionState` and updates the view. - * - * @param {OptionState} menuOptionState - * - * @throws Will throw an error if an unexpected menu option state is passed, or if this - * `MenuOption` was not initialized prior to calling this method. - * @see {@link MenuOptionState} - * @see {initialize} - */ - updateMenuOptionState (menuOptionState) { - if (this.rootElement) { - // `rootElement` not set until `initialize` is called - this.optionState = menuOptionState; - const statusIndicator = this.rootElement.querySelector( - '.selected-tag__status', - ); + * Sets the value of `optionState` and updates the view. + * + * @param {OptionState} menuOptionState + * + * @throws Will throw an error if an unexpected menu option state is passed, or if this + * `MenuOption` was not initialized prior to calling this method. + * @see {@link MenuOptionState} + * @see {initialize} + */ + updateMenuOptionState(menuOptionState) { + if (this.rootElement) { // `rootElement` not set until `initialize` is called + this.optionState = menuOptionState + const statusIndicator = this.rootElement.querySelector('.selected-tag__status') switch (menuOptionState) { case MenuOptionState.NONE_TAGGED: - statusIndicator.classList.remove( - 'selected-tag__status--all-tagged', - 'selected-tag__status--some-tagged', - ); - statusIndicator.classList.add('selected-tag__status--none-tagged'); + statusIndicator.classList.remove('selected-tag__status--all-tagged', 'selected-tag__status--some-tagged') + statusIndicator.classList.add('selected-tag__status--none-tagged') break; case MenuOptionState.SOME_TAGGED: - statusIndicator.classList.remove( - 'selected-tag__status--all-tagged', - 'selected-tag__status--none-tagged', - ); - statusIndicator.classList.add('selected-tag__status--some-tagged'); + statusIndicator.classList.remove('selected-tag__status--all-tagged', 'selected-tag__status--none-tagged') + statusIndicator.classList.add('selected-tag__status--some-tagged') break; case MenuOptionState.ALL_TAGGED: - statusIndicator.classList.remove( - 'selected-tag__status--none-tagged', - 'selected-tag__status--some-tagged', - ); - statusIndicator.classList.add('selected-tag__status--all-tagged'); + statusIndicator.classList.remove('selected-tag__status--none-tagged', 'selected-tag__status--some-tagged') + statusIndicator.classList.add('selected-tag__status--all-tagged') break; default: // XXX : `optionState` is now incorrect - throw new Error('Unexpected value passed for menu option state.'); + throw new Error('Unexpected value passed for menu option state.') } } else { - throw new Error( - 'MenuOption must be initialized before state can be updated.', - ); + throw new Error('MenuOption must be initialized before state can be updated.') } } /** - * Hides this menu option. - * - * Fires an `option-hidden` event when this is called. - */ - hide () { - this.rootElement.classList.add('hidden'); - this.rootElement.dispatchEvent(new CustomEvent('option-hidden')); + * Hides this menu option. + * + * Fires an `option-hidden` event when this is called. + */ + hide() { + this.rootElement.classList.add('hidden') + this.rootElement.dispatchEvent(new CustomEvent('option-hidden')) } /** - * Shows this menu option. - */ - show () { - this.rootElement.classList.remove('hidden'); + * Shows this menu option. + */ + show() { + this.rootElement.classList.remove('hidden') } /** - * Stages the selected menu option. - */ - stage () { + * Stages the selected menu option. + */ + stage() { this.rootElement.classList.add('selected-tag--staged'); } } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js index 302cf0ed3d8..22ebcb29908 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js @@ -6,139 +6,130 @@ * to the DOM in the correct order, based on tag name and type. */ export class SortedMenuOptionContainer { + /** - * Creates a new sorted menu options container, with the given - * element as the root element. - * - * This container is meant to exclusively hold bulk tagger menu - * options. Adding other elements as direct descendents of this - * container will result bugs during insertion and deletion of - * menu options. - * - * @param {HTMLElement} element The container - */ - constructor (element) { - this.rootElement = element; - this.sortedMenuOptions = []; + * Creates a new sorted menu options container, with the given + * element as the root element. + * + * This container is meant to exclusively hold bulk tagger menu + * options. Adding other elements as direct descendents of this + * container will result bugs during insertion and deletion of + * menu options. + * + * @param {HTMLElement} element The container + */ + constructor(element) { + this.rootElement = element + this.sortedMenuOptions = [] } /** - * Attaches the given menu options to this container, in order. - * - * @param {...MenuOption} menuOptions Menu options to be added to the container. - */ - add (...menuOptions) { + * Attaches the given menu options to this container, in order. + * + * @param {...MenuOption} menuOptions Menu options to be added to the container. + */ + add(...menuOptions) { for (const option of menuOptions) { - const index = this.findIndex(option); - this.sortedMenuOptions.splice(index, 0, option); - this.updateViewOnAdd(option, index); + const index = this.findIndex(option) + this.sortedMenuOptions.splice(index, 0, option) + this.updateViewOnAdd(option, index) } } /** - * Adds the given menu option to this container at the given index. - * - * @param {MenuOption} menuOption The option being attached to the DOM. - * @param {Number} index The index where the given option will be inserted. - */ - updateViewOnAdd (menuOption, index) { + * Adds the given menu option to this container at the given index. + * + * @param {MenuOption} menuOption The option being attached to the DOM. + * @param {Number} index The index where the given option will be inserted. + */ + updateViewOnAdd(menuOption, index) { if (index === 0) { - this.rootElement.prepend(menuOption.rootElement); + this.rootElement.prepend(menuOption.rootElement) } else { - const sibling = this.rootElement.children[index - 1]; - sibling.insertAdjacentElement('afterend', menuOption.rootElement); + const sibling = this.rootElement.children[index - 1] + sibling.insertAdjacentElement('afterend', menuOption.rootElement) } } /** - * Removes the given menu options from this container. - * - * @param {...MenuOption} menuOptions Options that are to be removed from this container - */ - remove (...menuOptions) { + * Removes the given menu options from this container. + * + * @param {...MenuOption} menuOptions Options that are to be removed from this container + */ + remove(...menuOptions) { for (const option of menuOptions) { - const index = this.findIndex(option); - const removed = this.sortedMenuOptions.splice(index, 1); - removed.forEach((option) => option.remove()); + const index = this.findIndex(option) + const removed = this.sortedMenuOptions.splice(index, 1) + removed.forEach((option) => option.remove()) } } /** - * Finds the correct index to insert the given menu option, such that - * the array is alphabetically ordered (case-insensitive). - * - * @param {MenuOption} menuOption - * @returns {Number} Index where the given menu option should be inserted. - */ - findIndex (menuOption) { - let index = 0; + * Finds the correct index to insert the given menu option, such that + * the array is alphabetically ordered (case-insensitive). + * + * @param {MenuOption} menuOption + * @returns {Number} Index where the given menu option should be inserted. + */ + findIndex(menuOption) { + let index = 0 // XXX : Binary search? while (index < this.sortedMenuOptions.length) { - const currentMenuOption = this.sortedMenuOptions[index]; + const currentMenuOption = this.sortedMenuOptions[index] - if ( - currentMenuOption.tag.tagName.toLowerCase() === - menuOption.tag.tagName.toLowerCase() - ) { + if (currentMenuOption.tag.tagName.toLowerCase() === menuOption.tag.tagName.toLowerCase()) { // Compare types - if ( - currentMenuOption.tag.tagType.toLowerCase() >= - menuOption.tag.tagType.toLowerCase() - ) { - return index; + if (currentMenuOption.tag.tagType.toLowerCase() >= menuOption.tag.tagType.toLowerCase()) { + return index } - } else if ( - currentMenuOption.tag.tagName.toLowerCase() > - menuOption.tag.tagName.toLowerCase() - ) { - return index; } - ++index; + else if (currentMenuOption.tag.tagName.toLowerCase() > menuOption.tag.tagName.toLowerCase()) { + return index + } + ++index } - return index; + return index } /** - * Checks if the given menu option is in this container. - * - * @param {MenuOption} menuOption The object that we are searching for - * @returns {boolean} `true` if a matching menu option exists in this container - */ - contains (menuOption) { - return this.sortedMenuOptions.some((option) => - menuOption.tag.equals(option.tag), - ); + * Checks if the given menu option is in this container. + * + * @param {MenuOption} menuOption The object that we are searching for + * @returns {boolean} `true` if a matching menu option exists in this container + */ + contains(menuOption) { + return this.sortedMenuOptions.some((option) => menuOption.tag.equals(option.tag)) } /** - * Checks if a menu option which represents the given tag is in this container. - * - * @param {Tag} tag - * @returns {boolean} `true` if a menu option which represents the given tag is in this container. - */ - containsOptionWithTag (tag) { - return this.sortedMenuOptions.some((option) => tag.equals(option.tag)); + * Checks if a menu option which represents the given tag is in this container. + * + * @param {Tag} tag + * @returns {boolean} `true` if a menu option which represents the given tag is in this container. + */ + containsOptionWithTag(tag) { + return this.sortedMenuOptions.some((option) => tag.equals(option.tag)) } /** - * Returns the first menu option found which represents the given tag, or `undefined` if none were found. - * - * @param {Tag} tag - * @returns {MenuOption|undefined} The first matching menu option, or `undefined` if none were found. - */ - findByTag (tag) { - return this.sortedMenuOptions.find((option) => tag.equals(option.tag)); + * Returns the first menu option found which represents the given tag, or `undefined` if none were found. + * + * @param {Tag} tag + * @returns {MenuOption|undefined} The first matching menu option, or `undefined` if none were found. + */ + findByTag(tag) { + return this.sortedMenuOptions.find((option) => tag.equals(option.tag)) } /** - * Removes all menu options from this container. - */ - clear () { + * Removes all menu options from this container. + */ + clear() { while (this.sortedMenuOptions.length > 0) { - this.sortedMenuOptions.pop(); + this.sortedMenuOptions.pop() } - this.rootElement.innerHTML = ''; + this.rootElement.innerHTML = '' } } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js index bdffeaf38f8..6da43278fd3 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js @@ -3,7 +3,7 @@ * * @returns HTML for the bulk tagging form */ -export function renderBulkTagger () { +export function renderBulkTagger() { return `<form action="/tags/bulk_tag_works" method="post" class="bulk-tagging-form hidden"> <div class="form-header"> <p>Manage Subjects</p> @@ -36,5 +36,5 @@ export function renderBulkTagger () { <div class="submit-tags-section"> <button type="submit" class="bulk-tagging-submit cta-btn cta-btn--primary" disabled>Submit</button> </div> - </form>`; + </form>` } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js index 397155236d3..aa3bb1887b3 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js @@ -8,7 +8,7 @@ const displayTypeMapping = { subject_places: 'place', subject_times: 'time', collections: 'collection', -}; +} /** * Maps UI-ready subject types to their corresponding @@ -19,8 +19,8 @@ export const subjectTypeMapping = { person: 'subject_people', place: 'subject_places', time: 'subject_times', - collection: 'collections', -}; + collection: 'collections' +} /** * Compare function for determining the order of two tags. @@ -33,23 +33,25 @@ export const subjectTypeMapping = { * @returns {Number} * @see {Array.sort} */ -export function compare (tagA, tagB) { - const lowerA = createComparableTag(tagA); - const lowerB = createComparableTag(tagB); +export function compare(tagA, tagB) { + const lowerA = createComparableTag(tagA) + const lowerB = createComparableTag(tagB) if (lowerA.tagName < lowerB.tagName) { - return -1; - } else if (lowerA.tagName > lowerB.tagName) { - return 1; + return -1 + } + else if (lowerA.tagName > lowerB.tagName) { + return 1 } else { if (lowerA.tagType < lowerB.tagType) { - return -1; - } else if (lowerA.tagType > lowerB.tagType) { - return 1; + return -1 + } + else if (lowerA.tagType > lowerB.tagtype) { + return 1 } } - return 0; + return 0 } /** @@ -62,11 +64,11 @@ export function compare (tagA, tagB) { * @returns {Object} Tag-like object that is suitable to use for sorting comparisons. * @see {compare} */ -function createComparableTag (tag) { +function createComparableTag(tag) { return { tagName: tag.tagName.toLowerCase(), - tagType: tag.tagType.toLowerCase(), - }; + tagType: tag.tagType.toLowerCase() + } } /** @@ -77,74 +79,71 @@ function createComparableTag (tag) { */ export class Tag { /** - * Creates a new Tag object. - * - * If only one tag type is passed to the constructor, the missing - * tag type will be inferred and set. - * - * @param {String} tagName The name of the Tag - * @param {String} tagType This tag's technical type - * @param {String} displayType This tag's type, in UI-ready form. - * - * @throws Will throw an error if both `tagType` and `displayType` are falsey - */ - constructor (tagName, tagType = null, displayType = null) { + * Creates a new Tag object. + * + * If only one tag type is passed to the constructor, the missing + * tag type will be inferred and set. + * + * @param {String} tagName The name of the Tag + * @param {String} tagType This tag's technical type + * @param {String} displayType This tag's type, in UI-ready form. + * + * @throws Will throw an error if both `tagType` and `displayType` are falsey + */ + constructor(tagName, tagType = null, displayType = null) { if (!(tagType || displayType)) { - throw new Error('Tag must have at least one type'); + throw new Error('Tag must have at least one type') } - this.tagName = tagName; - this.tagType = tagType || this.convertToType(displayType); - this.displayType = displayType || this.convertToDisplayType(tagType); + this.tagName = tagName + this.tagType = tagType || this.convertToType(displayType) + this.displayType = displayType || this.convertToDisplayType(tagType) } /** - * Returns the technical tag type corresponding to the given - * UI-ready type string. - * - * @param {String} displayType A UI-ready type string - * @returns {String} The corresponding technical tag type - * @throws Will throw an error if the given type is unrecognized. - */ - convertToType (displayType) { - const result = subjectTypeMapping[displayType]; + * Returns the technical tag type corresponding to the given + * UI-ready type string. + * + * @param {String} displayType A UI-ready type string + * @returns {String} The corresponding technical tag type + * @throws Will throw an error if the given type is unrecognized. + */ + convertToType(displayType) { + const result = subjectTypeMapping[displayType] if (!result) { - throw new Error('Unrecognized `displayType` value'); + throw new Error('Unrecognized `displayType` value') } - return result; + return result } /** - * Given a technical tag type, returns a type string that can be - * displayed in the UI. - * - * @param {String} tagType The technical tag type - * @returns {String} A type string that can be displayed in the UI - * @throws Will throw an error if the given type is unrecognized - */ - convertToDisplayType (tagType) { - const result = displayTypeMapping[tagType]; + * Given a technical tag type, returns a type string that can be + * displayed in the UI. + * + * @param {String} tagType The technical tag type + * @returns {String} A type string that can be displayed in the UI + * @throws Will throw an error if the given type is unrecognized + */ + convertToDisplayType(tagType) { + const result = displayTypeMapping[tagType] if (!result) { - throw new Error('Unrecognized `tagType` value'); + throw new Error('Unrecognized `tagType` value') } - return result; + return result } /** - * Determins if the given tag is equal to this tag. - * - * Two tags are considered equal if case-insensitive comparisons of - * their names and types are equivalent. - * - * @param {Tag} tag - * @returns `true` if the given tag is considered equivalent to this tag. - */ - equals (tag) { - const lowerSelf = createComparableTag(this); - const lowerTag = createComparableTag(tag); + * Determins if the given tag is equal to this tag. + * + * Two tags are considered equal if case-insensitive comparisons of + * their names and types are equivalent. + * + * @param {Tag} tag + * @returns `true` if the given tag is considered equivalent to this tag. + */ + equals(tag) { + const lowerSelf = createComparableTag(this) + const lowerTag = createComparableTag(tag) - return ( - lowerSelf.tagName === lowerTag.tagName && - lowerSelf.tagType === lowerTag.tagType - ); + return lowerSelf.tagName === lowerTag.tagName && lowerSelf.tagType === lowerTag.tagType } } diff --git a/openlibrary/plugins/openlibrary/js/carousel/Carousel.js b/openlibrary/plugins/openlibrary/js/carousel/Carousel.js index 965017da55b..6c959b80621 100644 --- a/openlibrary/plugins/openlibrary/js/carousel/Carousel.js +++ b/openlibrary/plugins/openlibrary/js/carousel/Carousel.js @@ -1,7 +1,7 @@ // Slick#1.6.0 is not on npm import 'slick-carousel'; import '../../../../../static/css/components/carousel--js.css'; -import { buildPartialsUrl } from '../utils.js'; +import { buildPartialsUrl } from '../utils.js'; /** * @typedef {Object} CarouselConfig @@ -23,17 +23,17 @@ import { buildPartialsUrl } from '../utils.js'; // used in templates/covers/add.html export class Carousel { /** - * @param {jQuery} $container - */ + * @param {jQuery} $container + */ constructor($container) { - /** @type {CarouselConfig} */ + /** @type {CarouselConfig} */ this.config = Object.assign( { booksPerBreakpoint: [6, 5, 4, 3, 2, 1], analyticsCategory: 'Carousel', carouselKey: '', }, - JSON.parse($container.attr('data-config')), + JSON.parse($container.attr('data-config')) ); /** @type {CarouselConfig['loadMore']} */ @@ -45,16 +45,14 @@ export class Carousel { allDone: false, page: 1, }, - this.config.loadMore || {}, + this.config.loadMore || {} ); /** @type {jquery} */ this.$container = $container; //This loads in i18n strings from a hidden input element, generated in the books/custom_carousel.html template. - const i18nInput = document.querySelector( - 'input[name="carousel-i18n-strings"]', - ); + const i18nInput = document.querySelector('input[name="carousel-i18n-strings"]') if (i18nInput) { this.i18n = JSON.parse(i18nInput.value); } @@ -70,14 +68,15 @@ export class Carousel { speed: 300, slidesToShow: this.config.booksPerBreakpoint[0], slidesToScroll: this.config.booksPerBreakpoint[0], - responsive: [1200, 1024, 600, 480, 360].map((breakpoint, i) => ({ - breakpoint: breakpoint, - settings: { - slidesToShow: this.config.booksPerBreakpoint[i + 1], - slidesToScroll: this.config.booksPerBreakpoint[i + 1], - infinite: false, - }, - })), + responsive: [1200, 1024, 600, 480, 360] + .map((breakpoint, i) => ({ + breakpoint: breakpoint, + settings: { + slidesToShow: this.config.booksPerBreakpoint[i + 1], + slidesToScroll: this.config.booksPerBreakpoint[i + 1], + infinite: false, + } + })) }); // Slick internally changes the click handlers on the next/prev buttons, @@ -109,18 +108,16 @@ export class Carousel { // Bind an action listener to this carousel on resize or advance this.$container.on('afterChange', (_ev, _slick, curSlide) => { const totalSlides = this.slick.$slides.length; - const numActiveSlides = - this.slick.$slides.filter('.slick-active').length; + const numActiveSlides = this.slick.$slides.filter('.slick-active').length; // this allows us to pre-load before hitting last page - const needsMoreCards = totalSlides - curSlide <= numActiveSlides * 2; + const needsMoreCards = totalSlides - curSlide <= (numActiveSlides * 2); if (!loadMore.locked && !loadMore.allDone && needsMoreCards) { loadMore.locked = true; // lock for critical section if (loadMore.pageMode === 'page') { loadMore.page++; - } else { - // i.e. offset, start from last slide + } else { // i.e. offset, start from last slide loadMore.page = totalSlides; } @@ -129,9 +126,7 @@ export class Carousel { }); document.addEventListener('filter', (ev) => { - loadMore.extraParams = { - published_in: `${ev.detail.yearFrom}-${ev.detail.yearTo}`, - }; + loadMore.extraParams = {published_in: `${ev.detail.yearFrom}-${ev.detail.yearTo}`}; // Reset the page count - the result set is now 'new' if (loadMore.pageMode === 'page') { @@ -148,7 +143,7 @@ export class Carousel { } fetchPartials() { - const loadMore = this.loadMore; + const loadMore = this.loadMore const url = buildPartialsUrl('CarouselLoadMore', { queryType: loadMore.queryType, q: loadMore.q, @@ -160,19 +155,20 @@ export class Carousel { hasFulltextOnly: loadMore.hasFulltextOnly, secondaryAction: loadMore.secondaryAction, key: loadMore.key, - ...loadMore.extraParams, + ...loadMore.extraParams }); this.appendLoadingSlide(); - $.ajax({ url: url, type: 'GET' }).then((results) => { - this.removeLoadingSlide(); - const cards = results.partials || []; - cards.forEach((card) => this.slick.addSlide(card)); - - if (!cards.length) { - loadMore.allDone = true; - } - loadMore.locked = false; - }); + $.ajax({url: url, type: 'GET'}) + .then((results) => { + this.removeLoadingSlide(); + const cards = results.partials || [] + cards.forEach(card => this.slick.addSlide(card)) + + if (!cards.length) { + loadMore.allDone = true; + } + loadMore.locked = false; + }) } clearCarousel() { @@ -180,9 +176,7 @@ export class Carousel { } appendLoadingSlide() { - this.slick.addSlide( - `<div class="carousel__item carousel__loading-end">${this.i18n['loading']}</div>`, - ); + this.slick.addSlide(`<div class="carousel__item carousel__loading-end">${this.i18n['loading']}</div>`); } removeLoadingSlide() { diff --git a/openlibrary/plugins/openlibrary/js/carousel/index.js b/openlibrary/plugins/openlibrary/js/carousel/index.js index 16ccfbbfb7d..76f54623338 100644 --- a/openlibrary/plugins/openlibrary/js/carousel/index.js +++ b/openlibrary/plugins/openlibrary/js/carousel/index.js @@ -1,14 +1,14 @@ -import { Carousel } from './Carousel'; +import {Carousel} from './Carousel'; export function initialzeCarousels(elems) { - elems.forEach((elem) => { - new Carousel($(elem)).init(); - const elemSlides = elem.querySelectorAll('.slick-slide'); - elemSlides.forEach((slide) => { - const $slide = $(slide); + elems.forEach(elem => { + new Carousel($(elem)).init() + const elemSlides = elem.querySelectorAll('.slick-slide') + elemSlides.forEach(slide => { + const $slide = $(slide) if ($slide.attr('aria-describedby') !== undefined) { - $slide.attr('id', $slide.attr('aria-describedby')); + $slide.attr('id',$(this).attr('aria-describedby')); } - }); - }); + }) + }) } diff --git a/openlibrary/plugins/openlibrary/js/clampers.js b/openlibrary/plugins/openlibrary/js/clampers.js index 8e8434c056a..26e1faadd65 100644 --- a/openlibrary/plugins/openlibrary/js/clampers.js +++ b/openlibrary/plugins/openlibrary/js/clampers.js @@ -2,11 +2,12 @@ * @param {NodeListOf<Element>} clampers * */ -export function initClampers (clampers) { +export function initClampers(clampers) { for (const clamper of clampers) { if (clamper.clientHeight === clamper.scrollHeight) { clamper.classList.remove('clamp'); } else { + /* Clamper used to collapse category list by toggling `hidden` style on parent element @@ -17,18 +18,14 @@ export function initClampers (clampers) { return; } - clamper.style.display = - clamper.style.display === '-webkit-box' || - clamper.style.display === '' - ? 'unset' - : '-webkit-box'; + clamper.style.display = clamper.style.display === '-webkit-box' || clamper.style.display === '' ? 'unset' : '-webkit-box' if (clamper.getAttribute('data-before') === '\u25BE ') { - clamper.setAttribute('data-before', '\u25B8 '); + clamper.setAttribute('data-before', '\u25B8 ') } else { - clamper.setAttribute('data-before', '\u25BE '); + clamper.setAttribute('data-before', '\u25BE ') } - }); + }) } } } diff --git a/openlibrary/plugins/openlibrary/js/compact-title/index.js b/openlibrary/plugins/openlibrary/js/compact-title/index.js index 99da228c803..cbf8d1b7e0b 100644 --- a/openlibrary/plugins/openlibrary/js/compact-title/index.js +++ b/openlibrary/plugins/openlibrary/js/compact-title/index.js @@ -25,15 +25,13 @@ let mainTitleElem; * @param {HTMLElement} navbar The book page navbar component * @param {HTMLElement} title The compact title component */ -export function initCompactTitle (navbar, title) { - mainTitleElem = document.querySelector( - '.work-title-and-author.desktop .work-title', - ); +export function initCompactTitle(navbar, title) { + mainTitleElem = document.querySelector('.work-title-and-author.desktop .work-title') // Show compact title on page reload: onScroll(navbar, title); // And update on scroll - window.addEventListener('scroll', () => { - onScroll(navbar, title); + window.addEventListener('scroll', function() { + onScroll(navbar, title) }); } @@ -46,43 +44,37 @@ export function initCompactTitle (navbar, title) { * @param {HTMLElement} navbar The book page navbar component * @param {HTMLElement} title The compact title component */ -function onScroll (navbar, title) { - const compactTitleBounds = title.getBoundingClientRect(); - const navbarBounds = navbar.getBoundingClientRect(); - const mainTitleBounds = mainTitleElem.getBoundingClientRect(); - if (mainTitleBounds.bottom < navbarBounds.bottom) { - // The main title is off-screen - if (!navbar.classList.contains('sticky--lowest')) { - // Compact title not displayed +function onScroll(navbar, title) { + const compactTitleBounds = title.getBoundingClientRect() + const navbarBounds = navbar.getBoundingClientRect() + const mainTitleBounds = mainTitleElem.getBoundingClientRect() + if (mainTitleBounds.bottom < navbarBounds.bottom) { // The main title is off-screen + if (!navbar.classList.contains('sticky--lowest')) { // Compact title not displayed // Display compact title - title.classList.remove('hidden'); + title.classList.remove('hidden') // Animate navbar - $(navbar) - .addClass('nav-bar-wrapper--slidedown') + $(navbar).addClass('nav-bar-wrapper--slidedown') .one('animationend', () => { - $(navbar).addClass('sticky--lowest'); - $(navbar).removeClass('nav-bar-wrapper--slidedown'); + $(navbar).addClass('sticky--lowest') + $(navbar).removeClass('nav-bar-wrapper--slidedown') // Ensure correct nav item is selected after compact title slides in: - updateSelectedNavItem(); - }); + updateSelectedNavItem() + }) } else { - if (navbarBounds.top < compactTitleBounds.bottom) { - // We've scrolled to the bottom of the container, and the navbar is unstuck - title.classList.add('hidden'); + if (navbarBounds.top < compactTitleBounds.bottom) { // We've scrolled to the bottom of the container, and the navbar is unstuck + title.classList.add('hidden') } else { - title.classList.remove('hidden'); + title.classList.remove('hidden') } } - } else { - // At least some of the main title is below the navbar + } else { // At least some of the main title is below the navbar if (!title.classList.contains('hidden')) { - title.classList.add('hidden'); - $(navbar) - .addClass('nav-bar-wrapper--slideup') + title.classList.add('hidden') + $(navbar).addClass('nav-bar-wrapper--slideup') .one('animationend', () => { - $(navbar).removeClass('sticky--lowest'); - $(navbar).removeClass('nav-bar-wrapper--slideup'); - }); + $(navbar).removeClass('sticky--lowest') + $(navbar).removeClass('nav-bar-wrapper--slideup') + }) } } } diff --git a/openlibrary/plugins/openlibrary/js/covers.js b/openlibrary/plugins/openlibrary/js/covers.js index 86e81cf25ea..c208baa4bfc 100644 --- a/openlibrary/plugins/openlibrary/js/covers.js +++ b/openlibrary/plugins/openlibrary/js/covers.js @@ -8,7 +8,7 @@ import 'jquery-ui-touch-punch'; // this makes drag-to-reorder work on touch devi import { closePopup } from './utils'; //cover/change.html -export function initCoversChange () { +export function initCoversChange() { // Pull data from data-config of class "manageCovers" in covers/manage.html const data_config_json = $('.manageCovers').data('config'); const doc_type_key = data_config_json['key']; @@ -18,58 +18,54 @@ export function initCoversChange () { // Add iframes lazily when the popup is loaded. // This avoids fetching the iframes along with main page. $('.coverPop') - .on('click', () => { + .on('click', function () { // clear the content of #imagesAdd and #imagesManage before adding new $('.imagesAdd').html(''); $('.imagesManage').html(''); if (doc_type_key === '/type/work') { - $('.imagesAdd').prepend( - '<div class="throbber"><h3>$_("Searching for covers")</h3></div>', - ); + $('.imagesAdd').prepend('<div class="throbber"><h3>$_("Searching for covers")</h3></div>'); } - setTimeout(() => { + setTimeout(function () { // add iframe to add images add_iframe('.imagesAdd', add_url); // add iframe to manage images add_iframe('.imagesManage', manage_url); }, 0); }) - .on('cbox_cleanup', () => { + .on('cbox_cleanup', function () { $('.imagesAdd').html(''); $('.imagesManage').html(''); }); } -function add_iframe (selector, src) { +function add_iframe(selector, src) { $(selector) - .append( - '<iframe frameborder="0" height="580" width="580" marginheight="0" marginwidth="0" scrolling="auto"></iframe>', - ) + .append('<iframe frameborder="0" height="580" width="580" marginheight="0" marginwidth="0" scrolling="auto"></iframe>') .find('iframe') .attr('src', src); } -function showLoadingIndicator () { +function showLoadingIndicator() { const loadingIndicator = document.querySelector('.loadingIndicator'); const formDivs = document.querySelectorAll('.ol-cover-form, .imageIntro'); if (loadingIndicator) { loadingIndicator.classList.remove('hidden'); - formDivs.forEach((div) => div.classList.add('hidden')); + formDivs.forEach(div => div.classList.add('hidden')); } } // covers/manage.html and covers/add.html -export function initCoversAddManage () { - $('.ol-cover-form').on('submit', () => { +export function initCoversAddManage() { + $('.ol-cover-form').on('submit', function() { showLoadingIndicator(); }); $('.column').sortable({ - connectWith: '.trash', + connectWith: '.trash' }); $('.trash').sortable({ - connectWith: '.column', + connectWith: '.column' }); $('.column').disableSelection(); $('.trash').disableSelection(); @@ -77,7 +73,7 @@ export function initCoversAddManage () { // covers/saved.html // Uses parent.$ in place of $ where elements lie outside of the "saved" window -export function initCoversSaved () { +export function initCoversSaved() { // Save the new image // Pull data from data-config of class "imageSaved" in covers/saved.html const data_config_json = parent.$('.manageCovers').data('config'); @@ -95,28 +91,25 @@ export function initCoversSaved () { cover_url = `${coverstore_url}/b/id/${image}-M.jpg`; // XXX-Anand: Fix this hack // set url and show SRPCover and hide SRPCoverBlank - parent - .$(cover_selector) - .attr('src', cover_url) - .parents('div:first') - .show() - .next() - .hide(); - parent - .$(cover_selector) - .attr('srcset', cover_url) - .parents('div:first') - .show() - .next() - .hide(); - } else { + parent.$(cover_selector).attr('src', cover_url) + .parents('div:first').show() + .next().hide(); + parent.$(cover_selector).attr('srcset', cover_url) + .parents('div:first').show() + .next().hide(); + } + else { // hide SRPCover and show SRPCoverBlank - parent.$(cover_selector).parents('div:first').hide().next().show(); + parent.$(cover_selector) + .parents('div:first').hide() + .next().show(); } - } else { + } + else { if (image) { cover_url = `${coverstore_url}/a/id/${image}-M.jpg`; - } else { + } + else { cover_url = '/images/icons/avatar_author-lg.png'; } parent.$(cover_selector).attr('src', cover_url); @@ -124,36 +117,23 @@ export function initCoversSaved () { } // This function will be triggered when the user clicks the "Paste" button -async function pasteImage () { +async function pasteImage() { let formData = null; try { const clipboardItems = await navigator.clipboard.read(); for (const item of clipboardItems) { - if ( - !item.types.includes('image/png') && - !item.types.includes('image/jpeg') && - !item.types.includes('image/jpg') - ) { + if (!item.types.includes('image/png') && !item.types.includes('image/jpeg') && !item.types.includes('image/jpg')) { continue; } - const mimeType = item.types.includes('image/png') - ? 'image/png' - : item.types.includes('image/jpeg') - ? 'image/jpeg' - : 'image/jpg'; - const fileExtension = - mimeType === 'image/png' - ? 'png' - : mimeType === 'image/jpeg' - ? 'jpeg' - : 'jpg'; + const mimeType = item.types.includes('image/png') ? 'image/png' : (item.types.includes('image/jpeg') ? 'image/jpeg' : 'image/jpg'); + const fileExtension = mimeType === 'image/png' ? 'png' : (mimeType === 'image/jpeg' ? 'jpeg' : 'jpg'); const blob = await item.getType(mimeType); const image = document.createElement('img'); image.src = URL.createObjectURL(blob); - image.alt = ''; - const imageContainer = document.querySelector('.image-container'); - imageContainer.replaceChildren(image); + image.alt = '' + const imageContainer = document.querySelector('.image-container') + imageContainer.replaceChildren(image) // Update the global formData with the new image blob formData = new FormData(); @@ -161,32 +141,30 @@ async function pasteImage () { // Automatically fill in the hidden file input with the FormData const fileInput = document.getElementById('hiddenFileInput'); - const file = new File([blob], `pasted-image.${fileExtension}`, { - type: mimeType, - }); + const file = new File([blob], `pasted-image.${fileExtension}`, { type: mimeType }); const dataTransfer = new DataTransfer(); dataTransfer.items.add(file); fileInput.files = dataTransfer.files; // This sets the file input with the image // Show the upload button const uploadButton = document.getElementById('uploadButtonPaste'); - uploadButton.classList.remove('hidden'); + uploadButton.classList.remove('hidden') return formData; } alert('No image found in clipboard'); - } catch { - return null; + } catch (error) { + // Silence errors - user alert already shown } } -export function initPasteForm (coverForm) { +export function initPasteForm(coverForm) { const pasteButton = coverForm.querySelector('#pasteButton'); let formData = null; pasteButton.addEventListener('click', async () => { formData = await pasteImage(coverForm); - pasteButton.textContent = 'Change Image'; + pasteButton.textContent = 'Change Image' }); coverForm.addEventListener('submit', (event) => { diff --git a/openlibrary/plugins/openlibrary/js/dialog.js b/openlibrary/plugins/openlibrary/js/dialog.js index 9aca4fb7763..1ed311631c8 100644 --- a/openlibrary/plugins/openlibrary/js/dialog.js +++ b/openlibrary/plugins/openlibrary/js/dialog.js @@ -11,27 +11,24 @@ function initConfirmationDialogs() { const CONFIRMATION_PROMPT_DEFAULTS = { autoOpen: false, modal: true }; $('#noMaster').dialog(CONFIRMATION_PROMPT_DEFAULTS); - const $confirmMerge = $('#confirmMerge'); + const $confirmMerge = $('#confirmMerge') if ($confirmMerge.length) { $confirmMerge.dialog( $.extend({}, CONFIRMATION_PROMPT_DEFAULTS, { buttons: { - 'Yes, Merge': function () { - const commentInput = document.querySelector( - '#author-merge-comment', - ); + 'Yes, Merge': function() { + const commentInput = document.querySelector('#author-merge-comment') if (commentInput.value) { - document.querySelector('#hidden-comment-input').value = - commentInput.value; + document.querySelector('#hidden-comment-input').value = commentInput.value } $('#mergeForm').trigger('submit'); - $(this).parents().find('button').attr('disabled', 'disabled'); + $(this).parents().find('button').attr('disabled','disabled'); }, - 'No, Cancel': function () { + 'No, Cancel': function() { $(this).dialog('close'); - }, - }, - }), + } + } + }) ); } $('#leave-waitinglist-dialog').dialog( @@ -39,45 +36,44 @@ function initConfirmationDialogs() { width: 450, resizable: false, buttons: { - 'Yes, I\'m sure': function () { + 'Yes, I\'m sure': function() { $(this).dialog('close'); $(this).data('origin').closest('td').find('form').trigger('submit'); }, - 'No, cancel': function () { + 'No, cancel': function() { $(this).dialog('close'); - }, - }, - }), + } + } + }) ); } + export function initPreviewDialogs() { // Delegated click handler for Book Preview buttons. // Uses event delegation so dynamically-added buttons (e.g. from // lazy-loaded carousels) work without re-initialization. - $(document) - .off('click.bookPreview') - .on('click.bookPreview', '[data-book-preview]', function (e) { - e.preventDefault(); - const $button = $(this); - $.colorbox({ - width: '100%', - maxWidth: '640px', - inline: true, - opacity: '0.5', - href: '#bookPreview', - onOpen() { - const $iframe = $('#bookPreview iframe'); - $iframe.prop('src', $button.data('iframe-src')); + $(document).off('click.bookPreview').on('click.bookPreview', '[data-book-preview]', function (e) { + e.preventDefault(); + const $button = $(this); + $.colorbox({ + width: '100%', + maxWidth: '640px', + inline: true, + opacity: '0.5', + href: '#bookPreview', + onOpen() { + const $iframe = $('#bookPreview iframe'); + $iframe.prop('src', $button.data('iframe-src')); - const $link = $('#bookPreview .learn-more a'); - $link[0].href = $button.data('iframe-link'); - }, - onCleanup() { - $('#bookPreview iframe').prop('src', ''); - }, - }); + const $link = $('#bookPreview .learn-more a'); + $link[0].href = $button.data('iframe-link'); + }, + onCleanup() { + $('#bookPreview iframe').prop('src', ''); + }, }); + }); } /** @@ -91,25 +87,15 @@ export function initDialogs() { const $link = $(this), href = `#${$link.attr('aria-controls')}`; - $link.colorbox({ - inline: true, - opacity: '0.5', - href, - maxWidth: '640px', - width: '100%', - }); + $link.colorbox({ inline: true, opacity: '0.5', href, + maxWidth: '640px', width: '100%' }); }); initConfirmationDialogs(); initPreviewDialogs(); // This will close the dialog in the current page. - $('.dialog--close') - .attr('href', '#') - .on('click', (e) => { - e.preventDefault(); - $.fn.colorbox.close(); - }); + $('.dialog--close').attr('href', 'javascript:;').on('click', () => $.fn.colorbox.close()); // This will close the colorbox from the parent. $('.dialog--close-parent').on('click', () => parent.$.fn.colorbox.close()); } @@ -120,7 +106,7 @@ export function initDialogs() { * @param {NodeList<Element>} closers */ export function initDialogClosers(closers) { - closers.forEach((closer) => { - $(closer).on('click', () => $.fn.colorbox.close()); - }); + closers.forEach(closer => { + $(closer).on('click', () => $.fn.colorbox.close()) + }) } diff --git a/openlibrary/plugins/openlibrary/js/dropper/Dropper.js b/openlibrary/plugins/openlibrary/js/dropper/Dropper.js index 961f9bb4c90..e2caa919f1e 100644 --- a/openlibrary/plugins/openlibrary/js/dropper/Dropper.js +++ b/openlibrary/plugins/openlibrary/js/dropper/Dropper.js @@ -22,138 +22,134 @@ */ export class Dropper { /** - * Creates a new dropper. - * - * Sets the initial state of the dropper, and sets references to key - * dropper elements. - * - * @param {HTMLElement} dropper Reference to the dropper's root element - */ - constructor (dropper) { - /** - * References the root element of the dropper. + * Creates a new dropper. + * + * Sets the initial state of the dropper, and sets references to key + * dropper elements. * - * @member {HTMLElement} + * @param {HTMLElement} dropper Reference to the dropper's root element */ - this.dropper = dropper; + constructor(dropper) { + /** + * References the root element of the dropper. + * + * @member {HTMLElement} + */ + this.dropper = dropper /** - * jQuery object containing the root element of the dropper. - * - * **Note:** jQuery is only used here for its slide animations. - * This can be removed when and if these animations are handled - * strictly with CSS. - * - * @member {JQuery<HTMLElement>} - */ - this.$dropper = $(dropper); + * jQuery object containing the root element of the dropper. + * + * **Note:** jQuery is only used here for its slide animations. + * This can be removed when and if these animations are handled + * strictly with CSS. + * + * @member {JQuery<HTMLElement>} + */ + this.$dropper = $(dropper) /** - * Reference to the affordance that, when clicked, toggles - * the "Open" state of this dropper. - * - * @member {HTMLElement} - */ - this.dropClick = dropper.querySelector('.generic-dropper__dropclick'); + * Reference to the affordance that, when clicked, toggles + * the "Open" state of this dropper. + * + * @member {HTMLElement} + */ + this.dropClick = dropper.querySelector('.generic-dropper__dropclick') /** - * Tracks the current "Open" state of this dropper. - * - * @member {boolean} - */ - this.isDropperOpen = dropper.classList.contains( - 'generic-dropper-wrapper--active', - ); + * Tracks the current "Open" state of this dropper. + * + * @member {boolean} + */ + this.isDropperOpen = dropper.classList.contains('generic-dropper-wrapper--active') /** - * Tracks whether this dropper is disabled. - * - * A disabled dropper cannot be toggled. - * - * @member {boolean} - */ - this.isDropperDisabled = dropper.classList.contains( - 'generic-dropper--disabled', - ); + * Tracks whether this dropper is disabled. + * + * A disabled dropper cannot be toggled. + * + * @member {boolean} + */ + this.isDropperDisabled = dropper.classList.contains('generic-dropper--disabled') } /** - * Adds click listener to dropper's toggle arrow. - */ - initialize () { + * Adds click listener to dropper's toggle arrow. + */ + initialize() { this.dropClick.addEventListener('click', () => { - this.toggleDropper(); - }); + this.toggleDropper() + }) } /** - * Function that is called after a dropper has opened. - * - * Subclasses of `Dropper` may override this to add - * functionality that should occur on dropper open. - */ - onOpen () {} + * Function that is called after a dropper has opened. + * + * Subclasses of `Dropper` may override this to add + * functionality that should occur on dropper open. + */ + onOpen() {} /** - * Function that is called after a dropper has closed. - * - * Subclasses of `Dropper` may override this to add - * functionality that should occur on dropper close. - */ - onClose () {} + * Function that is called after a dropper has closed. + * + * Subclasses of `Dropper` may override this to add + * functionality that should occur on dropper close. + */ + onClose() {} /** - * Function that is called when the drop-click affordance of - * a disabled dropper is clicked. - * - * Subclasses of `Dropper` may override this as needed. - */ - onDisabledClick () {} + * Function that is called when the drop-click affordance of + * a disabled dropper is clicked. + * + * Subclasses of `Dropper` may override this as needed. + */ + onDisabledClick() {} /** - * Closes dropper if opened; opens dropper if closed. - * - * Toggles value of `isDropperOpen`. - * - * Calls `onDisabledClick()` if this dropper is disabled. - * Calls either `onOpen()` or `onClose()` after the dropper - * has been toggled. - */ - toggleDropper () { + * Closes dropper if opened; opens dropper if closed. + * + * Toggles value of `isDropperOpen`. + * + * Calls `onDisabledClick()` if this dropper is disabled. + * Calls either `onOpen()` or `onClose()` after the dropper + * has been toggled. + */ + toggleDropper() { if (this.isDropperDisabled) { this.onDisabledClick(); } else { this.$dropper.find('.generic-dropper__dropdown').slideToggle(25); - this.$dropper.find('.arrow').toggleClass('up'); - this.$dropper.toggleClass('generic-dropper-wrapper--active'); - this.isDropperOpen = !this.isDropperOpen; + this.$dropper.find('.arrow').toggleClass('up') + this.$dropper.toggleClass('generic-dropper-wrapper--active') + this.isDropperOpen = !this.isDropperOpen if (this.isDropperOpen) { - this.onOpen(); + this.onOpen() } else { - this.onClose(); + this.onClose() } } } /** - * Closes this dropper. - * - * Sets `isDropperOpen` to `false`. - * - * Calls `onDisabledClick()` if this dropper is disabled. - * Otherwise, closes dropper and calls `onClose()`. - */ - closeDropper () { + * Closes this dropper. + * + * Sets `isDropperOpen` to `false`. + * + * Calls `onDisabledClick()` if this dropper is disabled. + * Otherwise, closes dropper and calls `onClose()`. + */ + closeDropper() { if (this.isDropperDisabled) { this.onDisabledClick(); } else { - this.$dropper.find('.generic-dropper__dropdown').slideUp(25); + this.$dropper.find('.generic-dropper__dropdown').slideUp(25) this.$dropper.find('.arrow').removeClass('up'); - this.$dropper.removeClass('generic-dropper-wrapper--active'); - this.isDropperOpen = false; + this.$dropper.removeClass('generic-dropper-wrapper--active') + this.isDropperOpen = false - this.onClose(); + this.onClose() } } } diff --git a/openlibrary/plugins/openlibrary/js/dropper/index.js b/openlibrary/plugins/openlibrary/js/dropper/index.js index 5c4aec9886c..62ff4c52c59 100644 --- a/openlibrary/plugins/openlibrary/js/dropper/index.js +++ b/openlibrary/plugins/openlibrary/js/dropper/index.js @@ -4,49 +4,33 @@ import { debounce } from '../nonjquery_utils'; * Holds references to each dropper on a page. * @type {Array<HTMLElement>} */ -const droppers = []; +const droppers = [] /** * Adds expand and collapse functionality to our droppers. * * @param {HTMLCollection<HTMLElement>} dropperElements */ -export function initDroppers (dropperElements) { +export function initDroppers(dropperElements) { for (const dropper of dropperElements) { - droppers.push(dropper); + droppers.push(dropper) - $(dropper).on( - 'click', - '.dropclick', - debounce( - function () { - $(this).next('.dropdown').slideToggle(25); - $(this).parent().next('.dropdown').slideToggle(25); - $(this).parent().find('.arrow').toggleClass('up'); - }, - 300, - false, - ), - ); + $(dropper).on('click', '.dropclick', debounce(function() { + $(this).next('.dropdown').slideToggle(25); + $(this).parent().next('.dropdown').slideToggle(25); + $(this).parent().find('.arrow').toggleClass('up'); + }, 300, false)) - $(dropper).on( - 'click', - '.dropper__close', - debounce( - () => { - closeDropper($(dropper)); - }, - 300, - false, - ), - ); + $(dropper).on('click', '.dropper__close', debounce(function() { + closeDropper($(dropper)) + }, 300, false)) } // Close any open dropdown list if the user clicks outside of component: - $(document).on('click', (event) => { + $(document).on('click', function(event) { for (const dropper of droppers) { if (!dropper.contains(event.target)) { - closeDropper($(dropper)); + closeDropper($(dropper)) } } }); @@ -56,11 +40,11 @@ export function initDroppers (dropperElements) { * close an open dropdown in a given container * @param {jQuery.Object} $container */ -function closeDropper ($container) { - $container.find('.dropdown').slideUp(25); // Legacy droppers - $container.find('.generic-dropper__dropdown').slideUp(25); // New generic droppers +function closeDropper($container) { + $container.find('.dropdown').slideUp(25); // Legacy droppers + $container.find('.generic-dropper__dropdown').slideUp(25) // New generic droppers $container.find('.arrow').removeClass('up'); - $container.removeClass('generic-dropper-wrapper--active'); + $container.removeClass('generic-dropper-wrapper--active') } /** @@ -72,14 +56,14 @@ function closeDropper ($container) { * * @param {NodeList<HTMLElement>} dropperElements */ -export function initGenericDroppers (dropperElements) { - const genericDroppers = Array.from(dropperElements); +export function initGenericDroppers(dropperElements) { + const genericDroppers = Array.from(dropperElements) // Close any open dropdown if the user clicks outside of component: - $(document).on('click', (event) => { + $(document).on('click', function(event) { for (const dropper of genericDroppers) { if (!dropper.contains(event.target)) { - closeDropper($(dropper)); + closeDropper($(dropper)) } } }); diff --git a/openlibrary/plugins/openlibrary/js/edit.js b/openlibrary/plugins/openlibrary/js/edit.js index fdd52bde891..17b9066e4c0 100644 --- a/openlibrary/plugins/openlibrary/js/edit.js +++ b/openlibrary/plugins/openlibrary/js/edit.js @@ -1,15 +1,15 @@ -import { init as initAutocomplete } from './autocomplete'; +import { isbnOverride } from './isbnOverride'; import { + parseIsbn, + parseLccn, isChecksumValidIsbn10, isChecksumValidIsbn13, isFormatValidIsbn10, isFormatValidIsbn13, - isIdDupe, isValidLccn, - parseIsbn, - parseLccn, + isIdDupe } from './idValidation'; -import { isbnOverride } from './isbnOverride'; +import { init as initAutocomplete } from './autocomplete'; import { init as initJqueryRepeat } from './jquery.repeat'; import { trimInputValues } from './utils.js'; @@ -37,9 +37,7 @@ function update_len() { } else { color = 'gray'; } - $('#excerpts-excerpt-len') - .html(2000 - len) - .css('color', color); + $('#excerpts-excerpt-len').html(2000 - len).css('color', color); } /** @@ -65,44 +63,31 @@ function limitChars(textid, limit) { * @param selector - css selector used by jQuery * @returns {*[]} - array of jQuery elements */ -function getJqueryElements(selector) { +function getJqueryElements(selector){ const queryResult = $(selector); const jQueryElementArray = []; - for (let i = 0; i < queryResult.length; i++) { - jQueryElementArray.push(queryResult.eq(i)); + for (let i = 0; i < queryResult.length; i++){ + jQueryElementArray.push(queryResult.eq(i)) } return jQueryElementArray; } export function initRoleValidation() { initJqueryRepeat(); - const dataConfig = JSON.parse( - document.querySelector('#roles').dataset.config, - ); + const dataConfig = JSON.parse(document.querySelector('#roles').dataset.config); $('#roles').repeat({ - vars: { prefix: 'edition--' }, - validate: (data) => { + vars: {prefix: 'edition--'}, + validate: function (data) { if (data.role === '' || data.role === '---') { - return error( - '#role-errors', - '#select-role', - dataConfig['Please select a role.'], - ); + return error('#role-errors', '#select-role', dataConfig['Please select a role.']); } if (data.name === '') { - return error( - '#role-errors', - '#role-name', - dataConfig['You need to give this ROLE a name.'].replace( - /ROLE/, - data.role, - ), - ); + return error('#role-errors', '#role-name', dataConfig['You need to give this ROLE a name.'].replace(/ROLE/, data.role)); } $('#role-errors').hide(); $('#select-role, #role-name').val(''); return true; - }, + } }); } @@ -117,21 +102,21 @@ export function isbnConfirmAdd(data) { // Display the error and option to add the ISBN anyway. $('#id-errors').show().html(isbnConfirmString); - const yesButtonSelector = '#yes-add-isbn'; - const noButtonSelector = '#do-not-add-isbn'; + const yesButtonSelector = '#yes-add-isbn' + const noButtonSelector = '#do-not-add-isbn' const onYes = () => { $('#id-errors').hide(); }; const onNo = () => { $('#id-errors').hide(); isbnOverride.clear(); - }; + } $(document).on('click', yesButtonSelector, onYes); $(document).on('click', noButtonSelector, onNo); // Save the data to isbnOverride so it can be picked up via onAdd in // js/jquery.repeat.js when the user confirms adding the invalid ISBN. - isbnOverride.set(data); + isbnOverride.set(data) return false; } @@ -147,24 +132,14 @@ function validateIsbn10(data, dataConfig, label) { data.value = parseIsbn(data.value); if (!isFormatValidIsbn10(data.value)) { - return error( - '#id-errors', - '#id-value', - dataConfig['ID must be exactly 10 characters [0-9] or X.'].replace( - /ID/, - label, - ), - ); + return error('#id-errors', '#id-value', dataConfig['ID must be exactly 10 characters [0-9] or X.'].replace(/ID/, label)); } // Here the ISBN has a valid format, but also has an invalid checksum. Give the user a chance to verify // the ISBN, as books sometimes issue with invalid ISBNs and we want to be able to add them. // See https://en-academic.com/dic.nsf/enwiki/8948#cite_ref-18 for more. - else if ( - isFormatValidIsbn10(data.value) === true && - isChecksumValidIsbn10(data.value) === false - ) { - isbnConfirmAdd(data); - return false; + else if (isFormatValidIsbn10(data.value) === true && isChecksumValidIsbn10(data.value) === false) { + isbnConfirmAdd(data) + return false } return true; } @@ -181,23 +156,14 @@ function validateIsbn13(data, dataConfig, label) { data.value = parseIsbn(data.value); if (isFormatValidIsbn13(data.value) === false) { - return error( - '#id-errors', - '#id-value', - dataConfig[ - 'ID must be exactly 13 digits [0-9]. For example: 978-1-56619-909-4' - ].replace(/ID/, label), - ); + return error('#id-errors', '#id-value', dataConfig['ID must be exactly 13 digits [0-9]. For example: 978-1-56619-909-4'].replace(/ID/, label)); } // Here the ISBN has a valid format, but also has an invalid checksum. Give the user a chance to verify // the ISBN, as books sometimes issue with invalid ISBNs and we want to be able to add them. // See https://en-academic.com/dic.nsf/enwiki/8948#cite_ref-18 for more. - else if ( - isFormatValidIsbn13(data.value) === true && - isChecksumValidIsbn13(data.value) === false - ) { - isbnConfirmAdd(data); - return false; + else if (isFormatValidIsbn13(data.value) === true && isChecksumValidIsbn13(data.value) === false) { + isbnConfirmAdd(data) + return false } return true; } @@ -215,11 +181,7 @@ function validateLccn(data, dataConfig, label) { if (isValidLccn(data.value) === false) { $('#id-value').val(data.value); - return error( - '#id-errors', - '#id-value', - dataConfig['Invalid ID format'].replace(/ID/, label), - ); + return error('#id-errors', '#id-value', dataConfig['Invalid ID format'].replace(/ID/, label)); } return true; } @@ -232,40 +194,28 @@ function validateLccn(data, dataConfig, label) { * @returns {boolean} true if identifier passes validation */ export function validateIdentifiers(data) { - const dataConfig = JSON.parse( - document.querySelector('#identifiers').dataset.config, - ); + const dataConfig = JSON.parse(document.querySelector('#identifiers').dataset.config); if (data.name === '' || data.name === '---') { $('#id-value').val(data.value); - return error( - '#id-errors', - '#select-id', - dataConfig['Please select an identifier.'], - ); + return error('#id-errors', '#select-id', dataConfig['Please select an identifier.']) } const label = $('#select-id').find(`option[value='${data.name}']`).html(); if (data.value === '') { - return error( - '#id-errors', - '#id-value', - dataConfig['You need to give a value to ID.'].replace(/ID/, label), - ); + return error('#id-errors', '#id-value', dataConfig['You need to give a value to ID.'].replace(/ID/, label)); } if (['ocaid'].includes(data.name) && /\s/g.test(data.value)) { - return error( - '#id-errors', - '#id-value', - dataConfig['ID ids cannot contain whitespace.'].replace(/ID/, label), - ); + return error('#id-errors', '#id-value', dataConfig['ID ids cannot contain whitespace.'].replace(/ID/, label)); } let validId = true; if (data.name === 'isbn_10') { validId = validateIsbn10(data, dataConfig, label); - } else if (data.name === 'isbn_13') { + } + else if (data.name === 'isbn_13') { validId = validateIsbn13(data, dataConfig, label); - } else if (data.name === 'lccn') { + } + else if (data.name === 'lccn') { validId = validateLccn(data, dataConfig, label); } @@ -273,18 +223,9 @@ export function validateIdentifiers(data) { // expects parsed ids so placed after validate const entries = document.querySelectorAll(`.${data.name}`); if (isIdDupe(entries, data.value) === true) { - // isbnOverride being set will override the dupe checker, so clear isbnOverride if there's a dupe. - if (isbnOverride.get()) { - isbnOverride.clear(); - } - return error( - '#id-errors', - '#id-value', - dataConfig['That ID already exists for this edition.'].replace( - /ID/, - label, - ), - ); + // isbnOverride being set will override the dupe checker, so clear isbnOverride if there's a dupe. + if (isbnOverride.get()) {isbnOverride.clear()} + return error('#id-errors', '#id-value', dataConfig['That ID already exists for this edition.'].replace(/ID/, label)); } if (validId === false) return false; @@ -294,12 +235,10 @@ export function validateIdentifiers(data) { export function initClassificationValidation() { initJqueryRepeat(); - const dataConfig = JSON.parse( - document.querySelector('#classifications').dataset.config, - ); + const dataConfig = JSON.parse(document.querySelector('#classifications').dataset.config); // Prevent form submission on Enter for classification fields - $('#classification-value').on('keydown', (e) => { + $('#classification-value').on('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); $('#classifications .repeat-add').trigger('click'); @@ -308,133 +247,82 @@ export function initClassificationValidation() { }); $('#classifications').repeat({ - vars: { prefix: 'edition--' }, - validate: (data) => { + vars: {prefix: 'edition--'}, + validate: function (data) { if (data.name === '' || data.name === '---') { - return error( - '#classification-errors', - '#select-classification', - dataConfig['Please select a classification.'], - ); + return error('#classification-errors', '#select-classification', dataConfig['Please select a classification.']); } if (data.value === '') { - const label = $('#select-classification') - .find(`option[value='${data.name}']`) - .html(); - return error( - '#classification-errors', - '#classification-value', - dataConfig['You need to give a value to CLASS.'].replace( - /CLASS/, - label, - ), - ); + const label = $('#select-classification').find(`option[value='${data.name}']`).html(); + return error('#classification-errors', '#classification-value', dataConfig['You need to give a value to CLASS.'].replace(/CLASS/, label)); } $('#classification-errors').hide(); $('#select-classification, #classification-value').val(''); return true; - }, + } }); } export function initLanguageMultiInputAutocomplete() { initAutocomplete(); - $(() => { - getJqueryElements('.multi-input-autocomplete--language').forEach( - (jqueryElement) => { - jqueryElement.setup_multi_input_autocomplete( - render_language_field, - { - endpoint: '/languages/_autocomplete', - sortable: true, - }, - { - max: 6, - formatItem: render_language_autocomplete_item, - }, - ); - }, - ); + $(function() { + getJqueryElements('.multi-input-autocomplete--language').forEach(jqueryElement => { + jqueryElement.setup_multi_input_autocomplete( + render_language_field, + { + endpoint: '/languages/_autocomplete', + sortable: true, + }, + { + max: 6, + formatItem: render_language_autocomplete_item + } + ); + }) }); } export function initWorksMultiInputAutocomplete() { initAutocomplete(); - $(() => { - getJqueryElements('.multi-input-autocomplete--works').forEach( - (jqueryElement) => { - /* Values in the html passed from Python code */ - const dataConfig = JSON.parse(jqueryElement[0].dataset.config || '{}'); - jqueryElement.setup_multi_input_autocomplete( - render_work_field, - { - endpoint: '/works/_autocomplete', - addnew: dataConfig['addnew'] || false, - new_name: dataConfig['new_name'] || '', - allow_empty: dataConfig['allow_empty'] || false, - }, - { - minChars: 2, - max: 11, - matchSubset: false, - autoFill: true, - formatItem: render_work_autocomplete_item, - }, - ); - }, - ); + $(function() { + getJqueryElements('.multi-input-autocomplete--works').forEach(jqueryElement => { + /* Values in the html passed from Python code */ + const dataConfig = JSON.parse(jqueryElement[0].dataset.config || '{}'); + jqueryElement.setup_multi_input_autocomplete( + render_work_field, + { + endpoint: '/works/_autocomplete', + addnew: dataConfig['addnew'] || false, + new_name: dataConfig['new_name'] || '', + allow_empty: dataConfig['allow_empty'] || false, + }, + { + minChars: 2, + max: 11, + matchSubset: false, + autoFill: true, + formatItem: render_work_autocomplete_item, + }); + }); }); // Show the new work options checkboxes only if "New work" selected - $('input[name="works--0"]').on('autocompleteselect', (_event, ui) => { + $('input[name="works--0"]').on('autocompleteselect', function(_event, ui) { $('.new-work-options').toggle(ui.item.key === '__new__'); }); } export function initSeedsMultiInputAutocomplete() { initAutocomplete(); - $(() => { - getJqueryElements('.multi-input-autocomplete--seeds').forEach( - (jqueryElement) => { - /* Values in the html passed from Python code */ - jqueryElement.setup_multi_input_autocomplete( - render_seed_field, - { - endpoint: '/works/_autocomplete', - addnew: false, - allow_empty: true, - sortable: true, - }, - { - minChars: 2, - max: 11, - matchSubset: false, - autoFill: true, - formatItem: render_lazy_work_preview, - }, - ); - }, - ); - }); -} - -export function initAuthorMultiInputAutocomplete() { - initAutocomplete(); - getJqueryElements('.multi-input-autocomplete--author').forEach( - (jqueryElement) => { + $(function() { + getJqueryElements('.multi-input-autocomplete--seeds').forEach(jqueryElement => { /* Values in the html passed from Python code */ - const dataConfig = JSON.parse(jqueryElement[0].dataset.config); jqueryElement.setup_multi_input_autocomplete( - render_author.bind( - null, - dataConfig.name_path, - dataConfig.dict_path, - false, - ), + render_seed_field, { - endpoint: '/authors/_autocomplete', - // Don't render "Create new author" if searching by key - addnew: (query) => !/OL\d+A/i.test(query), + endpoint: '/works/_autocomplete', + addnew: false, + allow_empty: true, sortable: true, }, { @@ -442,47 +330,61 @@ export function initAuthorMultiInputAutocomplete() { max: 11, matchSubset: false, autoFill: true, - formatItem: render_author_autocomplete_item, - }, - ); - }, - ); + formatItem: render_lazy_work_preview, + }); + }); + }); +} + +export function initAuthorMultiInputAutocomplete() { + initAutocomplete(); + getJqueryElements('.multi-input-autocomplete--author').forEach(jqueryElement => { + /* Values in the html passed from Python code */ + const dataConfig = JSON.parse(jqueryElement[0].dataset.config); + jqueryElement.setup_multi_input_autocomplete( + render_author.bind(null, dataConfig.name_path, dataConfig.dict_path, false), + { + endpoint: '/authors/_autocomplete', + // Don't render "Create new author" if searching by key + addnew: query => !/OL\d+A/i.test(query), + sortable: true, + }, + { + minChars: 2, + max: 11, + matchSubset: false, + autoFill: true, + formatItem: render_author_autocomplete_item + }); + }); } export function initSeriesMultiInputAutocomplete() { initAutocomplete(); - getJqueryElements('.multi-input-autocomplete--series').forEach( - (jqueryElement) => { - /* Values in the html passed from Python code */ - const dataConfig = JSON.parse(jqueryElement[0].dataset.config); - jqueryElement.setup_multi_input_autocomplete( - render_series.bind( - null, - dataConfig.name_path, - dataConfig.dict_path, - false, - ), - { - endpoint: '/series/_autocomplete', - // Don't render "Create new series" if searching by key - addnew: (query) => !/OL\d+L/i.test(query), - sortable: true, - }, - { - minChars: 2, - max: 11, - matchSubset: false, - autoFill: true, - formatItem: render_series_autocomplete_item, - }, - ); - }, - ); + getJqueryElements('.multi-input-autocomplete--series').forEach(jqueryElement => { + /* Values in the html passed from Python code */ + const dataConfig = JSON.parse(jqueryElement[0].dataset.config); + jqueryElement.setup_multi_input_autocomplete( + render_series.bind(null, dataConfig.name_path, dataConfig.dict_path, false), + { + endpoint: '/series/_autocomplete', + // Don't render "Create new series" if searching by key + addnew: query => !/OL\d+L/i.test(query), + sortable: true, + }, + { + minChars: 2, + max: 11, + matchSubset: false, + autoFill: true, + formatItem: render_series_autocomplete_item + }); + }); } export function initSubjectsAutocomplete() { initAutocomplete(); - getJqueryElements('.csv-autocomplete--subjects').forEach((jqueryElement) => { + getJqueryElements('.csv-autocomplete--subjects').forEach(jqueryElement => { const dataConfig = JSON.parse(jqueryElement[0].dataset.config); jqueryElement.setup_csv_autocomplete( 'textarea', @@ -492,7 +394,7 @@ export function initSubjectsAutocomplete() { }, { formatItem: render_subject_autocomplete_item, - }, + } ); }); @@ -503,10 +405,8 @@ export function initSubjectsAutocomplete() { }); } -export function initEditRow() { - document - .querySelector('#add_row_button') - .addEventListener('click', () => add_row('website')); +export function initEditRow(){ + document.querySelector('#add_row_button').addEventListener('click', ()=>add_row('website')); } /** @@ -518,7 +418,7 @@ function add_row(name) { const inputBox = document.createElement('input'); inputBox.name = `${name}#${inputBoxes.length}`; inputBox.type = 'text'; - inputBoxes[inputBoxes.length - 1].after(inputBox); + inputBoxes[inputBoxes.length-1].after(inputBox); } function show_hide_title() { @@ -535,33 +435,23 @@ export function initEditExcerpts() { vars: { prefix: 'work--excerpts', }, - validate: (data) => { - const i18nStrings = JSON.parse( - document.querySelector('#excerpts-errors').dataset.i18n, - ); + validate: function(data) { + const i18nStrings = JSON.parse(document.querySelector('#excerpts-errors').dataset.i18n); if (!data.excerpt) { - return error( - '#excerpts-errors', - '#excerpts-excerpt', - i18nStrings['empty_excerpt'], - ); + return error('#excerpts-errors', '#excerpts-excerpt', i18nStrings['empty_excerpt']); } if (data.excerpt.length > 2000) { - return error( - '#excerpts-errors', - '#excerpts-excerpt', - i18nStrings['over_wordcount'], - ); + return error('#excerpts-errors', '#excerpts-excerpt', i18nStrings['over_wordcount']); } $('#excerpts-errors').hide(); $('#excerpts-excerpt').val(''); return true; - }, + } }); // update length on every keystroke - $('#excerpts-excerpt').on('keyup', () => { + $('#excerpts-excerpt').on('keyup', function() { limitChars('excerpts-excerpt', 2000); update_len(); }); @@ -590,12 +480,10 @@ export function initEditLinks() { initJqueryRepeat(); $('#links').repeat({ vars: { - prefix: $('#links').data('prefix'), + prefix: $('#links').data('prefix') }, - validate: (data) => { - const i18nStrings = JSON.parse( - document.querySelector('#link-errors').dataset.i18n, - ); + validate: function(data) { + const i18nStrings = JSON.parse(document.querySelector('#link-errors').dataset.i18n); const url = data.url.trim(); if (data.title.trim() === '') { @@ -619,7 +507,7 @@ export function initEditLinks() { $('#link-errors').addClass('hidden'); $('#link-label, #link-url').val(''); return true; - }, + } }); } @@ -644,8 +532,8 @@ export function initEdit() { // input field is enabled only after the tab is selected and that takes some time after clicking the link. // wait for 1 sec after clicking the link and focus the input field if ($(fieldname).length !== 0) { - setTimeout(() => { - // scroll such that top of the content is visible + setTimeout(function() { + // scroll such that top of the content is visible $(fieldname).trigger('focus'); $(window).scrollTop($('#contentHead').offset().top); }, 1000); diff --git a/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js b/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js index 89f135e3772..f83406c7cf2 100644 --- a/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js +++ b/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js @@ -8,63 +8,63 @@ export default class EdtionNavBar { * * @param {HTMLElement} navbarWrapper */ - constructor (navbarWrapper) { + constructor(navbarWrapper) { /** * Reference to the parent element of the navbar. * @type {HTMLElement} */ - this.navbarWrapper = navbarWrapper; + this.navbarWrapper = navbarWrapper /** * The navbar * @type {HTMLElement} */ - this.navbarElem = navbarWrapper.querySelector('.work-menu'); + this.navbarElem = navbarWrapper.querySelector('.work-menu') /** * The mobile-only navigation arrow. Not guaranteed to exist. * @type {HTMLElement|null} */ - this.navArrowLeft = navbarWrapper.querySelector('.nav-arrow-left'); + this.navArrowLeft = navbarWrapper.querySelector('.nav-arrow-left') /** * The mobile-only navigation arrow. Not guaranteed to exist. * @type {HTMLElement|null} */ - this.navArrowRight = navbarWrapper.querySelector('.nav-arrow-right'); + this.navArrowRight = navbarWrapper.querySelector('.nav-arrow-right') /** * References each nav item in this navbar. * @type {Array<HTMLLIElement>} */ - this.navItems = Array.from(this.navbarElem.querySelectorAll('li')); + this.navItems = Array.from(this.navbarElem.querySelectorAll('li')) /** * Index of the currently selected nav item. * @type {number} */ - this.selectedIndex = 0; + this.selectedIndex = 0 /** * The nav items' target anchor elements. * @type {HTMLAnchorElement} */ - this.targetAnchors = []; + this.targetAnchors = [] - this.initialize(); + this.initialize() } /** * Adds the necessary event handlers to the navbar. */ - initialize () { + initialize() { // Add click listeners to navbar items: for (let i = 0; i < this.navItems.length; ++i) { this.navItems[i].addEventListener('click', () => { - this.selectedIndex = i; - this.selectElement(this.navItems[i]); - }); + this.selectedIndex = i + this.selectElement(this.navItems[i]) + }) // Add this nav item's target anchor to array: - this.targetAnchors.push(document.getElementById(this.navItems[i].children[0].hash.substring(1))); + this.targetAnchors.push(document.getElementById(this.navItems[i].children[0].hash.substring(1))) // Set selectedIndex to the correct value: if (this.navItems[i].classList.contains('selected')) { - this.selectedIndex = i; + this.selectedIndex = i } } @@ -72,43 +72,43 @@ export default class EdtionNavBar { if (this.navArrowLeft) { this.navArrowLeft.addEventListener('click', () => { if (this.selectedIndex > 0) { - --this.selectedIndex; - this.navItems[this.selectedIndex].children[0].click(); + --this.selectedIndex + this.navItems[this.selectedIndex].children[0].click() } - }); + }) } if (this.navArrowRight) { this.navArrowRight.addEventListener('click', () => { if (this.selectedIndex < this.navItems.length - 1) { // Simulate click on the next nav item: - ++this.selectedIndex; - this.navItems[this.selectedIndex].children[0].click(); + ++this.selectedIndex + this.navItems[this.selectedIndex].children[0].click() } - }); + }) } // Add scroll listener for position-aware nav item selection document.addEventListener('scroll', () => { - this.updateSelected(); - }); + this.updateSelected() + }) } /** * Determines this navbar's position on the page and updates the selected * nav item. */ - updateSelected () { - const navbarHeight = this.navbarWrapper.getBoundingClientRect().height; + updateSelected() { + const navbarHeight = this.navbarWrapper.getBoundingClientRect().height if (navbarHeight > 0) { - let i = this.navItems.length; + let i = this.navItems.length // 10 is for a little bit of padding while (--i > 0 && this.navbarWrapper.offsetTop + navbarHeight < (this.targetAnchors[i].offsetTop - 10)) { // Do nothing } if (i !== this.selectedIndex) { - this.selectedIndex = i; - this.selectElement(this.navItems[i]); + this.selectedIndex = i + this.selectElement(this.navItems[i]) } } } @@ -118,7 +118,7 @@ export default class EdtionNavBar { * * @param {HTMLElement} selectedItem Newly selected nav item */ - scrollNavbar (selectedItem) { + scrollNavbar(selectedItem) { // Note: We don't use the browser native scrollIntoView method because // that method scrolls _recursively_, so it also tries to scroll the // body to center the element on the screen, causing weird jitters. @@ -126,7 +126,7 @@ export default class EdtionNavBar { this.navbarElem.scrollTo({ left: selectedItem.offsetLeft - (this.navbarElem.clientWidth - selectedItem.offsetWidth) / 2, behavior: 'instant' - }); + }) } /** @@ -137,11 +137,11 @@ export default class EdtionNavBar { * * @param {HTMLLIElement} selectedElem Element corresponding to the 'selected' navbar item. */ - selectElement (selectedElem) { + selectElement(selectedElem) { for (const li of this.navItems) { - li.classList.remove('selected'); + li.classList.remove('selected') } - selectedElem.classList.add('selected'); - this.scrollNavbar(selectedElem); + selectedElem.classList.add('selected') + this.scrollNavbar(selectedElem) } } diff --git a/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js b/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js index 8b36a5766c1..dcccf4c477c 100644 --- a/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js +++ b/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js @@ -4,17 +4,17 @@ import EdtionNavBar from './EditionNavBar'; * Holds references to each book page navbar. * @type {Array<EditionNavBar>} */ -const navbars = []; +const navbars = [] /** * Initializes and stores references to each book page navbar. * * @param {HTMLCollection<HTMLElement>} navbarWrappers Each navbar found on the book page */ -export function initNavbars (navbarWrappers) { +export function initNavbars(navbarWrappers) { for (const wrapper of navbarWrappers) { - const navbar = new EdtionNavBar(wrapper); - navbars.push(navbar); + const navbar = new EdtionNavBar(wrapper) + navbars.push(navbar) } } @@ -26,8 +26,8 @@ export function initNavbars (navbarWrappers) { * something other then a scroll event (e.g. when * stickied to a new position). */ -export function updateSelectedNavItem () { +export function updateSelectedNavItem() { for (const navbar of navbars) { - navbar.updateSelected(); + navbar.updateSelected() } } diff --git a/openlibrary/plugins/openlibrary/js/editions-table/index.js b/openlibrary/plugins/openlibrary/js/editions-table/index.js index 52e3dbe29be..8b297e90999 100755 --- a/openlibrary/plugins/openlibrary/js/editions-table/index.js +++ b/openlibrary/plugins/openlibrary/js/editions-table/index.js @@ -4,33 +4,33 @@ import '../../../../../static/css/legacy-datatables.css'; const DEFAULT_LENGTH = 3; const LS_RESULTS_LENGTH_KEY = 'editions-table.resultsLength'; -export function initEditionsTable () { +export function initEditionsTable() { var rowCount; let currentLength; // Prevent reinitialization of the editions datatable if ($.fn.DataTable.isDataTable($('#editions'))) { return; } - $('#editions th.title').on('mouseover', function () { + $('#editions th.title').on('mouseover', function(){ if ($(this).hasClass('sorting_asc')) { - $(this).attr('title', 'Sort latest to earliest'); + $(this).attr('title','Sort latest to earliest'); } else if ($(this).hasClass('sorting_desc')) { - $(this).attr('title', 'Sort earliest to latest'); + $(this).attr('title','Sort earliest to latest'); } else { - $(this).attr('title', 'Sort by publish date'); + $(this).attr('title','Sort by publish date'); } }); - $('#editions th.read').on('mouseover', function () { + $('#editions th.read').on('mouseover', function(){ if ($(this).hasClass('sorting_asc')) { - $(this).attr('title', 'Push readable versions to the bottom'); + $(this).attr('title','Push readable versions to the bottom'); } else if ($(this).hasClass('sorting_desc')) { - $(this).attr('title', 'Sort by editions to read'); + $(this).attr('title','Sort by editions to read'); } else { - $(this).attr('title', 'Available to read'); + $(this).attr('title','Available to read'); } }); - function toggleSorting (e) { + function toggleSorting(e) { $('#editions th span').html(''); $(e).find('span').html(' ↑'); if ($(e).hasClass('sorting_asc')) { @@ -41,40 +41,37 @@ export function initEditionsTable () { } $('#editions th.read span').html(' ↑'); - $('#editions th').on('mouseup', function () { - toggleSorting(this); + $('#editions th').on('mouseup', function() { + toggleSorting(this) }); - $('#editions').on('length.dt', (e, settings, length) => { + $('#editions').on('length.dt', function(e, settings, length) { localStorage.setItem(LS_RESULTS_LENGTH_KEY, length); }); - $('#editions th').on('keydown', function (e) { + $('#editions th').on('keydown', function(e) { if (e.key === 'Enter') { toggleSorting(this); } - }); + }) rowCount = $('#editions tbody tr').length; if (rowCount < 4) { $('#editions').DataTable({ - aoColumns: [{ sType: 'html' }, null], - order: [[1, 'asc']], + aoColumns: [{sType: 'html'},null], + order: [ [1,'asc'] ], bPaginate: false, bInfo: false, bFilter: false, bStateSave: false, - bAutoWidth: false, + bAutoWidth: false }); } else { currentLength = Number(localStorage.getItem(LS_RESULTS_LENGTH_KEY)); $('#editions').DataTable({ - aoColumns: [{ sType: 'html' }, null], - order: [[1, 'asc']], - lengthMenu: [ - [3, 10, 25, 50, 100, -1], - [3, 10, 25, 50, 100, 'All'], - ], + aoColumns: [{sType: 'html'},null], + order: [ [1,'asc'] ], + lengthMenu: [ [3, 10, 25, 50, 100, -1], [3, 10, 25, 50, 100, 'All'] ], bPaginate: true, bInfo: true, sPaginationType: 'full_numbers', @@ -82,18 +79,16 @@ export function initEditionsTable () { bStateSave: false, bAutoWidth: false, pageLength: currentLength ? currentLength : DEFAULT_LENGTH, - drawCallback: () => { + drawCallback: function() { if ($('#ile-toolbar')) { - const editionStorage = JSON.parse( - sessionStorage.getItem('ile-items'), - )['edition']; + const editionStorage = JSON.parse(sessionStorage.getItem('ile-items'))['edition'] const matchEdition = (string) => { - return string.match(/OL[0-9]+[a-zA-Z]/); - }; + return string.match(/OL[0-9]+[a-zA-Z]/) + } for (const el of $('.ile-selected')) { const anchor = el.getElementsByTagName('a'); if (anchor.length) { - const edIdentifier = matchEdition(anchor[0].getAttribute('href')); + const edIdentifier = matchEdition(anchor[0].getAttribute('href')) if (!editionStorage.includes(edIdentifier[0])) { el.classList.remove('ile-selected'); } @@ -109,7 +104,7 @@ export function initEditionsTable () { } } } - }, - }); + } + }) } } diff --git a/openlibrary/plugins/openlibrary/js/following.js b/openlibrary/plugins/openlibrary/js/following.js index af99bd43bea..0930da0acbf 100644 --- a/openlibrary/plugins/openlibrary/js/following.js +++ b/openlibrary/plugins/openlibrary/js/following.js @@ -1,7 +1,7 @@ import { PersistentToast } from './Toast'; -export async function initAsyncFollowing (followForms) { - followForms.forEach((form) => { +export async function initAsyncFollowing(followForms) { + followForms.forEach(form => { form.addEventListener('submit', async (e) => { e.preventDefault(); const url = form.action; @@ -18,17 +18,15 @@ export async function initAsyncFollowing (followForms) { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, - body: new URLSearchParams(formData), + body: new URLSearchParams(formData) }) - .then((resp) => { + .then(resp => { if (!resp.ok) { throw new Error('Network response was not ok'); } submitButton.classList.toggle('cta-btn--primary'); submitButton.classList.toggle('cta-btn--delete'); - submitButton.textContent = isFollowRequest - ? i18nStrings.unfollow - : i18nStrings.follow; + submitButton.textContent = isFollowRequest ? i18nStrings.unfollow : i18nStrings.follow; stateInput.value = isFollowRequest ? '1' : '0'; }) .catch(() => { diff --git a/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js b/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js index bf915cd8aa9..1ac759942b1 100644 --- a/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js +++ b/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js @@ -1,66 +1,55 @@ -import { buildPartialsUrl } from './utils'; +import { buildPartialsUrl } from './utils' -export function initFulltextSearchSuggestion (fulltextSearchSuggestion) { - const isLoading = showLoadingIndicators(fulltextSearchSuggestion); +export function initFulltextSearchSuggestion(fulltextSearchSuggestion) { + const isLoading = showLoadingIndicators(fulltextSearchSuggestion) if (isLoading) { - const query = fulltextSearchSuggestion.dataset.query; - getPartials(fulltextSearchSuggestion, query); + const query = fulltextSearchSuggestion.dataset.query + getPartials(fulltextSearchSuggestion, query) } } -function showLoadingIndicators (fulltextSearchSuggestion) { - let isLoading = false; - const loadingIndicator = - fulltextSearchSuggestion.querySelector('.loadingIndicator'); +function showLoadingIndicators(fulltextSearchSuggestion) { + let isLoading = false + const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator') if (loadingIndicator) { - isLoading = true; - loadingIndicator.classList.remove('hidden'); + isLoading = true + loadingIndicator.classList.remove('hidden') } - return isLoading; + return isLoading } -async function getPartials (fulltextSearchSuggestion, query) { - return fetch(buildPartialsUrl('FulltextSearchSuggestion', { data: query })) +async function getPartials(fulltextSearchSuggestion, query) { + return fetch(buildPartialsUrl('FulltextSearchSuggestion', {data: query})) .then((resp) => { if (resp.status !== 200) { - throw new Error( - `Failed to fetch partials. Status code: ${resp.status}`, - ); + throw new Error(`Failed to fetch partials. Status code: ${resp.status}`) } - return resp.json(); + return resp.json() }) .then((data) => { - fulltextSearchSuggestion.innerHTML += data['partials']; - const loadingIndicator = - fulltextSearchSuggestion.querySelector('.loadingIndicator'); + fulltextSearchSuggestion.innerHTML += data['partials'] + const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator'); if (loadingIndicator) { - loadingIndicator.classList.add('hidden'); + loadingIndicator.classList.add('hidden') } }) .catch(() => { - const loadingIndicator = - fulltextSearchSuggestion.querySelector('.loadingIndicator'); + const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator') if (loadingIndicator) { - loadingIndicator.classList.add('hidden'); + loadingIndicator.classList.add('hidden') } - const existingRetryAffordance = fulltextSearchSuggestion.querySelector( - '.fulltext-suggestions__retry', - ); + const existingRetryAffordance = fulltextSearchSuggestion.querySelector('.fulltext-suggestions__retry') if (existingRetryAffordance) { - existingRetryAffordance.classList.remove('hidden'); + existingRetryAffordance.classList.remove('hidden') } else { - fulltextSearchSuggestion.insertAdjacentHTML( - 'afterbegin', - renderRetryLink(), - ); - const retryAffordance = fulltextSearchSuggestion.querySelector( - '.fulltext-suggestions__retry', - ); + fulltextSearchSuggestion.insertAdjacentHTML('afterbegin', renderRetryLink()) + const retryAffordance = fulltextSearchSuggestion.querySelector('.fulltext-suggestions__retry') retryAffordance.addEventListener('click', () => { - retryAffordance.classList.add('hidden'); - getPartials(fulltextSearchSuggestion, query); - }); + retryAffordance.classList.add('hidden') + getPartials(fulltextSearchSuggestion, query) + }) } - }); + + }) } /** @@ -68,6 +57,6 @@ async function getPartials (fulltextSearchSuggestion, query) { * * @returns {string} HTML for a retry link. */ -function renderRetryLink () { - return '<span class="fulltext-suggestions__retry">Failed to fetch fulltext search suggestions. <button type="button" style="border:none;background:none;color:blue;text-decoration:underline;cursor:pointer;">Retry?</button></span>'; +function renderRetryLink() { + return '<span class="fulltext-suggestions__retry">Failed to fetch fulltext search suggestions. <a href="javascript:;">Retry?</a></span>' } diff --git a/openlibrary/plugins/openlibrary/js/go-back-links.js b/openlibrary/plugins/openlibrary/js/go-back-links.js index 35930459b81..9a02f46bd3b 100644 --- a/openlibrary/plugins/openlibrary/js/go-back-links.js +++ b/openlibrary/plugins/openlibrary/js/go-back-links.js @@ -3,15 +3,15 @@ * where to redirect the user * * @param {NodeList<HTMLElement>} goBackLinks - */ -export function initGoBackLinks (goBackLinks) { +*/ +export function initGoBackLinks(goBackLinks) { for (const link of goBackLinks) { link.addEventListener('click', () => { if (history.length > 2) { - history.go(-1); + history.go(-1) } else { - window.location.href = '/'; + window.location.href='/' } - }); + }) } } diff --git a/openlibrary/plugins/openlibrary/js/goodreads_import.js b/openlibrary/plugins/openlibrary/js/goodreads_import.js index 60b3049e217..2a1fdfd46f3 100644 --- a/openlibrary/plugins/openlibrary/js/goodreads_import.js +++ b/openlibrary/plugins/openlibrary/js/goodreads_import.js @@ -1,6 +1,7 @@ import Promise from 'promise-polyfill'; export function initGoodreadsImport() { + var count, prevPromise; $(document).on('click', 'th.toggle-all input', function () { @@ -9,7 +10,8 @@ export function initGoodreadsImport() { $(this).prop('checked', checked); if (checked) { $(this).attr('checked', 'checked'); - } else { + } + else { $(this).removeAttr('checked'); } }); @@ -20,7 +22,8 @@ export function initGoodreadsImport() { $(document).on('click', 'input.add-book', function () { if ($(this).prop('checked')) { $(this).attr('checked', 'checked'); - } else { + } + else { $(this).removeAttr('checked'); } const l = $('.add-book[checked*="checked"]').length; @@ -35,14 +38,12 @@ export function initGoodreadsImport() { elem.innerHTML = `${value} Books`; if (value * (100 / l) >= 100) { elem.innerHTML = ''; - $('#myBar').append( - '<a href="/account/books" style="color:white"> Go to your Reading Log </a>', - ); + $('#myBar').append('<a href="/account/books" style="color:white"> Go to your Reading Log </a>'); $('.cancel-button').addClass('hidden'); } } - $('.import-submit').on('click', () => { + $('.import-submit').on('click', function () { $('#myProgress').removeClass('hidden'); $('.cancel-button').removeClass('hidden'); $('input.import-submit').addClass('hidden'); @@ -57,14 +58,13 @@ export function initGoodreadsImport() { var value = JSON.parse(input.val().replace(/'/g, '"')); var shelf = value['Exclusive Shelf']; var shelf_id = 0; - const hasFailure = () => - $(`[isbn=${value['ISBN']}]`).hasClass('import-failure'); - const fail = (reason) => { + const hasFailure = function () { + return $(`[isbn=${value['ISBN']}]`).hasClass('import-failure'); + }; + const fail = function (reason) { if (!hasFailure()) { const element = $(`[isbn=${value['ISBN']}]`); - element.append( - `<td class="error-imported">Error</td><td class="error-imported">${reason}</td>'`, - ); + element.append(`<td class="error-imported">Error</td><td class="error-imported">${reason}</td>'`) element.removeClass('selected'); element.addClass('import-failure'); } @@ -86,81 +86,72 @@ export function initGoodreadsImport() { return; } - prevPromise = prevPromise - .then(() => { - // prevPromise changes in each iteration - $(`[isbn=${value['ISBN']}]`).addClass('selected'); - return getWork(value['ISBN']); // return a new Promise - }) - .then((data) => { - var obj = JSON.parse(data); - $.ajax({ - url: `${obj['works'][0].key}/bookshelves.json`, - type: 'POST', - data: { - dont_remove: true, - edition_id: obj['key'], - bookshelf_id: shelf_id, - }, - dataType: 'json', - }) - .fail(() => { - fail('Failed to add book to reading log'); - }) - .done(() => { - if (value['My Rating'] !== '0') { - return $.ajax({ - url: `${obj['works'][0].key}/ratings.json`, - type: 'POST', - data: { - rating: parseInt(value['My Rating']), - edition_id: obj['key'], - bookshelf_id: shelf_id, - }, - dataType: 'json', - fail: () => { - fail('Failed to add rating'); - }, - }); + prevPromise = prevPromise.then(function () { // prevPromise changes in each iteration + $(`[isbn=${value['ISBN']}]`).addClass('selected'); + return getWork(value['ISBN']); // return a new Promise + }).then(function (data) { + var obj = JSON.parse(data); + $.ajax({ + url: `${obj['works'][0].key}/bookshelves.json`, + type: 'POST', + data: { + dont_remove: true, + edition_id: obj['key'], + bookshelf_id: shelf_id + }, + dataType: 'json' + }).fail(function () { + fail('Failed to add book to reading log'); + }).done(function () { + if (value['My Rating'] !== '0') { + return $.ajax({ + url: `${obj['works'][0].key}/ratings.json`, + type: 'POST', + data: { + rating: parseInt(value['My Rating']), + edition_id: obj['key'], + bookshelf_id: shelf_id + }, + dataType: 'json', + fail: function () { + fail('Failed to add rating'); } - }) - .then(() => { - if (value['Date Read'] !== '') { - const date_read = value['Date Read'].split('/'); // Format: "YYYY/MM/DD" - return $.ajax({ - url: `${obj['works'][0].key}/check-ins`, - type: 'POST', - data: JSON.stringify({ - edition_key: obj['key'], - event_type: 3, // BookshelfEvent.FINISH - year: parseInt(date_read[0]), - month: parseInt(date_read[1]), - day: parseInt(date_read[2]), - }), - dataType: 'json', - contentType: 'application/json', - beforeSend: (xhr) => { - xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.setRequestHeader('Accept', 'application/json'); - }, - fail: () => { - fail('Failed to set the read date'); - }, - }); + }); + } + }).then(function () { + if (value['Date Read'] !== '') { + const date_read = value['Date Read'].split('/'); // Format: "YYYY/MM/DD" + return $.ajax({ + url: `${obj['works'][0].key}/check-ins`, + type: 'POST', + data: JSON.stringify({ + edition_key: obj['key'], + event_type: 3, // BookshelfEvent.FINISH + year: parseInt(date_read[0]), + month: parseInt(date_read[1]), + day: parseInt(date_read[2]) + }), + dataType: 'json', + contentType: 'application/json', + beforeSend: function (xhr) { + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('Accept', 'application/json'); + }, + fail: function () { + fail('Failed to set the read date'); } }); - if (!hasFailure()) { - $(`[isbn=${value['ISBN']}]`).append( - '<td class="success-imported">Imported</td>', - ); - $(`[isbn=${value['ISBN']}]`).removeClass('selected'); } - func1(++count); - }) - .catch(() => { - fail('Book not in collection'); - func1(++count); }); + if (!hasFailure()) { + $(`[isbn=${value['ISBN']}]`).append('<td class="success-imported">Imported</td>') + $(`[isbn=${value['ISBN']}]`).removeClass('selected'); + } + func1(++count); + }).catch(function () { + fail('Book not in collection'); + func1(++count); + }); }); $('td.books-wo-isbn').each(function () { @@ -169,11 +160,11 @@ export function initGoodreadsImport() { }); function getWork(isbn) { - return new Promise((resolve, reject) => { + return new Promise(function (resolve, reject) { var request = new XMLHttpRequest(); request.open('GET', `/isbn/${isbn}.json`); - request.onload = () => { + request.onload = function () { if (request.status === 200) { resolve(request.response); // we get the data here, so resolve the Promise } else { @@ -181,7 +172,7 @@ export function initGoodreadsImport() { } }; - request.onerror = () => { + request.onerror = function () { reject(Error('Error fetching data.')); // error occurred, so reject the Promise }; diff --git a/openlibrary/plugins/openlibrary/js/graphs/index.js b/openlibrary/plugins/openlibrary/js/graphs/index.js index 1db0fc43206..192926e9842 100644 --- a/openlibrary/plugins/openlibrary/js/graphs/index.js +++ b/openlibrary/plugins/openlibrary/js/graphs/index.js @@ -1,7 +1,7 @@ +import { loadGraphIfExists, loadEditionsGraph } from './plot'; import options from './options.js'; -import { loadEditionsGraph, loadGraphIfExists } from './plot'; -export function plotAdminGraphs () { +export function plotAdminGraphs() { loadGraphIfExists('editgraph', {}, 'edit(s) on'); loadGraphIfExists('membergraph', {}, 'new members(s) on'); loadGraphIfExists('works_minigraph', {}, ' works on '); @@ -13,7 +13,7 @@ export function plotAdminGraphs () { loadGraphIfExists('books-added-per-day', options.booksAdded); } -export function initHomepageGraphs () { +export function initHomepageGraphs() { loadGraphIfExists('visitors-graph', {}, 'unique visitors on', '#e44028'); loadGraphIfExists('members-graph', {}, 'new members on', '#748d36'); loadGraphIfExists('edits-graph', {}, 'catalog edits on', '#00636a'); @@ -21,13 +21,13 @@ export function initHomepageGraphs () { loadGraphIfExists('ebooks-graph', {}, 'ebooks borrowed on', '#35672e'); } -export function initPublishersGraph () { +export function initPublishersGraph() { if (document.getElementById('chartPubHistory')) { loadEditionsGraph('chartPubHistory', {}, 'editions in'); } } -export function init () { +export function init() { plotAdminGraphs(); initHomepageGraphs(); initPublishersGraph(); diff --git a/openlibrary/plugins/openlibrary/js/graphs/options.js b/openlibrary/plugins/openlibrary/js/graphs/options.js index 9ce2b858644..547c7ea5f8c 100644 --- a/openlibrary/plugins/openlibrary/js/graphs/options.js +++ b/openlibrary/plugins/openlibrary/js/graphs/options.js @@ -11,15 +11,15 @@ const booksAdded = { hoverable: true, show: true, borderWidth: 1, - borderColor: '#d9d9d9', + borderColor: '#d9d9d9' }, xaxis: { - mode: 'time', + mode: 'time' }, legend: { show: true, - position: 'nw', - }, + position: 'nw' + } }; const loans = { @@ -35,21 +35,21 @@ const loans = { hoverable: true, show: true, borderWidth: 1, - borderColor: '#d9d9d9', + borderColor: '#d9d9d9' }, xaxis: { - mode: 'time', + mode: 'time' }, yaxis: { - position: 'right', + position: 'right' }, legend: { show: true, - position: 'nw', - }, + position: 'nw' + } }; export default { booksAdded, - loans, + loans }; diff --git a/openlibrary/plugins/openlibrary/js/graphs/plot.js b/openlibrary/plugins/openlibrary/js/graphs/plot.js index a7d3fe356fe..b4034a2ea39 100644 --- a/openlibrary/plugins/openlibrary/js/graphs/plot.js +++ b/openlibrary/plugins/openlibrary/js/graphs/plot.js @@ -15,27 +15,22 @@ import 'flot/jquery.flot.time.js'; * - http://localhost:8080/subjects/fantasy#sort=date_published&ebooks=true * - http://localhost:8080/publishers/Barnes_&_Noble */ -export function loadEditionsGraph () { - var data, options, placeholder, plot, dateFrom, dateTo, previousPoint; - data = [ - { - data: JSON.parse( - document.getElementById('graph-json-chartPubHistory').textContent, - ), - }, - ]; +export function loadEditionsGraph() { + var data, options, placeholder, + plot, dateFrom, dateTo, previousPoint; + data = [{data: JSON.parse(document.getElementById('graph-json-chartPubHistory').textContent)}]; options = { series: { bars: { show: true, fill: 0.6, color: '#615132', - align: 'center', + align: 'center' }, points: { - show: true, + show: true }, - color: '#615132', + color: '#615132' }, grid: { hoverable: true, @@ -44,7 +39,7 @@ export function loadEditionsGraph () { tickColor: '#d9d9d9', borderWidth: 1, borderColor: '#d9d9d9', - backgroundColor: '#fff', + backgroundColor: '#fff' }, xaxis: { tickDecimals: 0 }, yaxis: { tickDecimals: 0 }, @@ -52,31 +47,28 @@ export function loadEditionsGraph () { crosshair: { mode: 'xy', color: 'rgba(000, 099, 106, 0.4)', - lineWidth: 1, - }, + lineWidth: 1 + } }; placeholder = $('#chartPubHistory'); - function showTooltip (x, y, contents) { - $(`<div id="chartLabel">${contents}</div>`) - .css({ - position: 'absolute', - display: 'none', - top: y + 12, - left: x + 12, - border: '1px solid #615132', - padding: '2px', - 'background-color': '#fffdcd', - color: '#615132', - 'font-size': '11px', - opacity: 0.9, - 'z-index': 100, - }) - .appendTo('body') - .fadeIn(200); + function showTooltip(x, y, contents) { + $(`<div id="chartLabel">${contents}</div>`).css({ + position: 'absolute', + display: 'none', + top: y + 12, + left: x + 12, + border: '1px solid #615132', + padding: '2px', + 'background-color': '#fffdcd', + color: '#615132', + 'font-size': '11px', + opacity: 0.90, + 'z-index': 100 + }).appendTo('body').fadeIn(200); } previousPoint = null; - placeholder.bind('plothover', (event, pos, item) => { + placeholder.bind('plothover', function (event, pos, item) { var x, y; $('#x').text(pos.x.toFixed(0)); $('#y').text(pos.y.toFixed(0)); @@ -87,37 +79,40 @@ export function loadEditionsGraph () { x = item.datapoint[0].toFixed(0); y = item.datapoint[1].toFixed(0); if (y === 1) { - showTooltip(item.pageX, item.pageY, `${y} edition in ${x}`); + showTooltip(item.pageX, item.pageY, + `${y} edition in ${x}`); } else { - showTooltip(item.pageX, item.pageY, `${y} editions in ${x}`); + showTooltip(item.pageX, item.pageY, + `${y} editions in ${x}`); } } - } else { + } + else { $('#chartLabel').remove(); previousPoint = null; } }); - placeholder.bind('plotclick', (event, pos, item) => { + placeholder.bind('plotclick', function (event, pos, item) { + if (item) { plot.unhighlight(); const yearFrom = item.datapoint[0].toFixed(0); applyDateFilter(yearFrom, yearFrom); - plot.highlight(item.series, item.datapoint); - } else { + plot.highlight(item.series,item.datapoint); + } + else { plot.unhighlight(); } }); - placeholder.bind('plotselected', (event, ranges) => { - plot = $.plot( - placeholder, - data, + placeholder.bind('plotselected', function (event, ranges) { + plot = $.plot(placeholder, data, $.extend(true, {}, options, { xaxis: { min: ranges.xaxis.from, max: ranges.xaxis.to }, - yaxis: { min: ranges.yaxis.from, max: ranges.yaxis.to }, - }), + yaxis: { min: ranges.yaxis.from, max: ranges.yaxis.to } + }) ); const yearFrom = ranges.xaxis.from.toFixed(0); @@ -125,17 +120,8 @@ export function loadEditionsGraph () { applyDateFilter(yearFrom, yearTo); }); - function applyDateFilter ( - yearFrom, - yearTo, - hideSelector = '.chartUnzoom', - showSelector = '.chartZoom', - ) { - document.dispatchEvent( - new CustomEvent('filter', { - detail: { yearFrom: yearFrom, yearTo: yearTo }, - }), - ); + function applyDateFilter(yearFrom, yearTo, hideSelector='.chartUnzoom', showSelector='.chartZoom') { + document.dispatchEvent(new CustomEvent('filter', { detail: { yearFrom: yearFrom, yearTo: yearTo } })); $(hideSelector).hide(); $(showSelector).removeClass('hidden').show(); } @@ -144,7 +130,7 @@ export function loadEditionsGraph () { dateFrom = plot.getAxes().xaxis.min.toFixed(0); dateTo = plot.getAxes().xaxis.max.toFixed(0); - $('.resetSelection').on('click', () => { + $('.resetSelection').on('click', function() { plot = $.plot(placeholder, data, options); const yearFrom = plot.getAxes().xaxis.min.toFixed(0); @@ -152,42 +138,37 @@ export function loadEditionsGraph () { applyDateFilter(yearFrom, yearTo, '.chartZoom', '.chartUnzoom'); }); - $('.chartYaxis').css({ top: '60px', left: '-60px' }); + $('.chartYaxis').css({top: '60px', left: '-60px'}); - if (dateFrom === dateTo - 1) { + if (dateFrom === (dateTo - 1)) { $('.clickdata').text(`Published in ${dateFrom}`); } else { - $('.clickdata').text(`Published between ${dateFrom} & ${dateTo - 1}.`); + $('.clickdata').text(`Published between ${dateFrom} & ${dateTo-1}.`); } } -export function plot_minigraph (node, data) { +export function plot_minigraph(node, data) { var options = { series: { lines: { show: true, fill: 0, - color: '#748d36', + color: '#748d36' }, points: { - show: false, + show: false }, - color: '#748d36', + color: '#748d36' }, grid: { hoverable: false, - show: false, - }, + show: false + } }; $.plot(node, [data], options); } -export function plot_tooltip_graph ( - node, - data, - tooltip_message, - color = '#748d36', -) { +export function plot_tooltip_graph(node, data, tooltip_message, color='#748d36') { var i, options, graph; // empty set of rows. Escape early. if (!data.length) { @@ -205,44 +186,41 @@ export function plot_tooltip_graph ( fillColor: color, color, align: 'left', - barWidth: 24 * 60 * 60 * 1000, + barWidth: 24 * 60 * 60 * 1000 }, points: { - show: false, + show: false }, - color, + color }, grid: { hoverable: true, - show: false, + show: false }, xaxis: { - mode: 'time', - }, + mode: 'time' + } }; graph = $.plot(node, [data], options); - function showTooltip (x, y, contents) { - $(`<div id="chartLabelA">${contents}</div>`) - .css({ - position: 'absolute', - display: 'none', - top: y + 12, - left: x + 12, - border: '1px solid #ccc', - padding: '2px', - backgroundColor: '#efefef', - color: '#454545', - fontSize: '11px', - webkitBoxShadow: '1px 1px 3px #333', - mozBoxShadow: '1px 1px 1px #000', - boxShadow: '1px 1px 1px #000', - }) - .appendTo('body') - .fadeIn(200); + function showTooltip(x, y, contents) { + $(`<div id="chartLabelA">${contents}</div>`).css({ + position: 'absolute', + display: 'none', + top: y + 12, + left: x + 12, + border: '1px solid #ccc', + padding: '2px', + backgroundColor: '#efefef', + color: '#454545', + fontSize: '11px', + webkitBoxShadow: '1px 1px 3px #333', + mozBoxShadow: '1px 1px 1px #000', + boxShadow: '1px 1px 1px #000' + }).appendTo('body').fadeIn(200); } - node.bind('plothover', (event, pos, item) => { + node.bind('plothover', function (event, pos, item) { var date, milli, x, y; $('#x').text(pos.x); $('#y').text(pos.y.toFixed(0)); @@ -268,22 +246,19 @@ export function plot_tooltip_graph ( * @param {string} [color] in hexidecimal to apply to the bars of a tooltip graph. * Ignored if options and no tooltip_message is passed. */ -export function loadGraph ( - id, - options = {}, - tooltip_message = '', - color = null, -) { +export function loadGraph(id, options = {}, tooltip_message = '', color = null) { let data; const node = document.getElementById(id); const graphSelector = `graph-json-${id}`; const dataSource = document.getElementById(graphSelector); if (!node) { - throw new Error(`No graph associated with ${id} on the page.`); + throw new Error( + `No graph associated with ${id} on the page.` + ); } if (!dataSource) { throw new Error( - `No data associated with ${id} - make sure a script tag with type text/json and id "${graphSelector}" is present on the page.`, + `No data associated with ${id} - make sure a script tag with type text/json and id "${graphSelector}" is present on the page.` ); } else { try { @@ -294,7 +269,7 @@ export function loadGraph ( if (tooltip_message) { return plot_tooltip_graph($(node), data, tooltip_message, color); } else { - return $.plot($(node), [{ data: data }], options); + return $.plot($(node), [{data: data}], options); } } } @@ -307,7 +282,7 @@ export function loadGraph ( * @param {string} [color] in hexidecimal to apply to the bars of a tooltip graph. * Ignored if options and no tooltip_message is passed. */ -export function loadGraphIfExists (id, options, tooltip_message, color) { +export function loadGraphIfExists(id, options, tooltip_message, color) { if ($(`#${id}`).length) { loadGraph(id, options, tooltip_message, color); } diff --git a/openlibrary/plugins/openlibrary/js/i18n.js b/openlibrary/plugins/openlibrary/js/i18n.js index d84f1b35c33..2110502ff3a 100644 --- a/openlibrary/plugins/openlibrary/js/i18n.js +++ b/openlibrary/plugins/openlibrary/js/i18n.js @@ -2,9 +2,11 @@ export function sprintf(s) { var args = arguments; var i = 1; - return s.replace(/%[%s]/g, (match) => { - if (match === '%%') return '%'; - else return args[i++]; + return s.replace(/%[%s]/g, function(match) { + if (match === '%%') + return '%'; + else + return args[i++]; }); } @@ -17,5 +19,5 @@ export function ugettext(s) { // used in templates/borrow/read.html export function ungettext(s1, s2, n) { - return n === 1 ? s1 : s2; + return n === 1? s1 : s2; } diff --git a/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js b/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js index 4d2f8eba2d4..ede17d1940c 100644 --- a/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js +++ b/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js @@ -3,27 +3,28 @@ * * @param {*} element - The element to be modified by the handleMessageEvent function. */ -export function initMessageEventListener (element) { +export function initMessageEventListener(element) { /** - * Handles messages from archive.org and performs actions based on the message type. - * - * @param {MessageEvent} e - The message event. - */ - function handleMessageEvent (e) { + * Handles messages from archive.org and performs actions based on the message type. + * + * @param {MessageEvent} e - The message event. + */ + function handleMessageEvent(e) { if (!/[./]archive\.org$$/.test(e.origin)) return; if (e.data.type === 'resize') { element.setAttribute('scrolling', 'no'); if (e.data.height) element.style.height = `${e.data.height}px`; - } else if (e.data.type === 's3-keys') { - const s3AccessInput = document.querySelector('#access'); - const s3SecretInput = document.querySelector('#secret'); - s3AccessInput.value = e.data.s3.access; - s3SecretInput.value = e.data.s3.secret; + } + else if (e.data.type === 's3-keys') { + const s3AccessInput = document.querySelector('#access') + const s3SecretInput = document.querySelector('#secret') + s3AccessInput.value = e.data.s3.access + s3SecretInput.value = e.data.s3.secret - const loginForm = document.querySelector('#register'); - loginForm.action = '/account/login'; - loginForm.submit(); + const loginForm = document.querySelector('#register') + loginForm.action = '/account/login' + loginForm.submit() } } diff --git a/openlibrary/plugins/openlibrary/js/idValidation.js b/openlibrary/plugins/openlibrary/js/idValidation.js index 982bf84ab4f..23b9532fa42 100644 --- a/openlibrary/plugins/openlibrary/js/idValidation.js +++ b/openlibrary/plugins/openlibrary/js/idValidation.js @@ -3,7 +3,7 @@ * @param {String} isbn ISBN string for parsing * @returns {String} parsed isbn string */ -export function parseIsbn (isbn) { +export function parseIsbn(isbn) { return isbn.replace(/[ -]/g, ''); } @@ -13,7 +13,7 @@ export function parseIsbn (isbn) { * @param {String} isbn ISBN string to check * @returns {boolean} true if the isbn has a valid format */ -export function isFormatValidIsbn10 (isbn) { +export function isFormatValidIsbn10(isbn) { const regex = /^[0-9]{9}[0-9X]$/; return regex.test(isbn); } @@ -24,12 +24,12 @@ export function isFormatValidIsbn10 (isbn) { * @param {String} isbn ISBN string for validating * @returns {boolean} true if ISBN string is a valid ISBN 10 */ -export function isChecksumValidIsbn10 (isbn) { +export function isChecksumValidIsbn10(isbn) { const chars = isbn.replace('X', 'A').split(''); chars.reverse(); const sum = chars - .map((char, idx) => (idx + 1) * parseInt(char, 16)) + .map((char, idx) => ((idx + 1) * parseInt(char, 16))) .reduce((acc, sum) => acc + sum, 0); // The ISBN 10 is valid if the checksum mod 11 is 0. @@ -42,21 +42,21 @@ export function isChecksumValidIsbn10 (isbn) { * @param {String} isbn ISBN string to check * @returns {boolean} true if the isbn has a valid format */ -export function isFormatValidIsbn13 (isbn) { +export function isFormatValidIsbn13(isbn) { const regex = /^[0-9]{13}$/; return regex.test(isbn); } /** - * Verify the checksum for ISBN 13. - * Adapted from https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch04s13.html - * @param {String} isbn ISBN string for validating - * @returns {Boolean} true if ISBN string is a valid ISBN 13 - */ -export function isChecksumValidIsbn13 (isbn) { +* Verify the checksum for ISBN 13. +* Adapted from https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch04s13.html +* @param {String} isbn ISBN string for validating +* @returns {Boolean} true if ISBN string is a valid ISBN 13 +*/ +export function isChecksumValidIsbn13(isbn) { const chars = isbn.split(''); const sum = chars - .map((char, idx) => ((idx % 2) * 2 + 1) * parseInt(char, 10)) + .map((char, idx) => ((idx % 2 * 2 + 1) * parseInt(char, 10))) .reduce((sum, num) => sum + num, 0); // The ISBN 13 is valid if the checksum mod 10 is 0. @@ -69,23 +69,22 @@ export function isChecksumValidIsbn13 (isbn) { * @param {String} lccn LCCN string for parsing * @returns {String} parsed LCCN string */ -export function parseLccn (lccn) { +export function parseLccn(lccn) { // cleaning initial lccn entry const parsed = lccn - // any alpha characters need to be lowercase + // any alpha characters need to be lowercase .toLowerCase() - // remove any whitespace + // remove any whitespace .replace(/\s/g, '') - // remove leading and trailing dashes - .replace(/^[-]+/, '') - .replace(/[-]+$/, '') - // remove any revised text + // remove leading and trailing dashes + .replace(/^[-]+/, '').replace(/[-]+$/, '') + // remove any revised text .replace(/rev.*/g, '') - // remove first forward slash and everything to its right + // remove first forward slash and everything to its right .replace(/[/]+.*$/, ''); // splitting at hyphen and padding the right hand value with zeros up to 6 characters - const groups = parsed.match(/(.+)-+([0-9]+)/); + const groups = parsed.match(/(.+)-+([0-9]+)/) if (groups && groups.length === 3) { return groups[1] + groups[2].padStart(6, '0'); } @@ -98,7 +97,7 @@ export function parseLccn (lccn) { * @param {String} lccn LCCN string to test for valid syntax * @returns {boolean} true if given LCCN is valid syntax, false otherwise */ -export function isValidLccn (lccn) { +export function isValidLccn(lccn) { // matching parsed entry to regex representing valid lccn // regex taken from /openlibrary/utils/lccn.py const regex = /^([a-z]|[a-z]?([a-z]{2}|[0-9]{2})|[a-z]{2}[0-9]{2})?[0-9]{8}$/; @@ -110,15 +109,13 @@ export function isValidLccn (lccn) { * @param {String} oclc OCLC string for parsing * @returns {String} parsed OCLC string */ -export function parseOclc (oclc) { +export function parseOclc(oclc) { // cleaning initial oclc entry - return ( - oclc + return oclc // remove any whitespace - .replace(/\s/g, '') + .replace(/\s/g, '') // remove leading/padding zeroes - .replace(/^0+/, '') - ); + .replace(/^0+/, ''); } /** @@ -130,7 +127,7 @@ export function parseOclc (oclc) { * @param {String} oclc OCLC string to test for valid syntax * @returns {boolean} true if given OCLC is valid syntax, false otherwise */ -export function isValidOclc (oclc) { +export function isValidOclc(oclc) { // matching parsed entry to regex representing valid oclc const regex = /^[1-9][0-9]*$/; return regex.test(oclc); @@ -145,7 +142,9 @@ export function isValidOclc (oclc) { * @param {String} newId New identifier entry to be checked * @returns {boolean} true if the new identifier has already been entered */ -export function isIdDupe (idEntries, newId) { +export function isIdDupe(idEntries, newId) { // check each current entry value against new identifier - return Array.from(idEntries).some((entry) => entry['value'] === newId); + return Array.from(idEntries).some( + entry => entry['value'] === newId + ); } diff --git a/openlibrary/plugins/openlibrary/js/ile/index.js b/openlibrary/plugins/openlibrary/js/ile/index.js index 097a52f8f7c..88478a792c1 100644 --- a/openlibrary/plugins/openlibrary/js/ile/index.js +++ b/openlibrary/plugins/openlibrary/js/ile/index.js @@ -1,12 +1,11 @@ // @ts-check - -import { BulkTagger } from '../bulk-tagger/BulkTagger.js'; -import { renderBulkTagger } from '../bulk-tagger/index.js'; import SelectionManager from './utils/SelectionManager/SelectionManager.js'; +import { renderBulkTagger } from '../bulk-tagger/index.js'; +import { BulkTagger } from '../bulk-tagger/BulkTagger.js'; export function init() { const ile = new IntegratedLibrarianEnvironment(); - // @ts-expect-error + // @ts-ignore window.ILE = ile; ile.init(); } @@ -15,8 +14,7 @@ export class IntegratedLibrarianEnvironment { constructor() { this.selectionManager = new SelectionManager(this); /** This is the main ILE toolbar. Should be moved to a Vue component. */ - this.$toolbar = $( - ` + this.$toolbar = $(` <div id="ile-toolbar"> <div id="ile-selections"> <div id="ile-drag-status"> @@ -27,22 +25,21 @@ export class IntegratedLibrarianEnvironment { </div> <div id="ile-drag-actions"></div> <div id="ile-hidden-forms"></div> - </div>`.trim(), - ); + </div>`.trim()); this.$selectionActions = this.$toolbar.find('#ile-selection-actions'); this.$statusText = this.$toolbar.find('.text'); this.$statusImages = this.$toolbar.find('.images ul'); this.$actions = this.$toolbar.find('#ile-drag-actions'); this.$hiddenForms = this.$toolbar.find('#ile-hidden-forms'); - this.bulkTagger = null; + this.bulkTagger = null } init() { - // Add the ILE toolbar to bottom of screen + // Add the ILE toolbar to bottom of screen $(document.body).append(this.$toolbar.hide()); // Ready bulk tagger: - this.createBulkTagger(); + this.createBulkTagger() this.selectionManager.init(); } @@ -54,11 +51,11 @@ export class IntegratedLibrarianEnvironment { } /** - * Resets the status bar. - */ + * Resets the status bar. + */ reset() { for (const elem of $('.ile-selected')) { - elem.classList.remove('ile-selected'); + elem.classList.remove('ile-selected') } this.setStatusText(''); this.$selectionActions.empty(); @@ -67,39 +64,39 @@ export class IntegratedLibrarianEnvironment { } /** - * Clears all items selected in SelectionManager. - * - * This indirectly calls `IntegratedLibrarianEnvironment.reset()`. - */ + * Clears all items selected in SelectionManager. + * + * This indirectly calls `IntegratedLibrarianEnvironment.reset()`. + */ clearAndReset() { - this.selectionManager.clearSelectedItems(); + this.selectionManager.clearSelectedItems() } /** - * Creates a new Bulk Tagger component and attaches it to the DOM. - * - * Sets the value of `IntegratedLibrarianEnvironment.bulkTagger` - */ + * Creates a new Bulk Tagger component and attaches it to the DOM. + * + * Sets the value of `IntegratedLibrarianEnvironment.bulkTagger` + */ createBulkTagger() { - const target = this.$hiddenForms[0]; - target.innerHTML += renderBulkTagger(); - const bulkTaggerElem = document.querySelector('.bulk-tagging-form'); - // @ts-expect-error - this.bulkTagger = new BulkTagger(bulkTaggerElem); - this.bulkTagger.initialize(); + const target = this.$hiddenForms[0] + target.innerHTML += renderBulkTagger() + const bulkTaggerElem = document.querySelector('.bulk-tagging-form') + // @ts-ignore + this.bulkTagger = new BulkTagger(bulkTaggerElem) + this.bulkTagger.initialize() } /** - * Updates the Bulk Tagger with the selected works, then displays the tagger. - * - * @param {Array<String>} workIds - * @param {boolean} isBookPageEdit `true` if the bulk tagger is opened on a /books or /works page - */ + * Updates the Bulk Tagger with the selected works, then displays the tagger. + * + * @param {Array<String>} workIds + * @param {boolean} isBookPageEdit `true` if the bulk tagger is opened on a /books or /works page + */ updateAndShowBulkTagger(workIds, isBookPageEdit = false) { if (this.bulkTagger) { - this.bulkTagger.isBookPageEdit = isBookPageEdit; - this.bulkTagger.updateWorks(workIds); - this.bulkTagger.showTaggingMenu(); + this.bulkTagger.isBookPageEdit = isBookPageEdit + this.bulkTagger.updateWorks(workIds) + this.bulkTagger.showTaggingMenu() } } } diff --git a/openlibrary/plugins/openlibrary/js/ile/utils/ol.js b/openlibrary/plugins/openlibrary/js/ile/utils/ol.js index 374ff134517..2727cc5faed 100644 --- a/openlibrary/plugins/openlibrary/js/ile/utils/ol.js +++ b/openlibrary/plugins/openlibrary/js/ile/utils/ol.js @@ -11,12 +11,12 @@ import uniqBy from 'lodash/uniqBy'; * @param {WorkOLID} old_work * @param {WorkOLID} new_work */ -export async function move_to_work (edition_ids, old_work, new_work) { +export async function move_to_work(edition_ids, old_work, new_work) { for (const olid of edition_ids) { const url = `/books/${olid}.json`; - const record = await fetch(url).then((r) => r.json()); + const record = await fetch(url).then(r => r.json()); - record.works = [{ key: `/works/${new_work}` }]; + record.works = [{key: `/works/${new_work}`}]; record._comment = 'move to correct work'; const r = await fetch(url, { method: 'PUT', body: JSON.stringify(record) }); // eslint-disable-next-line no-console @@ -30,28 +30,22 @@ export async function move_to_work (edition_ids, old_work, new_work) { * @param {AuthorOLID} old_author * @param {AuthorOLID} new_author */ -export async function move_to_author (work_ids, old_author, new_author) { +export async function move_to_author(work_ids, old_author, new_author) { for (const olid of work_ids) { const url = `/works/${olid}.json`; - const record = await fetch(url).then((r) => r.json()); - if (record.authors.find((a) => a.author.key.includes(old_author))) { - record.authors = uniqBy( - record.authors.map((a) => { - if (!a.author.key.includes(old_author)) return a; + const record = await fetch(url).then(r => r.json()); + if (record.authors.find(a => a.author.key.includes(old_author))) { + record.authors = uniqBy(record.authors.map(a => { + if (!a.author.key.includes(old_author)) return a; - const copy = JSON.parse(JSON.stringify(a)); - copy.author.key = `/authors/${new_author}`; - return copy; - }), - (a) => a.author.key, - ); + const copy = JSON.parse(JSON.stringify(a)); + copy.author.key = `/authors/${new_author}`; + return copy; + }), a => a.author.key); record._comment = 'move to correct author'; - const r = await fetch(url, { - method: 'PUT', - body: JSON.stringify(record), - }); + const r = await fetch(url, { method: 'PUT', body: JSON.stringify(record) }); // eslint-disable-next-line no-console - console.log(`moved ${olid}; ${r.status}`); + console.log(`moved ${olid}; ${r.status}`) } else { // eslint-disable-next-line no-console console.warn(`${old_author} not in ${url}!`); diff --git a/openlibrary/plugins/openlibrary/js/index.js b/openlibrary/plugins/openlibrary/js/index.js index 7640fca2368..1a262393ba0 100644 --- a/openlibrary/plugins/openlibrary/js/index.js +++ b/openlibrary/plugins/openlibrary/js/index.js @@ -2,7 +2,7 @@ import 'jquery'; import { exposeGlobally } from './jsdef'; import initAnalytics from './ol.analytics'; import init from './ol.js'; -import initServiceWorker from './service-worker-init.js'; +import initServiceWorker from './service-worker-init.js' import '../../../../static/css/js-all.css'; // polyfill Promise support for IE11 import Promise from 'promise-polyfill'; @@ -24,7 +24,7 @@ initServiceWorker(); initAnalytics(); // Initialise some things -jQuery(() => { +jQuery(function () { // conditionally load polyfill for <details> tags (IE11) // See http://diveintohtml5.info/everything.html#details if (!('open' in document.createElement('details'))) { @@ -34,13 +34,13 @@ jQuery(() => { // Polyfill for .matches() if (!Element.prototype.matches) { Element.prototype.matches = - Element.prototype.msMatchesSelector || - Element.prototype.webkitMatchesSelector; + Element.prototype.msMatchesSelector || + Element.prototype.webkitMatchesSelector; } // Polyfill for .closest() if (!Element.prototype.closest) { - Element.prototype.closest = function (s) { + Element.prototype.closest = function(s) { let el = this; do { if (Element.prototype.matches.call(el, s)) return el; @@ -52,55 +52,40 @@ jQuery(() => { const $tabs = $('.ol-tabs'); if ($tabs.length) { - import(/* webpackChunkName: "tabs" */ './tabs').then((module) => - module.initTabs($tabs), - ); + import(/* webpackChunkName: "tabs" */ './tabs') + .then((module) => module.initTabs($tabs)); } const $autocomplete = $('.multi-input-autocomplete'); if ($autocomplete.length) { - import(/* webpackChunkName: "autocomplete" */ './autocomplete').then( - (module) => module.init($), - ); + import(/* webpackChunkName: "autocomplete" */ './autocomplete') + .then((module) => module.init($)); } // hide all images in .no-img $('.no-img img').hide(); // disable save button after click - $('button[name=\'_save\']').on('submit', function () { + $('button[name=\'_save\']').on('submit', function() { $(this).attr('disabled', true); }); // wmd editor const $markdownTextAreas = $('textarea.markdown'); if ($markdownTextAreas.length) { - import(/* webpackChunkName: "markdown-editor" */ './markdown-editor').then( - (module) => module.initMarkdownEditor($markdownTextAreas), - ); + import(/* webpackChunkName: "markdown-editor" */ './markdown-editor') + .then((module) => module.initMarkdownEditor($markdownTextAreas)); } init($); const edition = document.getElementById('addWork'); - const autocompleteAuthor = document.querySelector( - '.multi-input-autocomplete--author', - ); - const autocompleteSeries = document.querySelector( - '.multi-input-autocomplete--series', - ); - const autocompleteLanguage = document.querySelector( - '.multi-input-autocomplete--language', - ); - const autocompleteWorks = document.querySelector( - '.multi-input-autocomplete--works', - ); - const autocompleteSeeds = document.querySelector( - '.multi-input-autocomplete--seeds', - ); - const autocompleteSubjects = document.querySelector( - '.csv-autocomplete--subjects', - ); + const autocompleteAuthor = document.querySelector('.multi-input-autocomplete--author'); + const autocompleteSeries = document.querySelector('.multi-input-autocomplete--series'); + const autocompleteLanguage = document.querySelector('.multi-input-autocomplete--language'); + const autocompleteWorks = document.querySelector('.multi-input-autocomplete--works'); + const autocompleteSeeds = document.querySelector('.multi-input-autocomplete--seeds'); + const autocompleteSubjects = document.querySelector('.csv-autocomplete--subjects'); const addRowButton = document.getElementById('add_row_button'); const roles = document.querySelector('#roles'); const classifications = document.querySelector('#classifications'); @@ -110,258 +95,230 @@ jQuery(() => { // conditionally load for user edit page if ( edition || - autocompleteAuthor || - autocompleteSeries || - autocompleteLanguage || - autocompleteWorks || - autocompleteSeeds || - autocompleteSubjects || - addRowButton || - roles || - classifications || - excerpts || - links + autocompleteAuthor || autocompleteSeries || autocompleteLanguage || autocompleteWorks || + autocompleteSeeds || autocompleteSubjects || + addRowButton || roles || classifications || + excerpts || links ) { - import(/* webpackChunkName: "user-website" */ './edit').then((module) => { - if (edition) { - module.initEdit(); - } - if (addRowButton) { - module.initEditRow(); - } - if (excerpts) { - module.initEditExcerpts(); - } - if (links) { - module.initEditLinks(); - } - if (autocompleteAuthor) { - module.initAuthorMultiInputAutocomplete(); - } - if (autocompleteSeries) { - module.initSeriesMultiInputAutocomplete(); - } - if (roles) { - module.initRoleValidation(); - } - if (classifications) { - module.initClassificationValidation(); - } - if (autocompleteLanguage) { - module.initLanguageMultiInputAutocomplete(); - } - if (autocompleteWorks) { - module.initWorksMultiInputAutocomplete(); - } - if (autocompleteSubjects) { - module.initSubjectsAutocomplete(); - } - if (autocompleteSeeds) { - module.initSeedsMultiInputAutocomplete(); - } - }); + import(/* webpackChunkName: "user-website" */ './edit') + .then(module => { + if (edition) { + module.initEdit(); + } + if (addRowButton) { + module.initEditRow(); + } + if (excerpts) { + module.initEditExcerpts(); + } + if (links) { + module.initEditLinks(); + } + if (autocompleteAuthor) { + module.initAuthorMultiInputAutocomplete(); + } + if (autocompleteSeries) { + module.initSeriesMultiInputAutocomplete(); + } + if (roles) { + module.initRoleValidation(); + } + if (classifications) { + module.initClassificationValidation(); + } + if (autocompleteLanguage) { + module.initLanguageMultiInputAutocomplete(); + } + if (autocompleteWorks) { + module.initWorksMultiInputAutocomplete(); + } + if (autocompleteSubjects) { + module.initSubjectsAutocomplete(); + } + if (autocompleteSeeds) { + module.initSeedsMultiInputAutocomplete(); + } + }); } // conditionally load for author merge page const mergePageElement = document.querySelector('#author-merge-page'); const preMergePageElement = document.getElementById('preMerge'); if (mergePageElement || preMergePageElement) { - import(/* webpackChunkName: "merge" */ './merge').then((module) => { - if (mergePageElement) { - module.initAuthorMergePage(); - } - if (preMergePageElement) { - module.initAuthorView(); - } - }); + import(/* webpackChunkName: "merge" */ './merge') + .then(module => { + if (mergePageElement) { + module.initAuthorMergePage(); + } + if (preMergePageElement) { + module.initAuthorView(); + } + }); } // conditionally load for type changing input - const typeChanger = document.getElementById('type.key'); + const typeChanger = document.getElementById('type.key') if (typeChanger) { - import(/* webpackChunkName: "type-changer" */ './type_changer.js').then( - (module) => module.initTypeChanger(typeChanger), - ); + import(/* webpackChunkName: "type-changer" */ './type_changer.js') + .then(module => module.initTypeChanger(typeChanger)); } // conditionally load validation and submission js for registration form if (document.querySelector('form[name=signup]')) { - import(/* webpackChunkName: "signup" */ './signup.js').then((module) => - module.initSignupForm(), - ); + import(/* webpackChunkName: "signup" */'./signup.js') + .then(module => module.initSignupForm()); } // conditionally load submission js for login form if (document.querySelector('form[name=login]')) { - import(/* webpackChunkName: "signup" */ './signup.js').then((module) => - module.initLoginForm(), - ); + import(/* webpackChunkName: "signup" */'./signup.js') + .then(module => module.initLoginForm()); } // conditionally load clamping components const clampers = document.querySelectorAll('.clamp'); if (clampers.length) { - import(/* webpackChunkName: "clampers" */ './clampers.js').then( - (module) => { + import(/* webpackChunkName: "clampers" */ './clampers.js') + .then(module => { if (clampers.length) { module.initClampers(clampers); } - }, - ); + }); } // conditionally loads Goodreads import based on class in the page if (document.getElementsByClassName('import-table').length) { - import( - /* webpackChunkName: "goodreads-import" */ './goodreads_import.js' - ).then((module) => module.initGoodreadsImport()); + import(/* webpackChunkName: "goodreads-import" */'./goodreads_import.js') + .then(module => module.initGoodreadsImport()); } // conditionally load list seed item deletion dialog functionality based on id on lists pages if (document.getElementById('listResults')) { - import(/* webpackChunkName: "ListViewBody" */ './lists/ListViewBody.js'); + import(/* webpackChunkName: "ListViewBody" */'./lists/ListViewBody.js'); } // Enable any carousels in the page - const carouselElements = document.querySelectorAll( - '.carousel--progressively-enhanced', - ); + const carouselElements = document.querySelectorAll('.carousel--progressively-enhanced') if (carouselElements.length) { - import(/* webpackChunkName: "carousel" */ './carousel').then((module) => { - module.initialzeCarousels(carouselElements); - }); + import(/* webpackChunkName: "carousel" */ './carousel') + .then((module) => { + module.initialzeCarousels(carouselElements) + }) } if ($('script[type="text/json+graph"]').length > 0) { - import(/* webpackChunkName: "graphs" */ './graphs').then((module) => - module.init(), - ); + import(/* webpackChunkName: "graphs" */ './graphs') + .then((module) => module.init()); } - const readingLogCharts = document.querySelector('.readinglog-charts'); + const readingLogCharts = document.querySelector('.readinglog-charts') if (readingLogCharts) { - const readingLogConfig = JSON.parse(readingLogCharts.dataset.config); - import( - /* webpackChunkName: "readinglog-stats" */ './readinglog_stats' - ).then((module) => module.init(readingLogConfig)); + const readingLogConfig = JSON.parse(readingLogCharts.dataset.config) + import(/* webpackChunkName: "readinglog-stats" */ './readinglog_stats') + .then(module => module.init(readingLogConfig)); } if (document.getElementsByClassName('toast').length) { - import(/* webpackChunkName: "Toast" */ './Toast').then((module) => { - Array.from(document.getElementsByClassName('toast')).forEach( - (el) => new module.Toast($(el)), - ); - }); + import(/* webpackChunkName: "Toast" */ './Toast') + .then((module) => { + Array.from(document.getElementsByClassName('toast')) + .forEach(el => new module.Toast($(el))); + }); } if ($('.lazy-thing-preview').length) { - import( - /* webpackChunkName: "lazy-thing-preview" */ './lazy-thing-preview' - ).then((module) => new module.LazyThingPreview().init()); + import(/* webpackChunkName: "lazy-thing-preview" */ './lazy-thing-preview') + .then((module) => new module.LazyThingPreview().init()); } // Disable data export buttons on form submit - const patronImportForms = document.querySelectorAll('.patron-export-form'); + const patronImportForms = document.querySelectorAll('.patron-export-form') if (patronImportForms.length) { - import(/* webpackChunkName: "patron-exports" */ './patron_exports').then( - (module) => module.initPatronExportForms(patronImportForms), - ); + import(/* webpackChunkName: "patron-exports" */ './patron_exports') + .then(module => module.initPatronExportForms(patronImportForms)); } const $observationModalLinks = $('.observations-modal-link'); const $notesModalLinks = $('.notes-modal-link'); const $notesPageButtons = $('.note-page-buttons'); const $shareModalLinks = $('.share-modal-link'); - if ( - $observationModalLinks.length || - $notesModalLinks.length || - $notesPageButtons.length || - $shareModalLinks.length - ) { - import(/* webpackChunkName: "modal-links" */ './modals').then((module) => { - if ($observationModalLinks.length) { - module.initObservationsModal($observationModalLinks); - } - if ($notesModalLinks.length) { - module.initNotesModal($notesModalLinks); - } - if ($notesPageButtons.length) { - module.addNotesPageButtonListeners(); - } - if ($shareModalLinks.length) { - module.initShareModal($shareModalLinks); - } - }); + if ($observationModalLinks.length || $notesModalLinks.length || $notesPageButtons.length || $shareModalLinks.length) { + import(/* webpackChunkName: "modal-links" */ './modals') + .then(module => { + if ($observationModalLinks.length) { + module.initObservationsModal($observationModalLinks); + } + if ($notesModalLinks.length) { + module.initNotesModal($notesModalLinks); + } + if ($notesPageButtons.length) { + module.addNotesPageButtonListeners(); + } + if ($shareModalLinks.length) { + module.initShareModal($shareModalLinks) + } + }); } - const manageCoversElement = - document.getElementsByClassName('manageCovers').length; + + const manageCoversElement = document.getElementsByClassName('manageCovers').length; const addCoversElement = document.getElementsByClassName('imageIntro').length; - const saveCoversElement = - document.getElementsByClassName('imageSaved').length; + const saveCoversElement = document.getElementsByClassName('imageSaved').length; const coverForm = document.querySelector('.ol-cover-form--clipboard'); - if ( - addCoversElement || - manageCoversElement || - saveCoversElement || - coverForm - ) { - import(/* webpackChunkName: "covers" */ './covers').then((module) => { - if (manageCoversElement) { - module.initCoversChange(); - } - if (addCoversElement) { - module.initCoversAddManage(); - } - if (saveCoversElement) { - module.initCoversSaved(); - } - if (coverForm) { - module.initPasteForm(coverForm); - } - }); + if (addCoversElement || manageCoversElement || saveCoversElement || coverForm) { + import(/* webpackChunkName: "covers" */ './covers') + .then((module) => { + if (manageCoversElement) { + module.initCoversChange(); + } + if (addCoversElement) { + module.initCoversAddManage(); + } + if (saveCoversElement) { + module.initCoversSaved(); + } + if (coverForm) { + module.initPasteForm(coverForm); + } + }); } if (document.getElementById('addbook')) { - import(/* webpackChunkName: "add-book" */ './add-book').then((module) => - module.initAddBookImport(), - ); + import(/* webpackChunkName: "add-book" */ './add-book') + .then(module => module.initAddBookImport()); } if (document.getElementById('autofill-dev-credentials')) { - document.getElementById('username').value = 'openlibrary@example.com'; - document.getElementById('password').value = 'admin123'; - document.getElementById('remember').checked = true; - } - const anonymizationButton = document.querySelector( - '.account-anonymization-button', - ); - const adminLinks = document.getElementById('adminLinks'); - const confirmButtons = document.querySelectorAll('.do-confirm'); + document.getElementById('username').value = 'openlibrary@example.com' + document.getElementById('password').value = 'admin123' + document.getElementById('remember').checked = true + } + const anonymizationButton = document.querySelector('.account-anonymization-button') + const adminLinks = document.getElementById('adminLinks') + const confirmButtons = document.querySelectorAll('.do-confirm') if (adminLinks || anonymizationButton || confirmButtons.length) { - import(/* webpackChunkName: "admin" */ './admin').then((module) => { - if (adminLinks) { - module.initAdmin(); - } - if (anonymizationButton) { - module.initAnonymizationButton(anonymizationButton); - } - if (confirmButtons.length) { - module.initConfirmationButtons(confirmButtons); - } - }); + import(/* webpackChunkName: "admin" */ './admin') + .then(module => { + if (adminLinks) { + module.initAdmin(); + } + if (anonymizationButton) { + module.initAnonymizationButton(anonymizationButton); + } + if (confirmButtons.length) { + module.initConfirmationButtons(confirmButtons); + } + }); } if (window.matchMedia('(display-mode: standalone)').matches) { - import(/* webpackChunkName: "offline-banner" */ './offline-banner').then( - (module) => module.initOfflineBanner(), - ); + import(/* webpackChunkName: "offline-banner" */ './offline-banner') + .then((module) => module.initOfflineBanner()); } - const searchFacets = document.getElementById('searchFacets'); + const searchFacets = document.getElementById('searchFacets') if (searchFacets) { - import(/* webpackChunkName: "search" */ './search').then((module) => - module.initSearchFacets(searchFacets), - ); + import(/* webpackChunkName: "search" */ './search') + .then((module) => module.initSearchFacets(searchFacets)); } // Conditionally load Integrated Librarian Environment @@ -371,155 +328,121 @@ jQuery(() => { .then(() => { // book page subject editing // Handle pencil clicks - document.querySelectorAll('.edit-subject-btn').forEach((btn) => { + document.querySelectorAll('.edit-subject-btn').forEach(btn => { btn.addEventListener('click', (e) => { - e.preventDefault(); - const workOlid = btn.dataset.workOlid; - if ( - !window.ILE.selectionManager.selectedItems.work.includes(workOlid) - ) { - window.ILE.selectionManager.addSelectedItem(workOlid); - window.ILE.selectionManager.updateToolbar(); + e.preventDefault() + const workOlid = btn.dataset.workOlid + if (!window.ILE.selectionManager.selectedItems.work.includes(workOlid)) { + window.ILE.selectionManager.addSelectedItem(workOlid) + window.ILE.selectionManager.updateToolbar() } - window.ILE.updateAndShowBulkTagger([workOlid], true); - }); - }); - }); + window.ILE.updateAndShowBulkTagger([workOlid], true) + }) + }) + }) // Import ile then the datatable to apply clickable classes to all listed editions - if ( - document.getElementsByClassName('editions-table--progressively-enhanced') - .length - ) { - import(/* webpackChunkName: "editions-table" */ './editions-table').then( - (module) => module.initEditionsTable(), - ); + if (document.getElementsByClassName('editions-table--progressively-enhanced').length) { + import(/* webpackChunkName: "editions-table" */ './editions-table') + .then(module => module.initEditionsTable()) } } // conditionally load functionality based on what's in the page - if ( - document.getElementsByClassName('editions-table--progressively-enhanced') - .length - ) { - import(/* webpackChunkName: "editions-table" */ './editions-table').then( - (module) => module.initEditionsTable(), - ); + if (document.getElementsByClassName('editions-table--progressively-enhanced').length) { + import(/* webpackChunkName: "editions-table" */ './editions-table') + .then(module => module.initEditionsTable()); } if ($('#cboxPrevious').length) { - $('#cboxPrevious').attr({ - 'aria-label': 'Previous button', - 'aria-hidden': 'true', - }); + $('#cboxPrevious').attr({'aria-label': 'Previous button', 'aria-hidden': 'true'}); } if ($('#cboxNext').length) { - $('#cboxNext').attr({ 'aria-label': 'Next button', 'aria-hidden': 'true' }); + $('#cboxNext').attr({'aria-label': 'Next button', 'aria-hidden': 'true'}); } if ($('#cboxSlideshow').length) { - $('#cboxSlideshow').attr({ - 'aria-label': 'Slideshow button', - 'aria-hidden': 'true', - }); + $('#cboxSlideshow').attr({'aria-label': 'Slideshow button', 'aria-hidden': 'true'}); } - const droppers = document.querySelectorAll('.dropper'); - const genericDroppers = document.querySelectorAll('.generic-dropper-wrapper'); + const droppers = document.querySelectorAll('.dropper') + const genericDroppers = document.querySelectorAll('.generic-dropper-wrapper') if (droppers.length || genericDroppers.length) { - import(/* webpackChunkName: "droppers" */ './dropper').then((module) => { - module.initDroppers(droppers); - module.initGenericDroppers(genericDroppers); - }); + import(/* webpackChunkName: "droppers" */ './dropper') + .then((module) => { + module.initDroppers(droppers) + module.initGenericDroppers(genericDroppers) + }) } // My Books Droppers (includes New List Form and Reading Check-Ins): - const myBooksDroppers = document.querySelectorAll('.my-books-dropper'); + const myBooksDroppers = document.querySelectorAll('.my-books-dropper') if (myBooksDroppers.length) { - const actionableListShowcases = - document.querySelectorAll('.actionable-item'); + const actionableListShowcases = document.querySelectorAll('.actionable-item') - import(/* webpackChunkName: "my-books" */ './my-books').then((module) => { - module.initMyBooksAffordances(myBooksDroppers, actionableListShowcases); - }); + import(/* webpackChunkName: "my-books" */ './my-books') + .then((module) => { + module.initMyBooksAffordances(myBooksDroppers, actionableListShowcases) + }) } // TODO: Make these selectors a consistent interface - const $dialogs = $( - '.dialog--open,.dialog--close,#noMaster,#confirmMerge,#leave-waitinglist-dialog,#bookPreview', - ); + const $dialogs = $('.dialog--open,.dialog--close,#noMaster,#confirmMerge,#leave-waitinglist-dialog,#bookPreview'); if ($dialogs.length) { - import(/* webpackChunkName: "dialog" */ './dialog').then((module) => - module.initDialogs(), - ); + import(/* webpackChunkName: "dialog" */ './dialog') + .then(module => module.initDialogs()) } const nativeDialogs = document.querySelectorAll('.native-dialog'); if (nativeDialogs.length) { - import(/* webpackChunkName: "native-dialog" */ './native-dialog').then( - (module) => module.initDialogs(nativeDialogs), - ); + import(/* webpackChunkName: "native-dialog" */ './native-dialog') + .then(module => module.initDialogs(nativeDialogs)) } // Yearly reading goal functionality - const setGoalLinks = document.querySelectorAll('.set-reading-goal-link'); - const goalEditLinks = document.querySelectorAll('.edit-reading-goal-link'); - const goalSubmitButtons = document.querySelectorAll( - '.reading-goal-submit-button', - ); - const yearElements = document.querySelectorAll('.use-local-year'); - if ( - setGoalLinks.length || - goalEditLinks.length || - goalSubmitButtons.length || - yearElements.length - ) { - import(/* webpackChunkName: "reading-goals" */ './reading-goals').then( - (module) => { + const setGoalLinks = document.querySelectorAll('.set-reading-goal-link') + const goalEditLinks = document.querySelectorAll('.edit-reading-goal-link') + const goalSubmitButtons = document.querySelectorAll('.reading-goal-submit-button') + const yearElements = document.querySelectorAll('.use-local-year') + if (setGoalLinks.length || goalEditLinks.length || goalSubmitButtons.length || yearElements.length) { + import(/* webpackChunkName: "reading-goals" */ './reading-goals') + .then((module) => { if (setGoalLinks.length) { - module.initYearlyGoalPrompt(setGoalLinks); + module.initYearlyGoalPrompt(setGoalLinks) } if (goalEditLinks.length) { - module.initGoalEditLinks(goalEditLinks); + module.initGoalEditLinks(goalEditLinks) } if (goalSubmitButtons.length) { - module.initGoalSubmitButtons(goalSubmitButtons); + module.initGoalSubmitButtons(goalSubmitButtons) } if (yearElements.length) { - module.displayLocalYear(yearElements); + module.displayLocalYear(yearElements) } - }, - ); + }) } $(document).on('click', '.slide-toggle', function () { $(`#${$(this).attr('aria-controls')}`).slideToggle(); }); - $('#wikiselect').on('focus', function () { - $(this).trigger('select'); - }); + $('#wikiselect').on('focus', function(){$(this).trigger('select');}) $('.hamburger-component .mask-menu').on('click', function () { $('details[open]').not(this).removeAttr('open'); }); - $('.header-dropdown').on('keydown', (event) => { + $('.header-dropdown').on('keydown', function (event) { if (event.key === 'Escape') { $('.header-dropdown > details[open]').removeAttr('open'); } }); - $('.dropdown-menu').each(function () { - $(this) - .find('a') - .last() - .on('focusout', () => { - $('.header-dropdown > details[open]').removeAttr('open'); - }); + $('.dropdown-menu').each(function() { + $(this).find('a').last().on('focusout', function() { + $('.header-dropdown > details[open]').removeAttr('open'); + }); }); // Open one dropdown at a time. - $(document).on('click', (event) => { - const $openMenus = $('.header-dropdown details[open]').parents( - '.header-dropdown', - ); + $(document).on('click', function (event) { + const $openMenus = $('.header-dropdown details[open]').parents('.header-dropdown'); $openMenus .filter((_, menu) => !$(event.target).closest(menu).length) .find('details') @@ -527,193 +450,159 @@ jQuery(() => { }); // Prevent default star rating behavior: - const ratingForms = document.querySelectorAll('.star-rating-form'); + const ratingForms = document.querySelectorAll('.star-rating-form') if (ratingForms.length) { - import(/* webpackChunkName: "star-ratings" */ './star-ratings').then( - (module) => module.initRatingHandlers(ratingForms), - ); + import(/* webpackChunkName: "star-ratings" */'./star-ratings') + .then((module) => module.initRatingHandlers(ratingForms)); } // Book page navbar initialization: - const navbarWrappers = document.querySelectorAll('.nav-bar-wrapper'); + const navbarWrappers = document.querySelectorAll('.nav-bar-wrapper') if (navbarWrappers.length) { - // Add JS for book page navbar: - import(/* webpackChunkName: "nav-bar" */ './edition-nav-bar').then( - (module) => { - module.initNavbars(navbarWrappers); - }, - ); + // Add JS for book page navbar: + import(/* webpackChunkName: "nav-bar" */ './edition-nav-bar') + .then((module) => { + module.initNavbars(navbarWrappers) + }); // Add sticky title component animations to desktop views: - import(/* webpackChunkName: "compact-title" */ './compact-title').then( - (module) => { - const compactTitle = document.querySelector('.compact-title'); - const desktopNavbar = [...navbarWrappers].find((elem) => - elem.classList.contains('desktop-only'), - ); - module.initCompactTitle(desktopNavbar, compactTitle); - }, - ); + import(/* webpackChunkName: "compact-title" */ './compact-title') + .then((module) => { + const compactTitle = document.querySelector('.compact-title') + const desktopNavbar = [...navbarWrappers].find(elem => elem.classList.contains('desktop-only')) + module.initCompactTitle(desktopNavbar, compactTitle) + }) } // Add functionality for librarian merge request table: - const librarianQueue = document.querySelector('.librarian-queue-wrapper'); + const librarianQueue = document.querySelector('.librarian-queue-wrapper') if (librarianQueue) { - import( - /* webpackChunkName: "merge-request-table" */ './merge-request-table' - ).then((module) => { - module.initLibrarianQueue(librarianQueue); - }); + import(/* webpackChunkName: "merge-request-table" */'./merge-request-table') + .then(module => { + module.initLibrarianQueue(librarianQueue) + }) } // Add functionality to the team page for filtering members: - const teamCards = document.querySelector('.teamCards_container'); + const teamCards = document.querySelector('.teamCards_container') if (teamCards) { - import(/* webpackChunkName "team" */ './team').then((module) => { - module.initTeamFilter(); - }); + import(/* webpackChunkName "team" */ './team') + .then(module => { + module.initTeamFilter(); + }) } // Add new providers in edit edition view: - const addProviderRowLink = document.querySelector('#add-new-provider-row'); + const addProviderRowLink = document.querySelector('#add-new-provider-row') if (addProviderRowLink) { - import(/* webpackChunkName "add-provider-link" */ './add_provider').then( - (module) => module.initAddProviderRowLink(addProviderRowLink), - ); + import(/* webpackChunkName "add-provider-link" */ './add_provider') + .then(module => module.initAddProviderRowLink(addProviderRowLink)) } + // Allow banner announcements to be dismissed by logged-in users: - const banners = document.querySelectorAll('.page-banner--dismissable'); + const banners = document.querySelectorAll('.page-banner--dismissable') if (banners.length) { - import(/* webpackChunkName: "dismissible-banner" */ './banner').then( - (module) => module.initDismissibleBanners(banners), - ); + import(/* webpackChunkName: "dismissible-banner" */ './banner') + .then(module => module.initDismissibleBanners(banners)) } - const returnForms = document.querySelectorAll('.return-form'); + const returnForms = document.querySelectorAll('.return-form') if (returnForms.length) { - import(/* webpackChunkName: "return-form" */ './return-form').then( - (module) => module.initReturnForms(returnForms), - ); + import(/* webpackChunkName: "return-form" */ './return-form') + .then(module => module.initReturnForms(returnForms)) } const crumbs = document.querySelectorAll('.crumb select'); if (crumbs.length) { - import( - /* webpackChunkName: "breadcrumb-select" */ './breadcrumb_select' - ).then((module) => module.initBreadcrumbSelect(crumbs)); + import(/* webpackChunkName: "breadcrumb-select" */ './breadcrumb_select') + .then(module => module.initBreadcrumbSelect(crumbs)); } const interstitial = document.querySelector('.interstitial'); if (interstitial) { - import(/* webpackChunkName: "interstitial" */ './interstitial').then( - (module) => module.initInterstitial(interstitial), - ); + import (/* webpackChunkName: "interstitial" */ './interstitial') + .then(module => module.initInterstitial(interstitial)); } const leaveWaitlistLinks = document.querySelectorAll('a.leave'); - if ( - leaveWaitlistLinks.length && - document.getElementById('leave-waitinglist-dialog') - ) { - import(/* webpackChunkName: "waitlist" */ './waitlist').then((module) => - module.initLeaveWaitlist(leaveWaitlistLinks), - ); + if (leaveWaitlistLinks.length && document.getElementById('leave-waitinglist-dialog')) { + import(/* webpackChunkName: "waitlist" */ './waitlist') + .then(module => module.initLeaveWaitlist(leaveWaitlistLinks)); } - const thirdPartyLoginsIframe = document.getElementById( - 'ia-third-party-logins', - ); + const thirdPartyLoginsIframe = document.getElementById('ia-third-party-logins'); if (thirdPartyLoginsIframe) { - import( - /* webpackChunkName: "ia_thirdparty_logins" */ './ia_thirdparty_logins' - ).then((module) => module.initMessageEventListener(thirdPartyLoginsIframe)); + import(/* webpackChunkName: "ia_thirdparty_logins" */ './ia_thirdparty_logins') + .then((module) => module.initMessageEventListener(thirdPartyLoginsIframe)); } // Password visibility toggle: - const passwordVisibilityToggle = document.querySelector( - '.password-visibility-toggle', - ); + const passwordVisibilityToggle = document.querySelector('.password-visibility-toggle') if (passwordVisibilityToggle) { - import( - /* webpackChunkName: "password-visibility-toggle" */ './password-toggle' - ).then((module) => module.initPasswordToggling(passwordVisibilityToggle)); + import(/* webpackChunkName: "password-visibility-toggle" */ './password-toggle') + .then(module => module.initPasswordToggling(passwordVisibilityToggle)) } // Affiliate links: - const affiliateLinksSection = document.querySelectorAll( - '.affiliate-links-section', - ); + const affiliateLinksSection = document.querySelectorAll('.affiliate-links-section') if (affiliateLinksSection.length) { - import(/* webpackChunkName: "affiliate-links" */ './affiliate-links').then( - (module) => module.initAffiliateLinks(affiliateLinksSection), - ); + import(/* webpackChunkName: "affiliate-links" */ './affiliate-links') + .then(module => module.initAffiliateLinks(affiliateLinksSection)) } // Fulltext search box: - const fulltextSearchSuggestion = document.querySelector( - '#fulltext-search-suggestion', - ); + const fulltextSearchSuggestion = document.querySelector('#fulltext-search-suggestion') if (fulltextSearchSuggestion) { - import( - /* webpackChunkName: "fulltext-search-suggestion" */ './fulltext-search-suggestion' - ).then((module) => - module.initFulltextSearchSuggestion(fulltextSearchSuggestion), - ); + import(/* webpackChunkName: "fulltext-search-suggestion" */ './fulltext-search-suggestion') + .then(module => module.initFulltextSearchSuggestion(fulltextSearchSuggestion)) } // Go back redirect: - const backLinks = document.querySelectorAll('.go-back-link'); + const backLinks = document.querySelectorAll('.go-back-link') if (backLinks.length) { - import(/* webpackChunkName: "go-back-links" */ './go-back-links').then( - (module) => module.initGoBackLinks(backLinks), - ); + import (/* webpackChunkName: "go-back-links" */ './go-back-links') + .then(module => module.initGoBackLinks(backLinks)) } // Lazy-load book page lists section - const listSection = document.querySelector('.lists-section'); + const listSection = document.querySelector('.lists-section') if (listSection) { - import(/* webpackChunkName: "book-page-lists" */ './book-page-lists').then( - (module) => module.initListsSection(listSection), - ); + import(/* webpackChunkName: "book-page-lists" */ './book-page-lists') + .then(module => module.initListsSection(listSection)) } // Initialize follow forms lazily const followForms = document.querySelectorAll('.follow-form'); if (followForms.length) { - import(/* webpackChunkName: "following" */ './following').then((module) => - module.initAsyncFollowing(followForms), - ); + import(/* webpackChunkName: "following" */ './following') + .then(module => module.initAsyncFollowing(followForms)) } // Generalized carousel lazy-loading - const lazyCarousels = document.querySelectorAll('.lazy-carousel'); + const lazyCarousels = document.querySelectorAll('.lazy-carousel') if (lazyCarousels.length) { - import(/* webpackChunkName: "lazy-carousels" */ './lazy-carousel').then( - (module) => module.initLazyCarousel(lazyCarousels), - ); + import(/* webpackChunkName: "lazy-carousels" */ './lazy-carousel') + .then(module => module.initLazyCarousel(lazyCarousels)) } // Librarian Dashboard - const librarianDashboard = document.querySelector('.librarian-dashboard'); + const librarianDashboard = document.querySelector('.librarian-dashboard') if (librarianDashboard) { - import( - /* webpackChunkName: "librarian-dashboard" */ './librarian-dashboard' - ).then((module) => module.initLibrarianDashboard(librarianDashboard)); + import(/* webpackChunkName: "librarian-dashboard" */ './librarian-dashboard') + .then(module => module.initLibrarianDashboard(librarianDashboard)) } // List books if (document.querySelector('.list-books')) { - import(/* webpackChunkName: "list-books" */ './list_books').then((module) => - module.ListBooks.init(), - ); + import(/* webpackChunkName: "list-books" */ './list_books') + .then(module => module.ListBooks.init()); } // Stats page login counts - const monthlyLoginStats = document.querySelector('.monthly-login-counts'); + const monthlyLoginStats = document.querySelector('.monthly-login-counts') if (monthlyLoginStats) { - import(/* webpackChunkName: "stats" */ './stats').then((module) => - module.initUniqueLoginCounts(monthlyLoginStats), - ); + import(/* webpackChunkName: "stats" */ './stats') + .then(module => module.initUniqueLoginCounts(monthlyLoginStats)) } }); diff --git a/openlibrary/plugins/openlibrary/js/interstitial.js b/openlibrary/plugins/openlibrary/js/interstitial.js index 4d0dcfd03fb..43542b622da 100644 --- a/openlibrary/plugins/openlibrary/js/interstitial.js +++ b/openlibrary/plugins/openlibrary/js/interstitial.js @@ -1,21 +1,21 @@ -export function initInterstitial (elem) { - let seconds = elem.dataset.wait; - const url = elem.dataset.url; - const timerElement = elem.querySelector('#timer'); +export function initInterstitial(elem) { + let seconds = elem.dataset.wait + const url = elem.dataset.url + const timerElement = elem.querySelector('#timer') const countdown = setInterval(() => { - seconds--; - timerElement.textContent = seconds; + seconds-- + timerElement.textContent = seconds if (seconds === 0) { - clearInterval(countdown); - window.location.href = url; + clearInterval(countdown) + window.location.href = url } - }, 1000); // 1 second interval + }, 1000) // 1 second interval // Add cancel button handler const cancelButton = elem.querySelector('.close-window'); if (cancelButton) { cancelButton.addEventListener('click', () => { - window.close(); + window.close() }); } } diff --git a/openlibrary/plugins/openlibrary/js/isbnOverride.js b/openlibrary/plugins/openlibrary/js/isbnOverride.js index 18f4adb4259..23b4bf80b56 100644 --- a/openlibrary/plugins/openlibrary/js/isbnOverride.js +++ b/openlibrary/plugins/openlibrary/js/isbnOverride.js @@ -9,13 +9,7 @@ */ export const isbnOverride = { data: null, - set (isbnData) { - this.data = isbnData; - }, - get () { - return this.data; - }, - clear () { - this.data = null; - }, -}; + set(isbnData) { this.data = isbnData }, + get() { return this.data }, + clear() { this.data = null }, +} diff --git a/openlibrary/plugins/openlibrary/js/jquery.repeat.js b/openlibrary/plugins/openlibrary/js/jquery.repeat.js index 04b1690cb3a..5e83377fd68 100644 --- a/openlibrary/plugins/openlibrary/js/jquery.repeat.js +++ b/openlibrary/plugins/openlibrary/js/jquery.repeat.js @@ -1,15 +1,16 @@ -import { isbnOverride } from '../../openlibrary/js/isbnOverride'; -import Template from './template'; +import Template from './template' +import { isbnOverride } from '../../openlibrary/js/isbnOverride' /** * jquery repeat: jquery plugin to handle repetitive inputs in a form. * * Used in addbook process. */ -export function init () { +export function init() { // used in books/edit/exercpt, books/edit/web and books/edit/edition - $.fn.repeat = function (options) { - var addSelector, removeSelector, id, elems, t, code, nextRowId; + $.fn.repeat = function(options) { + var addSelector, removeSelector, id, elems, t, code, + nextRowId; options = options || {}; id = `#${this.attr('id')}`; @@ -18,12 +19,11 @@ export function init () { add: $(`${id}-add`), form: $(`${id}-form`), display: $(`${id}-display`), - template: $(`${id}-template`), - }; + template: $(`${id}-template`) + } - function createTemplate (selector) { - code = $(selector) - .html() + function createTemplate(selector) { + code = $(selector).html() .replace(/%7B%7B/gi, '<%=') .replace(/%7D%7D/gi, '%>') .replace(/{{/g, '<%=') @@ -34,13 +34,13 @@ export function init () { t = createTemplate(`${id}-template`); /** - * Search elems.form for input fields and create an - * object representing. - * @return {object} data mapping names to values - */ - function formdata () { + * Search elems.form for input fields and create an + * object representing. + * @return {object} data mapping names to values + */ + function formdata() { var data = {}; - $(':input', elems.form).each(function () { + $(':input', elems.form).each(function() { var $e = $(this), name = $e.attr('name'), type = $e.attr('type'), @@ -56,11 +56,11 @@ export function init () { } /** - * triggered when "add link" button is clicked on author edit field. - * Creates a removable `repeat-item`. - * @param {jQuery.Event} event - */ - function onAdd (event) { + * triggered when "add link" button is clicked on author edit field. + * Creates a removable `repeat-item`. + * @param {jQuery.Event} event + */ + function onAdd(event) { var data, newid; const isbnOverrideData = isbnOverride.get(); event.preventDefault(); @@ -100,7 +100,7 @@ export function init () { elems._this.trigger('repeat-add'); } - function onRemove (event) { + function onRemove(event) { event.preventDefault(); $(this).parents('.repeat-item').eq(0).remove(); elems._this.trigger('repeat-remove'); @@ -110,5 +110,5 @@ export function init () { // Click handlers should apply to newly created add/remove selectors $(document).on('click', addSelector, onAdd); $(document).on('click', removeSelector, onRemove); - }; + } } diff --git a/openlibrary/plugins/openlibrary/js/jsdef.js b/openlibrary/plugins/openlibrary/js/jsdef.js index 43e144d3038..df0f7fda1bd 100644 --- a/openlibrary/plugins/openlibrary/js/jsdef.js +++ b/openlibrary/plugins/openlibrary/js/jsdef.js @@ -4,10 +4,10 @@ * For more details, see: * http://github.com/anandology/notebook/tree/master/2010/03/jsdef/ */ -import { sprintf, ugettext, ungettext } from './i18n'; +import { ungettext, ugettext, sprintf } from './i18n'; // TODO: Can likely move some of these methods into this file -import { commify, slice, urlencode } from './python'; -import { cond, truncate } from './utils'; +import { commify, urlencode, slice } from './python'; +import { truncate, cond } from './utils'; /** * Python range function. @@ -21,7 +21,7 @@ import { cond, truncate } from './utils'; */ //used in templates/lib/pagination.html -export function range (begin, end, step) { +export function range(begin, end, step) { var r, i; step = step || 1; if (end === undefined) { @@ -30,7 +30,7 @@ export function range (begin, end, step) { } r = []; - for (i = begin; i < end; i += step) { + for (i=begin; i<end; i += step) { r[r.length] = i; } return r; @@ -42,7 +42,7 @@ export function range (begin, end, step) { * > " - ".join(["a", "b", "c"]) * a - b - c */ -export function join (items) { +export function join(items) { return items.join(this); } @@ -51,12 +51,12 @@ export function join (items) { */ // used in templates/admin/loans.html -export function len (array) { +export function len(array) { return array.length; } // used in templates/type/permission/edit.html -export function enumerate (a) { +export function enumerate(a) { var b = new Array(a.length); var i; for (i in a) { @@ -65,7 +65,7 @@ export function enumerate (a) { return b; } -export function ForLoop (parent, seq) { +export function ForLoop(parent, seq) { this.parent = parent; this.seq = seq; @@ -73,29 +73,29 @@ export function ForLoop (parent, seq) { this.index0 = -1; } -ForLoop.prototype.next = function () { - var i = this.index0 + 1; +ForLoop.prototype.next = function() { + var i = this.index0+1; this.index0 = i; - this.index = i + 1; + this.index = i+1; - this.first = i === 0; - this.last = i === this.length - 1; + this.first = (i === 0); + this.last = (i === this.length-1); - this.odd = this.index % 2 === 1; - this.even = this.index % 2 === 0; + this.odd = (this.index % 2 === 1); + this.even = (this.index % 2 === 0); this.parity = ['even', 'odd'][this.index % 2]; this.revindex0 = this.length - i; this.revindex = this.length - i + 1; -}; +} // used in plugins/upstream/jsdef.py -export function foreach (seq, parent_loop, callback) { +export function foreach(seq, parent_loop, callback) { var loop = new ForLoop(parent_loop, seq); var i, args, j; - for (i = 0; i < seq.length; i++) { + for (i=0; i<seq.length; i++) { loop.next(); args = [loop]; @@ -105,7 +105,8 @@ export function foreach (seq, parent_loop, callback) { for (j in seq[i]) { args.push(seq[i][j]); } - } else { + } + else { args[1] = seq[i]; } callback.apply(this, args); @@ -113,16 +114,18 @@ export function foreach (seq, parent_loop, callback) { } // used in templates/lists/widget.html -export function websafe (value) { +export function websafe(value) { // Safari 6 is failing with weird javascript error in this function. // Added try-catch to avoid it. try { if (value === null || value === undefined) { return ''; - } else { + } + else { return htmlquote(value.toString()); } - } catch (e) { + } + catch (e) { return ''; } } @@ -132,7 +135,7 @@ export function websafe (value) { * Quote a string * @param {string|number} text to quote */ -export function htmlquote (text) { +export function htmlquote(text) { // This code exists for compatibility with template.js text = String(text); text = text.replace(/&/g, '&'); // Must be done first! @@ -143,10 +146,11 @@ export function htmlquote (text) { return text; } -export function is_jsdef () { +export function is_jsdef() { return true; } + /** * foo.get(KEY, default) isn't defined in js, so we can't use that construct * in our jsdef methods. This helper function provides a workaround, and works @@ -156,11 +160,11 @@ export function is_jsdef () { * @param {string} key - the key to get from the object * @param {any} def - the default value to return if the key isn't found */ -export function jsdef_get (obj, key, def = null) { - return key in obj ? obj[key] : def; +export function jsdef_get(obj, key, def=null) { + return (key in obj) ? obj[key] : def; } -export function exposeGlobally () { +export function exposeGlobally() { // Extend existing prototypes String.prototype.join = join; diff --git a/openlibrary/plugins/openlibrary/js/lazy-carousel.js b/openlibrary/plugins/openlibrary/js/lazy-carousel.js index a17ac40038c..603b10715e8 100644 --- a/openlibrary/plugins/openlibrary/js/lazy-carousel.js +++ b/openlibrary/plugins/openlibrary/js/lazy-carousel.js @@ -1,4 +1,4 @@ -import { initialzeCarousels } from './carousel'; +import {initialzeCarousels} from './carousel'; import { buildPartialsUrl } from './utils'; /** @@ -7,25 +7,25 @@ import { buildPartialsUrl } from './utils'; * * @param elems {NodeList<HTMLElement>} Collection of placeholder carousel elements */ -export function initLazyCarousel (elems) { +export function initLazyCarousel(elems) { // Create intersection observer const intersectionObserver = new IntersectionObserver(intersectionCallback, { root: null, rootMargin: '200px', - threshold: 0, - }); + threshold: 0 + }) - elems.forEach((elem) => { - // Observe element for intersections - intersectionObserver.observe(elem); + elems.forEach(elem => { + // Observe element for intersections + intersectionObserver.observe(elem) // Add retry listener - const retryElem = elem.querySelector('.retry-btn'); + const retryElem = elem.querySelector('.retry-btn') retryElem.addEventListener('click', (e) => { - e.preventDefault(); + e.preventDefault() handleRetry(elem); - }); - }); + }) + }) } /** @@ -34,8 +34,8 @@ export function initLazyCarousel (elems) { * @param data {object} * @returns {Promise<Response>} */ -async function fetchPartials (data) { - return fetch(buildPartialsUrl('LazyCarousel', { ...data })); +async function fetchPartials(data) { + return fetch(buildPartialsUrl('LazyCarousel', {...data})) } /** @@ -49,24 +49,22 @@ async function fetchPartials (data) { * * @param target {HTMLElement} A placeholder element for a carousel */ -function doFetchAndUpdate (target) { - const config = JSON.parse(target.dataset.config); - const loadingIndicator = target.querySelector('.loadingIndicator'); +function doFetchAndUpdate(target) { + const config = JSON.parse(target.dataset.config) + const loadingIndicator = target.querySelector('.loadingIndicator') fetchPartials(config) - .then((resp) => { + .then(resp => { if (!resp.ok) { - throw new Error('Failed to fetch partials from server'); + throw new Error('Failed to fetch partials from server') } - return resp.json(); + return resp.json() }) - .then((data) => { - const newElem = document.createElement('div'); - newElem.innerHTML = data.partials.trim(); - const carouselElements = newElem.querySelectorAll( - '.carousel--progressively-enhanced', - ); - loadingIndicator.classList.add('hidden'); + .then(data => { + const newElem = document.createElement('div') + newElem.innerHTML = data.partials.trim() + const carouselElements = newElem.querySelectorAll('.carousel--progressively-enhanced') + loadingIndicator.classList.add('hidden') if (carouselElements.length === 0 && config.fallback) { // No results, disable filters @@ -77,20 +75,18 @@ function doFetchAndUpdate (target) { config.fallback = false; // Prevents infinite retries target.dataset.config = JSON.stringify(config); - target - .querySelector('.lazy-carousel-fallback') - .classList.remove('hidden'); + target.querySelector('.lazy-carousel-fallback').classList.remove('hidden'); } else { - target.parentNode.insertBefore(newElem, target); - target.remove(); - initialzeCarousels(carouselElements); + target.parentNode.insertBefore(newElem, target) + target.remove() + initialzeCarousels(carouselElements) } }) .catch(() => { loadingIndicator.classList.add('hidden'); - const retryElem = target.querySelector('.lazy-carousel-retry'); - retryElem.classList.remove('hidden'); - }); + const retryElem = target.querySelector('.lazy-carousel-retry') + retryElem.classList.remove('hidden') + }) } /** @@ -99,14 +95,14 @@ function doFetchAndUpdate (target) { * * @param target {Element} */ -function handleRetry (target) { - target.querySelector('.loadingIndicator').classList.remove('hidden'); - target.querySelector('.lazy-carousel-retry').classList.add('hidden'); - const carouselFallbackElem = target.querySelector('.lazy-carousel-fallback'); +function handleRetry(target) { + target.querySelector('.loadingIndicator').classList.remove('hidden') + target.querySelector('.lazy-carousel-retry').classList.add('hidden') + const carouselFallbackElem = target.querySelector('.lazy-carousel-fallback') if (carouselFallbackElem) { - carouselFallbackElem.classList.add('hidden'); + carouselFallbackElem.classList.add('hidden') } - doFetchAndUpdate(target); + doFetchAndUpdate(target) } /** @@ -117,12 +113,12 @@ function handleRetry (target) { * @param entries {Array<IntersectionObserverEntry>} * @param observer {IntersectionObserver} */ -function intersectionCallback (entries, observer) { - entries.forEach((entry) => { +function intersectionCallback(entries, observer) { + entries.forEach(entry => { if (entry.isIntersecting) { - const target = entry.target; - observer.unobserve(target); - doFetchAndUpdate(target); + const target = entry.target + observer.unobserve(target) + doFetchAndUpdate(target) } - }); + }) } diff --git a/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js b/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js index d0819198494..aca22bf8a28 100644 --- a/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js +++ b/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js @@ -1,7 +1,6 @@ // @ts-check - -import chunk from 'lodash/chunk'; import debounce from 'lodash/debounce'; +import chunk from 'lodash/chunk'; /** * Responds to HTML like: @@ -19,8 +18,8 @@ import debounce from 'lodash/debounce'; * Currently only works with works, editions, and authors. */ export class LazyThingPreview { - constructor () { - /** @type {Array<{key: string, render_fn: Function}>} */ + constructor() { + /** @type {Array<{key: string, render_fn: Function}>} */ this.queue = []; /** @type {Object<string, object>} */ this.cache = {}; @@ -28,7 +27,7 @@ export class LazyThingPreview { this.renderDebounced = debounce(this.render.bind(this), 100); } - init () { + init() { $('.lazy-thing-preview').each((i, el) => { this.push({ key: el.dataset.key, @@ -38,68 +37,61 @@ export class LazyThingPreview { } /** - * @param {{key: string, render_fn_name: string}} arg0 - */ - push ({ key, render_fn_name }) { + * @param {{key: string, render_fn_name: string}} arg0 + */ + push({key, render_fn_name}) { const render_fn = window[render_fn_name]; if (this.cache[key]) { this.renderKey(key, render_fn, this.cache[key]); } else { - this.queue.push({ key, render_fn }); + this.queue.push({key, render_fn}); this.renderDebounced(); } } /** - * @param {string} key - * @param {Function} render_fn - * @param {object} book - */ - renderKey (key, render_fn, book) { + * @param {string} key + * @param {Function} render_fn + * @param {object} book + */ + renderKey(key, render_fn, book) { const $el = $(`.lazy-thing-preview[data-key="${key}"]`); $el.html(render_fn(book)); } /** - * @param {string[]} keys - * @returns {AsyncGenerator<object[]>} - */ - async *getThings (keys) { - const workKeys = keys.filter((key) => key.startsWith('/works/')); - const editionKeys = keys.filter((key) => key.startsWith('/books/')); - const authorKeys = keys.filter((key) => key.startsWith('/authors/')); - const fields = - 'key,type,cover_i,first_publish_year,author_name,title,subtitle,edition_count,editions'; + * @param {string[]} keys + * @returns {AsyncGenerator<object[]>} + */ + async* getThings(keys) { + const workKeys = keys.filter(key => key.startsWith('/works/')); + const editionKeys = keys.filter(key => key.startsWith('/books/')); + const authorKeys = keys.filter(key => key.startsWith('/authors/')); + const fields = 'key,type,cover_i,first_publish_year,author_name,title,subtitle,edition_count,editions'; for (const keys of chunk(workKeys, 100)) { - const resp = await fetch( - `/search.json?${new URLSearchParams({ - q: `key:(${keys.join(' OR ')})`, - fields, - limit: '100', - })}`, - ).then((r) => r.json()); + const resp = await fetch(`/search.json?${new URLSearchParams({ + q: `key:(${keys.join(' OR ')})`, + fields, + limit: '100', + })}`).then(r => r.json()); yield resp.docs; } for (const keys of chunk(editionKeys, 100)) { - const resp = await fetch( - `/search.json?${new URLSearchParams({ - q: `edition_key:(${keys - .map((key) => key.split('/').pop()) - .join(' OR ')})`, - fields, - limit: '100', - })}`, - ).then((r) => r.json()); + const resp = await fetch(`/search.json?${new URLSearchParams({ + q: `edition_key:(${keys + .map(key => key.split('/').pop()) + .join(' OR ')})`, + fields, + limit: '100', + })}`).then(r => r.json()); yield resp.docs; } for (const keys of chunk(authorKeys, 100)) { - const resp = await fetch( - `/search/authors.json?${new URLSearchParams({ - q: `key:(${keys.join(' OR ')})`, - fields: 'key,type,name,top_work,top_subjects,birth_date,death_date', - limit: '100', - })}`, - ).then((r) => r.json()); + const resp = await fetch(`/search/authors.json?${new URLSearchParams({ + q: `key:(${keys.join(' OR ')})`, + fields: 'key,type,name,top_work,top_subjects,birth_date,death_date', + limit: '100', + })}`).then(r => r.json()); for (const doc of resp.docs) { // This API returns keys without the /authors/ prefix 😭 doc.key = `/authors/${doc.key}`; @@ -108,24 +100,20 @@ export class LazyThingPreview { } } - async render () { - const keys = this.queue.map(({ key }) => key); + async render() { + const keys = this.queue.map(({key}) => key); const render_fn_map = Object.fromEntries( - this.queue.map(({ key, render_fn }) => [key, render_fn]), + this.queue.map(({key, render_fn}) => [key, render_fn]) ); for await (const thingBatch of this.getThings(keys)) { for (const thing of thingBatch) { this.cache[thing.key] = thing; if (thing.type === 'work') { const book = thing; - book.full_title = book.subtitle - ? `${book.title}: ${book.subtitle}` - : book.title; + book.full_title = book.subtitle ? `${book.title}: ${book.subtitle}` : book.title; if (book.editions.docs.length) { const ed = book.editions.docs[0]; - ed.full_title = ed.subtitle - ? `${ed.title}: ${ed.subtitle}` - : ed.title; + ed.full_title = ed.subtitle ? `${ed.title}: ${ed.subtitle}` : ed.title; ed.author_name = book.author_name; ed.edition_count = book.edition_count; this.cache[ed.key] = ed; @@ -142,7 +130,7 @@ export class LazyThingPreview { } } - const missingKeys = keys.filter((key) => !this.cache[key]); + const missingKeys = keys.filter(key => !this.cache[key]); // eslint-disable-next-line no-console console.warn('Books missing from cache', missingKeys); this.queue = []; diff --git a/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js b/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js index 4a036122388..73bb92ba585 100644 --- a/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js +++ b/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js @@ -8,16 +8,12 @@ let i18nStrings; * * @param {HTMLDetailsElement} rootElement */ -export function initLibrarianDashboard (rootElement) { - i18nStrings = JSON.parse(rootElement.dataset.i18n); - const table = rootElement.querySelector('.dq-table'); - rootElement.addEventListener( - 'click', - () => { - populateTable(table); - }, - { once: true }, - ); +export function initLibrarianDashboard(rootElement) { + i18nStrings = JSON.parse(rootElement.dataset.i18n) + const table = rootElement.querySelector('.dq-table') + rootElement.addEventListener('click', () => { + populateTable(table) + }, {once: true}) } /** @@ -26,11 +22,11 @@ export function initLibrarianDashboard (rootElement) { * @param {HTMLTableElement} table * @returns {Promise<void>} */ -async function populateTable (table) { - const bookCount = Number(table.dataset.totalBooks); - const rows = table.querySelectorAll('.dq-table__row'); +async function populateTable(table) { + const bookCount = Number(table.dataset.totalBooks) + const rows = table.querySelectorAll('.dq-table__row') - await Promise.all([...rows].map((row) => updateRow(row, bookCount))); + await Promise.all([...rows].map(row => updateRow(row, bookCount))) } /** @@ -40,45 +36,45 @@ async function populateTable (table) { * @param {number} totalCount Total number of search results * @returns {Promise<void>} */ -async function updateRow (row, totalCount) { - const queryFragment = row.dataset.queryFragment; - const apiUrl = buildUrl(queryFragment, false); - const searchPageUrl = buildUrl(queryFragment); +async function updateRow(row, totalCount) { + const queryFragment = row.dataset.queryFragment + const apiUrl = buildUrl(queryFragment, false) + const searchPageUrl = buildUrl(queryFragment) // Make query const data = await fetch(apiUrl) .then((resp) => { if (!resp.ok) { - throw new Error(`Data quality response status : ${resp.status}`); + throw new Error(`Data quality response status : ${resp.status}`) } - return resp.json(); + return resp.json() }) .catch(() => { return null; - }); + }) // Render status cell markup - let newCellMarkup; + let newCellMarkup if (data === null) { - newCellMarkup = renderErrorCell(searchPageUrl); + newCellMarkup = renderErrorCell(searchPageUrl) } else { - newCellMarkup = renderResultsCells(data, totalCount, searchPageUrl); + newCellMarkup = renderResultsCells(data, totalCount, searchPageUrl) } // Include retry affordance, regardless of result - newCellMarkup += renderRetryCell(); + newCellMarkup += renderRetryCell() - replaceStatusCells(row, newCellMarkup); + replaceStatusCells(row, newCellMarkup) // Add listener to retry affordance - const retryAffordance = row.querySelector('.dqs-run-again'); + const retryAffordance = row.querySelector('.dqs-run-again') retryAffordance.addEventListener('click', () => { - // Update view to "pending" - replaceStatusCells(row, renderPendingCell()); + // Update view to "pending" + replaceStatusCells(row, renderPendingCell()) // Retry query - updateRow(row, totalCount); - }); + updateRow(row, totalCount) + }) } /** @@ -87,15 +83,13 @@ async function updateRow (row, totalCount) { * @param {string} queryFragment * @param {boolean} forUi */ -function buildUrl (queryFragment, forUi = true) { - const match = window.location.pathname.match(/authors\/(OL\d+A)/); - const queryParamString = match - ? `?q=author_key:${match[1]}` - : window.location.search; - - const params = new URLSearchParams(queryParamString); - params.set('q', `${params.get('q')} ${queryFragment}`); - return `/search${forUi ? '' : '.json'}?${params.toString()}`; +function buildUrl(queryFragment, forUi = true) { + const match = window.location.pathname.match(/authors\/(OL\d+A)/) + const queryParamString = match ? `?q=author_key:${match[1]}` : window.location.search + + const params = new URLSearchParams(queryParamString) + params.set('q', `${params.get('q')} ${queryFragment}`) + return `/search${forUi ? '' : '.json'}?${params.toString()}` } /** @@ -104,15 +98,15 @@ function buildUrl (queryFragment, forUi = true) { * @param {HTMLTableRowElement} row * @param {string} newCellMarkup Markup for the new status cells */ -function replaceStatusCells (row, newCellMarkup) { - const statusCells = row.querySelectorAll('td:not(.dq-table__criterion-cell)'); +function replaceStatusCells(row, newCellMarkup) { + const statusCells = row.querySelectorAll('td:not(.dq-table__criterion-cell)') for (const cell of statusCells) { - cell.remove(); + cell.remove() } - const template = document.createElement('template'); - template.innerHTML = newCellMarkup; - row.append(...template.content.children); + const template = document.createElement('template') + template.innerHTML = newCellMarkup + row.append(...template.content.children) } /** @@ -124,9 +118,9 @@ function replaceStatusCells (row, newCellMarkup) { * * @returns {string} HTML string */ -function renderResultsCells (results, totalCount, failingHref) { - const numFound = results.numFound; - const percentage = Math.floor(((totalCount - numFound) / totalCount) * 100); +function renderResultsCells(results, totalCount, failingHref) { + const numFound = results.numFound + const percentage = Math.floor(((totalCount - numFound) / totalCount) * 100) return `<td class="dq-table__results-cell"> <meter title="${numFound} of ${totalCount}" min="0" max="100" value="${percentage}"></meter> @@ -134,7 +128,7 @@ function renderResultsCells (results, totalCount, failingHref) { </td> <td style="text-align:right"> <a href="${failingHref}">${numFound} ${i18nStrings['failing']}</a> - </td>`; + </td>` } /** @@ -142,12 +136,12 @@ function renderResultsCells (results, totalCount, failingHref) { * * @returns {string} HTML string */ -function renderRetryCell () { +function renderRetryCell() { return `<td> <button class="dqs-run-again"> ${i18nStrings['reload']} </button> - </td>`; + </td>` } /** @@ -156,10 +150,10 @@ function renderRetryCell () { * @param {string} href Link to search page for failing query * @returns {string} */ -function renderErrorCell (href) { +function renderErrorCell(href) { return `<td colspan="2"> <a href="${href}">${i18nStrings['error']}</a> - </td>`; + </td>` } /** @@ -167,6 +161,6 @@ function renderErrorCell (href) { * * @returns {string} */ -function renderPendingCell () { - return `<td colspan="3">${i18nStrings['loading']}</td>`; +function renderPendingCell() { + return `<td colspan="3">${i18nStrings['loading']}</td>` } diff --git a/openlibrary/plugins/openlibrary/js/list_books.js b/openlibrary/plugins/openlibrary/js/list_books.js index db740cb2b01..ab4c6b6ef4f 100644 --- a/openlibrary/plugins/openlibrary/js/list_books.js +++ b/openlibrary/plugins/openlibrary/js/list_books.js @@ -1,23 +1,23 @@ export class ListBooks { /** - * @param {HTMLElement} listBooks - * @param {HTMLElement} layoutToolbar - **/ - constructor (listBooks, layoutToolbar) { + * @param {HTMLElement} listBooks + * @param {HTMLElement} layoutToolbar + **/ + constructor(listBooks, layoutToolbar) { this.listBooks = listBooks; this.layoutToolbar = layoutToolbar; this.activeLayout = this.layoutToolbar.querySelector('a.active'); } - attach () { + attach() { $(this.layoutToolbar).on('click', 'a', this.updateLayout.bind(this)); } /** - * @param {MouseEvent} event - */ - updateLayout (event) { + * @param {MouseEvent} event + */ + updateLayout(event) { event.preventDefault(); const layoutAnchor = event.target; this.layoutToolbar.querySelector('a.active').classList.remove('active'); @@ -27,8 +27,8 @@ export class ListBooks { document.cookie = `LBL=${layout}; path=/; max-age=31536000`; } - static init () { - // Assume only one list-books/layout per page + static init() { + // Assume only one list-books/layout per page new ListBooks( document.querySelector('.list-books'), document.querySelector('.tools--layout'), diff --git a/openlibrary/plugins/openlibrary/js/lists/ListService.js b/openlibrary/plugins/openlibrary/js/lists/ListService.js index 7311c6e24f1..7f501321c2c 100644 --- a/openlibrary/plugins/openlibrary/js/lists/ListService.js +++ b/openlibrary/plugins/openlibrary/js/lists/ListService.js @@ -12,7 +12,7 @@ import { buildPartialsUrl } from '../utils'; * @param {object} data Object containing the new list's name, description, and seeds. * @returns {Promise<Response>} The results of the POST request */ -export async function createList (userKey, data) { +export async function createList(userKey, data) { return await fetch(`${userKey}/lists.json`, { method: 'post', headers: { @@ -30,7 +30,7 @@ export async function createList (userKey, data) { * @param {object} seed Object containing the new list's name, description, and seeds. * @returns {Promise<Response>} The result of the POST request */ -export async function addItem (listKey, seed) { +export async function addItem(listKey, seed) { const body = { add: [seed] }; return await fetch(`${listKey}/seeds.json`, { method: 'post', @@ -49,7 +49,7 @@ export async function addItem (listKey, seed) { * @param {string|{ key: string }} seed The item being removed from the list. * @returns {Promise<Response>} The POST response */ -export async function removeItem (listKey, seed) { +export async function removeItem(listKey, seed) { const body = { remove: [seed] }; return await fetch(`${listKey}/seeds.json`, { method: 'post', @@ -62,7 +62,7 @@ export async function removeItem (listKey, seed) { } // XXX : jsdoc -export async function getListPartials () { +export async function getListPartials() { return await fetch(buildPartialsUrl('MyBooksDropperLists'), { headers: { 'Content-Type': 'application/json', diff --git a/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js b/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js index 8af93fb5f9f..44e77eb245a 100644 --- a/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js +++ b/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js @@ -10,7 +10,7 @@ import 'jquery-ui/ui/widgets/dialog'; const itemsWithDeleteList = $('.deleteList .resultTitle'); if (itemsWithDeleteList.length) { const deleteListLink = $('.listDelete--myLists'); - itemsWithDeleteList.each(function () { + itemsWithDeleteList.each(function() { $(deleteListLink).clone().prependTo(this).removeClass('hidden'); }); @@ -24,7 +24,7 @@ if (itemsWithDeleteList.length) { const itemsWithDeleteSeed = $('.deleteSeed .resultTitle'); if (itemsWithDeleteSeed.length) { const deleteSeedLink = $('.seedDelete--myLists'); - itemsWithDeleteSeed.each(function () { + itemsWithDeleteSeed.each(function() { $(deleteSeedLink).clone().prependTo(this).removeClass('hidden'); }); @@ -38,9 +38,9 @@ if (itemsWithDeleteSeed.length) { * @param {string} seed - path to seed book being removed, ex: /books/OL23269118M * @param {function} success - click function */ -function remove_seed (list_key, seed, success) { +function remove_seed(list_key, seed, success) { if (seed[0] === '/') { - seed = { key: seed }; + seed = {key: seed} } $.ajax({ @@ -48,22 +48,22 @@ function remove_seed (list_key, seed, success) { url: `${list_key}/seeds.json`, contentType: 'application/json', data: JSON.stringify({ - remove: [seed], + remove: [seed] }), dataType: 'json', - beforeSend: (xhr) => { + beforeSend: function(xhr) { xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Accept', 'application/json'); }, - success: success, + success: success }); } /** * @returns {number} count of number of seed books in a list */ -function get_seed_count () { +function get_seed_count() { return $('ul#listResults').children().length; } @@ -85,17 +85,15 @@ const getConfirmButtonLabelText = () => { // Add listeners to each .listDelete link element // Sometimes .listDelete is dynamically added to the DOM, so we'll add the listener to a parent element -$('#listResults').on('click', '.listDelete a', function () { - if ( - get_seed_count() > 1 && - !$(this).parent().hasClass('listDelete--myLists') - ) { +$('#listResults').on('click', '.listDelete a', function() { + if (get_seed_count() > 1 && !$(this).parent().hasClass('listDelete--myLists')) { $('#remove-seed-dialog') .data('seed-key', $(this).closest('[data-seed-key]').data('seed-key')) .data('list-key', $(this).closest('[data-list-key]').data('list-key')) .dialog('open'); $('#remove-seed-dialog').removeClass('hidden'); - } else { + } + else { $('#delete-list-dialog') .data('list-key', $(this).closest('[data-list-key]').data('list-key')) .dialog('open'); @@ -113,31 +111,33 @@ $('#remove-seed-dialog').dialog({ ConfirmRemoveSeed: { text: getConfirmButtonLabelText(), id: 'remove-seed-dialog--confirm', - click: function () { + click: function() { var list_key = $(this).data('list-key'); var seed_key = $(this).data('seed-key'); - remove_seed(list_key, seed_key, () => { + var _this = this; + + remove_seed(list_key, seed_key, function() { $(`[data-seed-key='${seed_key}']`).remove(); // update seed count $('#list-items-count').load(`${location.href} #list-items-count`); // TODO: update edition count - $(this).dialog('close'); + $(_this).dialog('close'); $('#remove-seed-dialog').addClass('hidden'); }); - }, + } }, CancelRemoveSeed: { text: getCancelButtonLabelText(), id: 'remove-seed-dialog--cancel', - click: function () { + click: function() { $(this).dialog('close'); $('#remove-seed-dialog').addClass('hidden'); - }, - }, - }, + } + } + } }); // Set up 'Delete List' dialog; force user to confirm the destructive action of deleting a list @@ -150,21 +150,22 @@ $('#delete-list-dialog').dialog({ ConfirmDeleteList: { text: getConfirmButtonLabelText(), id: 'delete-list-dialog--confirm', - click: function () { + click: function() { var list_key = $(this).data('list-key'); + var _this = this; - $.post(`${list_key}/delete.json`, () => { - $(this).dialog('close'); + $.post(`${list_key}/delete.json`, function() { + $(_this).dialog('close'); window.location.reload(); }); - }, + } }, CancelDeleteList: { text: getCancelButtonLabelText(), id: 'delete-list-dialog--cancel', - click: function () { + click: function() { $(this).dialog('close'); - }, - }, - }, + } + } + } }); diff --git a/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js b/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js index ae71782188e..f20a633ec3c 100644 --- a/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js +++ b/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js @@ -1,9 +1,8 @@ /** * @module lists/ShowcaseItem.js */ - -import myBooksStore from '../my-books/store'; -import { removeItem } from './ListService'; +import { removeItem } from './ListService' +import myBooksStore from '../my-books/store' /** * Represents an actionable list showcase item. @@ -27,173 +26,167 @@ import { removeItem } from './ListService'; */ export class ShowcaseItem { /** - * Creates a new `ShowcaseItem` obect. - * - * Sets references needed for this ShowcaseItem's functionality. - * - * @param {HTMLElement} showcaseElem - */ - constructor (showcaseElem) { - /** - * Reference to the root element of this component. - * @member {HTMLElement} + * Creates a new `ShowcaseItem` obect. + * + * Sets references needed for this ShowcaseItem's functionality. + * + * @param {HTMLElement} showcaseElem */ - this.showcaseElem = showcaseElem; + constructor(showcaseElem) { + /** + * Reference to the root element of this component. + * @member {HTMLElement} + */ + this.showcaseElem = showcaseElem /** - * `true` if this object represents the active lists showcase. - * @member {boolean} - */ - this.isActiveShowcase = - showcaseElem.parentElement.classList.contains('already-lists'); + * `true` if this object represents the active lists showcase. + * @member {boolean} + */ + this.isActiveShowcase = showcaseElem.parentElement.classList.contains('already-lists') /** - * Reference to the affordance which removes an item from this list. - * @member {HTMLElement} - */ - this.removeFromListAffordance = - showcaseElem.querySelector('.remove-from-list'); + * Reference to the affordance which removes an item from this list. + * @member {HTMLElement} + */ + this.removeFromListAffordance = showcaseElem.querySelector('.remove-from-list') /** - * Unique identifier for the showcased list. - * @member {string} - */ - this.listKey = this.removeFromListAffordance.dataset.listKey; + * Unique identifier for the showcased list. + * @member {string} + */ + this.listKey = this.removeFromListAffordance.dataset.listKey /** - * Unique identifier for the showcased list member. - * @member {string} - */ - this.seedKey = showcaseElem.querySelector('input[name=seed-key]').value; + * Unique identifier for the showcased list member. + * @member {string} + */ + this.seedKey = showcaseElem.querySelector('input[name=seed-key]').value /** - * The list item's type. - * @member {'subject'|'edition'|'work'|'author'} - */ - this.type = showcaseElem.querySelector('input[name=seed-type]').value; + * The list item's type. + * @member {'subject'|'edition'|'work'|'author'} + */ + this.type = showcaseElem.querySelector('input[name=seed-type]').value /** - * `true` if this list item is a subject. - * @member {boolean} - */ - this.isSubject = this.type === 'subject'; + * `true` if this list item is a subject. + * @member {boolean} + */ + this.isSubject = this.type === 'subject' /** - * `true` if this list item is a work - * @member {boolean} - */ - this.isWork = !this.isSubject && this.seedKey.slice(-1) === 'W'; + * `true` if this list item is a work + * @member {boolean} + */ + this.isWork = !this.isSubject && this.seedKey.slice(-1) === 'W' /** - * `POST` request-ready representation of the list's seed key. - * @member {string|object} - */ - this.seed; + * `POST` request-ready representation of the list's seed key. + * @member {string|object} + */ + this.seed if (this.isSubject) { - this.seed = this.seedKey; + this.seed = this.seedKey } else { - this.seed = { key: this.seedKey }; + this.seed = { key: this.seedKey } } } /** - * Attaches click listeners to the showcase item's "Remove from list" - * affordance. - */ - initialize () { + * Attaches click listeners to the showcase item's "Remove from list" + * affordance. + */ + initialize() { this.removeFromListAffordance.addEventListener('click', (event) => { - event.preventDefault(); - this.removeShowcaseItem(); - }); + event.preventDefault() + this.removeShowcaseItem() + }) } /** - * Sends request to remove an item from a list, then updates the view. - * - * Removes any affiliated showcase items from the DOM, and updates all - * dropper list affordances. - */ - async removeShowcaseItem () { + * Sends request to remove an item from a list, then updates the view. + * + * Removes any affiliated showcase items from the DOM, and updates all + * dropper list affordances. + */ + async removeShowcaseItem() { await removeItem(this.listKey, this.seed) - .then((response) => response.json()) + .then(response => response.json()) .then(() => { - const showcases = myBooksStore.getShowcases(); + const showcases = myBooksStore.getShowcases() // Remove self: - this.removeSelf(); + this.removeSelf() // Remove other showcase items that are associated with the list and seed key: for (const showcase of showcases) { if (showcase.isShowcaseForListAndSeed(this.listKey, this.seedKey)) { - showcase.removeSelf(); + showcase.removeSelf() } } // Update droppers: - const droppers = myBooksStore.getDroppers(); + const droppers = myBooksStore.getDroppers() for (const dropper of droppers) { - dropper.readingLists.updateViewAfterModifyingList( - this.listKey, - this.isWork, - false, - ); + dropper.readingLists.updateViewAfterModifyingList(this.listKey, this.isWork, false) } - }); + }) } /** - * Removes associated showcase item from the DOM. - * - * Removes self from the myBooksStore's showcase array - * upon success. - */ - removeSelf () { - const showcases = myBooksStore.getShowcases(); - const thisIndex = showcases.indexOf(this); + * Removes associated showcase item from the DOM. + * + * Removes self from the myBooksStore's showcase array + * upon success. + */ + removeSelf() { + const showcases = myBooksStore.getShowcases() + const thisIndex = showcases.indexOf(this) if (thisIndex >= 0) { - this.showcaseElem.remove(); - showcases.splice(thisIndex, 1); + this.showcaseElem.remove() + showcases.splice(thisIndex, 1) } } /** - * Toggles the visiblity of active showcase items depending on their seed type. - * - * If `showWorks` is `true`, the only active showcase items that will be visible will - * be those with a work seed type. Otherwise, these active work showcase items are - * hidden and all others are displayed. - * - * This function has no effect on non-active showcase items. - * - * @param {boolean} showWorks `true` if only active showcase items related to works should be displayed - */ - toggleVisibility (showWorks) { + * Toggles the visiblity of active showcase items depending on their seed type. + * + * If `showWorks` is `true`, the only active showcase items that will be visible will + * be those with a work seed type. Otherwise, these active work showcase items are + * hidden and all others are displayed. + * + * This function has no effect on non-active showcase items. + * + * @param {boolean} showWorks `true` if only active showcase items related to works should be displayed + */ + toggleVisibility(showWorks) { if (this.isActiveShowcase) { if (showWorks) { if (this.isWork) { - this.showcaseElem.classList.remove('hidden'); + this.showcaseElem.classList.remove('hidden') } else { - this.showcaseElem.classList.add('hidden'); + this.showcaseElem.classList.add('hidden') } } else { if (this.isWork) { - this.showcaseElem.classList.add('hidden'); + this.showcaseElem.classList.add('hidden') } else { - this.showcaseElem.classList.remove('hidden'); + this.showcaseElem.classList.remove('hidden') } } } } /** - * Determines if this showcase item is linked to the given keys. - * - * @param {string} listKey - * @param {string} seedKey - * @return {boolean} `true` if the given keys match this item's keys - */ - isShowcaseForListAndSeed (listKey, seedKey) { - return this.listKey === listKey && this.seedKey === seedKey; + * Determines if this showcase item is linked to the given keys. + * + * @param {string} listKey + * @param {string} seedKey + * @return {boolean} `true` if the given keys match this item's keys + */ + isShowcaseForListAndSeed(listKey, seedKey) { + return (this.listKey === listKey) && (this.seedKey === seedKey) } } @@ -202,9 +195,9 @@ export class ShowcaseItem { * showcase items. * @type {Record<string, string>} */ -let i18nStrings; +let i18nStrings -const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; +const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png' /** * Returns the inferred type of the given seed key. @@ -212,19 +205,19 @@ const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; * @param {string} seed * @returns {string} Type of the given seed key. */ -function getSeedType (seed) { +function getSeedType(seed) { // XXX : validate input? if (seed[0] !== '/') { - return 'subject'; + return 'subject' } if (seed.endsWith('M')) { - return 'edition'; + return 'edition' } if (seed.endsWith('W')) { - return 'work'; + return 'work' } if (seed.endsWith('A')) { - return 'author'; + return 'author' } } @@ -240,20 +233,15 @@ function getSeedType (seed) { * @param {string} [coverUrl] * @returns {HTMLLIElement} */ -export function createActiveShowcaseItem ( - listKey, - seedKey, - listTitle, - coverUrl = DEFAULT_COVER_URL, -) { +export function createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl = DEFAULT_COVER_URL) { if (!i18nStrings) { - const i18nInput = document.querySelector('input[name=list-i18n-strings]'); - i18nStrings = JSON.parse(i18nInput.value); + const i18nInput = document.querySelector('input[name=list-i18n-strings]') + i18nStrings = JSON.parse(i18nInput.value) } - const splitKey = listKey.split('/'); - const userKey = `/${splitKey[1]}/${splitKey[2]}`; - const seedType = getSeedType(seedKey); + const splitKey = listKey.split('/') + const userKey = `/${splitKey[1]}/${splitKey[2]}` + const seedType = getSeedType(seedKey) const itemMarkUp = `<span class="image"> <a href="${listKey}"><img src="${coverUrl}" alt="${i18nStrings['cover_of']}${listTitle}" title="${i18nStrings['cover_of']}${listTitle}"/></a> @@ -267,14 +255,14 @@ export function createActiveShowcaseItem ( <a href="${listKey}" class="remove-from-list red smaller arial plain" data-list-key="${listKey}" title="${i18nStrings['remove_from_list']}">[X]</a> </span> <span class="owner">${i18nStrings['from']} <a href="${userKey}">${i18nStrings['you']}</a></span> - </span>`; + </span>` - const li = document.createElement('li'); - li.classList.add('actionable-item'); - li.dataset.listKey = listKey; - li.innerHTML = itemMarkUp; + const li = document.createElement('li') + li.classList.add('actionable-item') + li.dataset.listKey = listKey + li.innerHTML = itemMarkUp - return li; + return li } /** @@ -287,9 +275,9 @@ export function createActiveShowcaseItem ( * * @param {boolean} showWorksOnly */ -export function toggleActiveShowcaseItems (showWorksOnly) { +export function toggleActiveShowcaseItems(showWorksOnly) { for (const item of myBooksStore.getShowcases()) { - item.toggleVisibility(showWorksOnly); + item.toggleVisibility(showWorksOnly) } } @@ -308,21 +296,16 @@ export function toggleActiveShowcaseItems (showWorksOnly) { * @param {string} listTitle * @param {string} [coverUrl] */ -export function attachNewActiveShowcaseItem ( - listKey, - seedKey, - listTitle, - coverUrl = DEFAULT_COVER_URL, -) { - const activeListsShowcase = document.querySelector('.already-lists'); +export function attachNewActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl = DEFAULT_COVER_URL) { + const activeListsShowcase = document.querySelector('.already-lists') if (activeListsShowcase) { - const li = createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl); - activeListsShowcase.appendChild(li); + const li = createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl) + activeListsShowcase.appendChild(li) - const showcase = new ShowcaseItem(li); - showcase.initialize(); + const showcase = new ShowcaseItem(li) + showcase.initialize() - myBooksStore.getShowcases().push(showcase); + myBooksStore.getShowcases().push(showcase) } } diff --git a/openlibrary/plugins/openlibrary/js/markdown-editor/index.js b/openlibrary/plugins/openlibrary/js/markdown-editor/index.js index e90490f0741..67af03eb33f 100644 --- a/openlibrary/plugins/openlibrary/js/markdown-editor/index.js +++ b/openlibrary/plugins/openlibrary/js/markdown-editor/index.js @@ -1,22 +1,20 @@ // unversioned. -import '../../../../../vendor/js/wmd/jquery.wmd.js'; +import '../../../../../vendor/js/wmd/jquery.wmd.js' /** * Sets up Wikitext markdown editor interface inside $textarea * @param {jQuery.Object} $textareas */ export function initMarkdownEditor($textareas) { - $textareas - .on('focus', () => { - // reveal the previous when the user focuses on the textarea for the first time - $('.wmd-preview').show(); - if ($('#prevHead').length === 0) { - $('.wmd-preview').before('<h3 id="prevHead">Preview</h3>'); - } - }) - .wmd({ - helpLink: '/help/markdown', - helpHoverTitle: 'Formatting Help', - helpTarget: '_new', - }); + $textareas.on('focus', function (){ + // reveal the previous when the user focuses on the textarea for the first time + $('.wmd-preview').show(); + if ($('#prevHead').length === 0) { + $('.wmd-preview').before('<h3 id="prevHead">Preview</h3>'); + } + }).wmd({ + helpLink: '/help/markdown', + helpHoverTitle: 'Formatting Help', + helpTarget: '_new' + }); } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestService.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestService.js index 4aa12170252..41d212542c9 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestService.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestService.js @@ -1,36 +1,31 @@ + export const REQUEST_TYPES = { WORK_MERGE: 1, - AUTHOR_MERGE: 2, -}; + AUTHOR_MERGE: 2 +} -export async function createRequest( - olids, - action, - type, - comment = null, - primary = null, -) { +export async function createRequest(olids, action, type, comment = null, primary = null) { const data = { rtype: 'create-request', action: action, mr_type: type, - olids: olids, - }; + olids: olids + } if (comment) { - data['comment'] = comment; + data['comment'] = comment } if (primary) { - data['primary'] = primary; + data['primary'] = primary } return fetch('/merges', { method: 'POST', headers: { Accept: 'application/json', - 'Content-Type': 'application/json', + 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }); + body: JSON.stringify(data) + }) } /** @@ -45,20 +40,20 @@ async function updateRequest(action, mrid, comment = null) { const data = { rtype: 'update-request', action: action, - mrid: mrid, - }; + mrid: mrid + } if (comment) { - data['comment'] = comment; + data['comment'] = comment } return fetch('/merges', { method: 'POST', headers: { Accept: 'application/json', - 'Content-Type': 'application/json', + 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }); + body: JSON.stringify(data) + }) } /** @@ -69,7 +64,7 @@ async function updateRequest(action, mrid, comment = null) { * @returns {Promise<Response>} The results of the update POST request */ export async function commentOnRequest(mrid, comment) { - return updateRequest('comment', mrid, comment); + return updateRequest('comment', mrid, comment) } /** @@ -78,17 +73,17 @@ export async function commentOnRequest(mrid, comment) { * @param {Number} mrid Unique identifier for the request being claimed */ export async function claimRequest(mrid) { - return updateRequest('claim', mrid); + return updateRequest('claim', mrid) } export async function unassignRequest(mrid) { - return updateRequest('unassign', mrid); + return updateRequest('unassign', mrid) } export async function declineRequest(mrid, comment) { - return updateRequest('decline', mrid, comment); + return updateRequest('decline', mrid, comment) } export async function approveRequest(mrid, comment) { - return updateRequest('approve', mrid, comment); + return updateRequest('approve', mrid, comment) } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js index 2bb68c4228e..7e850020557 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js @@ -5,8 +5,8 @@ * @module merge-request-table/MergeRequestTable */ -import TableHeader from './MergeRequestTable/TableHeader'; -import { setI18nStrings, TableRow } from './MergeRequestTable/TableRow'; +import TableHeader from './MergeRequestTable/TableHeader' +import { setI18nStrings, TableRow } from './MergeRequestTable/TableRow' /** * Class representing the librarian request table. @@ -14,51 +14,48 @@ import { setI18nStrings, TableRow } from './MergeRequestTable/TableRow'; * @class */ export default class MergeRequestTable { + /** - * Creates references to the table and its header and hydrates each. - * - * @param {HTMLElement} mergeRequestTable - */ - constructor (mergeRequestTable) { - /** - * The `username` of the authenticated patron, or '' if logged out. + * Creates references to the table and its header and hydrates each. * - * @param {string} + * @param {HTMLElement} mergeRequestTable */ - this.username = mergeRequestTable.dataset.username; + constructor(mergeRequestTable) { + /** + * The `username` of the authenticated patron, or '' if logged out. + * + * @param {string} + */ + this.username = mergeRequestTable.dataset.username - const localizedStrings = JSON.parse(mergeRequestTable.dataset.i18n); - setI18nStrings(localizedStrings); + const localizedStrings = JSON.parse(mergeRequestTable.dataset.i18n) + setI18nStrings(localizedStrings) /** - * Reference to this table's header. - * - * @param {HTMLElement} - */ - this.tableHeader = new TableHeader( - mergeRequestTable.querySelector('.table-header'), - ); + * Reference to this table's header. + * + * @param {HTMLElement} + */ + this.tableHeader = new TableHeader(mergeRequestTable.querySelector('.table-header')) /** - * References to each row in the table. - * - * @param {Array<TableRow>} - */ - this.tableRows = []; - const rowElements = mergeRequestTable.querySelectorAll('.mr-table-row'); + * References to each row in the table. + * + * @param {Array<TableRow>} + */ + this.tableRows = [] + const rowElements = mergeRequestTable.querySelectorAll('.mr-table-row') for (const elem of rowElements) { - this.tableRows.push(new TableRow(elem, this.username)); + this.tableRows.push(new TableRow(elem, this.username)) } } /** - * Hydrates the librarian request table. - */ - initialize () { - this.tableHeader.initialize(); - document.addEventListener('click', (event) => - this.tableHeader.closeMenusIfClickOutside(event), - ); - this.tableRows.forEach((elem) => elem.initialize()); + * Hydrates the librarian request table. + */ + initialize() { + this.tableHeader.initialize() + document.addEventListener('click', (event) => this.tableHeader.closeMenusIfClickOutside(event)) + this.tableRows.forEach(elem => elem.initialize()) } } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js index 256c5cabf60..39be68c2b3d 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js @@ -15,130 +15,125 @@ */ export default class TableHeader { /** - * Sets references to many table header affordances. - * - * @param {HTMLElement} tableHeader - */ - constructor (tableHeader) { - /** - * References to each select menu. These are always visible - * in the header bar, and, when clicked, display a drop-down - * menu with filtering options. + * Sets references to many table header affordances. * - * @param {NodeList<HTMLElement>} + * @param {HTMLElement} tableHeader */ - this.dropMenuButtons = tableHeader.querySelectorAll('.mr-dropdown'); + constructor(tableHeader) { /** - * References each drop-down filter option menu. - * - * @param {NodeList<HTMLElement>} - */ - this.dropMenus = tableHeader.querySelectorAll('.mr-dropdown-menu'); + * References to each select menu. These are always visible + * in the header bar, and, when clicked, display a drop-down + * menu with filtering options. + * + * @param {NodeList<HTMLElement>} + */ + this.dropMenuButtons = tableHeader.querySelectorAll('.mr-dropdown') /** - * References each drop-down menu "X" affordance, which closes - * the appropriate drop-down menu. - * - * @param{NodeList<HTMLElement>} - */ - this.closeButtons = tableHeader.querySelectorAll('.dropdown-close'); + * References each drop-down filter option menu. + * + * @param {NodeList<HTMLElement>} + */ + this.dropMenus = tableHeader.querySelectorAll('.mr-dropdown-menu') /** - * References each text input filter. - * - * @param{NodeList<HTMLElement>} - */ - this.searchInputs = tableHeader.querySelectorAll('.filter'); + * References each drop-down menu "X" affordance, which closes + * the appropriate drop-down menu. + * + * @param{NodeList<HTMLElement>} + */ + this.closeButtons = tableHeader.querySelectorAll('.dropdown-close') + /** + * References each text input filter. + * + * @param{NodeList<HTMLElement>} + */ + this.searchInputs = tableHeader.querySelectorAll('.filter') } /** - * Hydrates the table header affordances. - */ - initialize () { - this.initFilters(); + * Hydrates the table header affordances. + */ + initialize() { + this.initFilters() } /** - * Toggle a dropdown menu while closing other dropdown menus. - * - * @param {Event} event - * @param {string} menuButtonId - */ - toggleAMenuWhileClosingOthers (event, menuButtonId) { - // prevent closing of menu on bubbling unless click menuButton itself + * Toggle a dropdown menu while closing other dropdown menus. + * + * @param {Event} event + * @param {string} menuButtonId + */ + toggleAMenuWhileClosingOthers(event, menuButtonId) { + // prevent closing of menu on bubbling unless click menuButton itself if (event.target.id === menuButtonId) { // close other open menus, then toggle selected menu - this.closeOtherMenus(menuButtonId); - event.target.firstElementChild.classList.toggle('hidden'); + this.closeOtherMenus(menuButtonId) + event.target.firstElementChild.classList.toggle('hidden') } } /** - * Close dropdown menus whose menu button doesn't match a given id. - * - * @param {string} menuButtonId - */ - closeOtherMenus (menuButtonId) { + * Close dropdown menus whose menu button doesn't match a given id. + * + * @param {string} menuButtonId + */ + closeOtherMenus(menuButtonId) { this.dropMenuButtons.forEach((menuButton) => { if (menuButton.id !== menuButtonId) { - menuButton.firstElementChild.classList.add('hidden'); + menuButton.firstElementChild.classList.add('hidden') } - }); + }) } /** - * Filters of dropdown menu items using case-insensitive string matching. - * - * @param {Event} event - */ - filterMenuItems (event) { - const input = document.getElementById(event.target.id); - const filter = input.value.toUpperCase(); - const menu = input.closest('.mr-dropdown-menu'); - const items = menu.getElementsByClassName('dropdown-item'); + * Filters of dropdown menu items using case-insensitive string matching. + * + * @param {Event} event + */ + filterMenuItems(event) { + const input = document.getElementById(event.target.id) + const filter = input.value.toUpperCase() + const menu = input.closest('.mr-dropdown-menu') + const items = menu.getElementsByClassName('dropdown-item') // skip first item in menu - for (let i = 1; i < items.length; i++) { - const text = items[i].textContent; - items[i].classList.toggle( - 'hidden', - text.toUpperCase().indexOf(filter) === -1, - ); + for (let i=1; i < items.length; i++) { + const text = items[i].textContent + items[i].classList.toggle('hidden', text.toUpperCase().indexOf(filter) === -1); } } /** - * Close all dropdown menus when click anywhere on screen that's not part of - * the dropdown menu; otherwise, keep dropdown menu open. - * - * @param {Event} event - */ - closeMenusIfClickOutside (event) { - const menusClicked = Array.from(this.dropMenuButtons).filter( - (menuButton) => { - return menuButton.contains(event.target); - }, - ); + * Close all dropdown menus when click anywhere on screen that's not part of + * the dropdown menu; otherwise, keep dropdown menu open. + * + * @param {Event} event + */ + closeMenusIfClickOutside(event) { + const menusClicked = Array.from(this.dropMenuButtons).filter((menuButton) => { + return menuButton.contains(event.target) + }) // want to preserve clicking in a menu, i.e. when filtering for users if (!menusClicked.length) { - this.dropMenus.forEach((menu) => menu.classList.add('hidden')); + this.dropMenus.forEach((menu) => menu.classList.add('hidden')) } } /** - * Initialize events for merge queue filter dropdown menu functionality. - * - */ - initFilters () { + * Initialize events for merge queue filter dropdown menu functionality. + * + */ + initFilters() { this.dropMenuButtons.forEach((menuButton) => { menuButton.addEventListener('click', (event) => { - this.toggleAMenuWhileClosingOthers(event, menuButton.id); - }); - }); + this.toggleAMenuWhileClosingOthers(event, menuButton.id) + }) + }) this.closeButtons.forEach((button) => { button.addEventListener('click', (event) => { - event.target.closest('.mr-dropdown-menu').classList.toggle('hidden'); - }); - }); + event.target.closest('.mr-dropdown-menu').classList.toggle('hidden') + }) + }) this.searchInputs.forEach((input) => { - input.addEventListener('keyup', (event) => this.filterMenuItems(event)); - }); + input.addEventListener('keyup', (event) => this.filterMenuItems(event)) + }) } } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js index 4ba90586c99..b78e5988c56 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js @@ -4,17 +4,12 @@ * @module merge-request-table/MergeRequestTable/TableRow */ -import { FadingToast } from '../../Toast'; -import { - claimRequest, - commentOnRequest, - declineRequest, - unassignRequest, -} from '../MergeRequestService'; +import { claimRequest, commentOnRequest, declineRequest, unassignRequest } from '../MergeRequestService' +import { FadingToast } from '../../Toast' let i18nStrings; -export function setI18nStrings (localizedStrings) { +export function setI18nStrings(localizedStrings) { i18nStrings = localizedStrings; } @@ -32,273 +27,244 @@ export function setI18nStrings (localizedStrings) { */ export class TableRow { /** - * Creates a new librarian request table row. - * - * Stores reference to each interactive element in a row. - * - * @param {HTMLElement} row Root element of a table row - * @param {string} username `username` of logged-in patron. Empty if unauthenticated. - */ - constructor (row, username) { - /** - * Reference to this row. + * Creates a new librarian request table row. * - * @param {HTMLElement} - */ - this.row = row; - /** - * `username` of authenticated patron, or '' if unauthenticated. + * Stores reference to each interactive element in a row. * - * @param {HTMLElement} + * @param {HTMLElement} row Root element of a table row + * @param {string} username `username` of logged-in patron. Empty if unauthenticated. */ - this.username = username; + constructor(row, username) { /** - * Unique identifier for this row. - * - * @param {Number} - */ - this.mrid = row.dataset.mrid; + * Reference to this row. + * + * @param {HTMLElement} + */ + this.row = row /** - * Button used to toggle the full comments display's visibility. - * - * @param {HTMLElement} - */ - this.toggleCommentButton = row.querySelector( - '.mr-comment-toggle__comment-expand', - ); + * `username` of authenticated patron, or '' if unauthenticated. + * + * @param {HTMLElement} + */ + this.username = username /** - * Element which displays this row's comment count. - * - * @param {HTMLElement} - */ - this.commentCountDisplay = row.querySelector( - '.mr-comment-toggle__comment-count', - ); + * Unique identifier for this row. + * + * @param {Number} + */ + this.mrid = row.dataset.mrid /** - * Element displaying the most recent comment on this request. - * - * @param {HTMLElement} - */ - this.commentPreview = row.querySelector('.mr-details__comment-preview'); + * Button used to toggle the full comments display's visibility. + * + * @param {HTMLElement} + */ + this.toggleCommentButton = row.querySelector('.mr-comment-toggle__comment-expand') /** - * Hidden comments display. Also contains reply inputs, if rendered. - * - * @param {HTMLElement} - */ - this.fullCommentsPanel = row.querySelector('.comment-panel'); + * Element which displays this row's comment count. + * + * @param {HTMLElement} + */ + this.commentCountDisplay = row.querySelector('.mr-comment-toggle__comment-count') /** - * Element that displays all of the comments for this request. - * - * @param {HTMLElement} - */ - this.commentsDisplay = this.fullCommentsPanel.querySelector( - '.comment-panel__comment-display', - ); + * Element displaying the most recent comment on this request. + * + * @param {HTMLElement} + */ + this.commentPreview = row.querySelector('.mr-details__comment-preview') /** - * The comment text input. - * - * @param {HTMLElement|null} - */ - this.commentReplyInput = this.fullCommentsPanel.querySelector( - '.comment-panel__reply-input', - ); + * Hidden comments display. Also contains reply inputs, if rendered. + * + * @param {HTMLElement} + */ + this.fullCommentsPanel = row.querySelector('.comment-panel') /** - * The comment reply button. - * - * @param {HTMLElement|null} - */ - this.replyButton = this.fullCommentsPanel.querySelector( - '.comment-panel__reply-btn', - ); + * Element that displays all of the comments for this request. + * + * @param {HTMLElement} + */ + this.commentsDisplay = this.fullCommentsPanel.querySelector('.comment-panel__comment-display') /** - * Affordance which allows one to close their own request. - * - * Only available on a patron's own open requests. - * - * @param {HTMLElement|null} - */ - this.closeRequestButton = this.row.querySelector('.mr-close-link'); + * The comment text input. + * + * @param {HTMLElement|null} + */ + this.commentReplyInput = this.fullCommentsPanel.querySelector('.comment-panel__reply-input') /** - * Button used by super-librarians to claim a request. - * - * @param {HTMLElement} - */ - this.reviewButton = this.row.querySelector( - '.mr-review-actions__review-btn', - ); + * The comment reply button. + * + * @param {HTMLElement|null} + */ + this.replyButton = this.fullCommentsPanel.querySelector('.comment-panel__reply-btn') /** - * Reference to root element of the assignee display. - * - * @param {HTMLElement} - */ - this.assigneeElement = this.row.querySelector( - '.mr-review-actions__assignee', - ); + * Affordance which allows one to close their own request. + * + * Only available on a patron's own open requests. + * + * @param {HTMLElement|null} + */ + this.closeRequestButton = this.row.querySelector('.mr-close-link') /** - * Assignee display element which displays the assignee's name. - * - * @param {HTMLElement} - */ - this.assigneeLabel = this.row.querySelector( - '.mr-review-actions__assignee-name', - ); + * Button used by super-librarians to claim a request. + * + * @param {HTMLElement} + */ + this.reviewButton = this.row.querySelector('.mr-review-actions__review-btn') /** - * Element that unassignees the current reviewer when clicked. - * - * @param {HTMLElement} - */ - this.unassignReviewerButton = this.row.querySelector( - '.mr-review-actions__unassign', - ); + * Reference to root element of the assignee display. + * + * @param {HTMLElement} + */ + this.assigneeElement = this.row.querySelector('.mr-review-actions__assignee') + /** + * Assignee display element which displays the assignee's name. + * + * @param {HTMLElement} + */ + this.assigneeLabel = this.row.querySelector('.mr-review-actions__assignee-name') + /** + * Element that unassignees the current reviewer when clicked. + * + * @param {HTMLElement} + */ + this.unassignReviewerButton = this.row.querySelector('.mr-review-actions__unassign') } /** - * Hydrates interactive elements in this row. - */ - initialize () { - this.toggleCommentButton.addEventListener('click', () => - this.toggleComments(), - ); + * Hydrates interactive elements in this row. + */ + initialize() { + this.toggleCommentButton.addEventListener('click', () => this.toggleComments()) if (this.closeRequestButton) { - this.closeRequestButton.addEventListener('click', () => - this.closeRequest(), - ); + this.closeRequestButton.addEventListener('click', () => this.closeRequest()) } if (this.replyButton && this.commentReplyInput) { - this.replyButton.addEventListener('click', () => this.addComment()); + this.replyButton.addEventListener('click', () => this.addComment()) } - this.reviewButton.addEventListener('click', () => this.claimRequest()); + this.reviewButton.addEventListener('click', () => this.claimRequest()) if (this.unassignReviewerButton) { - this.unassignReviewerButton.addEventListener('click', () => - this.unassignReviewer(), - ); + this.unassignReviewerButton.addEventListener('click', () => this.unassignReviewer()) } } /** - * Toggles which comment display is currently visible. - * - * On page load the comment preview display is visible, while - * the full comments panel is hidden. This function toggles - * each element's visibility. - */ - toggleComments () { - this.commentPreview.classList.toggle('hidden'); - this.fullCommentsPanel.classList.toggle('hidden'); + * Toggles which comment display is currently visible. + * + * On page load the comment preview display is visible, while + * the full comments panel is hidden. This function toggles + * each element's visibility. + */ + toggleComments() { + this.commentPreview.classList.toggle('hidden') + this.fullCommentsPanel.classList.toggle('hidden') // Add depressed effect to toggle button: - this.toggleCommentButton.classList.toggle( - 'mr-comment-toggle__comment-expand--active', - ); + this.toggleCommentButton.classList.toggle('mr-comment-toggle__comment-expand--active'); } /** - * Closes the request linked to this row, and removes this - * row from the DOM. - */ - async closeRequest () { - const comment = prompt(i18nStrings['close_request_comment_prompt']); - if (comment !== null) { - // Comment will be `null` if "Cancel" button pressed + * Closes the request linked to this row, and removes this + * row from the DOM. + */ + async closeRequest() { + const comment = prompt(i18nStrings['close_request_comment_prompt']) + if (comment !== null) { // Comment will be `null` if "Cancel" button pressed await declineRequest(this.mrid, comment) - .then((result) => result.json()) - .then((data) => { + .then(result => result.json()) + .then(data => { if (data.status === 'ok') { - this.row.parentElement.removeChild(this.row); + this.row.parentElement.removeChild(this.row) } }) - .catch((e) => { + .catch(e => { // XXX : toast? - throw e; - }); + throw e + }) } } /** - * `POST`s a new comment to the server. - * - * Updates the view on success. - */ - async addComment () { - const comment = this.commentReplyInput.value.trim(); + * `POST`s a new comment to the server. + * + * Updates the view on success. + */ + async addComment() { + const comment = this.commentReplyInput.value.trim() if (comment) { await commentOnRequest(this.mrid, comment) - .then((result) => result.json()) - .then((data) => { + .then(result => result.json()) + .then(data => { if (data.status === 'ok') { - this.updateCommentViews(comment); - this.commentReplyInput.value = ''; + this.updateCommentViews(comment) + this.commentReplyInput.value = '' } else { - new FadingToast( - i18nStrings['comment_submission_failure_message'], - ).show(); + new FadingToast(i18nStrings['comment_submission_failure_message']).show() } }) - .catch((e) => { - throw e; - }); + .catch(e => { + throw e + }) } } /** - * Updates row, setting given comment as most recent. - * - * First, escapes given comment. Replaces text of comment - * preview with escaped comment. Add new comment element to - * full comments display. Increments the row's comment count. - * - * @param {string} comment The newly added comment. - */ - updateCommentViews (comment) { - const escapedComment = document.createTextNode(comment); + * Updates row, setting given comment as most recent. + * + * First, escapes given comment. Replaces text of comment + * preview with escaped comment. Add new comment element to + * full comments display. Increments the row's comment count. + * + * @param {string} comment The newly added comment. + */ + updateCommentViews(comment) { + const escapedComment = document.createTextNode(comment) // Update preview: - this.commentPreview.innerText = escapedComment.textContent; + this.commentPreview.innerText = escapedComment.textContent // Update full display: - const newComment = document.createElement('div'); - newComment.classList.add('comment-panel__comment'); - newComment.innerHTML = `<span class="commenter">@${this.username}</span> `; - newComment.appendChild(escapedComment); + const newComment = document.createElement('div') + newComment.classList.add('comment-panel__comment') + newComment.innerHTML = `<span class="commenter">@${this.username}</span> ` + newComment.appendChild(escapedComment) - this.commentsDisplay.appendChild(newComment); - this.commentsDisplay.scrollTop = this.commentsDisplay.scrollHeight; + this.commentsDisplay.appendChild(newComment) + this.commentsDisplay.scrollTop = this.commentsDisplay.scrollHeight // Update comment count: - const count = Number(this.commentCountDisplay.innerText) + 1; - this.commentCountDisplay.innerText = count; + const count = Number(this.commentCountDisplay.innerText) + 1 + this.commentCountDisplay.innerText = count } /** - * `POST`s claim to review this request, then updates the view. - * - * Hides the review button, and shows the assignee display. - */ - async claimRequest () { + * `POST`s claim to review this request, then updates the view. + * + * Hides the review button, and shows the assignee display. + */ + async claimRequest() { await claimRequest(this.mrid) - .then((result) => result.json()) - .then((data) => { + .then(result => result.json()) + .then(data => { if (data.status === 'ok') { - this.assigneeLabel.innerText = `@${this.username}`; - this.assigneeElement.classList.remove('hidden'); - this.reviewButton.classList.add('hidden'); + this.assigneeLabel.innerText = `@${this.username}` + this.assigneeElement.classList.remove('hidden') + this.reviewButton.classList.add('hidden') } - }); + }) } /** - * `POST`s request to remove current assignee, then updates the view. - * - * Hides the assignee display and shows the review button on success. - */ - async unassignReviewer () { + * `POST`s request to remove current assignee, then updates the view. + * + * Hides the assignee display and shows the review button on success. + */ + async unassignReviewer() { await unassignRequest(this.mrid) - .then((result) => result.json()) - .then((data) => { + .then(result => result.json()) + .then(data => { if (data.status === 'ok') { - this.assigneeElement.classList.add('hidden'); - this.reviewButton.classList.remove('hidden'); + this.assigneeElement.classList.add('hidden') + this.reviewButton.classList.remove('hidden') } - }); + }) } } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/index.js b/openlibrary/plugins/openlibrary/js/merge-request-table/index.js index 113dfdf5daf..a98cff2c265 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/index.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/index.js @@ -5,7 +5,7 @@ import MergeRequestTable from './MergeRequestTable'; * * @param {HTMLElement} elem Reference to the queue's root element. */ -export function initLibrarianQueue (elem) { - const librarianQueue = new MergeRequestTable(elem); - librarianQueue.initialize(); +export function initLibrarianQueue(elem) { + const librarianQueue = new MergeRequestTable(elem) + librarianQueue.initialize() } diff --git a/openlibrary/plugins/openlibrary/js/merge.js b/openlibrary/plugins/openlibrary/js/merge.js index 86018ebf552..4a61df9080d 100644 --- a/openlibrary/plugins/openlibrary/js/merge.js +++ b/openlibrary/plugins/openlibrary/js/merge.js @@ -2,15 +2,15 @@ import 'jquery-ui/ui/widgets/dialog'; import { declineRequest } from './merge-request-table/MergeRequestService'; export function initAuthorMergePage() { - $('#save').on('click', () => { + $('#save').on('click', function () { const n = $('#mergeForm input[type=radio]:checked').length; - const confirmMergeButton = document.querySelector('#confirmMerge'); + const confirmMergeButton = document.querySelector('#confirmMerge') if (n === 0) { $('#noMaster').dialog('open'); } else if (confirmMergeButton) { $('#confirmMerge').dialog('open'); } else { - $('#mergeForm').trigger('submit'); + $('#mergeForm').trigger('submit') } return false; }); @@ -28,11 +28,7 @@ export function initAuthorMergePage() { previousMaster.removeClass('master mergeSelection'); previousMaster.find('input[type=checkbox]').prop('checked', false); $(this).parent().parent().addClass('master'); - $(this) - .parent() - .parent() - .find('input[type=checkbox]') - .prop('checked', true); + $(this).parent().parent().find('input[type=checkbox]').prop('checked', true); }); $('#include input[type=checkbox]').on('change', function () { if (!$(this).parent().parent().hasClass('master')) { @@ -43,25 +39,25 @@ export function initAuthorMergePage() { } } }); - initRejectButton(); + initRejectButton() } function initRejectButton() { - const rejectButton = document.querySelector('#reject-author-merge-btn'); + const rejectButton = document.querySelector('#reject-author-merge-btn') if (rejectButton) { - rejectButton.addEventListener('click', () => { - rejectMerge(); - rejectButton.disabled = true; - const approveButton = document.querySelector('#save'); - approveButton.disabled = true; - }); + rejectButton.addEventListener('click', function() { + rejectMerge() + rejectButton.disabled = true + const approveButton = document.querySelector('#save') + approveButton.disabled = true + }) } } function rejectMerge() { - const commentInput = document.querySelector('#author-merge-comment'); - const mridInput = document.querySelector('#mrid-input'); - declineRequest(Number(mridInput.value), commentInput.value); + const commentInput = document.querySelector('#author-merge-comment') + const mridInput = document.querySelector('#mrid-input') + declineRequest(Number(mridInput.value), commentInput.value) } /** @@ -79,30 +75,30 @@ export function initAuthorView() { const data = { master: dataKeysJSON['master'], duplicates: dataKeysJSON['duplicates'], - olids: dataKeysJSON['olids'], + olids: dataKeysJSON['olids'] }; - const mrid = dataKeysJSON['mrid']; - const comment = dataKeysJSON['comment']; + const mrid = dataKeysJSON['mrid'] + const comment = dataKeysJSON['comment'] if (mrid) { - data['mrid'] = mrid; + data['mrid'] = mrid } if (comment) { - data['comment'] = comment; + data['comment'] = comment } $.ajax({ url: '/authors/merge.json', type: 'POST', data: JSON.stringify(data), - error: () => { + error: function() { $('#preMerge').fadeOut(); $('#errorMerge').fadeIn(); }, - success: () => { + success: function() { $('#preMerge').fadeOut(); $('#postMerge').fadeIn(); - }, + } }); } diff --git a/openlibrary/plugins/openlibrary/js/modals/index.js b/openlibrary/plugins/openlibrary/js/modals/index.js index 2016c3fa3f6..cb3f921c2b1 100644 --- a/openlibrary/plugins/openlibrary/js/modals/index.js +++ b/openlibrary/plugins/openlibrary/js/modals/index.js @@ -2,6 +2,8 @@ import 'jquery-colorbox'; import { FadingToast } from '../Toast.js'; import '../../../../../static/css/components/metadata-form.css'; + + /** * Initializes share modal. */ @@ -12,13 +14,13 @@ export function initShareModal($modalLinks) { /** * Adds click listeners to buttons in all notes modals on a page. */ -function addShareModalButtonListeners() { - $('#social-modal-content .copy-url-btn').on('click', (event) => { +function addShareModalButtonListeners (){ + $('#social-modal-content .copy-url-btn').on('click', function(event){ event.preventDefault(); navigator.clipboard.writeText(window.location.href); - showToast('URL copied to clipboard'); - $.colorbox.close(); - }); + showToast('URL copied to clipboard') + $.colorbox.close() + }) } /** @@ -36,7 +38,7 @@ export function initNotesModal($modalLinks) { * Adds click listeners to buttons in all notes modals on a page. */ function addNotesModalButtonListeners() { - $('.update-note-button').on('click', function (event) { + $('.update-note-button').on('click', function(event){ event.preventDefault(); // Get form data const formData = new FormData($(this).closest('form')[0]); @@ -53,16 +55,16 @@ function addNotesModalButtonListeners() { type: 'POST', contentType: false, processData: false, - success: () => { - showToast('Update successful!'); + success: function() { + showToast('Update successful!') $.colorbox.close(); $deleteButton.removeClass('hidden'); - }, + } }); } }); - $('.delete-note-button').on('click', function () { + $('.delete-note-button').on('click', function() { if (confirm('Really delete this book note?')) { const $button = $(this); @@ -80,25 +82,25 @@ function addNotesModalButtonListeners() { type: 'POST', contentType: false, processData: false, - success: () => { + success: function() { showToast('Note deleted.'); $.colorbox.close(); $button.toggleClass('hidden'); $button.closest('form').find('textarea').val(''); - }, + } }); } }); } /** - * Add listeners to update and delete buttons on the notes page. - * - * On successful delete, list elements related to the note are removedd - * from the view. - */ +* Add listeners to update and delete buttons on the notes page. +* +* On successful delete, list elements related to the note are removedd +* from the view. +*/ export function addNotesPageButtonListeners() { - $('.update-note-link-button').on('click', function (event) { + $('.update-note-link-button').on('click', function(event) { event.preventDefault(); const workId = $(this).parent().siblings('input')[0].value; const editionId = $(this).parent().attr('id').split('-')[0]; @@ -114,13 +116,13 @@ export function addNotesPageButtonListeners() { type: 'POST', contentType: false, processData: false, - success: () => { - showToast('Update successful!'); - }, + success: function() { + showToast('Update successful!') + } }); }); - $('.delete-note-button').on('click', function () { + $('.delete-note-button').on('click', function() { if (confirm('Really delete this book note?')) { const $parent = $(this).parent(); @@ -136,7 +138,7 @@ export function addNotesPageButtonListeners() { type: 'POST', contentType: false, processData: false, - success: () => { + success: function() { showToast('Note deleted.'); // Remove list element from UI: @@ -153,7 +155,7 @@ export function addNotesPageButtonListeners() { // Remove the edition's notes list item: $parent.closest('.notes-list-item').remove(); } - }, + } }); } }); @@ -168,13 +170,11 @@ export function addNotesPageButtonListeners() { * @param {JQuery} $notesTextareas All notes text areas on a page. */ function addNotesReloadListeners($notesTextareas) { - $notesTextareas.each((_i, textarea) => { + $notesTextareas.each(function(_i, textarea) { const $textarea = $(textarea); - $textarea.on('contentReload', () => { - const newValue = $textarea - .parent() - .find('.notes-modal-textarea')[0].value; + $textarea.on('contentReload', function() { + const newValue = $textarea.parent().find('.notes-modal-textarea')[0].value; $textarea.val(newValue); }); }); @@ -200,15 +200,15 @@ function showToast(message, $parent) { */ export function initObservationsModal($modalLinks) { addClickListeners($modalLinks, '800px'); - addObservationReloadListeners($('.observations-list')); + addObservationReloadListeners($('.observations-list')) addDeleteObservationsListeners($('.delete-observations-button')); - $modalLinks.each((_i, modalLinkElement) => { + $modalLinks.each(function(_i, modalLinkElement) { const $element = $(modalLinkElement); - const context = JSON.parse(getModalContent($element).dataset['context']); + const context = JSON.parse(getModalContent($element).dataset['context']) addObservationChangeListeners($element.next(), context); - }); + }) } /** @@ -220,13 +220,13 @@ export function initObservationsModal($modalLinks) { * @param {JQuery} $modalLinks A collection of modal links. */ function addClickListeners($modalLinks, maxWidth) { - $modalLinks.each((_i, modalLinkElement) => { - $(modalLinkElement).on('click', function () { + $modalLinks.each(function(_i, modalLinkElement) { + $(modalLinkElement).on('click', function() { // Get context, which is attached to the modal content - const content = getModalContent($(this)); + const content = getModalContent($(this)) displayModal(content, maxWidth); - }); - }); + }) + }) } /** @@ -237,7 +237,7 @@ function addClickListeners($modalLinks, maxWidth) { * @returns {HTMLElement} Reference to a modal's content */ function getModalContent($modalLink) { - return $modalLink.siblings()[0].children[0]; + return $modalLink.siblings()[0].children[0] } /** @@ -251,8 +251,8 @@ function getModalContent($modalLink) { * @param {JQuery} $observationLists All of the observations lists on a page */ function addObservationReloadListeners($observationLists) { - $observationLists.each((_i, list) => { - $(list).on('contentReload', function () { + $observationLists.each(function(_i, list) { + $(list).on('contentReload', function() { const $list = $(this); const $buttonsDiv = $list.siblings('div').first(); const id = $list.attr('id'); @@ -263,49 +263,49 @@ function addObservationReloadListeners($observationLists) { <li class="throbber-li"> <div class="throbber"><h3>Updating observations</h3></div> </li> - `); + `) $.ajax({ type: 'GET', url: `/works/${workOlid}/observations`, - dataType: 'json', - }).done((data) => { - let listItems = ''; - for (const [category, values] of Object.entries(data)) { - let observations = values.join(', '); - observations = - observations.charAt(0).toUpperCase() + observations.slice(1); - - listItems += ` + dataType: 'json' + }) + .done(function(data) { + let listItems = ''; + for (const [category, values] of Object.entries(data)) { + let observations = values.join(', '); + observations = observations.charAt(0).toUpperCase() + observations.slice(1); + + listItems += ` <li> <span class="observation-category">${category.charAt(0).toUpperCase() + category.slice(1)}:</span> ${observations} </li> `; - } + } - $list.empty(); + $list.empty(); - if (listItems.length === 0) { - listItems = ` + if (listItems.length === 0) { + listItems = ` <li> No observations for this work. </li> `; - $list.addClass('no-content'); - $buttonsDiv.removeClass('observation-buttons'); - $buttonsDiv.addClass('no-content'); - $buttonsDiv.children().first().addClass('hidden'); - } else { - $list.removeClass('no-content'); - $buttonsDiv.removeClass('no-content'); - $buttonsDiv.addClass('observation-buttons'); - $buttonsDiv.children().first().removeClass('hidden'); - } + $list.addClass('no-content'); + $buttonsDiv.removeClass('observation-buttons'); + $buttonsDiv.addClass('no-content'); + $buttonsDiv.children().first().addClass('hidden'); + } else { + $list.removeClass('no-content'); + $buttonsDiv.removeClass('no-content'); + $buttonsDiv.addClass('observation-buttons'); + $buttonsDiv.children().first().removeClass('hidden'); + } - $list.append(listItems); - }); - }); - }); + $list.append(listItems); + }) + }) + }) } /** @@ -319,17 +319,17 @@ function addObservationReloadListeners($observationLists) { * @param {JQuery} $deleteButtons All observation delete buttons found on a page. */ function addDeleteObservationsListeners($deleteButtons) { - $deleteButtons.each((_i, deleteButton) => { + $deleteButtons.each(function(_i, deleteButton) { const $button = $(deleteButton); - $button.on('click', () => { + $button.on('click', function() { const workOlid = `OL${$button.prop('id').split('-')[0]}W`; $.ajax({ url: `/works/${workOlid}/observations`, type: 'DELETE', contentType: 'application/json', - success: () => { + success: function() { // Remove observations in view const $observationsView = $button.closest('.observation-view'); const $list = $observationsView.find('ul'); @@ -339,7 +339,7 @@ function addDeleteObservationsListeners($deleteButtons) { <li> No observations for this work. </li> - `); + `) $list.addClass('no-content'); $button.parent().removeClass('observation-buttons'); @@ -348,9 +348,9 @@ function addDeleteObservationsListeners($deleteButtons) { // find and clear modal selections clearForm($button.siblings().find('form')); - }, + } }); - }); + }) }); } @@ -360,7 +360,7 @@ function addDeleteObservationsListeners($deleteButtons) { * @param {JQuery} $form An observations modal form */ function clearForm($form) { - $form.find('input').each((_i, input) => { + $form.find('input').each(function(_i, input) { if (input.checked) { input.checked = false; } @@ -376,10 +376,8 @@ function clearForm($form) { * @param {String} maxWidth The max width of the modal */ function displayModal(content, maxWidth) { - const modalId = `#${content.id}`; - const context = content.dataset['context'] - ? JSON.parse(content.dataset['context']) - : null; + const modalId = `#${content.id}` + const context = content.dataset['context'] ? JSON.parse(content.dataset['context']) : null; const reloadId = context ? context.reloadId : null; $.colorbox({ @@ -388,11 +386,11 @@ function displayModal(content, maxWidth) { href: modalId, width: '100%', maxWidth: maxWidth, - onClosed: () => { + onClosed: function() { if (reloadId) { $(`#${reloadId}`).trigger('contentReload'); } - }, + } }); } @@ -412,11 +410,11 @@ function addObservationChangeListeners($parent, context) { const username = context.username; const workOlid = context.work.split('/')[2]; - $questionSections.each(function () { - const $inputs = $(this).find('input'); + $questionSections.each(function() { + const $inputs = $(this).find('input') - $inputs.each(function () { - $(this).on('change', function () { + $inputs.each(function() { + $(this).on('change', function() { const type = $(this).attr('name'); const value = $(this).attr('value'); const observation = {}; @@ -424,13 +422,13 @@ function addObservationChangeListeners($parent, context) { const data = { username: username, - action: `${$(this).prop('checked') ? 'add' : 'delete'}`, - observation: observation, - }; + action: `${$(this).prop('checked') ? 'add': 'delete'}`, + observation: observation + } submitObservation($(this), workOlid, data, type); }); - }); + }) }); } @@ -443,23 +441,22 @@ function addObservationChangeListeners($parent, context) { */ function submitObservation($input, workOlid, data, sectionType) { let toastMessage; - const capitalizedType = - sectionType[0].toUpperCase() + sectionType.substring(1); + const capitalizedType = sectionType[0].toUpperCase() + sectionType.substring(1); // Make AJAX call $.ajax({ type: 'POST', url: `/works/${workOlid}/observations`, contentType: 'application/json', - data: JSON.stringify(data), + data: JSON.stringify(data) }) - .done(() => { + .done(function() { toastMessage = `${capitalizedType} saved!`; }) - .fail(() => { + .fail(function() { toastMessage = `${capitalizedType} save failed...`; }) - .always(() => { + .always(function() { showToast(toastMessage, $input.closest('.metadata-form')); }); } diff --git a/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js b/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js index ce3cfb95e9e..89660d454c7 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js +++ b/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js @@ -4,10 +4,10 @@ * @module my-books/CreateListForm.js */ import 'jquery-colorbox'; -import { websafe } from '../jsdef'; -import { createList } from '../lists/ListService'; -import { attachNewActiveShowcaseItem } from '../lists/ShowcaseItem'; -import myBooksStore from './store'; +import myBooksStore from './store' +import { websafe } from '../jsdef' +import { createList } from '../lists/ListService' +import { attachNewActiveShowcaseItem } from '../lists/ShowcaseItem' /** * Represents the list creation form displayed when a patron @@ -20,123 +20,119 @@ import myBooksStore from './store'; * @class */ export class CreateListForm { + /** - * Creates a new `CreateListForm` object. - * - * Sets references to form inputs and "Create List" button. - * - * @param {HTMLElement} form - */ - constructor (form) { - /** - * References this form's "Create List" button. + * Creates a new `CreateListForm` object. + * + * Sets references to form inputs and "Create List" button. * - * @member {HTMLElement} + * @param {HTMLElement} form */ - this.createListButton = form.querySelector('#create-list-button'); + constructor(form) { + /** + * References this form's "Create List" button. + * + * @member {HTMLElement} + */ + this.createListButton = form.querySelector('#create-list-button') /** - * References the form's list title input field. - * - * @member {HTMLElement} - */ - this.listTitleInput = form.querySelector('#list_label'); + * References the form's list title input field. + * + * @member {HTMLElement} + */ + this.listTitleInput = form.querySelector('#list_label') /** - * References the form's list description input field. - * - * @member {HTMLElement} - */ - this.listDescriptionInput = form.querySelector('#list_desc'); + * References the form's list description input field. + * + * @member {HTMLElement} + */ + this.listDescriptionInput = form.querySelector('#list_desc') // Clear form on page refresh: - this.resetForm(); + this.resetForm() } /** - * Attaches click listener to the "Create List" button. - */ - initialize () { - this.createListButton.addEventListener('click', (event) => { - event.preventDefault(); - this.createNewList(); - }); + * Attaches click listener to the "Create List" button. + */ + initialize() { + this.createListButton.addEventListener('click', (event) =>{ + event.preventDefault() + this.createNewList() + }) } /** - * Creates a new patron list. - * - * When a new list is created, the list's title and description - * are taken from the form. The patron's user key and the seed - * identifier of the first list item are provided by the open dropper - * referenced in the shared My Books store. - * - * On success, updates all My Books droppers on the page, - * resets the list creation form fields, and closes the - * modal containing the form. A new showcase item is added - * to the active lists showcase, if the showcase exists. - * - * @async - */ - async createNewList () { - // Construct seed object for first list item: - const listTitle = websafe(this.listTitleInput.value); - const listDescription = websafe(this.listDescriptionInput.value); + * Creates a new patron list. + * + * When a new list is created, the list's title and description + * are taken from the form. The patron's user key and the seed + * identifier of the first list item are provided by the open dropper + * referenced in the shared My Books store. + * + * On success, updates all My Books droppers on the page, + * resets the list creation form fields, and closes the + * modal containing the form. A new showcase item is added + * to the active lists showcase, if the showcase exists. + * + * @async + */ + async createNewList() { + // Construct seed object for first list item: + const listTitle = websafe(this.listTitleInput.value) + const listDescription = websafe(this.listDescriptionInput.value) - const openDropper = myBooksStore.getOpenDropper(); - const seed = openDropper.readingLists.getSeed(); + const openDropper = myBooksStore.getOpenDropper() + const seed = openDropper.readingLists.getSeed() const postData = { name: listTitle, description: listDescription, - seeds: [seed], - }; + seeds: [seed] + } // Call list creation service with seed object: await createList(myBooksStore.getUserKey(), postData) - .then((response) => response.json()) + .then(response => response.json()) .then((data) => { // Update active lists showcase: - attachNewActiveShowcaseItem(data['key'], seed, listTitle, data['key']); + attachNewActiveShowcaseItem(data['key'], seed, listTitle, data['key']) // Update all droppers with new list data - this.updateDroppersOnListCreation(data['key'], listTitle, data['key']); + this.updateDroppersOnListCreation(data['key'], listTitle, data['key']) // Clear list creation form fields, nullify seed - this.resetForm(); + this.resetForm() }) .finally(() => { // Close the modal - $.colorbox.close(); - }); + $.colorbox.close() + }) } /** - * Updates lists section of each dropper with a new list. - * - * @param {string} listKey Key of the newly created list - * @param {string} listTitle Title of the new list - */ - updateDroppersOnListCreation (listKey, listTitle, coverUrl) { - const droppers = myBooksStore.getDroppers(); - const openDropper = myBooksStore.getOpenDropper(); + * Updates lists section of each dropper with a new list. + * + * @param {string} listKey Key of the newly created list + * @param {string} listTitle Title of the new list + */ + updateDroppersOnListCreation(listKey, listTitle, coverUrl) { + const droppers = myBooksStore.getDroppers() + const openDropper = myBooksStore.getOpenDropper() for (const dropper of droppers) { - const isActive = dropper === openDropper; - dropper.readingLists.onListCreationSuccess( - listKey, - listTitle, - isActive, - coverUrl, - ); + const isActive = dropper === openDropper + dropper.readingLists.onListCreationSuccess(listKey, listTitle, isActive, coverUrl) } } /** - * Clears the list title and desciption fields in the form. - */ - resetForm () { - this.listTitleInput.value = ''; - this.listDescriptionInput.value = ''; + * Clears the list title and desciption fields in the form. + */ + resetForm() { + this.listTitleInput.value = '' + this.listDescriptionInput.value = '' } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js index 74b20b0016e..d01ec785bbe 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js @@ -2,16 +2,12 @@ * Defines functionality related to Open Library's My Books dropper components. * @module my-books/MyBooksDropper */ - -import { Dropper } from '../dropper/Dropper'; -import { removeChildren } from '../utils'; -import { CheckInComponents } from './MyBooksDropper/CheckInComponents'; -import { ReadingLists } from './MyBooksDropper/ReadingLists'; -import { - ReadingLogForms, - ReadingLogShelves, -} from './MyBooksDropper/ReadingLogForms'; -import myBooksStore from './store'; +import myBooksStore from './store' +import { CheckInComponents } from './MyBooksDropper/CheckInComponents' +import { ReadingLists } from './MyBooksDropper/ReadingLists' +import {ReadingLogForms, ReadingLogShelves} from './MyBooksDropper/ReadingLogForms' +import { Dropper } from '../dropper/Dropper' +import { removeChildren } from '../utils' /** * Represents a single My Books Dropper. @@ -31,207 +27,190 @@ import myBooksStore from './store'; */ export class MyBooksDropper extends Dropper { /** - * Creates references to the given dropper's reading log forms, read date affordances, and - * list affordances. - * - * @param {HTMLElement} dropper - */ - constructor (dropper) { - super(dropper); + * Creates references to the given dropper's reading log forms, read date affordances, and + * list affordances. + * + * @param {HTMLElement} dropper + */ + constructor(dropper) { + super(dropper) const dropperActionCallbacks = { closeDropper: this.closeDropper.bind(this), - toggleDropper: this.toggleDropper.bind(this), - }; + toggleDropper: this.toggleDropper.bind(this) + } /** - * Reference to this dropper's list content. - * @member {ReadingLists} - */ - this.readingLists = new ReadingLists(dropper, dropperActionCallbacks); + * Reference to this dropper's list content. + * @member {ReadingLists} + */ + this.readingLists = new ReadingLists(dropper, dropperActionCallbacks) /** - * Reference to the dropper's list loading indicator. - * - * This is only rendered when the patron is logged in. - * @member {HTMLElement|null} - */ - this.loadingIndicator = dropper.querySelector('.list-loading-indicator'); + * Reference to the dropper's list loading indicator. + * + * This is only rendered when the patron is logged in. + * @member {HTMLElement|null} + */ + this.loadingIndicator = dropper.querySelector('.list-loading-indicator') /** - * Reference to the interval ID of the animation `setInterval` call. - * @member {NodeJS.Timer|undefined} - */ - this.loadingAnimationId; + * Reference to the interval ID of the animation `setInterval` call. + * @member {NodeJS.Timer|undefined} + */ + this.loadingAnimationId /** - * The work key associated with this dropper, if any. - * - * @member {string|undefined} - */ - this.workKey = this.dropper.dataset.workKey; + * The work key associated with this dropper, if any. + * + * @member {string|undefined} + */ + this.workKey = this.dropper.dataset.workKey - const splitKey = this.workKey ? this.workKey.split('/') : ['']; - const workOlid = splitKey[splitKey.length - 1]; + const splitKey = this.workKey ? this.workKey.split('/') : [''] + const workOlid = splitKey[splitKey.length - 1] /** - * @type {CheckInComponents|null} - */ - this.checkInComponents = workOlid - ? new CheckInComponents( - document.querySelector(`#check-in-container-${workOlid}`), - ) - : null; + * @type {CheckInComponents|null} + */ + this.checkInComponents = workOlid ? new CheckInComponents(document.querySelector(`#check-in-container-${workOlid}`)) : null /** - * References this dropper's reading log buttons. - * @member {ReadingLogForms} - */ - this.readingLogForms = new ReadingLogForms( - dropper, - this.checkInComponents, - dropperActionCallbacks, - ); + * References this dropper's reading log buttons. + * @member {ReadingLogForms} + */ + this.readingLogForms = new ReadingLogForms(dropper, this.checkInComponents, dropperActionCallbacks) } /** - * Hydrates dropper contents and loads patron's lists. - */ - initialize () { - super.initialize(); + * Hydrates dropper contents and loads patron's lists. + */ + initialize() { + super.initialize() - this.readingLogForms.initialize(); - this.readingLists.initialize(); + this.readingLogForms.initialize() + this.readingLists.initialize() if (this.checkInComponents) { - this.checkInComponents.initialize(); + this.checkInComponents.initialize() } - this.loadingAnimationId = this.initLoadingAnimation( - this.dropper.querySelector('.loading-ellipsis'), - ); + this.loadingAnimationId = this.initLoadingAnimation(this.dropper.querySelector('.loading-ellipsis')) } /** - * Creates loading animation for list loading indicator. - * - * @param {HTMLElement} loadingIndicator - * @returns {NodeJS.Timer} - */ - initLoadingAnimation (loadingIndicator) { - let count = 0; - const intervalId = setInterval(() => { - let ellipsis = ''; + * Creates loading animation for list loading indicator. + * + * @param {HTMLElement} loadingIndicator + * @returns {NodeJS.Timer} + */ + initLoadingAnimation(loadingIndicator) { + let count = 0 + const intervalId = setInterval(function() { + let ellipsis = '' for (let i = 0; i < count % 4; ++i) { - ellipsis += '.'; + ellipsis += '.' } - loadingIndicator.innerText = ellipsis; - ++count; - }, 1500); + loadingIndicator.innerText = ellipsis + ++count + }, 1500) - return intervalId; + return intervalId } /** - * Replaces dropper loading indicator with the given - * partially rendered list affordances. - * - * @param {string} partialHtml - */ - updateReadingLists (partialHtml) { - clearInterval(this.loadingAnimationId); - this.replaceLoadingIndicators(this.loadingIndicator, partialHtml); + * Replaces dropper loading indicator with the given + * partially rendered list affordances. + * + * @param {string} partialHtml + */ + updateReadingLists(partialHtml) { + clearInterval(this.loadingAnimationId) + this.replaceLoadingIndicators(this.loadingIndicator, partialHtml) } /** - * Returns an array of seed keys associated with this dropper. - * - * If the seed identifies a book, there should be both an - * edition and work key in the results. Otherwise, the results - * array should only contain the primary seed key. - * - * @returns {Array<string>} - */ - getSeedKeys () { - const results = [this.readingLists.seedKey]; + * Returns an array of seed keys associated with this dropper. + * + * If the seed identifies a book, there should be both an + * edition and work key in the results. Otherwise, the results + * array should only contain the primary seed key. + * + * @returns {Array<string>} + */ + getSeedKeys() { + const results = [this.readingLists.seedKey] if (this.readingLists.workKey) { - results.push(this.readingLists.workKey); + results.push(this.readingLists.workKey) } - return results; + return results } /** - * Object returned by the list partials endpoint. - * - * @typedef {Object} ListPartials - * @property {string} dropper HTML string for dropdown list content - * @property {string} active HTML string for patron's active lists - */ + * Object returned by the list partials endpoint. + * + * @typedef {Object} ListPartials + * @property {string} dropper HTML string for dropdown list content + * @property {string} active HTML string for patron's active lists + */ /** - * Replaces list loading indicators with the given partially rendered HTML. - * - * @param {HTMLElement} dropperListsPlaceholder Loading indicator found inside of the dropdown content - * @param {ListPartials} partials - */ - replaceLoadingIndicators (dropperListsPlaceholder, partialHTML) { - const dropperParent = dropperListsPlaceholder - ? dropperListsPlaceholder.parentElement - : null; + * Replaces list loading indicators with the given partially rendered HTML. + * + * @param {HTMLElement} dropperListsPlaceholder Loading indicator found inside of the dropdown content + * @param {ListPartials} partials + */ + replaceLoadingIndicators(dropperListsPlaceholder, partialHTML) { + const dropperParent = dropperListsPlaceholder ? dropperListsPlaceholder.parentElement : null if (dropperParent) { - removeChildren(dropperParent); - dropperParent.insertAdjacentHTML('afterbegin', partialHTML); + removeChildren(dropperParent) + dropperParent.insertAdjacentHTML('afterbegin', partialHTML) - const anchors = this.dropper.querySelectorAll('.modify-list'); - this.readingLists.initModifyListAffordances(anchors); + const anchors = this.dropper.querySelectorAll('.modify-list') + this.readingLists.initModifyListAffordances(anchors) } } /** - * Updates this dropper's primary button's state and display to show that a book is active on the - * given shelf. - * - * When we update to the "Already Read" shelf, the appropriate last read date affordance is - * displayed. - * - * @param shelf {ReadingLogShelf} - */ - updateShelfDisplay (shelf) { - this.readingLogForms.updateActivatedStatus(true); - this.readingLogForms.updatePrimaryBookshelfId(Number(shelf)); - this.readingLogForms.updatePrimaryButtonText( - this.readingLogForms.getDisplayString(shelf), - ); + * Updates this dropper's primary button's state and display to show that a book is active on the + * given shelf. + * + * When we update to the "Already Read" shelf, the appropriate last read date affordance is + * displayed. + * + * @param shelf {ReadingLogShelf} + */ + updateShelfDisplay(shelf) { + this.readingLogForms.updateActivatedStatus(true) + this.readingLogForms.updatePrimaryBookshelfId(Number(shelf)) + this.readingLogForms.updatePrimaryButtonText(this.readingLogForms.getDisplayString(shelf)) if (this.checkInComponents) { - if ( - !this.checkInComponents.hasReadDate() && - shelf === ReadingLogShelves.ALREADY_READ - ) { - this.checkInComponents.showCheckInDisplay(); + if (!this.checkInComponents.hasReadDate() && shelf === ReadingLogShelves.ALREADY_READ) { + this.checkInComponents.showCheckInDisplay() } else { - this.checkInComponents.hideCheckInPrompt(); + this.checkInComponents.hideCheckInPrompt() } } } // Dropper overrides: /** - * Updates store with reference to the opened dropper. - * - * @override - */ - onOpen () { - myBooksStore.setOpenDropper(this); + * Updates store with reference to the opened dropper. + * + * @override + */ + onOpen() { + myBooksStore.setOpenDropper(this) } /** - * Redirects to login page when disabled dropper is clicked. - * - * My Books droppers are disabled for unauthenticated patrons. - * - * @override - */ - onDisabledClick () { - window.location = `/account/login?redirect=${location.pathname}`; + * Redirects to login page when disabled dropper is clicked. + * + * My Books droppers are disabled for unauthenticated patrons. + * + * @override + */ + onDisabledClick() { + window.location = `/account/login?redirect=${location.pathname}` } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js index a6a4aaf63d0..c7318194397 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js @@ -3,7 +3,7 @@ * @module my-books/MyBooksDropper/CheckInComponents */ import { initDialogClosers } from '../../dialog'; -import { PersistentToast } from '../../Toast'; +import { PersistentToast } from '../../Toast' /** * Array of days for each month, listed in order starting with January. @@ -12,7 +12,7 @@ import { PersistentToast } from '../../Toast'; * @readonly * @type {array<number>} */ -const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; +const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] /** * Determines if the given year is a leap year. @@ -20,8 +20,8 @@ const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; * @param {Number} year * @returns `true` if the given year is a leap year. */ -function isLeapYear (year) { - return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); +function isLeapYear(year) { + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) } /** @@ -38,323 +38,297 @@ function isLeapYear (year) { */ export class CheckInComponents { /** - * @param checkInContainer - */ - constructor (checkInContainer) { - // HTML for the check-in components is not rendered if - // the patron is unauthenticated, or if the dropper - // is for an orphaned edition. + * @param checkInContainer + */ + constructor(checkInContainer) { + // HTML for the check-in components is not rendered if + // the patron is unauthenticated, or if the dropper + // is for an orphaned edition. if (!checkInContainer) { - return; + return } /** - * @typedef {object} ReadDateConfig - * @property {string} workOlid - * @property {string} [editionKey] - * @property {string} [lastReadDate] - * @property {number} [eventId] - */ + * @typedef {object} ReadDateConfig + * @property {string} workOlid + * @property {string} [editionKey] + * @property {string} [lastReadDate] + * @property {number} [eventId] + */ /** - * @type {ReadDateConfig} - */ - this.config = JSON.parse(checkInContainer.dataset.config); + * @type {ReadDateConfig} + */ + this.config = JSON.parse(checkInContainer.dataset.config) - const checkInPromptElem = - checkInContainer.querySelector('.check-in-prompt'); + const checkInPromptElem = checkInContainer.querySelector('.check-in-prompt') /** - * @type {CheckInPrompt} - */ - this.checkInPrompt = new CheckInPrompt(checkInPromptElem); + * @type {CheckInPrompt} + */ + this.checkInPrompt = new CheckInPrompt(checkInPromptElem) - const checkInDisplayElem = - checkInContainer.querySelector('.last-read-date'); + const checkInDisplayElem = checkInContainer.querySelector('.last-read-date') /** - * @type {CheckInDisplay} - */ - this.checkInDisplay = new CheckInDisplay(checkInDisplayElem); + * @type {CheckInDisplay} + */ + this.checkInDisplay = new CheckInDisplay(checkInDisplayElem) /** - * References element that will be displayed in last read date form modal. - * Set during form initialization. - * - * @type {HTMLElement|undefined} - */ - this.modalContent = undefined; + * References element that will be displayed in last read date form modal. + * Set during form initialization. + * + * @type {HTMLElement|undefined} + */ + this.modalContent = undefined /** - * @type {CheckInForm|undefined} - */ - this.checkInForm = undefined; - } - - initialize () { - this.checkInPrompt.initialize(); - this.checkInPrompt - .getRootElement() - .addEventListener('submit-check-in', (event) => { - const year = event.detail.year; - const month = event.detail.month; - const day = event.detail.day; - - const eventData = this.prepareEventRequest(year, month, day); - this.postCheckIn(eventData, this.checkInForm.getFormAction()) - .then((resp) => { - if (!resp.ok) { - throw Error(`Check-in request failed. Status: ${resp.status}`); - } - this.updateDateAndShowDisplay(year, month, day); - }) - .catch(() => { - new PersistentToast( - 'Failed to submit check-in. Please try again in a few moments.', - ).show(); - }); - }); - - let hiddenModalContentContainer = document.querySelector( - '#hidden-modal-content-container', - ); + * @type {CheckInForm|undefined} + */ + this.checkInForm = undefined + } + + initialize() { + this.checkInPrompt.initialize() + this.checkInPrompt.getRootElement().addEventListener('submit-check-in', (event) => { + const year = event.detail.year + const month = event.detail.month + const day = event.detail.day + + const eventData = this.prepareEventRequest(year, month, day) + this.postCheckIn(eventData, this.checkInForm.getFormAction()) + .then((resp) => { + if (!resp.ok) { + throw Error(`Check-in request failed. Status: ${resp.status}`) + } + this.updateDateAndShowDisplay(year, month, day) + }) + .catch(() => { + new PersistentToast('Failed to submit check-in. Please try again in a few moments.').show() + }) + }) + + let hiddenModalContentContainer = document.querySelector('#hidden-modal-content-container') if (!hiddenModalContentContainer) { - hiddenModalContentContainer = document.createElement('div'); - hiddenModalContentContainer.classList.add('hidden'); - hiddenModalContentContainer.id = 'hidden-modal-content-container'; - document.body.appendChild(hiddenModalContentContainer); + hiddenModalContentContainer = document.createElement('div') + hiddenModalContentContainer.classList.add('hidden') + hiddenModalContentContainer.id = 'hidden-modal-content-container' + document.body.appendChild(hiddenModalContentContainer) } - const modalContent = this.createModalContentFromTemplate(); - hiddenModalContentContainer.appendChild(modalContent); - - this.modalContent = hiddenModalContentContainer.querySelector( - `#modal-content-${this.config.workOlid}`, - ); - - const formElem = this.modalContent.querySelector('form'); - this.checkInForm = new CheckInForm( - formElem, - this.config.workOlid, - this.config.editionKey || '', - this.config.lastReadDate || '', - this.config.eventId, - ); - this.checkInForm.initialize(); - this.checkInForm - .getRootElement() - .addEventListener('delete-check-in', () => { - this.deleteCheckIn(this.checkInForm.getEventId()) - .then((resp) => { - if (!resp.ok) { - throw Error( - `Check-in delete request failed. Status: ${resp.status}`, - ); - } - - this.checkInForm.resetForm(); - this.checkInDisplay.hide(); - this.checkInPrompt.show(); - }) - .catch(() => { - // TODO : Use localized strings - new PersistentToast( - 'Failed to delete check-in. Please try again in a few moments.', - ).show(); - }) - .finally(() => { - this.closeModal(); - }); - }); - this.checkInForm - .getRootElement() - .addEventListener('submit-check-in', (event) => { - const year = event.detail.year; - const month = event.detail.month; - const day = event.detail.day; - - const eventData = this.prepareEventRequest(year, month, day); - this.postCheckIn(eventData, this.checkInForm.getFormAction()) - .then((resp) => { - if (!resp.ok) { - throw Error(`Check-in request failed. Status: ${resp.status}`); - } - this.updateDateAndShowDisplay(year, month, day); - }) - .catch(() => { - // TODO : Use localized strings - new PersistentToast( - 'Failed to submit check-in. Please try again in a few moments.', - ).show(); - }) - .finally(() => { - this.closeModal(); - }); - }); - - const closeModalElements = - this.modalContent.querySelectorAll('.dialog--close'); - initDialogClosers(closeModalElements); - } - - /** - * Creates a new element containing the check-in form and `colorbox` modal content. - * - * @returns {HTMLElement} - */ - createModalContentFromTemplate () { - const templateElem = document.createElement('template'); - const modalContentTemplate = document.querySelector('#check-in-form-modal'); - templateElem.innerHTML = modalContentTemplate.outerHTML; - const modalContent = templateElem.content.firstElementChild; - modalContent.id = `modal-content-${this.config.workOlid}`; - - return modalContent; - } - - /** - * Updates the date display and form with the given date, and shows the display. - * - * @param {number} year - * @param {number|null} month - * @param {number|null} day - */ - updateDateAndShowDisplay (year, month = null, day = null) { - // Update last read date display - let dateString = String(year); + const modalContent = this.createModalContentFromTemplate() + hiddenModalContentContainer.appendChild(modalContent) + + this.modalContent = hiddenModalContentContainer.querySelector(`#modal-content-${this.config.workOlid}`) + + const formElem = this.modalContent.querySelector('form') + this.checkInForm = new CheckInForm(formElem, this.config.workOlid, this.config.editionKey || '', this.config.lastReadDate || '', this.config.eventId) + this.checkInForm.initialize() + this.checkInForm.getRootElement().addEventListener('delete-check-in', () => { + this.deleteCheckIn(this.checkInForm.getEventId()) + .then(resp => { + if (!resp.ok) { + throw Error(`Check-in delete request failed. Status: ${resp.status}`) + } + + this.checkInForm.resetForm() + this.checkInDisplay.hide() + this.checkInPrompt.show() + }) + .catch(() => { + // TODO : Use localized strings + new PersistentToast('Failed to delete check-in. Please try again in a few moments.').show() + }) + .finally(() => { + this.closeModal() + }) + }) + this.checkInForm.getRootElement().addEventListener('submit-check-in', (event) => { + const year = event.detail.year + const month = event.detail.month + const day = event.detail.day + + const eventData = this.prepareEventRequest(year, month, day) + this.postCheckIn(eventData, this.checkInForm.getFormAction()) + .then((resp) => { + if (!resp.ok) { + throw Error(`Check-in request failed. Status: ${resp.status}`) + } + this.updateDateAndShowDisplay(year, month, day) + }) + .catch(() => { + // TODO : Use localized strings + new PersistentToast('Failed to submit check-in. Please try again in a few moments.').show() + }) + .finally(() => { + this.closeModal() + }) + }) + + const closeModalElements = this.modalContent.querySelectorAll('.dialog--close') + initDialogClosers(closeModalElements) + } + + /** + * Creates a new element containing the check-in form and `colorbox` modal content. + * + * @returns {HTMLElement} + */ + createModalContentFromTemplate() { + const templateElem = document.createElement('template') + const modalContentTemplate = document.querySelector('#check-in-form-modal') + templateElem.innerHTML = modalContentTemplate.outerHTML + const modalContent = templateElem.content.firstElementChild + modalContent.id = `modal-content-${this.config.workOlid}` + + return modalContent + } + + /** + * Updates the date display and form with the given date, and shows the display. + * + * @param {number} year + * @param {number|null} month + * @param {number|null} day + */ + updateDateAndShowDisplay(year, month = null, day = null) { + // Update last read date display + let dateString = String(year) if (month) { - dateString += `-${String(month).padStart(2, '0')}`; + dateString += `-${String(month).padStart(2, '0')}` if (day) { - dateString += `-${String(day).padStart(2, '0')}`; + dateString += `-${String(day).padStart(2, '0')}` } } - this.checkInDisplay.updateDateDisplay(dateString); + this.checkInDisplay.updateDateDisplay(dateString) // Update component visibility - this.checkInPrompt.hide(); - this.checkInDisplay.show(); + this.checkInPrompt.hide() + this.checkInDisplay.show() // Update submission form - this.checkInForm.updateSelectedDate(year, month, day); - this.checkInForm.showDeleteButton(); - } - - /** - * @typedef {object} CheckInEventPostRequestData - * @property {number} event_type - * @property {number} year - * @property {number|null} month - * @property {number|null} day - * @property {number|null} event_id - * @property {string} [edition_key] - */ - /** - * Posts the given data to the backend check-in handler. - * - * @param {CheckInEventPostRequestData} eventData - * @param {string} url - * @returns {Promise<Response>} - */ - postCheckIn (eventData, url) { + this.checkInForm.updateSelectedDate(year, month, day) + this.checkInForm.showDeleteButton() + + } + + /** + * @typedef {object} CheckInEventPostRequestData + * @property {number} event_type + * @property {number} year + * @property {number|null} month + * @property {number|null} day + * @property {number|null} event_id + * @property {string} [edition_key] + */ + /** + * Posts the given data to the backend check-in handler. + * + * @param {CheckInEventPostRequestData} eventData + * @param {string} url + * @returns {Promise<Response>} + */ + postCheckIn(eventData, url) { return fetch(url, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', - accept: 'application/json', + accept: 'application/json' }, - body: JSON.stringify(eventData), - }); + body: JSON.stringify(eventData) + }) } /** - * Posts request to delete the read date record with the given ID. - * - * @param {string} eventId - * @returns {Promise<Response>} - */ - async deleteCheckIn (eventId) { + * Posts request to delete the read date record with the given ID. + * + * @param {string} eventId + * @returns {Promise<Response>} + */ + async deleteCheckIn(eventId) { return fetch(`/check-ins/${eventId}`, { - method: 'DELETE', - }); + method: 'DELETE' + }) } /** - * Prepares data for a `postEvent` call. - * - * @param {number} year - * @param {number|null} month - * @param {number|null} day - * @returns {CheckInEventPostRequestData} - */ - prepareEventRequest (year, month = null, day = null) { - // Get event id - const eventId = this.checkInForm.getEventId(); + * Prepares data for a `postEvent` call. + * + * @param {number} year + * @param {number|null} month + * @param {number|null} day + * @returns {CheckInEventPostRequestData} + */ + prepareEventRequest(year, month = null, day = null) { + // Get event id + const eventId = this.checkInForm.getEventId() // Get event type - const eventType = this.checkInForm.getEventType(); + const eventType = this.checkInForm.getEventType() const eventRequest = { event_id: eventId ? Number(eventId) : null, event_type: Number(eventType), year: year, month: month, - day: day, - }; + day: day + } - const editionKey = this.checkInForm.getEditionKey() || null; + const editionKey = this.checkInForm.getEditionKey() || null if (editionKey) { - eventRequest.edition_key = editionKey; + eventRequest.edition_key = editionKey } - return eventRequest; + return eventRequest } /** - * Returns `true` if the check-in display is visible on the screen. - * - * @returns {boolean} - */ - hasReadDate () { - return !this.checkInDisplay.getRootElement().classList.contains('hidden'); + * Returns `true` if the check-in display is visible on the screen. + * + * @returns {boolean} + */ + hasReadDate() { + return !this.checkInDisplay.getRootElement().classList.contains('hidden') } /** - * Resets the check-in form. - */ - resetForm () { - this.checkInForm.resetForm(); + * Resets the check-in form. + */ + resetForm() { + this.checkInForm.resetForm() } /** - * Show the check-in display. - */ - showCheckInDisplay () { - this.checkInDisplay.show(); + * Show the check-in display. + */ + showCheckInDisplay() { + this.checkInDisplay.show() } /** - * Hide the check-in display. - */ - hideCheckInDisplay () { - this.checkInDisplay.hide(); + * Hide the check-in display. + */ + hideCheckInDisplay() { + this.checkInDisplay.hide() } /** - * Show the check-in prompt. - */ - showCheckInPrompt () { - this.checkInPrompt.show(); + * Show the check-in prompt. + */ + showCheckInPrompt() { + this.checkInPrompt.show() } /** - * Hide the check-in prompt. - */ - hideCheckInPrompt () { - this.checkInPrompt.hide(); + * Hide the check-in prompt. + */ + hideCheckInPrompt() { + this.checkInPrompt.hide() } /** - * Closes the opened `colorbox` modal. - */ - closeModal () { - $.colorbox.close(); + * Closes the opened `colorbox` modal. + */ + closeModal() { + $.colorbox.close() } } @@ -366,71 +340,71 @@ export class CheckInComponents { */ class CheckInPrompt { /** - * @param {HTMLElement} checkInPrompt - */ - constructor (checkInPrompt) { - this.rootElem = checkInPrompt; + * @param {HTMLElement} checkInPrompt + */ + constructor(checkInPrompt) { + this.rootElem = checkInPrompt } - initialize () { - const yearLink = this.rootElem.querySelector('.prompt-current-year'); + initialize() { + const yearLink = this.rootElem.querySelector('.prompt-current-year') yearLink.addEventListener('click', () => { // Get the current year - const year = new Date().getFullYear(); + const year = new Date().getFullYear() - this.dispatchCheckInSubmission(year); - }); + this.dispatchCheckInSubmission(year) + }) - const todayLink = this.rootElem.querySelector('.prompt-today'); + const todayLink = this.rootElem.querySelector('.prompt-today') todayLink.addEventListener('click', () => { // Get today's date - const now = new Date(); - const year = now.getFullYear(); - const month = now.getMonth() + 1; - const day = now.getDate(); + const now = new Date() + const year = now.getFullYear() + const month = now.getMonth() + 1 + const day = now.getDate() - this.dispatchCheckInSubmission(year, month, day); - }); + this.dispatchCheckInSubmission(year, month, day) + }) } /** - * Dispatches a custom `submit-check-in` event with the given date. - * - * @param {number} year - * @param {number|null} month - * @param {number|null} day - */ - dispatchCheckInSubmission (year, month = null, day = null) { + * Dispatches a custom `submit-check-in` event with the given date. + * + * @param {number} year + * @param {number|null} month + * @param {number|null} day + */ + dispatchCheckInSubmission(year, month = null, day = null) { const submitEvent = new CustomEvent('submit-check-in', { detail: { year: year, month: month, - day: day, - }, - }); - this.rootElem.dispatchEvent(submitEvent); + day: day + } + }) + this.rootElem.dispatchEvent(submitEvent) } /** - * Hides this check-in prompt. - */ - hide () { - this.rootElem.classList.add('hidden'); + * Hides this check-in prompt. + */ + hide() { + this.rootElem.classList.add('hidden') } /** - * Shows this check-in prompt. - */ - show () { - this.rootElem.classList.remove('hidden'); + * Shows this check-in prompt. + */ + show() { + this.rootElem.classList.remove('hidden') } /** - * Returns reference to the root element of this check-in prompt. - * @returns {HTMLElement} - */ - getRootElement () { - return this.rootElem; + * Returns reference to the root element of this check-in prompt. + * @returns {HTMLElement} + */ + getRootElement() { + return this.rootElem } } @@ -441,41 +415,41 @@ class CheckInPrompt { */ class CheckInDisplay { /** - * @param {HTMLElement} checkInDisplay - */ - constructor (checkInDisplay) { - this.rootElem = checkInDisplay; - this.dateDisplayElem = this.rootElem.querySelector('.check-in-date'); + * @param {HTMLElement} checkInDisplay + */ + constructor(checkInDisplay) { + this.rootElem = checkInDisplay + this.dateDisplayElem = this.rootElem.querySelector('.check-in-date') } /** - * Updates the date displayed to the given string. - * - * @param {string} date - */ - updateDateDisplay (date) { - this.dateDisplayElem.textContent = date; + * Updates the date displayed to the given string. + * + * @param {string} date + */ + updateDateDisplay(date) { + this.dateDisplayElem.textContent = date } /** - * Hides this date display. - */ - hide () { - this.rootElem.classList.add('hidden'); + * Hides this date display. + */ + hide() { + this.rootElem.classList.add('hidden') } /** - * Shows this date display. - */ - show () { - this.rootElem.classList.remove('hidden'); + * Shows this date display. + */ + show() { + this.rootElem.classList.remove('hidden') } /** - * @returns {HTMLElement} - */ - getRootElement () { - return this.rootElem; + * @returns {HTMLElement} + */ + getRootElement() { + return this.rootElem } } @@ -491,346 +465,326 @@ class CheckInDisplay { */ export class CheckInForm { /** - * @param {HTMLFormElement} formElem - * @param {string} workOlid - * @param {string|null} editionKey - * @param {string|null} lastReadDate - * @param {number|null} eventId - */ - constructor ( - formElem, - workOlid, - editionKey = null, - lastReadDate = null, - eventId = null, - ) { - this.rootElem = formElem; - this.workOlid = workOlid; - this.editionKey = editionKey; - this.lastReadDate = lastReadDate; - this.eventId = eventId; + * @param {HTMLFormElement} formElem + * @param {string} workOlid + * @param {string|null} editionKey + * @param {string|null} lastReadDate + * @param {number|null} eventId + */ + constructor(formElem, workOlid, editionKey = null, lastReadDate = null, eventId = null) { + this.rootElem = formElem + this.workOlid = workOlid + this.editionKey = editionKey + this.lastReadDate = lastReadDate + this.eventId = eventId /** - * Reference to hidden `event_type` form input. - * - * @type {HTMLInputElement|undefined} - */ - this.eventTypeInput = this.rootElem.querySelector('input[name=event_type]'); + * Reference to hidden `event_type` form input. + * + * @type {HTMLInputElement|undefined} + */ + this.eventTypeInput = this.rootElem.querySelector('input[name=event_type]') /** - * Reference to hidden `event_id` form input. - * - * @type {HTMLInputElement|undefined} - */ - this.eventIdInput = this.rootElem.querySelector('input[name=event_id]'); + * Reference to hidden `event_id` form input. + * + * @type {HTMLInputElement|undefined} + */ + this.eventIdInput = this.rootElem.querySelector('input[name=event_id]') /** - * Reference to hidden `edition_key` form input. - * - * @type {HTMLInputElement} - */ - this.editionKeyInput = this.rootElem.querySelector( - 'input[name=edition_key]', - ); + * Reference to hidden `edition_key` form input. + * + * @type {HTMLInputElement} + */ + this.editionKeyInput = this.rootElem.querySelector('input[name=edition_key]') /** - * Reference to the form's year `select` element. - * - * @type {HTMLSelectElement} - */ - this.yearSelect = this.rootElem.querySelector('select[name=year]'); + * Reference to the form's year `select` element. + * + * @type {HTMLSelectElement} + */ + this.yearSelect = this.rootElem.querySelector('select[name=year]') /** - * Reference to the form's month `select` element. - * - * @type {HTMLSelectElement} - */ - this.monthSelect = this.rootElem.querySelector('select[name=month]'); + * Reference to the form's month `select` element. + * + * @type {HTMLSelectElement} + */ + this.monthSelect = this.rootElem.querySelector('select[name=month]') /** - * Reference to the form's day `select` element. - * - * @type {HTMLSelectElement} - */ - this.daySelect = this.rootElem.querySelector('select[name=day]'); + * Reference to the form's day `select` element. + * + * @type {HTMLSelectElement} + */ + this.daySelect = this.rootElem.querySelector('select[name=day]') /** - * Reference to the form's submit button. - * @type {HTMLButtonElement} - */ - this.submitButton = this.rootElem.querySelector('.check-in__submit-btn'); + * Reference to the form's submit button. + * @type {HTMLButtonElement} + */ + this.submitButton = this.rootElem.querySelector('.check-in__submit-btn') /** - * Reference to the form's delete button. - * - * @type {HTMLButtonElement} - */ - this.deleteButton = this.rootElem.querySelector('.check-in__delete-btn'); + * Reference to the form's delete button. + * + * @type {HTMLButtonElement} + */ + this.deleteButton = this.rootElem.querySelector('.check-in__delete-btn') } - initialize () { - // Set form's action - this.rootElem.action = `/works/${this.workOlid}/check-ins.json`; + initialize() { + // Set form's action + this.rootElem.action = `/works/${this.workOlid}/check-ins.json` // Set form's event ID if (this.eventId) { - this.setEventId(this.eventId); - this.showDeleteButton(); + this.setEventId(this.eventId) + this.showDeleteButton() } // Set form's edition_key if (this.editionKey) { - this.editionKeyInput.value = this.editionKey; + this.editionKeyInput.value = this.editionKey } // Set date select elements to the last read date - const [yearString, monthString, dayString] = this.lastReadDate - ? this.lastReadDate.split('-') - : [null, null, null]; - this.updateSelectedDate( - Number(yearString), - Number(monthString), - Number(dayString), - ); + const [yearString, monthString, dayString] = this.lastReadDate ? this.lastReadDate.split('-') : [null, null, null] + this.updateSelectedDate(Number(yearString), Number(monthString), Number(dayString)) // Update form for new years day const currentYear = new Date().getFullYear(); - const hiddenYear = this.yearSelect.querySelector('.show-if-local-year'); + const hiddenYear = this.yearSelect.querySelector('.show-if-local-year') // The year select element has a hidden option for next year. This // option is shown on 1 January if the client's local year is different // from the server's local year. if (Number(hiddenYear.value) === currentYear) { - hiddenYear.classList.remove('hidden'); + hiddenYear.classList.remove('hidden') } // Associate labels with select elements - const yearLabel = this.rootElem.querySelector('.check-in__year-label'); - const yearSelectId = `year-select-${this.workOlid}`; - this.yearSelect.id = yearSelectId; - yearLabel.htmlFor = yearSelectId; + const yearLabel = this.rootElem.querySelector('.check-in__year-label') + const yearSelectId = `year-select-${this.workOlid}` + this.yearSelect.id = yearSelectId + yearLabel.htmlFor = yearSelectId - const monthLabel = this.rootElem.querySelector('.check-in__month-label'); - const monthSelectId = `month-select-${this.workOlid}`; - this.monthSelect.id = monthSelectId; - monthLabel.htmlFor = monthSelectId; + const monthLabel = this.rootElem.querySelector('.check-in__month-label') + const monthSelectId = `month-select-${this.workOlid}` + this.monthSelect.id = monthSelectId + monthLabel.htmlFor = monthSelectId - const dayLabel = this.rootElem.querySelector('.check-in__day-label'); - const daySelectId = `day-select-${this.workOlid}`; - this.daySelect.id = daySelectId; - dayLabel.htmlFor = daySelectId; + const dayLabel = this.rootElem.querySelector('.check-in__day-label') + const daySelectId = `day-select-${this.workOlid}` + this.daySelect.id = daySelectId + dayLabel.htmlFor = daySelectId // Add listeners to form elements: this.yearSelect.addEventListener('change', () => { - this.onDateSelectionChange(); - }); + this.onDateSelectionChange() + }) this.monthSelect.addEventListener('change', () => { - this.onDateSelectionChange(); - }); + this.onDateSelectionChange() + }) this.deleteButton.addEventListener('click', (event) => { - event.preventDefault(); - const deleteEvent = new CustomEvent('delete-check-in'); - this.rootElem.dispatchEvent(deleteEvent); - }); + event.preventDefault() + const deleteEvent = new CustomEvent('delete-check-in') + this.rootElem.dispatchEvent(deleteEvent) + }) this.submitButton.addEventListener('click', (event) => { - event.preventDefault(); + event.preventDefault() const submitEvent = new CustomEvent('submit-check-in', { detail: { year: this.getSelectedYear(), month: this.getSelectedMonth(), - day: this.getSelectedDay(), - }, - }); - this.rootElem.dispatchEvent(submitEvent); - }); - const todayLink = this.rootElem.querySelector('.check-in__today'); + day: this.getSelectedDay() + } + }) + this.rootElem.dispatchEvent(submitEvent) + }) + const todayLink = this.rootElem.querySelector('.check-in__today') todayLink.addEventListener('click', () => { // Get today's date - const now = new Date(); - const year = now.getFullYear(); - const month = now.getMonth() + 1; - const day = now.getDate(); + const now = new Date() + const year = now.getFullYear() + const month = now.getMonth() + 1 + const day = now.getDate() - this.updateSelectedDate(year, month, day); - }); + this.updateSelectedDate(year, month, day) + }) } /** - * Gets currently selected date, then updates the form. - */ - onDateSelectionChange () { - const year = this.yearSelect.selectedIndex - ? Number(this.yearSelect.value) - : null; - this.updateSelectedDate( - year, - this.monthSelect.selectedIndex, - this.daySelect.selectedIndex, - ); + * Gets currently selected date, then updates the form. + */ + onDateSelectionChange() { + const year = this.yearSelect.selectedIndex ? Number(this.yearSelect.value) : null + this.updateSelectedDate(year, this.monthSelect.selectedIndex, this.daySelect.selectedIndex) } /** - * Updates date select elements based on the given year, month, and day. - * - * @param {number|null} year - * @param {number|null} month - * @param {number|null} day - */ - updateSelectedDate (year = null, month = null, day = null) { + * Updates date select elements based on the given year, month, and day. + * + * @param {number|null} year + * @param {number|null} month + * @param {number|null} day + */ + updateSelectedDate(year = null, month = null, day = null) { if (!month) { - day = null; + day = null } if (!year) { - month = null; - day = null; + month = null + day = null } if (year) { - this.yearSelect.value = year || ''; - this.monthSelect.disabled = false; - this.submitButton.disabled = false; + this.yearSelect.value = year || '' + this.monthSelect.disabled = false + this.submitButton.disabled = false } else { - this.yearSelect.selectedIndex = 0; - this.monthSelect.disabled = true; - this.submitButton.disabled = true; + this.yearSelect.selectedIndex = 0 + this.monthSelect.disabled = true + this.submitButton.disabled = true } if (month) { - this.monthSelect.value = month || ''; - this.daySelect.disabled = false; + this.monthSelect.value = month || '' + this.daySelect.disabled = false // Update daySelect options for month/leap year - let daysInMonth = DAYS_IN_MONTH[month - 1]; + let daysInMonth = DAYS_IN_MONTH[month - 1] if (month === 2 && isLeapYear(year)) { - ++daysInMonth; + ++daysInMonth } - this.updateDayOptions(daysInMonth); + this.updateDayOptions(daysInMonth) } else { - this.monthSelect.selectedIndex = 0; - this.daySelect.disabled = true; + this.monthSelect.selectedIndex = 0 + this.daySelect.disabled = true } if (day) { - const daysInMonth = DAYS_IN_MONTH[this.monthSelect.selectedIndex - 1]; - this.daySelect.selectedIndex = day > daysInMonth ? 0 : day; + const daysInMonth = DAYS_IN_MONTH[this.monthSelect.selectedIndex - 1] + this.daySelect.selectedIndex = day > daysInMonth ? 0 : day } else { - this.daySelect.selectedIndex = 0; + this.daySelect.selectedIndex = 0 } } /** - * Updates day select options, hiding days greater than the given amount. - * - * @param {number} daysInMonth - */ - updateDayOptions (daysInMonth) { + * Updates day select options, hiding days greater than the given amount. + * + * @param {number} daysInMonth + */ + updateDayOptions(daysInMonth) { for (let i = 0; i < this.daySelect.options.length; ++i) { if (i <= daysInMonth) { - this.daySelect.options[i].classList.remove('hidden'); + this.daySelect.options[i].classList.remove('hidden') } else { - this.daySelect.options[i].classList.add('hidden'); + this.daySelect.options[i].classList.add('hidden') } } } /** - * Resets the form. - * - * Unsets the `event_id` input value, hides the delete button, and - * resets the date select elements to their default values. - */ - resetForm () { - this.setEventId(''); - this.updateSelectedDate(); - this.hideDeleteButton(); + * Resets the form. + * + * Unsets the `event_id` input value, hides the delete button, and + * resets the date select elements to their default values. + */ + resetForm() { + this.setEventId('') + this.updateSelectedDate() + this.hideDeleteButton() } /** - * Shows this form's delete button. - */ - showDeleteButton () { - this.deleteButton.classList.remove('invisible'); + * Shows this form's delete button. + */ + showDeleteButton() { + this.deleteButton.classList.remove('invisible') } /** - * Hides this form's delete button. - */ - hideDeleteButton () { - this.deleteButton.classList.add('invisible'); + * Hides this form's delete button. + */ + hideDeleteButton() { + this.deleteButton.classList.add('invisible') } /** - * Returns the numeric value of the selected year. - * - * @returns {number|null} The selected year, or `null` if none selected - */ - getSelectedYear () { - return this.yearSelect.selectedIndex ? Number(this.yearSelect.value) : null; + * Returns the numeric value of the selected year. + * + * @returns {number|null} The selected year, or `null` if none selected + */ + getSelectedYear() { + return this.yearSelect.selectedIndex ? Number(this.yearSelect.value) : null } /** - * Returns the numeric value of the selected month. - * - * @returns {number|null} The selected month, or `null` if none selected - */ - getSelectedMonth () { - return this.monthSelect.selectedIndex || null; + * Returns the numeric value of the selected month. + * + * @returns {number|null} The selected month, or `null` if none selected + */ + getSelectedMonth() { + return this.monthSelect.selectedIndex || null } /** - * Returns the numeric value of the selected day. - * - * @returns {number|null} The selected day, or `null` if none selected - */ - getSelectedDay () { - return this.daySelect.selectedIndex || null; + * Returns the numeric value of the selected day. + * + * @returns {number|null} The selected day, or `null` if none selected + */ + getSelectedDay() { + return this.daySelect.selectedIndex || null } /** - * Returns the value of this form's `event_id` input. - * - * @returns {string} - */ - getEventId () { - return this.eventIdInput.value; + * Returns the value of this form's `event_id` input. + * + * @returns {string} + */ + getEventId() { + return this.eventIdInput.value } /** - * Updates the value of the form's `event_id` input. - * - * @param value - */ - setEventId (value) { - this.eventIdInput.value = value; + * Updates the value of the form's `event_id` input. + * + * @param value + */ + setEventId(value) { + this.eventIdInput.value = value } /** - * Returns the value of this form's `event_type` input. - * - * @returns {string} - */ - getEventType () { - return this.eventTypeInput.value; + * Returns the value of this form's `event_type` input. + * + * @returns {string} + */ + getEventType() { + return this.eventTypeInput.value } /** - * Returns the value of the form's edition key input. - * - * @returns {string} - */ - getEditionKey () { - return this.editionKeyInput.value; + * Returns the value of the form's edition key input. + * + * @returns {string} + */ + getEditionKey() { + return this.editionKeyInput.value } /** - * Returns this form's `action` - * - * @returns {string} - */ - getFormAction () { - return this.rootElem.action; + * Returns this form's `action` + * + * @returns {string} + */ + getFormAction() { + return this.rootElem.action } /** - * Returns a reference to this check-in form. - * - * @returns {HTMLFormElement} - */ - getRootElement () { - return this.rootElem; + * Returns a reference to this check-in form. + * + * @returns {HTMLFormElement} + */ + getRootElement() { + return this.rootElem } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js index 7fef061ee1f..c4ebbf1b2ed 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js @@ -3,16 +3,13 @@ * @module my-books/MyBooksDropper/ReadingLists */ import 'jquery-colorbox'; +import myBooksStore from '../store' -import { addItem, removeItem } from '../../lists/ListService'; -import { - attachNewActiveShowcaseItem, - toggleActiveShowcaseItems, -} from '../../lists/ShowcaseItem'; -import { FadingToast } from '../../Toast'; -import myBooksStore from '../store'; +import { addItem, removeItem } from '../../lists/ListService' +import { attachNewActiveShowcaseItem, toggleActiveShowcaseItems } from '../../lists/ShowcaseItem' +import { FadingToast } from '../../Toast' -const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; +const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png' /** * Represents a single My Books dropper's list affordances, and defines their @@ -22,384 +19,353 @@ const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; */ export class ReadingLists { /** - * Adds functionality to the given dropper's list affordances. - * @param {HTMLElement} dropper - */ - constructor (dropper) { - /** - * References the given My Books Dropper root element. - * - * @member {HTMLElement} + * Adds functionality to the given dropper's list affordances. + * @param {HTMLElement} dropper */ - this.dropper = dropper; + constructor(dropper) { + /** + * References the given My Books Dropper root element. + * + * @member {HTMLElement} + */ + this.dropper = dropper /** - * Reference to the "Use work" checkbox. - * - * @member {HTMLElement|null} - */ - this.workCheckBox = dropper.querySelector('.work-checkbox'); + * Reference to the "Use work" checkbox. + * + * @member {HTMLElement|null} + */ + this.workCheckBox = dropper.querySelector('.work-checkbox') if (this.workCheckBox) { // Uncheck "Use work" checkbox on page refresh - this.workCheckBox.checked = false; + this.workCheckBox.checked = false } /** - * Reference to the "My Reading Lists" section of the dropdown content. - * - * @member {HTMLElement} - */ - this.dropperListsElement = dropper.querySelector('.my-lists'); + * Reference to the "My Reading Lists" section of the dropdown content. + * + * @member {HTMLElement} + */ + this.dropperListsElement = dropper.querySelector('.my-lists') /** - * Key of the document that will be added to or removed from a list. - * - * @member {string} - */ - this.seedKey = this.dropperListsElement.dataset.seedKey; + * Key of the document that will be added to or removed from a list. + * + * @member {string} + */ + this.seedKey = this.dropperListsElement.dataset.seedKey /** - * Key of the work associated with this dropper. Will be an empty - * string if no work is associated. - * - * @member {string} - */ - this.workKey = this.dropperListsElement.dataset.workKey; + * Key of the work associated with this dropper. Will be an empty + * string if no work is associated. + * + * @member {string} + */ + this.workKey = this.dropperListsElement.dataset.workKey /** - * The patron's user key. - * - * @member {string} - */ - this.userKey = this.dropperListsElement.dataset.userKey; + * The patron's user key. + * + * @member {string} + */ + this.userKey = this.dropperListsElement.dataset.userKey /** - * Stores information about a single list. - * - * @typedef ActiveListData - * @type {object} - * @property {string} title The title of the list - * @property {string} coverUrl URL for the seed's image - * @property {boolean} itemOnList True if the list contains the default seed key - * @property {boolean} workOnList True if the list contains a reference to a work - * @property {HTMLElement} dropperListAffordance Reference to the "Add to list" dropdown affordance - */ + * Stores information about a single list. + * + * @typedef ActiveListData + * @type {object} + * @property {string} title The title of the list + * @property {string} coverUrl URL for the seed's image + * @property {boolean} itemOnList True if the list contains the default seed key + * @property {boolean} workOnList True if the list contains a reference to a work + * @property {HTMLElement} dropperListAffordance Reference to the "Add to list" dropdown affordance + */ /** - * Maps list keys to objects containing more data about the list. - * - * @member {Record<string, ActiveListData>} - */ - this.patronLists = {}; + * Maps list keys to objects containing more data about the list. + * + * @member {Record<string, ActiveListData>} + */ + this.patronLists = {} } /** - * Adds functionality to all of the dropper's list affordances. - */ - initialize () { - this.initModifyListAffordances( - this.dropper.querySelectorAll('.modify-list'), - ); + * Adds functionality to all of the dropper's list affordances. + */ + initialize() { + this.initModifyListAffordances(this.dropper.querySelectorAll('.modify-list')) - const openListModalButton = this.dropper.querySelector('.create-new-list'); + const openListModalButton = this.dropper.querySelector('.create-new-list') if (openListModalButton) { - this.addOpenListModalClickListener(openListModalButton); + this.addOpenListModalClickListener(openListModalButton) } if (this.workCheckBox) { this.workCheckBox.addEventListener('click', () => { - this.updateListDisplays(); - toggleActiveShowcaseItems(this.workCheckBox.checked); - }); + this.updateListDisplays() + toggleActiveShowcaseItems(this.workCheckBox.checked) + }) } } /** - * Updates dropdown list affordances when an update occurs. - */ - updateListDisplays () { - const isWorkSelected = this.workCheckBox && this.workCheckBox.checked; + * Updates dropdown list affordances when an update occurs. + */ + updateListDisplays() { + const isWorkSelected = this.workCheckBox && this.workCheckBox.checked for (const key of Object.keys(this.patronLists)) { - const listData = this.patronLists[key]; + const listData = this.patronLists[key] if (isWorkSelected) { - this.toggleDisplayedType(listData.workOnList, key); + this.toggleDisplayedType(listData.workOnList, key) } else { - this.toggleDisplayedType(listData.itemOnList, key); + this.toggleDisplayedType(listData.itemOnList, key) } } } /** - * Changes list affordance visibility in the dropper and "Already list" - * list based on an item's membership to the given list. - * - * If the item is on the list, the "Already list" list affordance is displayed - * and the dropdown affordance will display a checkmark. - * - * @param {boolean} isListMember True if the item is on the list - * @param {string} listKey Unique identifier for a list - */ - toggleDisplayedType (isListMember, listKey) { - const listData = this.patronLists[listKey]; + * Changes list affordance visibility in the dropper and "Already list" + * list based on an item's membership to the given list. + * + * If the item is on the list, the "Already list" list affordance is displayed + * and the dropdown affordance will display a checkmark. + * + * @param {boolean} isListMember True if the item is on the list + * @param {string} listKey Unique identifier for a list + */ + toggleDisplayedType(isListMember, listKey) { + const listData = this.patronLists[listKey] if (isListMember) { - listData.dropperListAffordance.classList.add('list--active'); + listData.dropperListAffordance.classList.add('list--active') } else { - listData.dropperListAffordance.classList.remove('list--active'); + listData.dropperListAffordance.classList.remove('list--active') } } /** - * Hydrates the given dropdown list affordance elements and stores list data. - * - * Each given element is decorated with additional information about the list. - * This method also populates the patronLists record. - * - * @param {NodeList<HTMLElement>} modifyListElements - */ - initModifyListAffordances (modifyListElements) { + * Hydrates the given dropdown list affordance elements and stores list data. + * + * Each given element is decorated with additional information about the list. + * This method also populates the patronLists record. + * + * @param {NodeList<HTMLElement>} modifyListElements + */ + initModifyListAffordances(modifyListElements) { for (const elem of modifyListElements) { - const listItemKeys = elem.dataset.listItems; - const listKey = elem.dataset.listKey; - const itemOnList = listItemKeys.includes(this.seedKey); - const elemParent = elem.parentElement; + const listItemKeys = elem.dataset.listItems + const listKey = elem.dataset.listKey + const itemOnList = listItemKeys.includes(this.seedKey) + const elemParent = elem.parentElement this.patronLists[listKey] = { title: elem.innerText, coverUrl: elem.dataset.listCoverUrl, itemOnList: itemOnList, - dropperListAffordance: elemParent, // The .list element - }; + dropperListAffordance: elemParent, // The .list element + } if (!this.patronLists[listKey].coverUrl) { - this.patronLists[listKey].coverUrl = DEFAULT_COVER_URL; + this.patronLists[listKey].coverUrl = DEFAULT_COVER_URL } if (this.workCheckBox) { // Check for work key membership: - const workOnList = listItemKeys.includes(this.workKey); - this.patronLists[listKey].workOnList = workOnList; + const workOnList = listItemKeys.includes(this.workKey) + this.patronLists[listKey].workOnList = workOnList if (this.workCheckBox.checked) { if (workOnList) { - elemParent.classList.add('list--active'); + elemParent.classList.add('list--active') } } else { if (itemOnList) { - elemParent.classList.add('list--active'); + elemParent.classList.add('list--active') } } } else { if (itemOnList) { - elemParent.classList.add('list--active'); + elemParent.classList.add('list--active') } } elem.addEventListener('click', (event) => { - event.preventDefault(); - const isAddingItem = - !this.patronLists[listKey].dropperListAffordance.classList.contains( - 'list--active', - ); - this.modifyList(listKey, isAddingItem); - }); + event.preventDefault() + const isAddingItem = !this.patronLists[listKey].dropperListAffordance.classList.contains('list--active') + this.modifyList(listKey, isAddingItem) + }) } } /** - * Adds or removes a document to or from the list identified by the given key. - * - * @async - * @param {string} listKey Unique key for list - * @param {boolean} isAddingItem `true` if an item is being added to a list - */ - async modifyList (listKey, isAddingItem) { - let seed; - const isWork = this.workCheckBox && this.workCheckBox.checked; + * Adds or removes a document to or from the list identified by the given key. + * + * @async + * @param {string} listKey Unique key for list + * @param {boolean} isAddingItem `true` if an item is being added to a list + */ + async modifyList(listKey, isAddingItem) { + let seed + const isWork = this.workCheckBox && this.workCheckBox.checked // Seed will be a string if its type is 'subject' - const isSubjectSeed = this.seedKey[0] !== '/'; + const isSubjectSeed = this.seedKey[0] !== '/' if (isWork) { - seed = { key: this.workKey }; + seed = { key: this.workKey } } else if (isSubjectSeed) { - seed = this.seedKey; + seed = this.seedKey } else { - seed = { key: this.seedKey }; + seed = { key: this.seedKey } } - const makeChange = isAddingItem ? addItem : removeItem; - this.patronLists[listKey].dropperListAffordance.classList.remove( - 'list--active', - ); - this.patronLists[listKey].dropperListAffordance.classList.add( - 'list--pending', - ); + const makeChange = isAddingItem ? addItem : removeItem + this.patronLists[listKey].dropperListAffordance.classList.remove('list--active') + this.patronLists[listKey].dropperListAffordance.classList.add('list--pending') await makeChange(listKey, seed) .then((response) => { if (response.status >= 400) { - throw new Error('List update failed'); + throw new Error('List update failed') } - response.json(); + response.json() }) .then(() => { - this.updateViewAfterModifyingList(listKey, isWork, isAddingItem); + this.updateViewAfterModifyingList(listKey, isWork, isAddingItem) - const seedKey = isWork ? this.workKey : this.seedKey; + const seedKey = isWork ? this.workKey : this.seedKey if (isAddingItem) { // make new active showcase item - const listTitle = this.patronLists[listKey].title; - attachNewActiveShowcaseItem( - listKey, - seedKey, - listTitle, - this.patronLists[listKey].coverUrl, - ); + const listTitle = this.patronLists[listKey].title + attachNewActiveShowcaseItem(listKey, seedKey, listTitle, this.patronLists[listKey].coverUrl) } else { // remove existing showcase items - const showcases = myBooksStore.getShowcases(); - const matchingShowcases = showcases.filter( - (item) => item.listKey === listKey && item.seedKey === seedKey, - ); + const showcases = myBooksStore.getShowcases() + const matchingShowcases = showcases.filter((item) => item.listKey === listKey && item.seedKey === seedKey) for (const item of matchingShowcases) { - item.removeSelf(); + item.removeSelf() } } }) .catch(() => { if (!isAddingItem) { // Replace check mark if patron was removing an item from a list - this.patronLists[listKey].dropperListAffordance.classList.add( - 'list--active', - ); + this.patronLists[listKey].dropperListAffordance.classList.add('list--active') } - new FadingToast( - 'Could not update list. Please try again later.', - ).show(); + new FadingToast('Could not update list. Please try again later.').show() }) - .finally(() => - this.patronLists[listKey].dropperListAffordance.classList.remove( - 'list--pending', - ), - ); + .finally(() => this.patronLists[listKey].dropperListAffordance.classList.remove('list--pending')) } /** - * Updates `patronLists` with the new list membership information, - * then updates the view. - * - * @param {string} listKey Unique identifier for the modified list - * @param {boolean} isWork `true` if a work was added or removed - * @param {boolean} wasItemAdded `true` if item was added to list - */ - updateViewAfterModifyingList (listKey, isWork, wasItemAdded) { + * Updates `patronLists` with the new list membership information, + * then updates the view. + * + * @param {string} listKey Unique identifier for the modified list + * @param {boolean} isWork `true` if a work was added or removed + * @param {boolean} wasItemAdded `true` if item was added to list + */ + updateViewAfterModifyingList(listKey, isWork, wasItemAdded) { if (isWork) { - this.patronLists[listKey].workOnList = wasItemAdded; + this.patronLists[listKey].workOnList = wasItemAdded } else { - this.patronLists[listKey].itemOnList = wasItemAdded; + this.patronLists[listKey].itemOnList = wasItemAdded } - this.updateListDisplays(); + this.updateListDisplays() } /** - * Adds click listener to the given "Create a new list" button. - * - * When the button is clicked, a modal containing the list creation form - * is displayed. When the modal is closed, the form's inputs are cleared. - * - * @param {HTMLElement} openListModalButton - */ - addOpenListModalClickListener (openListModalButton) { + * Adds click listener to the given "Create a new list" button. + * + * When the button is clicked, a modal containing the list creation form + * is displayed. When the modal is closed, the form's inputs are cleared. + * + * @param {HTMLElement} openListModalButton + */ + addOpenListModalClickListener(openListModalButton) { openListModalButton.addEventListener('click', (event) => { - event.preventDefault(); + event.preventDefault() $.colorbox({ inline: true, opacity: '0.5', - href: '#addList', - }); - }); + href: '#addList' + }) + }) } /** - * Adds new entry to `patronLists` record and updates list dropdown. - * - * Creates and hydrates an "Add to list" dropdown affordance. - * - * @param {string} listKey Unique identifier for the new list - * @param {string} listTitle Title of the list - * @param {boolean} isActive `True` if this dropper's seed is on the list - * @param {string} coverUrl URL for the list's cover image - */ - onListCreationSuccess (listKey, listTitle, isActive, coverUrl) { - const dropperListAffordance = this.createDropdownListAffordance( - listKey, - listTitle, - isActive, - ); + * Adds new entry to `patronLists` record and updates list dropdown. + * + * Creates and hydrates an "Add to list" dropdown affordance. + * + * @param {string} listKey Unique identifier for the new list + * @param {string} listTitle Title of the list + * @param {boolean} isActive `True` if this dropper's seed is on the list + * @param {string} coverUrl URL for the list's cover image + */ + onListCreationSuccess(listKey, listTitle, isActive, coverUrl) { + const dropperListAffordance = this.createDropdownListAffordance(listKey, listTitle, isActive) this.patronLists[listKey] = { title: listTitle, coverUrl: coverUrl, - dropperListAffordance: dropperListAffordance, - }; + dropperListAffordance: dropperListAffordance + } if (isActive) { if (this.workCheckBox && this.workCheckBox.checked) { - this.patronLists[listKey].itemOnList = false; - this.patronLists[listKey].workOnList = true; + this.patronLists[listKey].itemOnList = false + this.patronLists[listKey].workOnList = true } else { - this.patronLists[listKey].itemOnList = true; - this.patronLists[listKey].workOnList = false; + this.patronLists[listKey].itemOnList = true + this.patronLists[listKey].workOnList = false } } } /** - * Creates and hydrates a new "Add to list" dropdown affordance. - * - * @param {string} listKey Unique identifier for a list - * @param {string} listTitle The list's title - * @param {boolean} isActive `true` if the seed is on this list - * @returns {HTMLElement} Reference to the newly created element - */ - createDropdownListAffordance (listKey, listTitle, isActive) { + * Creates and hydrates a new "Add to list" dropdown affordance. + * + * @param {string} listKey Unique identifier for a list + * @param {string} listTitle The list's title + * @param {boolean} isActive `true` if the seed is on this list + * @returns {HTMLElement} Reference to the newly created element + */ + createDropdownListAffordance(listKey, listTitle, isActive) { const itemMarkUp = `<span class="list__status-indicator"></span> <a href="${listKey}" class="modify-list dropper__close" data-list-cover-url="${listKey}" data-list-key="${listKey}">${listTitle}</a> - `; - const p = document.createElement('p'); - p.classList.add('list'); + ` + const p = document.createElement('p') + p.classList.add('list') if (isActive) { - p.classList.add('list--active'); + p.classList.add('list--active') } - p.innerHTML = itemMarkUp; - this.dropperListsElement.appendChild(p); - const listAffordance = p.querySelector('.modify-list'); + p.innerHTML = itemMarkUp + this.dropperListsElement.appendChild(p) + const listAffordance = p.querySelector('.modify-list') listAffordance.addEventListener('click', (event) => { - event.preventDefault(); - const isAddingItem = - !this.patronLists[listKey].dropperListAffordance.classList.contains( - 'list--active', - ); - this.modifyList(listKey, isAddingItem); - }); - - return p; + event.preventDefault() + const isAddingItem = !this.patronLists[listKey].dropperListAffordance.classList.contains('list--active') + this.modifyList(listKey, isAddingItem) + }) + + return p } /** - * Returns the seed of the object that can be added to this list. - * - * @returns {string} The seed key - */ - getSeed () { + * Returns the seed of the object that can be added to this list. + * + * @returns {string} The seed key + */ + getSeed() { if (this.workCheckBox && this.workCheckBox.checked) { // seed is the work key: - return this.workKey; + return this.workKey } - return this.seedKey; + return this.seedKey } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLogForms.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLogForms.js index e50020ed14f..685127fecc7 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLogForms.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLogForms.js @@ -3,6 +3,7 @@ * @module my-books/MyBooksDropper/ReadingLogForms */ + /** * @typedef {string} ReadingLogShelf */ @@ -14,8 +15,8 @@ export const ReadingLogShelves = { WANT_TO_READ: '1', CURRENTLY_READING: '2', - ALREADY_READ: '3', -}; + ALREADY_READ: '3' +} /** * Class representing a dropper's reading log forms. @@ -37,268 +38,248 @@ export const ReadingLogShelves = { */ export class ReadingLogForms { /** - * Adds functionality to a single dropper's reading log forms. - * - * @param {HTMLElement} dropper - * @param {import('./CheckInComponents')} checkInComponents - * @param {Record<string, CallableFunction>} dropperActionCallbacks - */ - constructor(dropper, checkInComponents, dropperActionCallbacks) { - /** - * Contains references to the parent dropper's close and - * toggle functions. These functions are bound to the - * parent dropper element. + * Adds functionality to a single dropper's reading log forms. * - * @member {Record<string, CallableFunction>} + * @param {HTMLElement} dropper + * @param {import('./CheckInComponents')} checkInComponents + * @param {Record<string, CallableFunction>} dropperActionCallbacks */ - this.dropperActions = dropperActionCallbacks; + constructor(dropper, checkInComponents, dropperActionCallbacks) { + /** + * Contains references to the parent dropper's close and + * toggle functions. These functions are bound to the + * parent dropper element. + * + * @member {Record<string, CallableFunction>} + */ + this.dropperActions = dropperActionCallbacks /** - * Reference to each reading log submit button. This includes the - * primary dropper button and the buttons in the dropdown. - * - * @member {NodeList<HTMLElement>} - */ - this.submitButtons = dropper.querySelectorAll('.reading-log button'); + * Reference to each reading log submit button. This includes the + * primary dropper button and the buttons in the dropdown. + * + * @member {NodeList<HTMLElement>} + */ + this.submitButtons = dropper.querySelectorAll('.reading-log button') /** - * Reference to this dropper's primary form. - * - * @member {HTMLFormElement} - */ + * Reference to this dropper's primary form. + * + * @member {HTMLFormElement} + */ this.primaryForm = null; /** - * Reference to this dropper's primary button. - * - * @member {HTMLButtonElement} - */ + * Reference to this dropper's primary button. + * + * @member {HTMLButtonElement} + */ this.primaryButton = null; /** - * Reference to this dropper's "Remove from shelf" button. - * - * @member {HTMLButtonElement} - */ + * Reference to this dropper's "Remove from shelf" button. + * + * @member {HTMLButtonElement} + */ this.removeButton = null; for (const button of this.submitButtons) { if (button.classList.contains('primary-action')) { - this.primaryButton = button; - this.primaryForm = button.closest('form'); - } else if (button.classList.contains('remove-from-list')) { - // XXX : Rename class `remove-from-shelf`? - this.removeButton = button; + this.primaryButton = button + this.primaryForm = button.closest('form') + } + else if (button.classList.contains('remove-from-list')) { // XXX : Rename class `remove-from-shelf`? + this.removeButton = button } } - if (!this.primaryButton) { - // This dropper only contains list affordances - this.primaryButton = dropper.querySelector('.primary-action'); + if (!this.primaryButton) { // This dropper only contains list affordances + this.primaryButton = dropper.querySelector('.primary-action') } /** - * @member {import('./CheckInComponents') | null} - */ - this.checkInComponents = checkInComponents; + * @member {import('./CheckInComponents') | null} + */ + this.checkInComponents = checkInComponents - this.readingLogForms = dropper.querySelectorAll('form.reading-log'); - this.isDropperDisabled = dropper.classList.contains( - 'generic-dropper--disabled', - ); + this.readingLogForms = dropper.querySelectorAll('form.reading-log') + this.isDropperDisabled = dropper.classList.contains('generic-dropper--disabled') } /** - * Adds click listeners to each of the form's submit buttons. - * - * If dropper is disabled, no event listeners will be added. - */ + * Adds click listeners to each of the form's submit buttons. + * + * If dropper is disabled, no event listeners will be added. + */ initialize() { if (!this.isDropperDisabled) { if (this.readingLogForms.length) { for (const form of this.readingLogForms) { - const submitButton = form.querySelector('button[type=submit]'); + const submitButton = form.querySelector('button[type=submit]') submitButton.addEventListener('click', (event) => { - event.preventDefault(); - this.updateReadingLog(form); + event.preventDefault() + this.updateReadingLog(form) // Close the dropper - this.dropperActions.closeDropper(); - }); + this.dropperActions.closeDropper() + }) } } else { // Toggle the dropper when there is no "Reading Log" primary action: this.primaryButton.addEventListener('click', () => { - this.dropperActions.toggleDropper(); - }); + this.dropperActions.toggleDropper() + }) } } } /** - * POSTs the given form and updates the dropper accordingly. - * - * @param {HTMLFormElement} form - */ + * POSTs the given form and updates the dropper accordingly. + * + * @param {HTMLFormElement} form + */ updateReadingLog(form) { - let newPrimaryButtonText = - this.primaryButton.querySelector('.btn-text').innerText; + let newPrimaryButtonText = this.primaryButton.querySelector('.btn-text').innerText // XXX: Use i18n strings - this.updatePrimaryButtonText('saving...'); + this.updatePrimaryButtonText('saving...') - const formData = new FormData(form); - const url = form.getAttribute('action'); + const formData = new FormData(form) + const url = form.getAttribute('action') - const hasAddedBook = formData.get('action') === 'add'; + const hasAddedBook = formData.get('action') === 'add' - let canUpdateShelf = true; + let canUpdateShelf = true - if ( - !hasAddedBook && - this.checkInComponents && - this.checkInComponents.hasReadDate() - ) { + if (!hasAddedBook && this.checkInComponents && this.checkInComponents.hasReadDate()) { // XXX: Use i18n strings - canUpdateShelf = confirm( - 'Removing this book from your shelves will delete your check-ins for this work. Continue?', - ); + canUpdateShelf = confirm('Removing this book from your shelves will delete your check-ins for this work. Continue?') } if (canUpdateShelf) { fetch(url, { method: 'post', - body: formData, + body: formData }) - .then((response) => response.json()) + .then(response => response.json()) .then((data) => { - if (!('error' in data)) { - // XXX: Serve correct HTTP codes to avoid this - this.updateActivatedStatus(hasAddedBook); + if (!('error' in data)) { // XXX: Serve correct HTTP codes to avoid this + this.updateActivatedStatus(hasAddedBook) if (hasAddedBook) { - const primaryButtonClicked = - form.classList.contains('primary-action'); - const newBookshelfId = form.querySelector( - 'input[name=bookshelf_id]', - ).value; + const primaryButtonClicked = form.classList.contains('primary-action') + const newBookshelfId = form.querySelector('input[name=bookshelf_id]').value if (!primaryButtonClicked) { // A book has been added to a shelf chosen from the dropdown. // The primary form and dropdown selections must now be updated. - const clickedButton = form.querySelector('button[type=submit]'); - newPrimaryButtonText = clickedButton.innerText; + const clickedButton = form.querySelector('button[type=submit]') + newPrimaryButtonText = clickedButton.innerText - this.updatePrimaryBookshelfId(newBookshelfId); + this.updatePrimaryBookshelfId(newBookshelfId) - this.updateDropdownButtonVisibility(clickedButton); + this.updateDropdownButtonVisibility(clickedButton) } // Update check-ins: if (this.checkInComponents) { - if ( - !this.checkInComponents.hasReadDate() && - newBookshelfId === ReadingLogShelves.ALREADY_READ - ) { - this.checkInComponents.showCheckInPrompt(); + if (!this.checkInComponents.hasReadDate() && newBookshelfId === ReadingLogShelves.ALREADY_READ) { + this.checkInComponents.showCheckInPrompt() } else { - this.checkInComponents.hideCheckInPrompt(); + this.checkInComponents.hideCheckInPrompt() } } + } else if (this.checkInComponents) { // Update check-ins: - this.checkInComponents.hideCheckInPrompt(); - this.checkInComponents.hideCheckInDisplay(); - this.checkInComponents.resetForm(); + this.checkInComponents.hideCheckInPrompt() + this.checkInComponents.hideCheckInDisplay() + this.checkInComponents.resetForm() } } // Remove "saving..." message from button: - this.updatePrimaryButtonText(newPrimaryButtonText); - }); + this.updatePrimaryButtonText(newPrimaryButtonText) + }) } else { // Remove "saving..." message from button if shelf cannot be updated: - this.updatePrimaryButtonText(newPrimaryButtonText); + this.updatePrimaryButtonText(newPrimaryButtonText) } } /** - * Updates "active" status of the primary form. - * - * An "active" dropper will display a checkmark in the primary button, and a remove - * button in the dropdown. - * - * The primary form's `action` input is "remove" when the dropper is active, and - * "add" otherwise. - * - * @param {boolean} isActivated `true` if the dropper is changing to an "active" status - */ + * Updates "active" status of the primary form. + * + * An "active" dropper will display a checkmark in the primary button, and a remove + * button in the dropdown. + * + * The primary form's `action` input is "remove" when the dropper is active, and + * "add" otherwise. + * + * @param {boolean} isActivated `true` if the dropper is changing to an "active" status + */ updateActivatedStatus(isActivated) { if (isActivated) { - this.primaryButton - .querySelector('.activated-check') - .classList.remove('hidden'); - this.removeButton.classList.remove('hidden'); - this.primaryForm.querySelector('input[name=action]').value = 'remove'; + this.primaryButton.querySelector('.activated-check').classList.remove('hidden') + this.removeButton.classList.remove('hidden') + this.primaryForm.querySelector('input[name=action]').value = 'remove' } else { - this.primaryButton - .querySelector('.activated-check') - .classList.add('hidden'); - this.removeButton.classList.add('hidden'); - this.primaryForm.querySelector('input[name=action]').value = 'add'; + this.primaryButton.querySelector('.activated-check').classList.add('hidden') + this.removeButton.classList.add('hidden') + this.primaryForm.querySelector('input[name=action]').value = 'add' } - this.primaryButton.classList.toggle('activated'); - this.primaryButton.classList.toggle('unactivated'); + this.primaryButton.classList.toggle('activated') + this.primaryButton.classList.toggle('unactivated') } /** - * Sets that primary button's text to the given string. - * - * @param {string} newText - */ + * Sets that primary button's text to the given string. + * + * @param {string} newText + */ updatePrimaryButtonText(newText) { - this.primaryButton.querySelector('.btn-text').innerText = newText; + this.primaryButton.querySelector('.btn-text').innerText = newText } /** - * Changes value of primary form's `bookshelf_id` input to the given number. - * - * @param {number} newId - */ + * Changes value of primary form's `bookshelf_id` input to the given number. + * + * @param {number} newId + */ updatePrimaryBookshelfId(newId) { - this.primaryForm.querySelector('input[name=bookshelf_id]').value = newId; + this.primaryForm.querySelector('input[name=bookshelf_id]').value = newId } /** - * Updates the visibility of dropdown buttons, hiding the given button. - * - * All other dropdown buttons will be visible after this method exits. - * - * @param {HTMLButtonElement} transitioningButton - */ + * Updates the visibility of dropdown buttons, hiding the given button. + * + * All other dropdown buttons will be visible after this method exits. + * + * @param {HTMLButtonElement} transitioningButton + */ updateDropdownButtonVisibility(transitioningButton) { for (const button of this.submitButtons) { - button.classList.remove('hidden'); + button.classList.remove('hidden') } - transitioningButton.classList.add('hidden'); + transitioningButton.classList.add('hidden') } /** - * Returns the display string used to denote the given reading log shelf ID. - * - * @param shelfId {ReadingLogShelf} - */ + * Returns the display string used to denote the given reading log shelf ID. + * + * @param shelfId {ReadingLogShelf} + */ getDisplayString(shelfId) { - const matchingFormElem = Array.from(this.readingLogForms).find((elem) => { + const matchingFormElem = Array.from(this.readingLogForms).find(elem => { if (elem === this.primaryForm) { - return false; + return false } - const bookshelfInput = elem.querySelector('input[name=bookshelf_id]'); - return shelfId === bookshelfInput.value; - }); + const bookshelfInput = elem.querySelector('input[name=bookshelf_id]') + return shelfId === bookshelfInput.value + }) - const formButton = matchingFormElem.querySelector('button'); - return formButton.textContent; + const formButton = matchingFormElem.querySelector('button') + return formButton.textContent } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/index.js b/openlibrary/plugins/openlibrary/js/my-books/index.js index 40328a09c6e..1640be09742 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/index.js +++ b/openlibrary/plugins/openlibrary/js/my-books/index.js @@ -1,99 +1,88 @@ -import { getListPartials } from '../lists/ListService'; -import { - createActiveShowcaseItem, - ShowcaseItem, - toggleActiveShowcaseItems, -} from '../lists/ShowcaseItem'; -import { removeChildren } from '../utils'; -import { CreateListForm } from './CreateListForm'; -import { MyBooksDropper } from './MyBooksDropper'; -import myBooksStore from './store'; +import { CreateListForm } from './CreateListForm' +import { MyBooksDropper } from './MyBooksDropper' +import myBooksStore from './store' +import { getListPartials } from '../lists/ListService' +import { ShowcaseItem, createActiveShowcaseItem, toggleActiveShowcaseItems } from '../lists/ShowcaseItem' +import { removeChildren } from '../utils' // XXX : jsdoc // XXX : decompose -export function initMyBooksAffordances (dropperElements, showcaseElements) { - const showcases = []; +export function initMyBooksAffordances(dropperElements, showcaseElements) { + const showcases = [] for (const elem of showcaseElements) { - const showcase = new ShowcaseItem(elem); - showcase.initialize(); + const showcase = new ShowcaseItem(elem) + showcase.initialize() - showcases.push(showcase); + showcases.push(showcase) } - myBooksStore.setShowcases(showcases); + myBooksStore.setShowcases(showcases) - const form = document.querySelector('#create-list-form'); - const createListForm = new CreateListForm(form); - createListForm.initialize(); + const form = document.querySelector('#create-list-form') + const createListForm = new CreateListForm(form) + createListForm.initialize() - const droppers = []; - const seedKeys = []; + const droppers = [] + const seedKeys = [] for (const dropper of dropperElements) { - const myBooksDropper = new MyBooksDropper(dropper); - myBooksDropper.initialize(); + const myBooksDropper = new MyBooksDropper(dropper) + myBooksDropper.initialize() - droppers.push(myBooksDropper); - seedKeys.push(...myBooksDropper.getSeedKeys()); + droppers.push(myBooksDropper) + seedKeys.push(...myBooksDropper.getSeedKeys()) } // Remove duplicate keys: - const seedKeySet = new Set(seedKeys); + const seedKeySet = new Set(seedKeys) // Get user key from first Dropper and add to store: - const userKey = droppers[0].readingLists.userKey; - myBooksStore.setUserKey(userKey); - myBooksStore.setDroppers(droppers); + const userKey = droppers[0].readingLists.userKey + myBooksStore.setUserKey(userKey) + myBooksStore.setDroppers(droppers) getListPartials() - .then((response) => response.json()) + .then(response => response.json()) .then((data) => { // XXX : convert this block to one or two function calls - const listData = data.listData; - const activeShowcaseItems = []; + const listData = data.listData + const activeShowcaseItems = [] for (const listKey in listData) { // Check for matches between seed keys and list members // If match, create new active showcase item for (const seedKey of listData[listKey].members) { if (seedKeySet.has(seedKey)) { - const key = listData[listKey].members[0]; - const coverID = key.slice(key.indexOf('OL')); - const cover = `https://covers.openlibrary.org/b/olid/${coverID}-S.jpg`; - - activeShowcaseItems.push( - createActiveShowcaseItem( - listKey, - seedKey, - listData[listKey].listName, - cover, - ), - ); + const key = listData[listKey].members[0] + const coverID = key.slice(key.indexOf('OL')) + const cover = `https://covers.openlibrary.org/b/olid/${coverID}-S.jpg` + + activeShowcaseItems.push(createActiveShowcaseItem(listKey, seedKey, listData[listKey].listName, cover)) } } } - const activeListsShowcaseElem = document.querySelector('.already-lists'); + const activeListsShowcaseElem = document.querySelector('.already-lists') if (activeListsShowcaseElem) { // Remove the loading indicator: - removeChildren(activeListsShowcaseElem); + removeChildren(activeListsShowcaseElem) for (const li of activeShowcaseItems) { - activeListsShowcaseElem.appendChild(li); + activeListsShowcaseElem.appendChild(li) - const showcase = new ShowcaseItem(li); - showcase.initialize(); + const showcase = new ShowcaseItem(li) + showcase.initialize() - showcases.push(showcase); + showcases.push(showcase) } - toggleActiveShowcaseItems(false); + toggleActiveShowcaseItems(false) } // Update dropper content: for (const dropper of droppers) { - dropper.updateReadingLists(data['dropper']); + dropper.updateReadingLists(data['dropper']) } - }); + }) } /** @@ -102,8 +91,8 @@ export function initMyBooksAffordances (dropperElements, showcaseElements) { * @param workKey {string} * @returns {MyBooksDropper|undefined} */ -export function findDropperForWork (workKey) { - return myBooksStore.getDroppers().find((dropper) => { - return workKey === dropper.workKey; - }); +export function findDropperForWork(workKey) { + return myBooksStore.getDroppers().find(dropper => { + return workKey === dropper.workKey + }) } diff --git a/openlibrary/plugins/openlibrary/js/my-books/store/index.js b/openlibrary/plugins/openlibrary/js/my-books/store/index.js index 7aeb6258ccb..a867b9f7659 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/store/index.js +++ b/openlibrary/plugins/openlibrary/js/my-books/store/index.js @@ -9,75 +9,75 @@ */ class MyBooksStore { /** - * Initializes the store. - */ - constructor () { + * Initializes the store. + */ + constructor() { this._store = { droppers: [], showcases: [], - userKey: '', - openDropper: null, - }; + userkey: '', + openDropper: null + } } /** - * @returns {Array<MyBooksDropper>} - */ - getDroppers () { - return this._store.droppers; + * @returns {Array<MyBooksDropper>} + */ + getDroppers() { + return this._store.droppers } /** - * @param {Array<MyBooksDropper>} droppers - */ - setDroppers (droppers) { - this._store.droppers = droppers; + * @param {Array<MyBooksDropper>} droppers + */ + setDroppers(droppers) { + this._store.droppers = droppers } /** - * @returns {Array<ShowcaseItem>} - */ - getShowcases () { - return this._store.showcases; + * @returns {Array<ShowcaseItem>} + */ + getShowcases() { + return this._store.showcases } /** - * @param {Array<ShowcaseItem>} showcases - */ - setShowcases (showcases) { - this._store.showcases = showcases; + * @param {Array<ShowcaseItem>} showcases + */ + setShowcases(showcases) { + this._store.showcases = showcases } /** - * @returns {string} - */ - getUserKey () { - return this._store.userKey; + * @returns {string} + */ + getUserKey() { + return this._store.userKey } /** - * @param {string} userKey - */ - setUserKey (userKey) { - this._store.userKey = userKey; + * @param {string} userKey + */ + setUserKey(userKey) { + this._store.userKey = userKey } /** - * @returns {MyBooksDropper} - */ - getOpenDropper () { - return this._store.openDropper; + * @returns {MyBooksDropper} + */ + getOpenDropper() { + return this._store.openDropper } /** - * @param {MyBooksDropper} dropper - */ - setOpenDropper (dropper) { - this._store.openDropper = dropper; + * @param {MyBooksDropper} dropper + */ + setOpenDropper(dropper) { + this._store.openDropper = dropper } } -const myBooksStore = new MyBooksStore(); -Object.freeze(myBooksStore); +const myBooksStore = new MyBooksStore() +Object.freeze(myBooksStore) -export default myBooksStore; +export default myBooksStore diff --git a/openlibrary/plugins/openlibrary/js/native-dialog/index.js b/openlibrary/plugins/openlibrary/js/native-dialog/index.js index 743c290a68c..dffbd21ff6a 100644 --- a/openlibrary/plugins/openlibrary/js/native-dialog/index.js +++ b/openlibrary/plugins/openlibrary/js/native-dialog/index.js @@ -6,26 +6,23 @@ * 2. The dialog receives a `close-dialog` event. * @param {HTMLCollection<HTMLDialogElement>} elems */ -export function initDialogs (elems) { +export function initDialogs(elems) { for (const elem of elems) { - elem.addEventListener('click', (event) => { + elem.addEventListener('click', function(event) { + // Event target exclusions needed for FireFox, which sets mouse positions to zero on // <select> and <option> clicks - if ( - isOutOfBounds(event, elem) && - event.target.nodeName !== 'SELECT' && - event.target.nodeName !== 'OPTION' - ) { - elem.close(); + if (isOutOfBounds(event, elem) && event.target.nodeName !== 'SELECT' && event.target.nodeName !== 'OPTION') { + elem.close() } - }); - elem.addEventListener('close-dialog', () => { - elem.close(); - }); - const closeIcon = elem.querySelector('.native-dialog--close'); - closeIcon.addEventListener('click', () => { - elem.close(); - }); + }) + elem.addEventListener('close-dialog', function() { + elem.close() + }) + const closeIcon = elem.querySelector('.native-dialog--close') + closeIcon.addEventListener('click', function() { + elem.close() + }) } } @@ -36,12 +33,12 @@ export function initDialogs (elems) { * @param {HTMLDialogElement} dialog * @returns `true` if the click was out of bounds. */ -function isOutOfBounds (event, dialog) { - const rect = dialog.getBoundingClientRect(); +function isOutOfBounds(event, dialog) { + const rect = dialog.getBoundingClientRect() return ( event.clientX < rect.left || - event.clientX > rect.right || - event.clientY < rect.top || - event.clientY > rect.bottom + event.clientX > rect.right || + event.clientY < rect.top || + event.clientY > rect.bottom ); } diff --git a/openlibrary/plugins/openlibrary/js/nonjquery_utils.js b/openlibrary/plugins/openlibrary/js/nonjquery_utils.js index 350fc0ded8d..15d080a9ac6 100644 --- a/openlibrary/plugins/openlibrary/js/nonjquery_utils.js +++ b/openlibrary/plugins/openlibrary/js/nonjquery_utils.js @@ -12,13 +12,13 @@ * @param {Boolean} [execAsap] * @returns {Function} */ -export function debounce (func, threshold = 100, execAsap = false) { +export function debounce(func, threshold=100, execAsap=false) { let timeout; - return function debounced () { - const obj = this, - args = arguments; - function delayed () { - if (!execAsap) func.apply(obj, args); + return function debounced() { + const obj = this, args = arguments; + function delayed() { + if (!execAsap) + func.apply(obj, args); timeout = null; } diff --git a/openlibrary/plugins/openlibrary/js/offline-banner.js b/openlibrary/plugins/openlibrary/js/offline-banner.js index e9dec3bf7b4..917579fe51a 100644 --- a/openlibrary/plugins/openlibrary/js/offline-banner.js +++ b/openlibrary/plugins/openlibrary/js/offline-banner.js @@ -1,4 +1,5 @@ -export function initOfflineBanner () { +export function initOfflineBanner() { + window.addEventListener('offline', () => { $('#offline-info').slideDown(); $('#offline-info').fadeTo(5000, 1).slideUp(); diff --git a/openlibrary/plugins/openlibrary/js/ol.analytics.js b/openlibrary/plugins/openlibrary/js/ol.analytics.js index f022deac8bc..e9f4e1c820a 100644 --- a/openlibrary/plugins/openlibrary/js/ol.analytics.js +++ b/openlibrary/plugins/openlibrary/js/ol.analytics.js @@ -1,18 +1,18 @@ /** - * OpenLibrary-specific convenience functions for use with Archive.org athena.js - * - * Depends on Archive.org athena.js function archive_analytics.send_ping() - * - */ +* OpenLibrary-specific convenience functions for use with Archive.org athena.js +* +* Depends on Archive.org athena.js function archive_analytics.send_ping() +* +*/ -export default function initAnalytics () { +export default function initAnalytics() { var vs, i; var startTime = new Date(); if (window.archive_analytics) { - // Setup analytics, depends on script loaded from CDN + // Setup analytics, depends on script loaded from CDN window.archive_analytics.set_up_event_tracking(); - window.archive_analytics.ol_send_event_ping = (values) => { + window.archive_analytics.ol_send_event_ping = function(values) { var endTime = new Date(); window.archive_analytics.send_ping({ service: 'ol', @@ -21,22 +21,22 @@ export default function initAnalytics () { ea: values['action'], el: values['label'] || location.pathname, ev: 1, - loadtime: endTime.getTime() - startTime.getTime(), - cache_bust: Math.random(), + loadtime: (endTime.getTime() - startTime.getTime()), + cache_bust: Math.random() }); - }; + } vs = window.archive_analytics.get_data_packets(); for (i in vs) { - vs[i]['cache_bust'] = Math.random(); - vs[i]['server_ms'] = $('.analytics-stats-time-calculator').data('time'); - vs[i]['server_name'] = 'ol-web.us.archive.org'; - vs[i]['service'] = 'ol'; + vs[i]['cache_bust']=Math.random(); + vs[i]['server_ms']=$('.analytics-stats-time-calculator').data('time'); + vs[i]['server_name']='ol-web.us.archive.org'; + vs[i]['service']='ol'; } - if (window.flights) { + if (window.flights){ window.flights.init(); } - $(document).on('click', '[data-ol-link-track]', function () { + $(document).on('click', '[data-ol-link-track]', function() { var category_action = $(this).attr('data-ol-link-track').split('|'); // for testing, // console.log(category_action[0], category_action[1]); @@ -50,10 +50,7 @@ export default function initAnalytics () { window.vs = vs; // NOTE: This might cause issues if this script is made async #4474 - window.addEventListener( - 'DOMContentLoaded', - function send_analytics_pageview () { - window.archive_analytics.send_pageview({}); - }, - ); + window.addEventListener('DOMContentLoaded', function send_analytics_pageview() { + window.archive_analytics.send_pageview({}); + }); } diff --git a/openlibrary/plugins/openlibrary/js/ol.js b/openlibrary/plugins/openlibrary/js/ol.js index e9be0e3ee45..92b42878f22 100644 --- a/openlibrary/plugins/openlibrary/js/ol.js +++ b/openlibrary/plugins/openlibrary/js/ol.js @@ -6,11 +6,11 @@ import { SearchModeSelector, mode as searchMode } from './SearchUtils'; /* Sets the key in the website cookie to the specified value */ -function setValueInCookie (key, value) { +function setValueInCookie(key, value) { document.cookie = `${key}=${value};path=/`; } -export default function init () { +export default function init() { const urlParams = getJsonFromUrl(location.search); if (urlParams.mode) { searchMode.write(urlParams.mode); @@ -26,17 +26,17 @@ export default function init () { initWebsiteTranslationOptions(); } -export function initBorrowAndReadLinks () { +export function initBorrowAndReadLinks() { // LOADING ONCLICK FUNCTIONS FOR BORROW AND READ LINKS // used in openlibrary/macros/AvailabilityButton.html and openlibrary/macros/LoanStatus.html - $(function (){ - $('.cta-btn--ia.cta-btn--borrow,.cta-btn--ia.cta-btn--read').on('click', function (){ + $(function(){ + $('.cta-btn--ia.cta-btn--borrow,.cta-btn--ia.cta-btn--read').on('click', function(){ $(this).removeClass('cta-btn cta-btn--available').addClass('cta-btn cta-btn--available--load'); }); }); - $(function (){ - $('#waitlist_ebook').on('click', function (){ + $(function(){ + $('#waitlist_ebook').on('click', function(){ $(this).removeClass('cta-btn cta-btn--unavailable').addClass('cta-btn cta-btn--unavailable--load'); }); }); @@ -44,7 +44,7 @@ export function initBorrowAndReadLinks () { } -export function initWebsiteTranslationOptions () { +export function initWebsiteTranslationOptions() { $('.locale-options li a').on('click', function (event) { event.preventDefault(); const locale = $(this).data('lang-id'); diff --git a/openlibrary/plugins/openlibrary/js/partner_ol_lib.js b/openlibrary/plugins/openlibrary/js/partner_ol_lib.js index d7aa2dac34c..5748aedfd32 100644 --- a/openlibrary/plugins/openlibrary/js/partner_ol_lib.js +++ b/openlibrary/plugins/openlibrary/js/partner_ol_lib.js @@ -1,7 +1,7 @@ /** * @param {string} container */ -function getIsbnToElementMap (container) { +function getIsbnToElementMap(container) { const reISBN = /(978)?[0-9]{9}[0-9X]/i; const elements = Array.from(document.querySelectorAll(container)); const isbnElementMap = {}; @@ -10,7 +10,7 @@ function getIsbnToElementMap (container) { if (isbnMatches) { isbnElementMap[isbnMatches[0]] = e; } - }); + }) return isbnElementMap; } @@ -18,7 +18,7 @@ function getIsbnToElementMap (container) { * @param {string[]} isbnList * @returns {Promise<Array>} */ -async function getAvailabilityDataFromOpenLibrary (isbnList) { +async function getAvailabilityDataFromOpenLibrary(isbnList) { const apiBaseUrl = 'https://openlibrary.org/search.json'; const apiUrl = `${apiBaseUrl}?fields=*,availability&q=isbn:${isbnList.join('+OR+')}`; const response = await fetch(apiUrl); @@ -47,31 +47,27 @@ async function getAvailabilityDataFromOpenLibrary (isbnList) { * textOnBtn: "Open Library!" * }); */ -async function addOpenLibraryButtons (options) { - const { bookContainer, selectorToPlaceBtnIn, textOnBtn } = options; +async function addOpenLibraryButtons(options) { + const {bookContainer, selectorToPlaceBtnIn, textOnBtn} = options if (bookContainer === undefined) { throw Error( - 'book container must be specified in options for open library buttons to populate!', - ); + 'book container must be specified in options for open library buttons to populate!' + ) } const foundIsbnElementsMap = getIsbnToElementMap(bookContainer); - const availabilityResults = await getAvailabilityDataFromOpenLibrary( - Object.keys(foundIsbnElementsMap), - ); + const availabilityResults = await getAvailabilityDataFromOpenLibrary(Object.keys(foundIsbnElementsMap)) Object.keys(foundIsbnElementsMap).map((isbn) => { - const availability = availabilityResults[isbn]; + const availability = availabilityResults[isbn] if (availability && availability.status !== 'error') { - const e = foundIsbnElementsMap[isbn]; - const buttons = selectorToPlaceBtnIn - ? e.querySelector(selectorToPlaceBtnIn) - : e; - const openLibraryBtnLink = document.createElement('a'); - openLibraryBtnLink.href = `https://openlibrary.org/works/${availability.openlibrary_work}`; - openLibraryBtnLink.text = textOnBtn || 'Open Library'; - openLibraryBtnLink.classList.add('openlibrary-btn'); + const e = foundIsbnElementsMap[isbn] + const buttons = selectorToPlaceBtnIn ? e.querySelector(selectorToPlaceBtnIn) : e; + const openLibraryBtnLink = document.createElement('a') + openLibraryBtnLink.href = `https://openlibrary.org/works/${availability.openlibrary_work}` + openLibraryBtnLink.text = textOnBtn || 'Open Library' + openLibraryBtnLink.classList.add('openlibrary-btn') buttons.append(openLibraryBtnLink); } - }); + }) } // Expose globally so clients can use this method diff --git a/openlibrary/plugins/openlibrary/js/password-toggle.js b/openlibrary/plugins/openlibrary/js/password-toggle.js index 781f2591b58..dd69c2690be 100644 --- a/openlibrary/plugins/openlibrary/js/password-toggle.js +++ b/openlibrary/plugins/openlibrary/js/password-toggle.js @@ -4,16 +4,15 @@ * @param {HTMLElement} elem Reference to affordance that toggles a password input's visibility */ export function initPasswordToggling(elem) { - const passwordInput = document.querySelector('input[type=password]'); + const passwordInput = document.querySelector('input[type=password]') elem.addEventListener('click', () => { if (passwordInput.type === 'password') { - passwordInput.type = 'text'; - elem.querySelector('img').src = '/static/images/icons/icon_eye-open.svg'; + passwordInput.type = 'text' + elem.querySelector('img').src = '/static/images/icons/icon_eye-open.svg' } else { - passwordInput.type = 'password'; - elem.querySelector('img').src = - '/static/images/icons/icon_eye-closed.svg'; + passwordInput.type = 'password' + elem.querySelector('img').src = '/static/images/icons/icon_eye-closed.svg' } - }); + }) } diff --git a/openlibrary/plugins/openlibrary/js/patron_exports.js b/openlibrary/plugins/openlibrary/js/patron_exports.js index a4c41c20f8d..9315fa07ba4 100644 --- a/openlibrary/plugins/openlibrary/js/patron_exports.js +++ b/openlibrary/plugins/openlibrary/js/patron_exports.js @@ -3,7 +3,7 @@ * * @param {HTMLElement} buttonElement */ -function disableButton (buttonElement) { +function disableButton(buttonElement) { buttonElement.setAttribute('disabled', 'true'); buttonElement.setAttribute('aria-disabled', 'true'); } @@ -16,11 +16,11 @@ function disableButton (buttonElement) { * * @param {NodeList<HTMLFormElement>} elems */ -export function initPatronExportForms (elems) { +export function initPatronExportForms(elems) { elems.forEach((form) => { - const submitButton = form.querySelector('input[type=submit]'); + const submitButton = form.querySelector('input[type=submit]') form.addEventListener('submit', () => { disableButton(submitButton); - }); - }); + }) + }) } diff --git a/openlibrary/plugins/openlibrary/js/private-button.js b/openlibrary/plugins/openlibrary/js/private-button.js index 98d5fcc6d47..4ba965a02b7 100644 --- a/openlibrary/plugins/openlibrary/js/private-button.js +++ b/openlibrary/plugins/openlibrary/js/private-button.js @@ -1,15 +1,12 @@ -import { FadingToast } from './Toast'; +import { FadingToast } from './Toast' -export function initPrivateButtons (buttons) { - buttons.forEach((button) => { +export function initPrivateButtons(buttons) { + buttons.forEach(button => { button.addEventListener('click', (event) => { event.preventDefault(); - const toast = new FadingToast( - window.$_('This patron has not enabled following'), - null, - 3000, - ); + const toast = new FadingToast('This patron has not enabled following', null, 3000); toast.show(); + }); }); } diff --git a/openlibrary/plugins/openlibrary/js/python.js b/openlibrary/plugins/openlibrary/js/python.js index cda5e3c9d76..393e6b7fe88 100644 --- a/openlibrary/plugins/openlibrary/js/python.js +++ b/openlibrary/plugins/openlibrary/js/python.js @@ -7,7 +7,7 @@ * @param {mixed} n * @return {string} */ -export function commify (n) { +export function commify(n) { var text = n.toString(); var re = /(\d+)(\d{3})/; @@ -19,7 +19,7 @@ export function commify (n) { } // Implementation of Python urllib.urlencode in Javascript. -export function urlencode (query) { +export function urlencode(query) { var parts = []; var k; for (k in query) { @@ -28,10 +28,10 @@ export function urlencode (query) { return parts.join('&'); } -export function slice (array, begin, end) { +export function slice(array, begin, end) { var a = []; var i; - for (i = begin; i < Math.min(array.length, end); i++) { + for (i=begin; i < Math.min(array.length, end); i++) { a.push(array[i]); } return a; diff --git a/openlibrary/plugins/openlibrary/js/reading-goals/index.js b/openlibrary/plugins/openlibrary/js/reading-goals/index.js index 9e65ec67842..f992a545132 100644 --- a/openlibrary/plugins/openlibrary/js/reading-goals/index.js +++ b/openlibrary/plugins/openlibrary/js/reading-goals/index.js @@ -1,15 +1,15 @@ -import { initDialogs } from '../native-dialog'; -import { buildPartialsUrl } from '../utils'; +import { initDialogs } from '../native-dialog' +import { buildPartialsUrl } from '../utils' /** * Adds listener to open reading goal modal. * * @param {HTMLCollection<HTMLElement>} links Prompts for adding a reading goal */ -export function initYearlyGoalPrompt (links) { +export function initYearlyGoalPrompt(links) { for (const link of links) { if (!link.classList.contains('goal-set')) { - link.addEventListener('click', onYearlyGoalClick); + link.addEventListener('click', onYearlyGoalClick) } } } @@ -17,9 +17,9 @@ export function initYearlyGoalPrompt (links) { /** * Finds and shows the yearly goal modal. */ -function onYearlyGoalClick () { - const yearlyGoalModal = document.querySelector('#yearly-goal-modal'); - yearlyGoalModal.showModal(); +function onYearlyGoalClick() { + const yearlyGoalModal = document.querySelector('#yearly-goal-modal') + yearlyGoalModal.showModal() } /** @@ -33,12 +33,12 @@ function onYearlyGoalClick () { * * @param {HTMLCollection<HTMLElement>} elems ELements which display only the current year */ -export function displayLocalYear (elems) { - const localYear = new Date().getFullYear(); +export function displayLocalYear(elems) { + const localYear = new Date().getFullYear() for (const elem of elems) { - const serverYear = Number(elem.dataset.serverYear); + const serverYear = Number(elem.dataset.serverYear) if (localYear !== serverYear) { - elem.textContent = localYear; + elem.textContent = localYear } } } @@ -48,11 +48,11 @@ export function displayLocalYear (elems) { * * @param {HTMLCollection<HTMLElement>} editLinks Edit goal links */ -export function initGoalEditLinks (editLinks) { +export function initGoalEditLinks(editLinks) { for (const link of editLinks) { - const parent = link.closest('.reading-goal-progress'); - const modal = parent.querySelector('dialog'); - addGoalEditClickListener(link, modal); + const parent = link.closest('.reading-goal-progress') + const modal = parent.querySelector('dialog') + addGoalEditClickListener(link, modal) } } @@ -64,10 +64,10 @@ export function initGoalEditLinks (editLinks) { * @param {HTMLElement} editLink An edit goal link * @param {HTMLDialogElement} modal The modal that will be shown */ -function addGoalEditClickListener (editLink, modal) { - editLink.addEventListener('click', () => { - modal.showModal(); - }); +function addGoalEditClickListener(editLink, modal) { + editLink.addEventListener('click', function() { + modal.showModal() + }) } /** @@ -76,9 +76,9 @@ function addGoalEditClickListener (editLink, modal) { * * @param {HTMLCollection<HTMLElement>} submitButtons Submit goal buttons */ -export function initGoalSubmitButtons (submitButtons) { +export function initGoalSubmitButtons(submitButtons) { for (const button of submitButtons) { - addGoalSubmissionListener(button); + addGoalSubmissionListener(button) } } @@ -89,78 +89,69 @@ export function initGoalSubmitButtons (submitButtons) { * the action set a new goal, or updated an existing goal. * @param {HTMLELement} submitButton Reading goal form submit button */ -function addGoalSubmissionListener (submitButton) { - submitButton.addEventListener('click', (event) => { - event.preventDefault(); +function addGoalSubmissionListener(submitButton) { + submitButton.addEventListener('click', function(event) { + event.preventDefault() - const form = submitButton.closest('form'); + const form = submitButton.closest('form') if (!form.checkValidity()) { - form.reportValidity(); - throw new Error('Form invalid'); + form.reportValidity() + throw new Error('Form invalid') } - const formData = new FormData(form); + const formData = new FormData(form) fetch(form.action, { method: 'POST', headers: { - 'content-type': 'application/x-www-form-urlencoded', + 'content-type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams(formData), - }).then((response) => { - if (!response.ok) { - throw new Error('Failed to set reading goal'); - } - const modal = form.closest('dialog'); - if (modal) { - modal.close(); - } + body: new URLSearchParams(formData) + }) + .then((response) => { + if (!response.ok) { + throw new Error('Failed to set reading goal') + } + const modal = form.closest('dialog') + if (modal) { + modal.close() + } - const yearlyGoalSections = document.querySelectorAll( - '.yearly-goal-section', - ); - if (formData.get('is_update')) { - // Progress component exists on page - yearlyGoalSections.forEach((yearlyGoalSection) => { - const goalInput = form.querySelector('input[name=goal]'); - const isDeleted = Number(goalInput.value) === 0; - - if (isDeleted) { - const chipGroup = yearlyGoalSection.querySelector('.chip-group'); - const goalContainer = yearlyGoalSection.querySelector( - '#reading-goal-container', - ); - if (chipGroup) { - chipGroup.classList.remove('hidden'); + const yearlyGoalSections = document.querySelectorAll('.yearly-goal-section') + if (formData.get('is_update')) { // Progress component exists on page + yearlyGoalSections.forEach((yearlyGoalSection) => { + const goalInput = form.querySelector('input[name=goal]') + const isDeleted = Number(goalInput.value) === 0 + + if (isDeleted) { + const chipGroup = yearlyGoalSection.querySelector('.chip-group') + const goalContainer = yearlyGoalSection.querySelector('#reading-goal-container') + if (chipGroup) { + chipGroup.classList.remove('hidden') + } + if (goalContainer) { + goalContainer.remove() + } + // Restore "Set reading goal" link hidden when goal was first set + const setGoalLink = yearlyGoalSection.querySelector('.set-reading-goal-link') + if (setGoalLink) { + setGoalLink.classList.remove('hidden') + } + } else { + const progressComponent = modal.closest('.reading-goal-progress') + updateProgressComponent(progressComponent, Number(formData.get('goal'))) } - if (goalContainer) { - goalContainer.remove(); - } - // Restore "Set reading goal" link hidden when goal was first set - const setGoalLink = yearlyGoalSection.querySelector( - '.set-reading-goal-link', - ); - if (setGoalLink) { - setGoalLink.classList.remove('hidden'); - } - } else { - const progressComponent = modal.closest('.reading-goal-progress'); - updateProgressComponent( - progressComponent, - Number(formData.get('goal')), - ); + }) + } else { + const goalYear = formData.get('year') + fetchProgressAndUpdateViews(yearlyGoalSections, goalYear) + const banner = document.querySelector('.page-banner-mybooks') + if (banner) { + banner.remove() } - }); - } else { - const goalYear = formData.get('year'); - fetchProgressAndUpdateViews(yearlyGoalSections, goalYear); - const banner = document.querySelector('.page-banner-mybooks'); - if (banner) { - banner.remove(); } - } - }); - }); + }) + }) } /** @@ -170,19 +161,17 @@ function addGoalSubmissionListener (submitButton) { * @param {HTMLElement} elem A reading goal progress component * @param {Number} goal The new reading goal */ -function updateProgressComponent (elem, goal) { +function updateProgressComponent(elem, goal) { // Calculate new percentage: - const booksReadSpan = elem.querySelector( - '.reading-goal-progress__books-read', - ); - const booksRead = Number(booksReadSpan.textContent); - const percentComplete = Math.floor((booksRead / goal) * 100); + const booksReadSpan = elem.querySelector('.reading-goal-progress__books-read') + const booksRead = Number(booksReadSpan.textContent) + const percentComplete = Math.floor((booksRead / goal) * 100) // Update view: - const goalSpan = elem.querySelector('.reading-goal-progress__goal'); - const completedBar = elem.querySelector('.reading-goal-progress__completed'); - goalSpan.textContent = goal; - completedBar.style.width = `${Math.min(100, percentComplete)}%`; + const goalSpan = elem.querySelector('.reading-goal-progress__goal') + const completedBar = elem.querySelector('.reading-goal-progress__completed') + goalSpan.textContent = goal + completedBar.style.width = `${Math.min(100, percentComplete)}%` } /** @@ -194,43 +183,39 @@ function updateProgressComponent (elem, goal) { * @param {NodeList} yearlyGoalElems Containers for progress components and reading goal links. * @param {string} goalYear Year that the goal is set for. */ -function fetchProgressAndUpdateViews (yearlyGoalElems, goalYear) { - fetch(buildPartialsUrl('ReadingGoalProgress', { year: goalYear })) +function fetchProgressAndUpdateViews(yearlyGoalElems, goalYear) { + fetch(buildPartialsUrl('ReadingGoalProgress', {year: goalYear})) .then((response) => { if (!response.ok) { - throw new Error('Failed to fetch progress element'); + throw new Error('Failed to fetch progress element') } - return response.json(); + return response.json() }) - .then((data) => { - const html = data['partials']; + .then(function(data) { + const html = data['partials'] yearlyGoalElems.forEach((yearlyGoalElem) => { - const progress = document.createElement('SPAN'); - progress.id = 'reading-goal-container'; - progress.innerHTML = html; - yearlyGoalElem.appendChild(progress); + const progress = document.createElement('SPAN') + progress.id = 'reading-goal-container' + progress.innerHTML = html + yearlyGoalElem.appendChild(progress) const link = yearlyGoalElem.querySelector('.set-reading-goal-link'); if (link) { if (link.classList.contains('li-title-desktop')) { // Remove click listener in mobile views - link.removeEventListener('click', onYearlyGoalClick); + link.removeEventListener('click', onYearlyGoalClick) } else { // Hide desktop "set 20XX reading goal" link link.classList.add('hidden'); } } - const progressEditLink = progress.querySelector( - '.edit-reading-goal-link', - ); - const updateModal = progress.querySelector('dialog'); - initDialogs([updateModal]); - addGoalEditClickListener(progressEditLink, updateModal); - const submitButton = updateModal.querySelector( - '.reading-goal-submit-button', - ); - addGoalSubmissionListener(submitButton); - }); - }); + const progressEditLink = progress.querySelector('.edit-reading-goal-link') + const updateModal = progress.querySelector('dialog') + initDialogs([updateModal]) + addGoalEditClickListener(progressEditLink, updateModal) + const submitButton = updateModal.querySelector('.reading-goal-submit-button') + addGoalSubmissionListener(submitButton) + }) + }) } diff --git a/openlibrary/plugins/openlibrary/js/readinglog_stats.js b/openlibrary/plugins/openlibrary/js/readinglog_stats.js index 493d7239989..c3e1f33714d 100644 --- a/openlibrary/plugins/openlibrary/js/readinglog_stats.js +++ b/openlibrary/plugins/openlibrary/js/readinglog_stats.js @@ -1,14 +1,13 @@ // @ts-check - -import Chart from 'chart.js'; -import entries from 'lodash/entries'; import fromPairs from 'lodash/fromPairs'; -import groupBy from 'lodash/groupBy'; -import includes from 'lodash/includes'; import isUndefined from 'lodash/isUndefined'; +import includes from 'lodash/includes'; import orderBy from 'lodash/orderBy'; +import entries from 'lodash/entries'; +import groupBy from 'lodash/groupBy'; import uniq from 'lodash/uniq'; import uniqBy from 'lodash/uniqBy'; +import Chart from 'chart.js'; import 'chartjs-plugin-datalabels'; /** @@ -40,21 +39,19 @@ import 'chartjs-plugin-datalabels'; /** * @param {Config} config */ -export function init (config) { - Chart.scaleService.updateScaleDefaults('linear', { - ticks: { beginAtZero: true, stepSize: 1 }, - }); - const authors_by_id = fromPairs(config.authors.map((a) => [a.key, a])); +export function init(config) { + Chart.scaleService.updateScaleDefaults('linear', { ticks: { beginAtZero: true, stepSize: 1 } }); + const authors_by_id = fromPairs(config.authors.map(a => [a.key, a])); /** - * - * @param {Config} config - * @param {ChartConfig} chartConfig - * @param {Element} container - * @param {HTMLCanvasElement} canvas - */ - function createWorkChart (config, chartConfig, container, canvas) { - /** @type {{[key: string]: Work[]}} */ + * + * @param {Config} config + * @param {ChartConfig} chartConfig + * @param {Element} container + * @param {HTMLCanvasElement} canvas + */ + function createWorkChart(config, chartConfig, container, canvas) { + /** @type {{[key: string]: Work[]}} */ const grouped = {}; /** @type {Work[]} */ const excluded = []; @@ -62,9 +59,7 @@ export function init (config) { for (const work of config.works) { const allKeys = getPath(work, chartConfig.key) || []; const validKeys = uniq( - allKeys.filter( - (key) => !isUndefined(key) && !includes(chartConfig.exclude, key), - ), + allKeys.filter(key => !isUndefined(key) && !includes(chartConfig.exclude, key)) ); if (!validKeys.length) { excluded.push(work); @@ -76,39 +71,31 @@ export function init (config) { } } - const bars = orderBy(entries(grouped), (x) => x[1].length, 'desc').slice( - 0, - 20, - ); + const bars = orderBy(entries(grouped), x => x[1].length, 'desc').slice(0, 20); canvas.height = bars.length * 20 + 5; - canvas.width = 400; + canvas.width= 400; new Chart(canvas.getContext('2d'), { type: 'horizontalBar', data: { - labels: bars.map((b) => b[0]), - datasets: [ - { - backgroundColor: 'rgb(255, 99, 132)', - borderColor: 'rgb(255, 99, 132)', - borderWidth: 0, - data: bars.map((b) => b[1].length), - }, - ], + labels: bars.map(b => b[0]), + datasets: [{ + backgroundColor: 'rgb(255, 99, 132)', + borderColor: 'rgb(255, 99, 132)', + borderWidth: 0, + data: bars.map(b => b[1].length) + }] }, options: { responsive: false, legend: { display: false }, scales: { xAxes: [{ display: false }], - yAxes: [ - { barPercentage: 1, gridLines: { display: false }, stacked: true }, - ], + yAxes: [{ barPercentage: 1, gridLines: { display: false }, stacked: true }], }, onClick: (e, [chartEl]) => { if (chartEl) { const bar = bars[chartEl._index]; - document.querySelector('.selected-works--list').innerHTML = - window.render_works_list(bar[1]); + document.querySelector('.selected-works--list').innerHTML = window.render_works_list(bar[1]); } else { document.querySelector('.selected-works--list').innerHTML = ''; } @@ -118,19 +105,16 @@ export function init (config) { color: '#FFF', anchor: 'end', align: 'left', - offset: 0, - }, - }, - }, + offset: 0 + } + } + } }); - $( - window.render_excluded_works_list(excluded, config.works.length), - ).appendTo(container); + $(window.render_excluded_works_list(excluded, config.works.length)).appendTo(container); } - const defaultFieldRender = (field) => - `OPTIONAL { ?x ${field.relation} ?${field.name}. }`; + const defaultFieldRender = field => `OPTIONAL { ?x ${field.relation} ?${field.name}. }`; const SPARQL_FIELDS = [ { name: 'sex', type: 'uri', relation: 'wdt:P21' }, @@ -140,7 +124,7 @@ export function init (config) { name: 'country_of_birth', type: 'uri', relation: 'wdt:P19/wdt:P131*/wdt:P17', - render: (field) => ` + render: field => ` OPTIONAL { ?x ${field.relation} ?${field.name}. OPTIONAL { ?country_of_birth wdt:P571 ?country_inception. } @@ -149,64 +133,56 @@ export function init (config) { # Limit to modern-day countries or the country that existed at the time # of the author's birth FILTER(!BOUND(?country_dissolution) || !BOUND(?dob) || (?dob >= ?country_inception && ?dob <= ?country_dissolution) ). - `, + ` }, ]; - function buildSparql (authors) { + function buildSparql(authors) { return ` SELECT DISTINCT ?x ?xLabel ?olid - ${SPARQL_FIELDS.map( - (f) => - `?${f.name} ${f.type === 'uri' ? `?${f.name}Label ` : ''}`, - ).join('')} + ${ + SPARQL_FIELDS.map(f => `?${f.name} ${f.type === 'uri' ? `?${f.name}Label ` : ''}`).join('') +} WHERE { - VALUES ?olids { ${authors.map((a) => `"${a.key.split('/')[2]}"`).join(' ')} } + VALUES ?olids { ${authors.map(a => `"${a.key.split('/')[2]}"`).join(' ')} } ?x wdt:P648 ?olids; wdt:P648 ?olid. - ${SPARQL_FIELDS.map((f) => - (f.render || defaultFieldRender)(f), - ).join('\n')} + ${ + SPARQL_FIELDS.map(f => (f.render || defaultFieldRender)(f)) + .join('\n') +} SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],${config.lang},en". } } `; } - document.getElementById('wd-query-sample').href = - `https://query.wikidata.org/#${encodeURIComponent(buildSparql(config.authors.slice(0, 20)))}`; + document.getElementById('wd-query-sample').href = `https://query.wikidata.org/#${encodeURIComponent(buildSparql(config.authors.slice(0, 20)))}`; const wdPromise = fetch('https://query.wikidata.org/sparql?format=json', { method: 'POST', - body: new URLSearchParams({ query: buildSparql(config.authors) }), + body: new URLSearchParams({query: buildSparql(config.authors)}) }) - .then((r) => r.json()) - .then((resp) => { + .then(r => r.json()) + .then(resp => { const bindings = resp.results.bindings; - const grouped = groupBy(bindings, (o) => o.x.value.split('/')[4]); + const grouped = groupBy(bindings, o => o.x.value.split('/')[4]); const records = entries(grouped).map(([qid, bindings]) => { - const record = { qid, olids: uniq(bindings.map((x) => x.olid.value)) }; + const record = { qid, olids: uniq(bindings.map(x => x.olid.value)) }; // { qid: Q123, olids: [ { value: }, {value: }], blah: [ {value:}, {value:} ], blahLabel: [{value:}, {value:}, - for (const { name, type } of SPARQL_FIELDS) { + for (const {name, type} of SPARQL_FIELDS) { if (type === 'uri') { - // need to dedupe whilst keeping labels in mind + // need to dedupe whilst keeping labels in mind const deduped = uniqBy( bindings - .filter((x) => x[name]) - .map((x) => ({ - [name]: x[name], - [`${name}Label`]: x[`${name}Label`], - })), - (x) => x[name].value, - ); - record[name] = deduped.map((x) => x[name]); - record[`${name}Label`] = deduped.map((x) => x[`${name}Label`]); + .filter(x => x[name]) + .map(x => ({ [name]: x[name], [`${name}Label`]: x[`${name}Label`] })), + x => x[name].value) + record[name] = deduped.map(x => x[name]); + record[`${name}Label`] = deduped.map(x => x[`${name}Label`]); } else { - record[name] = uniqBy( - bindings.map((x) => x[name]), - 'value', - ); + record[name] = uniqBy(bindings.map(x => x[name]), 'value'); } } return record; @@ -223,7 +199,7 @@ export function init (config) { // Add full authors to the works objects for easy reference for (const work of config.works) { - work.authors = work.author_keys.map((key) => authors_by_id[key]); + work.authors = work.author_keys.map(key => authors_by_id[key]); } for (const container of document.querySelectorAll(config.charts_selector)) { @@ -234,9 +210,7 @@ export function init (config) { if (chartConfig.type === 'work-chart') { createWorkChart(config, chartConfig, container, canvas); } else if (chartConfig.type === 'wd-chart') { - wdPromise.then(() => - createWorkChart(config, chartConfig, container, canvas), - ); + wdPromise.then(() => createWorkChart(config, chartConfig, container, canvas)); } } } @@ -246,17 +220,16 @@ export function init (config) { * @param {string} key * @return {any} */ -function getPath (obj, key) { +function getPath(obj, key) { /** - * @param {object} obj - * @param {string[]} param1 - * @return {any} - */ - function main (obj, [head, ...rest]) { - if (typeof obj === 'undefined') return undefined; + * @param {object} obj + * @param {string[]} param1 + * @return {any} + */ + function main(obj, [head, ...rest]) { + if (typeof(obj) === 'undefined') return undefined; if (!head) return obj; - if (head.endsWith('[]')) - return obj[head.slice(0, -2)].flatMap((x) => main(x, rest)); + if (head.endsWith('[]')) return obj[head.slice(0, -2)].flatMap(x => main(x, rest)); else return main(obj[head], rest); } return main(obj, key.split('.')); diff --git a/openlibrary/plugins/openlibrary/js/return-form/index.js b/openlibrary/plugins/openlibrary/js/return-form/index.js index c6b08b6d00f..50b083737dd 100644 --- a/openlibrary/plugins/openlibrary/js/return-form/index.js +++ b/openlibrary/plugins/openlibrary/js/return-form/index.js @@ -4,13 +4,13 @@ * * @param {NodeList<HTMLElement>} returnForms */ -export function initReturnForms (returnForms) { +export function initReturnForms(returnForms) { for (const form of returnForms) { - const i18nStrings = JSON.parse(form.dataset.i18n); + const i18nStrings = JSON.parse(form.dataset.i18n) form.addEventListener('submit', (event) => { if (!confirm(i18nStrings['confirm_return'])) { event.preventDefault(); } - }); + }) } } diff --git a/openlibrary/plugins/openlibrary/js/search.js b/openlibrary/plugins/openlibrary/js/search.js index f99b2cecf81..6e11db26415 100644 --- a/openlibrary/plugins/openlibrary/js/search.js +++ b/openlibrary/plugins/openlibrary/js/search.js @@ -11,10 +11,10 @@ import { buildPartialsUrl } from './utils'; * @param {Number} start_facet_count initial number of displayed facets * @param {Number} facet_inc number of hidden facets to be displayed */ -export function more (header, start_facet_count, facet_inc) { - const facetEntry = `div.${header} div.facetEntry`; - const shown = $(`${facetEntry}:not(:hidden)`).length; - const total = $(facetEntry).length; +export function more(header, start_facet_count, facet_inc) { + const facetEntry = `div.${header} div.facetEntry` + const shown = $(`${facetEntry}:not(:hidden)`).length + const total = $(facetEntry).length if (shown === start_facet_count) { $(`#${header}_less`).show(); $(`#${header}_bull`).show(); @@ -33,12 +33,12 @@ export function more (header, start_facet_count, facet_inc) { * @param {Number} start_facet_count initial number of displayed facets * @param {Number} facet_inc number of displayed facets to be hidden */ -export function less (header, start_facet_count, facet_inc) { - const facetEntry = `div.${header} div.facetEntry`; - const shown = $(`${facetEntry}:not(:hidden)`).length; - const total = $(facetEntry).length; +export function less(header, start_facet_count, facet_inc) { + const facetEntry = `div.${header} div.facetEntry` + const shown = $(`${facetEntry}:not(:hidden)`).length + const total = $(facetEntry).length const increment_extra = (shown - start_facet_count) % facet_inc; - const facet_dec = increment_extra === 0 ? facet_inc : increment_extra; + const facet_dec = (increment_extra === 0) ? facet_inc:increment_extra; const next_shown = Math.max(start_facet_count, shown - facet_dec); if (shown === total) { $(`#${header}_more`).show(); @@ -48,9 +48,7 @@ export function less (header, start_facet_count, facet_inc) { $(`#${header}_less`).hide(); $(`#${header}_bull`).hide(); } - $(`${facetEntry}:not(:hidden)`) - .slice(next_shown, shown) - .addClass('ui-helper-hidden'); + $(`${facetEntry}:not(:hidden)`).slice(next_shown, shown).addClass('ui-helper-hidden'); } /** @@ -66,49 +64,48 @@ export function less (header, start_facet_count, facet_inc) { * * @param {HTMLElement} facetsElem Root element of the search facets sidebar component */ -export async function initSearchFacets (facetsElem) { - const asyncLoad = facetsElem.dataset.asyncLoad; +export async function initSearchFacets(facetsElem) { + const asyncLoad = facetsElem.dataset.asyncLoad if (asyncLoad) { - const param = JSON.parse(facetsElem.dataset.param); + const param = JSON.parse(facetsElem.dataset.param) await whenVisible(facetsElem); fetchPartials(param) .then((data) => { if (data.activeFacets) { - const activeFacetsElem = createElementFromMarkup(data.activeFacets); - const activeFacetsContainer = document.querySelector( - '.selected-search-facets-container', - ); - activeFacetsContainer.replaceChildren(activeFacetsElem); + const activeFacetsElem = createElementFromMarkup(data.activeFacets) + const activeFacetsContainer = document.querySelector('.selected-search-facets-container') + activeFacetsContainer.replaceChildren(activeFacetsElem) } - const newFacetsElem = createElementFromMarkup(data.sidebar); - facetsElem.replaceWith(newFacetsElem); - hydrateFacets(); + const newFacetsElem = createElementFromMarkup(data.sidebar) + facetsElem.replaceWith(newFacetsElem) + hydrateFacets() - document.title = data.title; + document.title = data.title }) .catch(() => { // XXX : Handle case where `/partials` response is not `2XX` here - }); + }) } else { - hydrateFacets(); + hydrateFacets() } } + /** * Adds click listeners to the "show more" and "show less" facet affordances. */ -function hydrateFacets () { +function hydrateFacets() { const data_config_json = $('#searchFacets').data('config'); const start_facet_count = data_config_json['start_facet_count']; const facet_inc = data_config_json['facet_inc']; $('.header_bull').hide(); - $('.header_more').on('click', function () { + $('.header_more').on('click', function(){ more($(this).data('header'), start_facet_count, facet_inc); }); - $('.header_less').on('click', function () { + $('.header_less').on('click', function(){ less($(this).data('header'), start_facet_count, facet_inc); }); } @@ -130,21 +127,20 @@ function hydrateFacets () { * * @throws Error when `/partials` response is not in 200-299 range. */ -function fetchPartials (param) { +function fetchPartials(param) { const data = { param: param, path: location.pathname, - query: location.search, - }; - - return fetch( - buildPartialsUrl('SearchFacets', { data: JSON.stringify(data) }), - ).then((resp) => { - if (!resp.ok) { - throw new Error(`Failed to fetch partials. Status code: ${resp.status}`); - } - return resp.json(); - }); + query: location.search + } + + return fetch(buildPartialsUrl('SearchFacets', {data: JSON.stringify(data)})) + .then((resp) => { + if (!resp.ok) { + throw new Error(`Failed to fetch partials. Status code: ${resp.status}`) + } + return resp.json() + }) } /** @@ -156,12 +152,13 @@ function fetchPartials (param) { * @param {string} markup HTML markup for a single element * @returns {HTMLElement} */ -function createElementFromMarkup (markup) { - const template = document.createElement('template'); - template.innerHTML = markup; - return template.content.children[0]; +function createElementFromMarkup(markup) { + const template = document.createElement('template') + template.innerHTML = markup + return template.content.children[0] } + /** * Waits until the given element is visible in the viewport, then resolves. * @@ -169,30 +166,27 @@ function createElementFromMarkup (markup) { * @param {IntersectionObserverInit} options * @returns {Promise<void>} */ -async function whenVisible (elem, options = {}) { +async function whenVisible(elem, options = {}) { return new Promise((resolve) => { const intersectionObserver = new IntersectionObserver( (entries, observer) => { - entries.forEach((entry) => { + entries.forEach(entry => { if (!entry.isIntersecting) { - return; + return } // Stop observing once the element is visible - observer.unobserve(entry.target); - observer.disconnect(); - resolve(); - }); + observer.unobserve(entry.target) + observer.disconnect() + resolve() + }) }, - Object.assign( - { - root: null, - rootMargin: '200px', - threshold: 0, - }, - options, - ), - ); + Object.assign({ + root: null, + rootMargin: '200px', + threshold: 0 + }, options) + ) intersectionObserver.observe(elem); }); diff --git a/openlibrary/plugins/openlibrary/js/service-worker-init.js b/openlibrary/plugins/openlibrary/js/service-worker-init.js index 80d4c1dbc7f..99f7ea3d7ee 100644 --- a/openlibrary/plugins/openlibrary/js/service-worker-init.js +++ b/openlibrary/plugins/openlibrary/js/service-worker-init.js @@ -1,9 +1,9 @@ -export default function initServiceWorker() { +export default function initServiceWorker(){ if ('serviceWorker' in navigator) { window.addEventListener('load', () => { - navigator.serviceWorker - .register('/sw.js') - .catch((error) => { + navigator.serviceWorker.register('/sw.js') + .then(() => { }) + .catch(error => { // eslint-disable-next-line no-console console.error(`Service worker registration failed: ${error}`); }); @@ -11,7 +11,7 @@ export default function initServiceWorker() { } window.addEventListener('beforeinstallprompt', (e) => { - // Prevent the mini-infobar from appearing on mobile + // Prevent the mini-infobar from appearing on mobile e.preventDefault(); }); } diff --git a/openlibrary/plugins/openlibrary/js/service-worker-matchers.js b/openlibrary/plugins/openlibrary/js/service-worker-matchers.js index fb19165e299..672e6fa4c5a 100644 --- a/openlibrary/plugins/openlibrary/js/service-worker-matchers.js +++ b/openlibrary/plugins/openlibrary/js/service-worker-matchers.js @@ -6,38 +6,35 @@ It is in a is a separate file to avoid this error when writing tests: > 1 | import { ExpirationPlugin } from 'workbox-expiration'; */ -export function matchMiscFiles ({ url }) { - const miscFiles = [ - '/favicon.ico', - '/static/manifest.json', - '/cdn/archive.org/athena.js', - '/cdn/archive.org/donate.js', - ]; + +export function matchMiscFiles({ url }) { + const miscFiles = ['/favicon.ico', '/static/manifest.json', '/cdn/archive.org/athena.js', + '/cdn/archive.org/donate.js'] return miscFiles.includes(url.pathname); } -export function matchSmallMediumCovers ({ url }) { +export function matchSmallMediumCovers({ url }) { const regex = /-[SM].jpg$/; return regex.test(url.pathname); } -export function matchLargeCovers ({ url }) { +export function matchLargeCovers({ url }) { const regex = /-L.jpg$/; return regex.test(url.pathname); } -export function matchStaticImages ({ url }) { +export function matchStaticImages({ url }) { const regex = /^\/images\/|^\/static\/images\//; return regex.test(url.pathname); } -export function matchStaticBuild ({ url }) { +export function matchStaticBuild({ url }) { const regex = /^\/static\/build\/.*(\.js|\.css)/; - const localhost = url.origin.includes('localhost'); + const localhost = url.origin.includes('localhost') return !localhost && regex.test(url.pathname); } -export function matchArchiveOrgImage ({ url }) { +export function matchArchiveOrgImage({ url }) { // most importantly, to cache your profile picture from loading every time // also caches some covers return url.href.startsWith('https://archive.org/services/img/'); diff --git a/openlibrary/plugins/openlibrary/js/service-worker.js b/openlibrary/plugins/openlibrary/js/service-worker.js index 68f5fc23002..8c3e992c191 100644 --- a/openlibrary/plugins/openlibrary/js/service-worker.js +++ b/openlibrary/plugins/openlibrary/js/service-worker.js @@ -1,29 +1,25 @@ -import { CacheableResponsePlugin } from 'workbox-cacheable-response'; -import { clientsClaim } from 'workbox-core'; import { ExpirationPlugin } from 'workbox-expiration'; import { offlineFallback } from 'workbox-recipes'; -import { registerRoute, setDefaultHandler } from 'workbox-routing'; -import { CacheFirst, NetworkOnly } from 'workbox-strategies'; -import { - matchArchiveOrgImage, - matchLargeCovers, - matchMiscFiles, - matchSmallMediumCovers, - matchStaticBuild, - matchStaticImages, -} from './service-worker-matchers'; +import { setDefaultHandler, registerRoute } from 'workbox-routing'; +import { NetworkOnly, CacheFirst } from 'workbox-strategies'; +import { CacheableResponsePlugin } from 'workbox-cacheable-response'; +import { clientsClaim } from 'workbox-core'; +import { matchMiscFiles, matchSmallMediumCovers, matchLargeCovers, matchStaticImages, matchStaticBuild, matchArchiveOrgImage } from './service-worker-matchers'; self.skipWaiting(); clientsClaim(); // This is needed for the offline page to show -setDefaultHandler(new NetworkOnly()); +setDefaultHandler( + new NetworkOnly() +); offlineFallback({ pageFallback: '/static/offline.html', - imageFallback: '/static/images/logo_OL-lg.png', + imageFallback: '/static/images/logo_OL-lg.png' }); + const HOUR_SECONDS = 60 * 60; const DAY_SECONDS = 24 * HOUR_SECONDS; // only cache if it the request returns 0 or 200 status @@ -37,11 +33,11 @@ registerRoute( cacheName: 'misc-files-cache', plugins: [ new ExpirationPlugin({ - maxAgeSeconds: DAY_SECONDS, + maxAgeSeconds: DAY_SECONDS }), - cacheableResponses, + cacheableResponses ], - }), + }) ); registerRoute( @@ -53,9 +49,9 @@ registerRoute( maxEntries: 100, maxAgeSeconds: DAY_SECONDS * 365, }), - cacheableResponses, + cacheableResponses ], - }), + }) ); registerRoute( @@ -71,10 +67,10 @@ registerRoute( new ExpirationPlugin({ maxAgeSeconds: 60 * 10, }), - cacheableResponses, + cacheableResponses ], - }), -); + }) +) registerRoute( matchSmallMediumCovers, @@ -86,9 +82,9 @@ registerRoute( maxEntries: 150, purgeOnQuotaError: true, }), - cacheableResponses, + cacheableResponses ], - }), + }) ); registerRoute( @@ -102,9 +98,9 @@ registerRoute( maxAgeSeconds: HOUR_SECONDS, purgeOnQuotaError: true, }), - cacheableResponses, + cacheableResponses ], - }), + }) ); registerRoute( @@ -117,7 +113,7 @@ registerRoute( maxAgeSeconds: DAY_SECONDS, purgeOnQuotaError: true, }), - cacheableResponses, + cacheableResponses ], - }), + }) ); diff --git a/openlibrary/plugins/openlibrary/js/signup.js b/openlibrary/plugins/openlibrary/js/signup.js index 92e42615a53..fb0b7b5e821 100644 --- a/openlibrary/plugins/openlibrary/js/signup.js +++ b/openlibrary/plugins/openlibrary/js/signup.js @@ -3,22 +3,14 @@ import { debounce } from './nonjquery_utils.js'; export function initSignupForm() { const signupForm = document.querySelector('form[name=signup]'); const submitBtn = document.querySelector('button[name=signup]'); - const rpdCheckbox = document.querySelector('#pd-request'); - const pdaSelectorContainer = document.querySelector('#pda-selector'); - const pdaSelector = document.querySelector('#pd_program'); + const rpdCheckbox = document.querySelector('#pd-request') + const pdaSelectorContainer = document.querySelector('#pda-selector') + const pdaSelector = document.querySelector('#pd_program') const i18nStrings = JSON.parse(signupForm.dataset.i18n); - const emailLoadingIcon = $( - '.ol-signup-form__input--emailAddr .ol-signup-form__icon--loading', - ); - const usernameLoadingIcon = $( - '.ol-signup-form__input--username .ol-signup-form__icon--loading', - ); - const emailSuccessIcon = $( - '.ol-signup-form__input--emailAddr .ol-signup-form__icon--success', - ); - const usernameSuccessIcon = $( - '.ol-signup-form__input--username .ol-signup-form__icon--success', - ); + const emailLoadingIcon = $('.ol-signup-form__input--emailAddr .ol-signup-form__icon--loading'); + const usernameLoadingIcon = $('.ol-signup-form__input--username .ol-signup-form__icon--loading'); + const emailSuccessIcon = $('.ol-signup-form__input--emailAddr .ol-signup-form__icon--success'); + const usernameSuccessIcon = $('.ol-signup-form__input--username .ol-signup-form__icon--success'); // Keep the same with openlibrary/plugins/upstream/forms.py const VALID_EMAIL_RE = /^.*@.*\..*$/; @@ -32,34 +24,33 @@ export function initSignupForm() { function submitCreateAccountForm() { signupForm.submit(); } - window.submitCreateAccountForm = submitCreateAccountForm; + window.submitCreateAccountForm = submitCreateAccountForm // Checks whether reportValidity exists for cross-browser compatibility // Includes invalid input count to account for checks not covered by reportValidity - $(signupForm).on('submit', (e) => { + $(signupForm).on('submit', function(e) { e.preventDefault(); - validatePDSelection(); + validatePDSelection() const numInvalidInputs = signupForm.querySelectorAll('.invalid').length; - const isFormattingValid = - !signupForm.reportValidity || signupForm.reportValidity(); + const isFormattingValid = !signupForm.reportValidity || signupForm.reportValidity(); if (numInvalidInputs === 0 && isFormattingValid && window.grecaptcha) { $(submitBtn).prop('disabled', true).text(i18nStrings['loading_text']); window.grecaptcha.execute(); } }); - $('#username').on('keyup', function () { + $('#username').on('keyup', function(){ const value = $(this).val(); - $('#userUrl').addClass('darkgreen').text(value).css('font-weight', '700'); + $('#userUrl').addClass('darkgreen').text(value).css('font-weight','700'); }); /** - * Renders an error message for a given input in a given error div. - * - * @param {string} inputId The ID (incl #) of the input the error relates to - * @param {string} errorDiv The ID (incl #) of the div where the error msg will be rendered - * @param {string} errorMsg The error message text - */ + * Renders an error message for a given input in a given error div. + * + * @param {string} inputId The ID (incl #) of the input the error relates to + * @param {string} errorDiv The ID (incl #) of the div where the error msg will be rendered + * @param {string} errorMsg The error message text + */ function renderError(inputId, errorDiv, errorMsg) { $(inputId).addClass('invalid'); $(`label[for=${inputId.slice(1)}]`).addClass('invalid'); @@ -67,11 +58,11 @@ export function initSignupForm() { } /** - * Clears error styling and message for a given input and error div. - * - * @param {string} inputId The ID (incl #) of the input the error relates to - * @param {string} errorDiv The ID (incl #) of the div where the error msg is currently rendered - */ + * Clears error styling and message for a given input and error div. + * + * @param {string} inputId The ID (incl #) of the input the error relates to + * @param {string} errorDiv The ID (incl #) of the div where the error msg is currently rendered + */ function clearError(inputId, errorDiv) { $(inputId).removeClass('invalid'); $(`label[for=${inputId.slice(1)}]`).removeClass('invalid'); @@ -88,24 +79,13 @@ export function initSignupForm() { return; } - if ( - value_username.length < USERNAME_MINLENGTH || - value_username.length > USERNAME_MAXLENGTH - ) { - renderError( - '#username', - '#usernameMessage', - i18nStrings['username_length_err'], - ); + if (value_username.length < USERNAME_MINLENGTH || value_username.length > USERNAME_MAXLENGTH) { + renderError('#username', '#usernameMessage', i18nStrings['username_length_err']); return; } - if (!VALID_USERNAME_RE.test(value_username)) { - renderError( - '#username', - '#usernameMessage', - i18nStrings['username_char_err'], - ); + if (!(VALID_USERNAME_RE.test(value_username))) { + renderError('#username', '#usernameMessage', i18nStrings['username_char_err']); return; } @@ -115,7 +95,7 @@ export function initSignupForm() { url: '/account/validate', data: { username: value_username }, type: 'GET', - success: (errors) => { + success: function(errors) { usernameLoadingIcon.hide(); if (errors.username) { @@ -124,7 +104,7 @@ export function initSignupForm() { clearError('#username', '#usernameMessage'); usernameSuccessIcon.show(); } - }, + } }); } @@ -139,11 +119,7 @@ export function initSignupForm() { } if (!VALID_EMAIL_RE.test(value_email)) { - renderError( - '#emailAddr', - '#emailAddrMessage', - i18nStrings['invalid_email_format'], - ); + renderError('#emailAddr', '#emailAddrMessage', i18nStrings['invalid_email_format']); return; } @@ -153,7 +129,7 @@ export function initSignupForm() { url: '/account/validate', data: { email: value_email }, type: 'GET', - success: (errors) => { + success: function(errors) { emailLoadingIcon.hide(); if (errors.email) { @@ -162,7 +138,7 @@ export function initSignupForm() { clearError('#emailAddr', '#emailAddrMessage'); emailSuccessIcon.show(); } - }, + } }); } @@ -174,15 +150,8 @@ export function initSignupForm() { return; } - if ( - value_password.length < PASSWORD_MINLENGTH || - value_password.length > PASSWORD_MAXLENGTH - ) { - renderError( - '#password', - '#passwordMessage', - i18nStrings['password_length_err'], - ); + if (value_password.length < PASSWORD_MINLENGTH || value_password.length > PASSWORD_MAXLENGTH) { + renderError('#password', '#passwordMessage', i18nStrings['password_length_err']); return; } @@ -191,21 +160,17 @@ export function initSignupForm() { function validatePDSelection() { if (!rpdCheckbox.checked) { - clearError('#pd_program', '#pd_programMessage'); + clearError('#pd_program', '#pd_programMessage') pdaSelector.setAttribute('aria-invalid', 'false'); - return; + return } if (pdaSelector.value === '') { - renderError( - '#pd_program', - '#pd_programMessage', - i18nStrings['missing_pda_err'], - ); + renderError('#pd_program', '#pd_programMessage', i18nStrings['missing_pda_err']) pdaSelector.setAttribute('aria-invalid', 'true'); - return; + return } - clearError('#pd_program', '#pd_programMessage'); + clearError('#pd_program', '#pd_programMessage') pdaSelector.setAttribute('aria-invalid', 'false'); } @@ -223,51 +188,46 @@ export function initSignupForm() { } } - const $nonCheckboxInputs = $( - 'form[name=signup] input:not([type="checkbox"])', - ); + const $nonCheckboxInputs = $('form[name=signup] input:not([type="checkbox"])') // Validates input fields already marked as invalid on value change - $nonCheckboxInputs.on( - 'input', - debounce(function () { - if ($(this).hasClass('invalid')) { - validateInput(this); - } - }, 50), - ); + $nonCheckboxInputs.on('input', debounce(function(){ + if ($(this).hasClass('invalid')) { + validateInput(this); + } + }, 50)); // Validates all other input fields (i.e. not already marked as invalid) on blur - $nonCheckboxInputs.on('blur', function () { + $nonCheckboxInputs.on('blur', function() { if (!$(this).hasClass('invalid')) { validateInput(this); } }); // Validates the print-disability authority selection when the selection changes - $('form[name=signup] select').on('change', () => { - validatePDSelection(); - }); + $('form[name=signup] select').on('change', function() { + validatePDSelection() + }) function updateSelectorVisibility() { if (rpdCheckbox.checked) { - pdaSelectorContainer.classList.remove('hidden'); - rpdCheckbox.setAttribute('aria-expanded', 'true'); - pdaSelectorContainer.setAttribute('aria-hidden', 'false'); - pdaSelector.setAttribute('aria-required', 'true'); + pdaSelectorContainer.classList.remove('hidden') + rpdCheckbox.setAttribute('aria-expanded','true') + pdaSelectorContainer.setAttribute('aria-hidden','false') + pdaSelector.setAttribute('aria-required', 'true') } else { - pdaSelectorContainer.classList.add('hidden'); - rpdCheckbox.setAttribute('aria-expanded', 'false'); - pdaSelectorContainer.setAttribute('aria-hidden', 'true'); - pdaSelector.setAttribute('aria-required', 'false'); + pdaSelectorContainer.classList.add('hidden') + rpdCheckbox.setAttribute('aria-expanded','false') + pdaSelectorContainer.setAttribute('aria-hidden','true') + pdaSelector.setAttribute('aria-required', 'false') } } - rpdCheckbox.addEventListener('change', updateSelectorVisibility); + rpdCheckbox.addEventListener('change', updateSelectorVisibility) // On page reload, display PD program options and validate selection - updateSelectorVisibility(); - validatePDSelection(); + updateSelectorVisibility() + validatePDSelection() } export function initLoginForm() { @@ -276,5 +236,5 @@ export function initLoginForm() { loginForm.on('submit', () => { $('button[type=submit]').prop('disabled', true).text(loadingText); - }); + }) } diff --git a/openlibrary/plugins/openlibrary/js/star-ratings/index.js b/openlibrary/plugins/openlibrary/js/star-ratings/index.js index a57a2ade1f4..08356d3935e 100644 --- a/openlibrary/plugins/openlibrary/js/star-ratings/index.js +++ b/openlibrary/plugins/openlibrary/js/star-ratings/index.js @@ -1,41 +1,42 @@ +import { FadingToast } from '../Toast.js'; import { findDropperForWork } from '../my-books'; import { ReadingLogShelves } from '../my-books/MyBooksDropper/ReadingLogForms'; -import { FadingToast } from '../Toast.js'; -export function initRatingHandlers (ratingForms) { +export function initRatingHandlers(ratingForms) { for (const form of ratingForms) { - form.addEventListener('submit', (e) => { + form.addEventListener('submit', function(e) { handleRatingSubmission(e, form); - }); + }) } } -function handleRatingSubmission (event, form) { +function handleRatingSubmission(event, form) { event.preventDefault(); // Continue only if selected star is different from previous rating if (!event.submitter.classList.contains('star-selected')) { - // Construct form data object: + + // Construct form data object: const formData = new FormData(form); let rating; if (event.submitter.value) { - rating = Number(event.submitter.value); - formData.append('rating', event.submitter.value); + rating = Number(event.submitter.value) + formData.append('rating', event.submitter.value) } // Make AJAX call fetch(form.action, { method: 'POST', headers: { - 'content-type': 'application/x-www-form-urlencoded', + 'content-type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams(formData), + body: new URLSearchParams(formData) }) .then((response) => { if (response.status === 401) { throw new Error('You must be logged in to rate books'); } if (!response.ok) { - throw new Error('Ratings update failed'); + throw new Error('Ratings update failed') } // Update view to deselect all stars form.querySelectorAll('.star-selected').forEach((elem) => { @@ -43,32 +44,30 @@ function handleRatingSubmission (event, form) { if (elem.hasAttribute('property')) { elem.removeAttribute('property'); } - }); + }) const clearButton = form.querySelector('.star-messaging'); - if (rating) { - // A rating was added or updated + if (rating) { // A rating was added or updated // Update view to show patron's new star rating: clearButton.classList.remove('hidden'); form.querySelectorAll(`.star-${rating}`).forEach((elem) => { elem.classList.add('star-selected'); if (elem.tagName === 'LABEL') { - elem.setAttribute('property', 'ratingValue'); + elem.setAttribute('property', 'ratingValue') } - }); + }) // Find dropper that is associated with this star rating affordance: - const dropper = findDropperForWork(form.dataset.workKey); + const dropper = findDropperForWork(form.dataset.workKey) if (dropper) { - dropper.updateShelfDisplay(ReadingLogShelves.ALREADY_READ); + dropper.updateShelfDisplay(ReadingLogShelves.ALREADY_READ) } - } else { - // A rating was deleted + } else { // A rating was deleted clearButton.classList.add('hidden'); } }) .catch((error) => { new FadingToast(error.message).show(); - }); + }) } } diff --git a/openlibrary/plugins/openlibrary/js/stats/index.js b/openlibrary/plugins/openlibrary/js/stats/index.js index 4c6adac6c00..b68b22d021a 100644 --- a/openlibrary/plugins/openlibrary/js/stats/index.js +++ b/openlibrary/plugins/openlibrary/js/stats/index.js @@ -5,22 +5,23 @@ * @returns {Promise<void>} * @see /openlibrary/templates/admin/index.html */ -export async function initUniqueLoginCounts (containerElem) { - const loadingIndicator = containerElem.querySelector('.loadingIndicator'); - const i18nStrings = JSON.parse(containerElem.dataset.i18n); +export async function initUniqueLoginCounts(containerElem) { + const loadingIndicator = containerElem.querySelector('.loadingIndicator') + const i18nStrings = JSON.parse(containerElem.dataset.i18n) - const counts = await fetchCounts().then((resp) => { - if (resp.status !== 200) { - throw new Error(`Failed to fetch partials. Status code: ${resp.status}`); - } - return resp.json(); - }); + const counts = await fetchCounts() + .then((resp) => { + if (resp.status !== 200) { + throw new Error(`Failed to fetch partials. Status code: ${resp.status}`) + } + return resp.json() + }) - const countDiv = document.createElement('DIV'); - countDiv.innerHTML = i18nStrings.uniqueLoginsCopy; - const countSpan = countDiv.querySelector('.login-counts'); - countSpan.textContent = counts.loginCount; - loadingIndicator.replaceWith(countDiv); + const countDiv = document.createElement('DIV') + countDiv.innerHTML = i18nStrings.uniqueLoginsCopy + const countSpan = countDiv.querySelector('.login-counts') + countSpan.textContent = counts.loginCount + loadingIndicator.replaceWith(countDiv) } /** @@ -29,6 +30,6 @@ export async function initUniqueLoginCounts (containerElem) { * @returns {Promise<Response>} * @see `monthly_logins` class in /openlibrary/plugins/openlibrary/api.py */ -async function fetchCounts () { - return fetch('/api/monthly_logins.json'); +async function fetchCounts() { + return fetch('/api/monthly_logins.json') } diff --git a/openlibrary/plugins/openlibrary/js/tabs.js b/openlibrary/plugins/openlibrary/js/tabs.js index 74f9bfba4ec..83c7335af28 100644 --- a/openlibrary/plugins/openlibrary/js/tabs.js +++ b/openlibrary/plugins/openlibrary/js/tabs.js @@ -1,9 +1,9 @@ const TABS_OPTIONS = { fx: { opacity: 'toggle' } }; import 'jquery-ui/ui/widgets/tabs'; -export function initTabs ($node) { +export function initTabs($node) { $node.tabs(TABS_OPTIONS); - $node.filter('.autohash').on('tabsselect', (event, ui) => { + $node.filter('.autohash').on('tabsselect', function(event, ui) { document.location.hash = ui.panel.id; }); } diff --git a/openlibrary/plugins/openlibrary/js/team.js b/openlibrary/plugins/openlibrary/js/team.js index 315601e4415..ba1084a76df 100644 --- a/openlibrary/plugins/openlibrary/js/team.js +++ b/openlibrary/plugins/openlibrary/js/team.js @@ -1,6 +1,6 @@ import team from '../../../templates/about/team.json'; import { updateURLParameters } from './utils'; -export function initTeamFilter () { +export function initTeamFilter() { const currentYear = new Date().getFullYear().toString(); // Photos const default_profile_image = @@ -38,41 +38,41 @@ export function initTeamFilter () { // ********** STAFF ********** const staff = team.filter((person) => matchSubstring(person.roles, 'staff')); const staffEmeritus = staff.filter((person) => - matchSubstring(person.roles, 'emeritus'), + matchSubstring(person.roles, 'emeritus') ); const staffCurrent = staff.filter( - (person) => !matchSubstring(person.roles, 'emeritus'), + (person) => !matchSubstring(person.roles, 'emeritus') ); // ********** FELLOWS ********** const fellows = team.filter( (person) => matchSubstring(person.roles, 'fellow') && - !matchSubstring(person.roles, 'staff'), + !matchSubstring(person.roles, 'staff') ); const currentFellows = fellows.filter((person) => - matchSubstring(person.roles, currentYear), + matchSubstring(person.roles, currentYear) ); const pastFellows = fellows.filter( - (person) => !matchSubstring(person.roles, currentYear), + (person) => !matchSubstring(person.roles, currentYear) ); // ********** VOLUNTEERS ********** const volunteers = team.filter( (person) => matchSubstring(person.roles, 'volunteer') && - !matchSubstring(person.roles, 'fellow'), + !matchSubstring(person.roles, 'fellow') ); // *************************************** Selectors and eventListeners *************************************** const roleFilter = document.getElementById('role'); const departmentFilter = document.getElementById('department'); roleFilter.value = initialRole; - roleFilter.addEventListener('change', (e) => { + roleFilter.addEventListener('change', (e) => { filterTeam(e.target.value, departmentFilter.value); updateURLParameters({ role: e.target.value, - department: departmentFilter.value, + department: departmentFilter.value }); }); departmentFilter.value = initialDepartment; @@ -80,7 +80,7 @@ export function initTeamFilter () { filterTeam(roleFilter.value, e.target.value); updateURLParameters({ role: roleFilter.value, - department: departmentFilter.value, + department: departmentFilter.value }); }); const cardsContainer = document.querySelector('.teamCards_container'); @@ -167,7 +167,7 @@ export function initTeamFilter () { // memberRole, // memberDepartment, memberTitle, - descriptionLinks, + descriptionLinks ); teamCard.append(teamCardPhotoContainer, teamCardDescription); teamCardContainer.append(teamCard); @@ -211,35 +211,35 @@ export function initTeamFilter () { const filteredTeam = team.filter( (person) => matchSubstring(person.roles, role) && - matchSubstring(person.departments, department), + matchSubstring(person.departments, department) ); const staff = filteredTeam.filter((person) => - matchSubstring(person.roles, 'staff'), + matchSubstring(person.roles, 'staff') ); const staffEmeritus = staff.filter((person) => - matchSubstring(person.roles, 'emeritus'), + matchSubstring(person.roles, 'emeritus') ); const staffCurrent = staff.filter( - (person) => !matchSubstring(person.roles, 'emeritus'), + (person) => !matchSubstring(person.roles, 'emeritus') ); const fellows = filteredTeam.filter( (person) => matchSubstring(person.roles, 'fellow') && - !matchSubstring(person.roles, 'staff'), + !matchSubstring(person.roles, 'staff') ); const currentFellows = fellows.filter((person) => - matchSubstring(person.roles, currentYear), + matchSubstring(person.roles, currentYear) ); const pastFellows = fellows.filter( - (person) => !matchSubstring(person.roles, currentYear), + (person) => !matchSubstring(person.roles, currentYear) ); const volunteers = filteredTeam.filter( (person) => matchSubstring(person.roles, 'volunteer') && - !matchSubstring(person.roles, 'fellow'), + !matchSubstring(person.roles, 'fellow') ); staff.length && createSectionHeading('Staff'); @@ -259,17 +259,17 @@ export function initTeamFilter () { createSectionHeading(capitalize(role)); if (role === 'volunteer') { const filteredVolunteers = volunteers.filter((person) => - matchSubstring(person.departments, department), + matchSubstring(person.departments, department) ); filteredVolunteers.length !== 0 ? createCards(filteredVolunteers) : showError(); } else if (role === 'staff') { const filteredCurrentStaff = staffCurrent.filter((person) => - matchSubstring(person.departments, department), + matchSubstring(person.departments, department) ); const filteredStaffEmeritus = staffEmeritus.filter((person) => - matchSubstring(person.departments, department), + matchSubstring(person.departments, department) ); filteredCurrentStaff.length && createsubSection(filteredCurrentStaff, 'Current'); @@ -280,10 +280,10 @@ export function initTeamFilter () { showError(); } else { const filteredCurrentFellows = currentFellows.filter((person) => - matchSubstring(person.departments, department), + matchSubstring(person.departments, department) ); const filteredPastFellows = pastFellows.filter((person) => - matchSubstring(person.departments, department), + matchSubstring(person.departments, department) ); filteredCurrentFellows.length && createsubSection(filteredCurrentFellows, 'Current'); diff --git a/openlibrary/plugins/openlibrary/js/template.js b/openlibrary/plugins/openlibrary/js/template.js index d195edbdf54..ff1a151d308 100644 --- a/openlibrary/plugins/openlibrary/js/template.js +++ b/openlibrary/plugins/openlibrary/js/template.js @@ -2,18 +2,18 @@ // // Inspired by http://ejohn.org/blog/javascript-micro-templating/ -export default function Template (tmpl_text) { +export default function Template(tmpl_text) { var s = []; var js = ['var _p=[];', 'with(env) {']; var tokens, i, t, f, g; - function addCode (text) { + function addCode(text) { js.push(text); } - function addExpr (text) { + function addExpr(text) { js.push(`_p.push(htmlquote(${text}));`); } - function addText (text) { + function addText(text) { js.push(`_p.push(__s[${s.length}]);`); s.push(text); } @@ -21,12 +21,13 @@ export default function Template (tmpl_text) { tokens = tmpl_text.split('<%'); addText(tokens[0]); - for (i = 1; i < tokens.length; i++) { + for (i=1; i < tokens.length; i++) { t = tokens[i].split('%>'); if (t[0][0] === '=') { addExpr(t[0].substr(1)); - } else { + } + else { addCode(t[0]); } addText(t[1]); @@ -34,8 +35,10 @@ export default function Template (tmpl_text) { js.push('}', 'return _p.join(\'\');'); f = new Function(['__s', 'env'], js.join('\n')); - g = (env) => f(s, env); - g.toString = () => tmpl_text; - g.toCode = () => f.toString(); + g = function(env) { + return f(s, env); + }; + g.toString = function() { return tmpl_text; }; + g.toCode = function() { return f.toString(); }; return g; } diff --git a/openlibrary/plugins/openlibrary/js/type_changer.js b/openlibrary/plugins/openlibrary/js/type_changer.js index b46d88aa6bd..650cc6ae546 100644 --- a/openlibrary/plugins/openlibrary/js/type_changer.js +++ b/openlibrary/plugins/openlibrary/js/type_changer.js @@ -2,11 +2,11 @@ * Functionality for TypeChanger.html */ -export function initTypeChanger (elem) { +export function initTypeChanger(elem) { // /about?m=edit - where this code is run - function changeTemplate () { - // Change the template of the page based on the selected value + function changeTemplate() { + // Change the template of the page based on the selected value const searchParams = new URLSearchParams(window.location.search); const t = elem.value; searchParams.set('t', t); diff --git a/openlibrary/plugins/openlibrary/js/utils.js b/openlibrary/plugins/openlibrary/js/utils.js index fa0d0d6d144..423cdcfa4bd 100644 --- a/openlibrary/plugins/openlibrary/js/utils.js +++ b/openlibrary/plugins/openlibrary/js/utils.js @@ -5,13 +5,13 @@ See: https://github.com/internetarchive/openlibrary/pull/9180#issuecomment-21079 */ // closes active popup -export function closePopup () { +export function closePopup() { // Note we don't import colorbox here, since it's on the parent parent.jQuery.fn.colorbox.close(); } // used in templates/admin/imports.html -export function truncate (text, limit) { +export function truncate(text, limit) { if (text.length > limit) { return `${text.substr(0, limit)}...`; } else { @@ -20,10 +20,11 @@ export function truncate (text, limit) { } // used in openlibrary/templates/books/edit/excerpts.html -export function cond (predicate, true_value, false_value) { +export function cond(predicate, true_value, false_value) { if (predicate) { return true_value; - } else { + } + else { return false_value; } } @@ -33,18 +34,18 @@ export function cond (predicate, true_value, false_value) { * * @param {...HTMLElement} elements */ -export function removeChildren (...elements) { +export function removeChildren(...elements) { for (const elem of elements) { if (elem) { while (elem.firstChild) { - elem.removeChild(elem.firstChild); + elem.removeChild(elem.firstChild) } } } } // Function to add or update multiple query parameters -export function updateURLParameters (params) { +export function updateURLParameters(params) { // Get the current URL const url = new URL(window.location.href); @@ -63,16 +64,16 @@ export function updateURLParameters (params) { * Remove leading/trailing empty space on field deselect. * @param string a value for document.querySelectorAll() */ -export function trimInputValues (param) { +export function trimInputValues(param) { const inputs = document.querySelectorAll(param); - inputs.forEach((input) => { - input.addEventListener('blur', function () { + inputs.forEach(input => { + input.addEventListener('blur', function() { this.value = this.value.trim(); }); }); } -export function buildPartialsUrl (component, params = {}) { +export function buildPartialsUrl(component, params = {}) { const curUrl = new URL(window.location.href); const url = new URL(`${location.origin}/partials/${component}.json`); diff --git a/openlibrary/plugins/openlibrary/js/waitlist.js b/openlibrary/plugins/openlibrary/js/waitlist.js index 5b298f4c0e6..1fb8d17c5b1 100644 --- a/openlibrary/plugins/openlibrary/js/waitlist.js +++ b/openlibrary/plugins/openlibrary/js/waitlist.js @@ -5,15 +5,17 @@ import 'jquery-ui/ui/widgets/dialog'; * * @param {NodeList<HTMLElement>} leaveWaitlistLinks - NodeList of leave waitlist links */ -export function initLeaveWaitlist (leaveWaitlistLinks) { +export function initLeaveWaitlist(leaveWaitlistLinks) { for (const link of leaveWaitlistLinks) { link.addEventListener('click', () => { - const $link = $(link); + const $link = $(link) const title = $link.parents('tr').find('.book').text(); $('#leave-waitinglist-dialog strong').text(title); // We remove the hidden class here because otherwise it flashes for a moment on page load $('#leave-waitinglist-dialog').removeClass('hidden'); - $('#leave-waitinglist-dialog').data('origin', $link).dialog('open'); - }); + $('#leave-waitinglist-dialog') + .data('origin', $link) + .dialog('open'); + }) } } diff --git a/scripts/gh_scripts/new_pr_labeler.mjs b/scripts/gh_scripts/new_pr_labeler.mjs index 923effe7dc3..1c7d233449e 100644 --- a/scripts/gh_scripts/new_pr_labeler.mjs +++ b/scripts/gh_scripts/new_pr_labeler.mjs @@ -17,91 +17,81 @@ * 3. Updates PR, adding same priority label as issue, and assigning the lead (if * they are not also the author) */ -import { Octokit } from '@octokit/action'; +import { Octokit } from "@octokit/action"; -const CLOSES_REGEX = - /\b(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved):?\s+#(\d+)/i; +const CLOSES_REGEX = /\b(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved):?\s+#(\d+)/i -console.log('Script starting....'); -const octokit = new Octokit(); -await main(); -console.log('Script terminated....'); +console.log('Script starting....') +const octokit = new Octokit() +await main() +console.log('Script terminated....') async function main() { - // Parse and assign all command-line variables - const { fullRepoName, prAuthor, prNumber, prBody } = parseArgs(); + // Parse and assign all command-line variables + const {fullRepoName, prAuthor, prNumber, prBody} = parseArgs() - // Look for "Closes:" statement, storing the issue number (if present) - const issueNumber = findLinkedIssue(prBody); + // Look for "Closes:" statement, storing the issue number (if present) + const issueNumber = findLinkedIssue(prBody) - if (!issueNumber) { - console.log('No linked issue found for this pull request.'); - return; - } - - // Fetch the issue - const [repoOwner, repoName] = fullRepoName.split('/'); - const linkedIssue = await octokit.request( - 'GET /repos/{owner}/{repo}/issues/{issue_number}', - { - owner: repoOwner, - repo: repoName, - issue_number: issueNumber, - headers: { - 'X-GitHub-Api-Version': '2022-11-28', - }, - }, - ); - if (!linkedIssue) { - console.log(`An issue occurred while fetching issue #${issueNumber}`); - process.exit(1); - } - - // Check the issue's labels for the priority and lead - let leadName; - let priority; - for (const label of linkedIssue.data.labels) { - if (!leadName && label.name.startsWith('Lead: @')) { - leadName = label.name.split('@')[1]; + if (!issueNumber) { + console.log('No linked issue found for this pull request.') + return } - if (!priority && label.name.match(/Priority: [012]/)) { - priority = label.name; - } - } - - // Don't assign lead to PR if PR author is the issue lead - const assignLead = leadName && !(leadName === prAuthor); - // Update PR, adding assignee and priority label - if (assignLead) { - await octokit.request( - 'POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', - { + // Fetch the issue + const [repoOwner, repoName] = fullRepoName.split('/') + const linkedIssue = await octokit.request('GET /repos/{owner}/{repo}/issues/{issue_number}', { owner: repoOwner, repo: repoName, - issue_number: prNumber, - assignees: [leadName], + issue_number: issueNumber, headers: { - 'X-GitHub-Api-Version': '2022-11-28', - }, - }, - ); - } + 'X-GitHub-Api-Version': '2022-11-28' + } + }) + if (!linkedIssue) { + console.log(`An issue occurred while fetching issue #${issueNumber}`) + process.exit(1) + } - if (priority) { - await octokit.request( - 'POST /repos/{owner}/{repo}/issues/{issue_number}/labels', - { - owner: repoOwner, - repo: repoName, - issue_number: prNumber, - labels: [priority], - headers: { - 'X-GitHub-Api-Version': '2022-11-28', - }, - }, - ); - } + // Check the issue's labels for the priority and lead + let leadName + let priority + for (const label of linkedIssue.data.labels) { + if (!leadName && label.name.startsWith('Lead: @')) { + leadName = label.name.split('@')[1] + } + if (!priority && label.name.match(/Priority: [012]/)) { + priority = label.name + } + } + + // Don't assign lead to PR if PR author is the issue lead + const assignLead = leadName && !(leadName === prAuthor) + + // Update PR, adding assignee and priority label + if (assignLead) { + await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', { + owner: repoOwner, + repo: repoName, + issue_number: prNumber, + assignees: [leadName], + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }) + } + + if (priority) { + await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/labels', { + owner: repoOwner, + repo: repoName, + issue_number: prNumber, + labels: [priority], + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }) + } } /** @@ -112,17 +102,17 @@ async function main() { * @returns {Record<string, string>} */ function parseArgs() { - if (process.argv.length < 6) { - console.log('Unexpected number of arguments.'); - process.exit(1); - } - const prBody = process.argv.slice(5).join(' '); - return { - fullRepoName: process.argv[2], - prAuthor: process.argv[3], - prNumber: process.argv[4], - prBody: prBody, - }; + if (process.argv.length < 6) { + console.log('Unexpected number of arguments.') + process.exit(1) + } + const prBody = process.argv.slice(5).join(' ') + return { + fullRepoName: process.argv[2], + prAuthor: process.argv[3], + prNumber: process.argv[4], + prBody: prBody + } } /** @@ -135,6 +125,6 @@ function parseArgs() { * "Closes" statement is found. */ function findLinkedIssue(body) { - const matches = body.match(CLOSES_REGEX); - return matches?.length ? Number(matches[1]) : ''; + const matches = body.match(CLOSES_REGEX) + return matches?.length ? Number(matches[1]) : '' } diff --git a/scripts/solr_restarter/index.js b/scripts/solr_restarter/index.js index 6c620a15878..ff92e16ec95 100644 --- a/scripts/solr_restarter/index.js +++ b/scripts/solr_restarter/index.js @@ -10,144 +10,127 @@ */ const { execSync } = require('child_process'); + /** * @param {number} ms */ async function sleep(ms) { - return new Promise((res) => setTimeout(() => res(), ms)); + return new Promise(res => setTimeout(() => res(), ms)); } class SolrRestarter { - /** Don't restart twice in 10 minutes */ - MAX_RESTART_WIN = 10 * 60 * 1000; - /** Must be unhealthy for this many minutes to trigger a refresh */ - UNHEALTHY_DURATION = 2 * 60 * 1000; - /** Check every minute */ - CHECK_FREQ = 60 * 1000; - /** How many times we're aloud to try restarting without going healthy before giving up */ - MAX_RESTARTS = 3; - /** Number of restarts we've done without transitioning to healthy */ - restartsRun = 0; - /** timestamp in ms */ - lastRestart = 0; - /** @type {'healthy' | 'unhealthy'} */ - state = 'healthy'; - /** timestamp in ms */ - lastStateChange = 0; - /** Number of consecutive health checks that haven't failed or succeeded, but errored. */ - healthCheckErrorRun = 0; + /** Don't restart twice in 10 minutes */ + MAX_RESTART_WIN = 10*60*1000; + /** Must be unhealthy for this many minutes to trigger a refresh */ + UNHEALTHY_DURATION = 2*60*1000; + /** Check every minute */ + CHECK_FREQ = 60*1000; + /** How many times we're aloud to try restarting without going healthy before giving up */ + MAX_RESTARTS = 3; + /** Number of restarts we've done without transitioning to healthy */ + restartsRun = 0; + /** timestamp in ms */ + lastRestart = 0; + /** @type {'healthy' | 'unhealthy'} */ + state = 'healthy'; + /** timestamp in ms */ + lastStateChange = 0; + /** Number of consecutive health checks that haven't failed or succeeded, but errored. */ + healthCheckErrorRun = 0; - /** The URL to fetch in our healthcheck */ - TEST_URL = - process.env.TEST_URL ?? - 'http://openlibrary.org/search.json?q=hello&mode=everything&limit=0'; + /** The URL to fetch in our healthcheck */ + TEST_URL = process.env.TEST_URL ?? 'http://openlibrary.org/search.json?q=hello&mode=everything&limit=0'; - /** Whether we should send slack messages, or just console.log */ - SEND_SLACK_MESSAGE = process.env.SEND_SLACK_MESSAGE == 'true'; + /** Whether we should send slack messages, or just console.log */ + SEND_SLACK_MESSAGE = process.env.SEND_SLACK_MESSAGE == 'true'; - /** The containers to restart */ - CONTAINER_NAMES = process.env.CONTAINER_NAMES; + /** The containers to restart */ + CONTAINER_NAMES = process.env.CONTAINER_NAMES; - async checkHealth() { - console.log(this.TEST_URL); - const resp = await Promise.race([ - fetch(this.TEST_URL), - sleep(3000).then(() => 'timeout'), - ]); + async checkHealth() { + console.log(this.TEST_URL); + const resp = await Promise.race([fetch(this.TEST_URL), sleep(3000).then(() => 'timeout')]); - if (resp == 'timeout') return false; + if (resp == 'timeout') return false; - try { - const json = await resp.json(); - return !json.error && json.numFound; - } catch (err) { - throw `Invalid response: ${await resp.text()}`; + try { + const json = await resp.json(); + return !json.error && json.numFound; + } catch (err) { + throw `Invalid response: ${await resp.text()}`; + } } - } - /** - * @param {string} text - */ - async sendSlackMessage(text) { - if (this.SEND_SLACK_MESSAGE) { - await fetch('https://slack.com/api/chat.postMessage', { - method: 'POST', - headers: { - Authorization: `Bearer ${process.env.SLACK_TOKEN}`, - 'Content-Type': 'application/json; charset=utf-8', - }, - body: JSON.stringify({ - text, - channel: process.env.SLACK_CHANNEL_ID, - }), - }).then((r) => r.text()); - } else { - console.log(text); + /** + * @param {string} text + */ + async sendSlackMessage(text) { + if (this.SEND_SLACK_MESSAGE) { + await fetch('https://slack.com/api/chat.postMessage', { + method: 'POST', + headers: { + Authorization: `Bearer ${process.env.SLACK_TOKEN}`, + "Content-Type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ + text, + channel: process.env.SLACK_CHANNEL_ID, + }) + }).then(r => r.text()); + } else { + console.log(text); + } } - } - async loop() { - while (true) { - let isHealthy = true; - try { - isHealthy = await this.checkHealth(); - } catch (err) { - this.healthCheckErrorRun++; - if (this.healthCheckErrorRun > 3) { - // This is an unexpected error; likely means OL is down for other reasons. - await this.sendSlackMessage( - `Health check errored 3+ times with ${err}; skipping?`, - ); - } - await sleep(this.CHECK_FREQ); - continue; - } - this.healthCheckErrorRun = 0; - const newState = isHealthy ? 'healthy' : 'unhealthy'; - if (this.state != newState) { - this.lastStateChange = Date.now(); - } - this.state = newState; - console.log(`State: ${this.state}`); + async loop() { + while (true) { + let isHealthy = true; + try { + isHealthy = await this.checkHealth(); + } catch (err) { + this.healthCheckErrorRun++; + if (this.healthCheckErrorRun > 3) { + // This is an unexpected error; likely means OL is down for other reasons. + await this.sendSlackMessage(`Health check errored 3+ times with ${err}; skipping?`); + } + await sleep(this.CHECK_FREQ); + continue; + } + this.healthCheckErrorRun = 0; + const newState = isHealthy ? 'healthy' : 'unhealthy'; + if (this.state != newState) { + this.lastStateChange = Date.now(); + } + this.state = newState; + console.log(`State: ${this.state}`); - if (!isHealthy) { - if (Date.now() - this.lastStateChange > this.UNHEALTHY_DURATION) { - const canRestart = - Date.now() - this.lastRestart > this.MAX_RESTART_WIN; - if (canRestart) { - if (this.restartsRun >= this.MAX_RESTARTS) { - await this.sendSlackMessage( - "Hit max restarts. we're clearly not helping. Exiting.", - ); - throw new Error('MAX_RESTARTS exceeded'); + if (!isHealthy) { + if (Date.now() - this.lastStateChange > this.UNHEALTHY_DURATION) { + const canRestart = Date.now() - this.lastRestart > this.MAX_RESTART_WIN; + if (canRestart) { + if (this.restartsRun >= this.MAX_RESTARTS) { + await this.sendSlackMessage("Hit max restarts. we're clearly not helping. Exiting."); + throw new Error("MAX_RESTARTS exceeded"); + } + await this.sendSlackMessage(`solr-restarter: Unhealthy for a few minutes; Restarting solr`); + execSync(`docker restart ${this.CONTAINER_NAMES}`, { stdio: "inherit" }); + this.restartsRun++; + this.lastRestart = Date.now(); + } else { + console.log('Cannot restart; too soon since last restart'); + } + } + } else { + // Send a message if we recently tried to restart + if (this.restartsRun) { + await this.sendSlackMessage(`solr-restarter: solr state now ${this.state} :success-kid:`); + } + this.restartsRun = 0; } - await this.sendSlackMessage( - `solr-restarter: Unhealthy for a few minutes; Restarting solr`, - ); - execSync(`docker restart ${this.CONTAINER_NAMES}`, { - stdio: 'inherit', - }); - this.restartsRun++; - this.lastRestart = Date.now(); - } else { - console.log('Cannot restart; too soon since last restart'); - } - } - } else { - // Send a message if we recently tried to restart - if (this.restartsRun) { - await this.sendSlackMessage( - `solr-restarter: solr state now ${this.state} :success-kid:`, - ); + await sleep(this.CHECK_FREQ); } - this.restartsRun = 0; - } - await sleep(this.CHECK_FREQ); } - } } -process.on('unhandledRejection', (err) => { - throw err; -}); +process.on('unhandledRejection', err => { throw err }); new SolrRestarter().loop(); diff --git a/static/bookmarklets/import_webbook.js b/static/bookmarklets/import_webbook.js index a698c978e07..e1103664991 100644 --- a/static/bookmarklets/import_webbook.js +++ b/static/bookmarklets/import_webbook.js @@ -1,5 +1,5 @@ /* eslint-disable no-unused-labels */ -javascript:(async ()=> { +javascript:(async()=> { const url = prompt('Enter the book URL you want to import:'); if (!url) return; const promptText = `You are an expert book metadata librarian and research assistant. Your role is to search the web and collect accurate data to produce the most useful, factual, complete, and patron-oriented book page possible for the URL: diff --git a/stories/.storybook/main.js b/stories/.storybook/main.js index 87d9d43833a..36475e47e22 100644 --- a/stories/.storybook/main.js +++ b/stories/.storybook/main.js @@ -21,4 +21,4 @@ module.exports = { addons: [ '@storybook/addon-essentials' ], -}; +} diff --git a/stories/.storybook/preview.js b/stories/.storybook/preview.js index 85cd3d12a8e..2eaf63f831e 100644 --- a/stories/.storybook/preview.js +++ b/stories/.storybook/preview.js @@ -1,4 +1,4 @@ export const parameters = { actions: { argTypesRegex: '^on[A-Z].*' }, -}; +} diff --git a/stories/Button.stories.js b/stories/Button.stories.js index 562adb35229..9e77fab571d 100644 --- a/stories/Button.stories.js +++ b/stories/Button.stories.js @@ -2,68 +2,65 @@ import '../static/css/components/buttonCta.css'; import '../static/css/components/buttonCta--js.css'; export default { - title: 'Legacy/Button', + title: 'Legacy/Button' }; -const ButtonTemplate = (buttonType, text, badgeCount = null) => - `<div class="cta-btn${ButtonTypes[buttonType]}">${text}${badgeCount ? BadgeTemplate(badgeCount) : ''}</div>`; +const ButtonTemplate = (buttonType, text, badgeCount=null) => `<div class="cta-btn${ButtonTypes[buttonType]}">${text}${badgeCount ? BadgeTemplate(badgeCount) : ''}</div>`; -const BadgeTemplate = (badgeCount) => - ` <span class="cta-btn__badge">${badgeCount}</span>`; +const BadgeTemplate = (badgeCount) => ` <span class="cta-btn__badge">${badgeCount}</span>` const ButtonTypes = { default: '', unavailable: ' cta-btn--unavailable', available: ' cta-btn--available', - preview: ' cta-btn--shell cta-btn--preview', -}; + preview: ' cta-btn--shell cta-btn--preview' +} -export const CtaBtn = () => ButtonTemplate('default', 'Leave waitlist'); +export const CtaBtn = () => ButtonTemplate('default','Leave waitlist'); CtaBtn.parameters = { docs: { source: { - code: ButtonTemplate('default', 'Leave waitlist'), - }, - }, -}; + code: ButtonTemplate('default', 'Leave waitlist') + } + } +} -export const CtaBtnUnavailable = () => - ButtonTemplate('unavailable', 'Join waitlist'); +export const CtaBtnUnavailable = () => ButtonTemplate('unavailable','Join waitlist'); CtaBtnUnavailable.parameters = { docs: { source: { - code: ButtonTemplate('unavailable', 'Join waitlist'), - }, - }, -}; + code: ButtonTemplate('unavailable', 'Join waitlist') + } + } +} -export const CtaBtnAvailable = () => ButtonTemplate('available', 'Borrow'); +export const CtaBtnAvailable = () => ButtonTemplate('available','Borrow'); CtaBtnAvailable.parameters = { docs: { source: { - code: ButtonTemplate('available', 'Borrow'), - }, - }, -}; + code: ButtonTemplate('available', 'Borrow') + } + } +} -export const CtaBtnPreview = () => ButtonTemplate('preview', 'Preview'); +export const CtaBtnPreview = () => ButtonTemplate('preview','Preview'); CtaBtnPreview.parameters = { docs: { source: { - code: ButtonTemplate('preview', 'Preview'), - }, - }, -}; + code: ButtonTemplate('preview', 'Preview') + } + } +} export const CtaBtnWithBadge = () => - ButtonTemplate('unavailable', 'Join waiting list', 4); + ButtonTemplate('unavailable','Join waiting list',4); CtaBtnWithBadge.parameters = { docs: { source: { - code: ButtonTemplate('unavailable', 'Join waiting list', 4), - }, - }, -}; + code: ButtonTemplate('unavailable', 'Join waiting list', 4) + } + } +} export const CtaBtnGroup = () => `<div class="cta-button-group"> <a href="/borrow/ia/sevenhabitsofhi00cove?ref=ol" title="Borrow ebook from Internet Archive" id="borrow_ebook" data-ol-link-track="CTAClick|Borrow" class="cta-btn cta-btn--available">Borrow</a> diff --git a/tests/unit/js/Browser.test.js b/tests/unit/js/Browser.test.js index f8defa24145..ac5e84f51ed 100644 --- a/tests/unit/js/Browser.test.js +++ b/tests/unit/js/Browser.test.js @@ -1,7 +1,4 @@ -import { - getJsonFromUrl, - removeURLParameter, -} from '../../../openlibrary/plugins/openlibrary/js/Browser'; +import { removeURLParameter, getJsonFromUrl } from '../../../openlibrary/plugins/openlibrary/js/Browser'; describe('removeURLParameter', () => { const fn = removeURLParameter; @@ -12,28 +9,20 @@ describe('removeURLParameter', () => { test('URL with the given parameter', () => { expect(fn('http://foo.com?x=3', 'x')).toBe('http://foo.com'); - expect(fn('http://foo.com?x=3&y=4&z=5', 'x')).toBe( - 'http://foo.com?y=4&z=5', - ); - expect(fn('http://foo.com?x=3&y=4&z=5', 'y')).toBe( - 'http://foo.com?x=3&z=5', - ); - expect(fn('http://foo.com?x=3&y=4&z=5', 'z')).toBe( - 'http://foo.com?x=3&y=4', - ); + expect(fn('http://foo.com?x=3&y=4&z=5', 'x')).toBe('http://foo.com?y=4&z=5'); + expect(fn('http://foo.com?x=3&y=4&z=5', 'y')).toBe('http://foo.com?x=3&z=5'); + expect(fn('http://foo.com?x=3&y=4&z=5', 'z')).toBe('http://foo.com?x=3&y=4'); }); test('URL without the given parameter', () => { expect(fn('http://foo.com?x=3', 'y')).toBe('http://foo.com?x=3'); - expect(fn('http://foo.com?x=3&y=4&z=5', 'w')).toBe( - 'http://foo.com?x=3&y=4&z=5', - ); + expect(fn('http://foo.com?x=3&y=4&z=5', 'w')).toBe('http://foo.com?x=3&y=4&z=5'); }); test('URL with multiple occurences of param', () => { expect(fn('http://foo.com?x=3&x=4&x=5', 'x')).toBe('http://foo.com'); expect(fn('http://foo.com?x=3&x=4&z=5', 'x')).toBe('http://foo.com?z=5'); - }); + }) }); describe('getJsonFromUrl', () => { @@ -45,15 +34,15 @@ describe('getJsonFromUrl', () => { }); test('Handles normal params', () => { - expect(fn('?hello=world')).toEqual({ hello: 'world' }); - expect(fn('?x=3&y=4&z=5')).toEqual({ x: '3', y: '4', z: '5' }); + expect(fn('?hello=world')).toEqual({hello: 'world'}); + expect(fn('?x=3&y=4&z=5')).toEqual({x: '3', y: '4', z: '5'}); }); test('Decodes parameter values', () => { - expect(fn('?q=foo%20bar')).toEqual({ q: 'foo bar' }); + expect(fn('?q=foo%20bar')).toEqual({q: 'foo bar'}); }); test('Parameters override each other', () => { - expect(fn('?x=1&x=2&x=3')).toEqual({ x: '3' }); + expect(fn('?x=1&x=2&x=3')).toEqual({x: '3'}); }); }); diff --git a/tests/unit/js/SearchBar.test.js b/tests/unit/js/SearchBar.test.js index 2f2e13259e9..e52be87166a 100644 --- a/tests/unit/js/SearchBar.test.js +++ b/tests/unit/js/SearchBar.test.js @@ -1,7 +1,7 @@ import sinon from 'sinon'; -import * as nonjquery_utils from '../../../openlibrary/plugins/openlibrary/js/nonjquery_utils.js'; import { SearchBar } from '../../../openlibrary/plugins/openlibrary/js/SearchBar'; import * as SearchUtils from '../../../openlibrary/plugins/openlibrary/js/SearchUtils'; +import * as nonjquery_utils from '../../../openlibrary/plugins/openlibrary/js/nonjquery_utils.js'; describe('SearchBar', () => { const DUMMY_COMPONENT_HTML = ` @@ -13,13 +13,11 @@ describe('SearchBar', () => { </div>`; describe('initFromUrlParams', () => { - /** @type {SearchBar} */ + /** @type {SearchBar} */ let sb; beforeEach(() => { sb = new SearchBar($(DUMMY_COMPONENT_HTML)); - sinon - .stub(sb, 'getCurUrl') - .returns(new URL('https://openlibrary.org/search')); + sinon.stub(sb, 'getCurUrl').returns(new URL('https://openlibrary.org/search')); }); afterEach(() => localStorage.clear()); test('Does not throw on empty params', () => { @@ -28,31 +26,31 @@ describe('SearchBar', () => { test('Updates facet from params', () => { expect(sb.facet.read()).not.toBe('title'); - sb.initFromUrlParams({ facet: 'title' }); + sb.initFromUrlParams({facet: 'title'}); expect(sb.facet.read()).toBe('title'); }); test('Ignore invalid facets', () => { const originalValue = sb.facet.read(); - sb.initFromUrlParams({ facet: 'spam' }); + sb.initFromUrlParams({facet: 'spam'}); expect(sb.facet.read()).toBe(originalValue); }); test('Sets input value from q param', () => { - sb.initFromUrlParams({ q: 'Harry Potter' }); + sb.initFromUrlParams({q: 'Harry Potter'}); expect(sb.$input.val()).toBe('Harry Potter'); }); test('Remove title prefix from q param', () => { - sb.initFromUrlParams({ q: 'title:"Harry Potter"', facet: 'title' }); + sb.initFromUrlParams({q: 'title:"Harry Potter"', facet: 'title'}); expect(sb.$input.val()).toBe('Harry Potter'); - sb.initFromUrlParams({ q: 'title: "Harry"', facet: 'title' }); + sb.initFromUrlParams({q: 'title: "Harry"', facet: 'title'}); expect(sb.$input.val()).toBe('Harry'); }); test('Persists value in url param', () => { expect(localStorage.getItem('facet')).not.toBe('title'); - sb.initFromUrlParams({ facet: 'title' }); + sb.initFromUrlParams({facet: 'title'}); expect(localStorage.getItem('facet')).toBe('title'); }); }); @@ -65,7 +63,7 @@ describe('SearchBar', () => { afterEach(() => localStorage.clear()); test('Queries are marshalled before submit for titles', () => { - sb.initFromUrlParams({ facet: 'title' }); + sb.initFromUrlParams({facet: 'title'}); const spy = sinon.spy(SearchBar, 'marshalBookSearchQuery'); sb.submitForm(); expect(spy.callCount).toBe(1); @@ -73,23 +71,23 @@ describe('SearchBar', () => { }); test('Form action is updated on submit', () => { - sb.initFromUrlParams({ facet: 'title' }); + sb.initFromUrlParams({facet: 'title'}); const originalAction = sb.$form[0].action; sb.submitForm(); expect(sb.$form[0].action).not.toBe(originalAction); }); test('Special inputs are added to the form on submit', () => { - const spy = sinon.spy(SearchUtils, 'addModeInputsToForm'); + const spy = sinon.spy(SearchUtils, 'addModeInputsToForm') sb.submitForm(); expect(spy.callCount).toBe(1); }); }); describe('toggleCollapsibleModeForSmallScreens', () => { - /** @type {SearchBar?} */ + /** @type {SearchBar?} */ let sb; - beforeEach(() => (sb = new SearchBar($(DUMMY_COMPONENT_HTML)))); + beforeEach(() => sb = new SearchBar($(DUMMY_COMPONENT_HTML))); afterEach(() => localStorage.clear()); test('Only enters collapsible mode if not already there', () => { @@ -147,9 +145,7 @@ describe('SearchBar', () => { test('Advanced facet triggers redirect', () => { const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); const navigateToStub = sandbox.stub(sb, 'navigateTo'); - const event = Object.assign(new $.Event(), { - target: { value: 'advanced' }, - }); + const event = Object.assign(new $.Event(), { target: { value: 'advanced' } }); sb.handleFacetSelectChange(event); expect(navigateToStub.callCount).toBe(1); expect(navigateToStub.args[0]).toEqual(['/advancedsearch']); @@ -158,7 +154,7 @@ describe('SearchBar', () => { for (const facet of ['title', 'author', 'all']) { test(`Facet "${facet}" searches tigger autocomplete`, () => { // Stub debounce to avoid have to manipulate time (!) - sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); + sandbox.stub(nonjquery_utils, 'debounce').callsFake(fn => fn); const sb = new SearchBar($(DUMMY_COMPONENT_HTML), { facet }); const getJSONStub = sandbox.stub($, 'getJSON'); @@ -170,8 +166,8 @@ describe('SearchBar', () => { test('Title searches tigger autocomplete even if containing title: prefix', () => { // Stub debounce to avoid have to manipulate time (!) - sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); - const sb = new SearchBar($(DUMMY_COMPONENT_HTML), { facet: 'title' }); + sandbox.stub(nonjquery_utils, 'debounce').callsFake(fn => fn); + const sb = new SearchBar($(DUMMY_COMPONENT_HTML), {facet: 'title'}); const getJSONStub = sandbox.stub($, 'getJSON'); sb.$input.val('title:"Harry"'); sb.$input.triggerHandler('focus'); @@ -180,8 +176,8 @@ describe('SearchBar', () => { test('Focussing on input when empty does not trigger autocomplete', () => { // Stub debounce to avoid have to manipulate time (!) - sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); - const sb = new SearchBar($(DUMMY_COMPONENT_HTML), { facet: 'title' }); + sandbox.stub(nonjquery_utils, 'debounce').callsFake(fn => fn); + const sb = new SearchBar($(DUMMY_COMPONENT_HTML), {facet: 'title'}); const getJSONStub = sandbox.stub($, 'getJSON'); sb.$input.val(''); sb.$input.triggerHandler('focus'); @@ -191,7 +187,7 @@ describe('SearchBar', () => { for (const facet of ['lists', 'subject', 'text']) { test(`Facet "${facet}" does not tigger autocomplete`, () => { // Stub debounce to avoid have to manipulate time (!) - sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); + sandbox.stub(nonjquery_utils, 'debounce').callsFake(fn => fn); const sb = new SearchBar($(DUMMY_COMPONENT_HTML)); const getJSONStub = sandbox.stub($, 'getJSON'); @@ -217,26 +213,20 @@ describe('SearchBar', () => { }); test('Autocomplete rendering behavior depends on existing results', () => { - sandbox.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); + sandbox.stub(nonjquery_utils, 'debounce').callsFake(fn => fn); const sb = new SearchBar($(DUMMY_COMPONENT_HTML), { facet: 'title' }); const renderSpy = sandbox.spy(sb, 'renderAutocompletionResults'); // Should render when results are empty sb.$input.triggerHandler('focus'); - expect(renderSpy.callCount).toBe( - 1, - 'Should render when no results exist', - ); + expect(renderSpy.callCount).toBe(1, 'Should render when no results exist'); renderSpy.resetHistory(); // Should not render when results exist sb.$results.append('<li>Some result</li>'); sb.$input.triggerHandler('focus'); - expect(renderSpy.callCount).toBe( - 0, - 'Should not render when results exist', - ); + expect(renderSpy.callCount).toBe(0, 'Should not render when results exist'); }); test('Tabbing from search result focuses search submit button and clears results', () => { @@ -258,22 +248,13 @@ describe('SearchBar', () => { $resultItem.trigger(tabEvent); // Verify clearAutocompletionResults was called - expect(clearResultsSpy.callCount).toBe( - 1, - 'Should clear autocomplete results', - ); + expect(clearResultsSpy.callCount).toBe(1, 'Should clear autocomplete results'); // Verify search submit was focused - expect(focusSpy.calledWith('focus')).toBe( - true, - 'Should focus search submit button', - ); + expect(focusSpy.calledWith('focus')).toBe(true, 'Should focus search submit button'); // Verify event default was prevented - expect(tabEvent.isDefaultPrevented()).toBe( - true, - 'Should prevent default tab behavior', - ); + expect(tabEvent.isDefaultPrevented()).toBe(true, 'Should prevent default tab behavior'); }); test('Shift+tabbing from search result focuses facet select and clears results', () => { @@ -295,22 +276,13 @@ describe('SearchBar', () => { $resultItem.trigger(shiftTabEvent); // Verify clearAutocompletionResults was called - expect(clearResultsSpy.callCount).toBe( - 1, - 'Should clear autocomplete results', - ); + expect(clearResultsSpy.callCount).toBe(1, 'Should clear autocomplete results'); // Verify facet select was focused - expect(focusSpy.calledWith('focus')).toBe( - true, - 'Should focus facet select', - ); + expect(focusSpy.calledWith('focus')).toBe(true, 'Should focus facet select'); // Verify event default was prevented - expect(shiftTabEvent.isDefaultPrevented()).toBe( - true, - 'Should prevent default tab behavior', - ); + expect(shiftTabEvent.isDefaultPrevented()).toBe(true, 'Should prevent default tab behavior'); }); }); }); diff --git a/tests/unit/js/SearchUtils.test.js b/tests/unit/js/SearchUtils.test.js index d91f0c9ae1e..ae5924efa07 100644 --- a/tests/unit/js/SearchUtils.test.js +++ b/tests/unit/js/SearchUtils.test.js @@ -32,7 +32,7 @@ describe('PersistentValue', () => { localStorage.setItem('foo', 'anything'); const pv = new PV('foo', { default: 'blue', - initValidation: () => false, + initValidation: () => false }); expect(pv.read()).toBe('blue'); }); @@ -40,7 +40,7 @@ describe('PersistentValue', () => { test('Writes null on invalid init', () => { localStorage.setItem('foo', 'anything'); const pv = new PV('foo', { - initValidation: () => false, + initValidation: () => false }); expect(pv.read()).toBe(null); }); @@ -49,7 +49,7 @@ describe('PersistentValue', () => { localStorage.setItem('foo', 'anything'); const pv = new PV('foo', { default: 'blue', - initValidation: () => true, + initValidation: () => true }); expect(pv.read()).toBe('anything'); }); @@ -57,7 +57,7 @@ describe('PersistentValue', () => { test('Writing applies transformation', () => { localStorage.setItem('foo', 'blue'); const pv = new PV('foo', { - writeTransformation: (newVal, oldVal) => oldVal + newVal, + writeTransformation: (newVal, oldVal) => oldVal + newVal }); pv.write('green'); expect(pv.read()).toBe('bluegreen'); diff --git a/tests/unit/js/SelectionManager.test.js b/tests/unit/js/SelectionManager.test.js index d224e12b7a0..6331ed9d5f4 100644 --- a/tests/unit/js/SelectionManager.test.js +++ b/tests/unit/js/SelectionManager.test.js @@ -1,6 +1,6 @@ import SelectionManager from '../../../openlibrary/plugins/openlibrary/js/ile/utils/SelectionManager/SelectionManager.js'; -function createTestElementsForProcessClick () { +function createTestElementsForProcessClick() { const listItem = document.createElement('li'); listItem.classList.add('searchResultItem', 'ile-selectable'); @@ -15,10 +15,10 @@ function createTestElementsForProcessClick () { listItem.appendChild(bookTitle); - return { listItem, link }; + return {listItem,link}; } -function setupSelectionManager () { +function setupSelectionManager() { const sm = new SelectionManager(null, '/search'); sm.ile = { $statusImages: { append: jest.fn() } }; sm.selectedItems = { work: [] }; @@ -52,9 +52,10 @@ describe('SelectionManager', () => { }); }); + test('processClick - clicking on a link or button', () => { const sm = setupSelectionManager(); - const { listItem, link } = createTestElementsForProcessClick(); + const { listItem,link } = createTestElementsForProcessClick(); link.addEventListener('click', () => { sm.processClick({ target: link, currentTarget: listItem }); diff --git a/tests/unit/js/autocomplete.test.js b/tests/unit/js/autocomplete.test.js index 48cc2d72703..65774407426 100644 --- a/tests/unit/js/autocomplete.test.js +++ b/tests/unit/js/autocomplete.test.js @@ -1,38 +1,45 @@ -import { - highlight, - mapApiResultsToAutocompleteSuggestions, -} from '../../../openlibrary/plugins/openlibrary/js/autocomplete.js'; +import { highlight, mapApiResultsToAutocompleteSuggestions } from '../../../openlibrary/plugins/openlibrary/js/autocomplete.js'; describe('highlight', () => { + test('Highlights terms with strong tag', () => { [ - ['Jon Robson', 'Jon', '<strong>Jon</strong> Robson'], - ['No match', 'abcde', 'No match'], + [ + 'Jon Robson', + 'Jon', + '<strong>Jon</strong> Robson' + ], + [ + 'No match', + 'abcde', + 'No match' + ] ].forEach((test) => { const highlightedText = highlight(test[0], test[1]); expect(highlightedText).toStrictEqual(test[2]); }); - }); + }) }); + describe('mapApiResultsToAutocompleteSuggestions', () => { test('API results are converted to suggestions using label function', () => { const suggestions = mapApiResultsToAutocompleteSuggestions( [ { key: 1, - name: 'Test', - }, + name: 'Test' + } ], - (r) => r.name, + (r) => r.name ); expect(suggestions).toStrictEqual([ { key: 1, label: 'Test', - value: 'Test', - }, + value: 'Test' + } ]); }); @@ -41,17 +48,19 @@ describe('mapApiResultsToAutocompleteSuggestions', () => { [ { key: 1, - name: 'Test', - }, + name: 'Test' + } ], (r) => r.name, - 'Add new item', + 'Add new item' ); - expect(suggestions[1]).toStrictEqual({ - key: '__new__', - label: 'Add new item', - value: 'Add new item', - }); - }); + expect(suggestions[1]).toStrictEqual( + { + key: '__new__', + label: 'Add new item', + value: 'Add new item' + } + ); + }) }); diff --git a/tests/unit/js/droppers.test.js b/tests/unit/js/droppers.test.js index 33433911e1e..620ef58e70d 100644 --- a/tests/unit/js/droppers.test.js +++ b/tests/unit/js/droppers.test.js @@ -1,21 +1,14 @@ import sinon from 'sinon'; -import { - initDroppers, - initGenericDroppers, -} from '../../../openlibrary/plugins/openlibrary/js/dropper'; -import { Dropper } from '../../../openlibrary/plugins/openlibrary/js/dropper/Dropper'; +import { initDroppers, initGenericDroppers } from '../../../openlibrary/plugins/openlibrary/js/dropper'; +import { Dropper } from '../../../openlibrary/plugins/openlibrary/js/dropper/Dropper' +import { legacyBookDropperMarkup, openDropperMarkup, closedDropperMarkup, disabledDropperMarkup } from './sample-html/dropper-test-data' import * as nonjquery_utils from '../../../openlibrary/plugins/openlibrary/js/nonjquery_utils.js'; -import { - closedDropperMarkup, - disabledDropperMarkup, - legacyBookDropperMarkup, - openDropperMarkup, -} from './sample-html/dropper-test-data'; + describe('initDroppers', () => { test('dropdown changes arrow direction on click', () => { - // Stub debounce to avoid have to manipulate time (!) - const stub = sinon.stub(nonjquery_utils, 'debounce').callsFake((fn) => fn); + // Stub debounce to avoid have to manipulate time (!) + const stub = sinon.stub(nonjquery_utils, 'debounce').callsFake(fn => fn); $(document.body).html(legacyBookDropperMarkup); const $dropclick = $('.dropclick'); @@ -36,317 +29,277 @@ describe('initDroppers', () => { describe('Generic Droppers', () => { test('Clicking dropclick element toggles the dropper', () => { - // Setup - document.body.innerHTML = closedDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const dropper = new Dropper(wrapper); - dropper.initialize(); + // Setup + document.body.innerHTML = closedDropperMarkup + const wrapper = document.querySelector('.generic-dropper-wrapper') + const dropper = new Dropper(wrapper) + dropper.initialize() - const dropClick = document.querySelector('.generic-dropper__dropclick'); - const arrow = dropClick.querySelector('.arrow'); + const dropClick = document.querySelector('.generic-dropper__dropclick') + const arrow = dropClick.querySelector('.arrow') // Dropper should be closed at the start - expect(arrow.classList.contains('up')).toBe(false); - expect(wrapper.classList.contains('dropper-wrapper--active')).toBe(false); + expect(arrow.classList.contains('up')).toBe(false) + expect(wrapper.classList.contains('dropper-wrapper--active')).toBe(false) // Open dropper - dropClick.click(); - expect(arrow.classList.contains('up')).toBe(true); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - true, - ); + dropClick.click() + expect(arrow.classList.contains('up')).toBe(true) + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true) // Close dropper - dropClick.click(); - expect(arrow.classList.contains('up')).toBe(false); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - }); + dropClick.click() + expect(arrow.classList.contains('up')).toBe(false) + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) + }) test('Opened droppers close if they are not the target of a click', () => { - // Setup - document.body.innerHTML = openDropperMarkup.concat( - openDropperMarkup, - openDropperMarkup, - ); - const wrappers = document.querySelectorAll('.generic-dropper-wrapper'); - initGenericDroppers(wrappers); + // Setup + document.body.innerHTML = openDropperMarkup.concat(openDropperMarkup, openDropperMarkup) + const wrappers = document.querySelectorAll('.generic-dropper-wrapper') + initGenericDroppers(wrappers) + // Ensure that all three droppers are open - expect(wrappers.length).toBe(3); + expect(wrappers.length).toBe(3) for (const wrapper of wrappers) { - const arrow = wrapper.querySelector('.arrow'); - expect( - wrapper.classList.contains('generic-dropper-wrapper--active'), - ).toBe(true); - expect(arrow.classList.contains('up')).toBe(true); + const arrow = wrapper.querySelector('.arrow') + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true) + expect(arrow.classList.contains('up')).toBe(true) } // After clicking the dropdown content of the first dropper: - const dropdownContent = wrappers[0].querySelector( - '.generic-dropper__dropdown', - ); - dropdownContent.click(); + const dropdownContent = wrappers[0].querySelector('.generic-dropper__dropdown') + dropdownContent.click() // First dropper should be open - expect( - wrappers[0].classList.contains('generic-dropper-wrapper--active'), - ).toBe(true); - expect(wrappers[0].querySelector('.arrow').classList.contains('up')).toBe( - true, - ); + expect(wrappers[0].classList.contains('generic-dropper-wrapper--active')).toBe(true) + expect(wrappers[0].querySelector('.arrow').classList.contains('up')).toBe(true) // ...while other droppers should be closed for (let i = 1; i < wrappers.length; ++i) { - const arrow = wrappers[i].querySelector('.arrow'); - expect( - wrappers[i].classList.contains('generic-dropper-wrapper--active'), - ).toBe(false); - expect(arrow.classList.contains('up')).toBe(false); + const arrow = wrappers[i].querySelector('.arrow') + expect(wrappers[i].classList.contains('generic-dropper-wrapper--active')).toBe(false) + expect(arrow.classList.contains('up')).toBe(false) } - }); + }) test('Disabled droppers cannot be opened nor closed', () => { - document.body.innerHTML = disabledDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const dropper = new Dropper(wrapper); - dropper.initialize(); - const dropclick = wrapper.querySelector('.generic-dropper__dropclick'); - const arrow = wrapper.querySelector('.arrow'); + document.body.innerHTML = disabledDropperMarkup + const wrapper = document.querySelector('.generic-dropper-wrapper') + const dropper = new Dropper(wrapper) + dropper.initialize() + const dropclick = wrapper.querySelector('.generic-dropper__dropclick') + const arrow = wrapper.querySelector('.arrow') // Sanity checks - expect(wrapper.classList.contains('generic-dropper--disabled')).toBe(true); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - expect(arrow.classList.contains('up')).toBe(false); + expect(wrapper.classList.contains('generic-dropper--disabled')).toBe(true) + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) + expect(arrow.classList.contains('up')).toBe(false) // Click on the dropclick: - dropclick.click(); + dropclick.click() - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - expect(arrow.classList.contains('up')).toBe(false); - }); -}); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) + expect(arrow.classList.contains('up')).toBe(false) + }) +}) describe('Dropper.js class', () => { test('Dropper references set correctly on instantiation', () => { - document.body.innerHTML = closedDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const dropper = new Dropper(wrapper); + document.body.innerHTML = closedDropperMarkup + const wrapper = document.querySelector('.generic-dropper-wrapper') + const dropper = new Dropper(wrapper) // Reference to component root stored - expect(dropper.dropper === wrapper).toBe(true); + expect(dropper.dropper === wrapper).toBe(true) // Dropclick reference stored - const dropClick = wrapper.querySelector('.generic-dropper__dropclick'); - expect(dropper.dropClick === dropClick).toBe(true); + const dropClick = wrapper.querySelector('.generic-dropper__dropclick') + expect(dropper.dropClick === dropClick).toBe(true) // Dropper is closed - expect(dropper.isDropperOpen).toBe(false); + expect(dropper.isDropperOpen).toBe(false) // This dropper is not disabled - expect(dropper.isDropperDisabled).toBe(false); - }); + expect(dropper.isDropperDisabled).toBe(false) + }) it('is not functional until initialize() is called', () => { - document.body.innerHTML = closedDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const dropClick = wrapper.querySelector('.generic-dropper__dropclick'); - const arrow = wrapper.querySelector('.arrow'); + document.body.innerHTML = closedDropperMarkup + const wrapper = document.querySelector('.generic-dropper-wrapper') + const dropClick = wrapper.querySelector('.generic-dropper__dropclick') + const arrow = wrapper.querySelector('.arrow') - const dropper = new Dropper(wrapper); - const spy = jest.spyOn(dropper, 'toggleDropper'); + const dropper = new Dropper(wrapper) + const spy = jest.spyOn(dropper, 'toggleDropper') // Dropper should be closed initially: - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - expect(arrow.classList.contains('up')).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) + expect(arrow.classList.contains('up')).toBe(false) // Clicking should not do anything yet: - dropClick.click(); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - expect(arrow.classList.contains('up')).toBe(false); - expect(spy).not.toHaveBeenCalled(); + dropClick.click() + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) + expect(arrow.classList.contains('up')).toBe(false) + expect(spy).not.toHaveBeenCalled() // Test again after initialization: - dropper.initialize(); - dropClick.click(); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - true, - ); - expect(arrow.classList.contains('up')).toBe(true); - expect(spy).toHaveBeenCalled(); - - jest.restoreAllMocks(); - }); + dropper.initialize() + dropClick.click() + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true) + expect(arrow.classList.contains('up')).toBe(true) + expect(spy).toHaveBeenCalled() + + jest.restoreAllMocks() + }) it('can be closed if not disabled', () => { - document.body.innerHTML = openDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const arrow = wrapper.querySelector('.arrow'); + document.body.innerHTML = openDropperMarkup + const wrapper = document.querySelector('.generic-dropper-wrapper') + const arrow = wrapper.querySelector('.arrow') - const dropper = new Dropper(wrapper); - dropper.initialize(); + const dropper = new Dropper(wrapper) + dropper.initialize() // Check initial state: - expect(dropper.isDropperDisabled).toBe(false); - expect(dropper.isDropperOpen).toBe(true); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - true, - ); - expect(arrow.classList.contains('up')).toBe(true); + expect(dropper.isDropperDisabled).toBe(false) + expect(dropper.isDropperOpen).toBe(true) + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true) + expect(arrow.classList.contains('up')).toBe(true) // Check again after closing: - dropper.closeDropper(); - expect(dropper.isDropperOpen).toBe(false); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - expect(arrow.classList.contains('up')).toBe(false); - }); + dropper.closeDropper() + expect(dropper.isDropperOpen).toBe(false) + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) + expect(arrow.classList.contains('up')).toBe(false) + }) it('can be toggled if not disabled', () => { - document.body.innerHTML = closedDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const arrow = wrapper.querySelector('.arrow'); + document.body.innerHTML = closedDropperMarkup + const wrapper = document.querySelector('.generic-dropper-wrapper') + const arrow = wrapper.querySelector('.arrow') - const dropper = new Dropper(wrapper); - dropper.initialize(); + const dropper = new Dropper(wrapper) + dropper.initialize() // Check initial state: - expect(dropper.isDropperDisabled).toBe(false); - expect(dropper.isDropperOpen).toBe(false); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - expect(arrow.classList.contains('up')).toBe(false); + expect(dropper.isDropperDisabled).toBe(false) + expect(dropper.isDropperOpen).toBe(false) + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) + expect(arrow.classList.contains('up')).toBe(false) // Check after toggling open: - dropper.toggleDropper(); - expect(dropper.isDropperOpen).toBe(true); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - true, - ); - expect(arrow.classList.contains('up')).toBe(true); + dropper.toggleDropper() + expect(dropper.isDropperOpen).toBe(true) + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true) + expect(arrow.classList.contains('up')).toBe(true) // Check after toggling once more: - dropper.toggleDropper(); - expect(dropper.isDropperOpen).toBe(false); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - expect(arrow.classList.contains('up')).toBe(false); - }); + dropper.toggleDropper() + expect(dropper.isDropperOpen).toBe(false) + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) + expect(arrow.classList.contains('up')).toBe(false) + }) it('cannot be opened while disabled', () => { - document.body.innerHTML = disabledDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const dropper = new Dropper(wrapper); - dropper.initialize(); - const arrow = wrapper.querySelector('.arrow'); + document.body.innerHTML = disabledDropperMarkup + const wrapper = document.querySelector('.generic-dropper-wrapper') + const dropper = new Dropper(wrapper) + dropper.initialize() + const arrow = wrapper.querySelector('.arrow') // Check initial state: - expect(dropper.isDropperDisabled).toBe(true); - expect(arrow.classList.contains('up')).toBe(false); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); + expect(dropper.isDropperDisabled).toBe(true) + expect(arrow.classList.contains('up')).toBe(false) + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) // Check state after toggling: - dropper.toggleDropper(); - expect(arrow.classList.contains('up')).toBe(false); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe( - false, - ); - }); + dropper.toggleDropper() + expect(arrow.classList.contains('up')).toBe(false) + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) + }) describe('Dropper event methods', () => { afterEach(() => { - jest.clearAllMocks(); - }); + jest.clearAllMocks() + }) it('calls `onDisabledClick()` when dropper is clicked while disabled', () => { - document.body.innerHTML = disabledDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const dropper = new Dropper(wrapper); - dropper.initialize(); + document.body.innerHTML = disabledDropperMarkup + const wrapper = document.querySelector('.generic-dropper-wrapper') + const dropper = new Dropper(wrapper) + dropper.initialize() - const onDisabledClickFn = jest.spyOn(dropper, 'onDisabledClick'); + const onDisabledClickFn = jest.spyOn(dropper, 'onDisabledClick') // Check initial state: - expect(dropper.isDropperDisabled).toBe(true); - expect(onDisabledClickFn).not.toHaveBeenCalled(); + expect(dropper.isDropperDisabled).toBe(true) + expect(onDisabledClickFn).not.toHaveBeenCalled() // Check state after toggling: - dropper.toggleDropper(); - expect(dropper.isDropperDisabled).toBe(true); - expect(onDisabledClickFn).toHaveBeenCalledTimes(1); + dropper.toggleDropper() + expect(dropper.isDropperDisabled).toBe(true) + expect(onDisabledClickFn).toHaveBeenCalledTimes(1) // Check state after closing: - dropper.closeDropper(); - expect(dropper.isDropperDisabled).toBe(true); - expect(onDisabledClickFn).toHaveBeenCalledTimes(2); - }); + dropper.closeDropper() + expect(dropper.isDropperDisabled).toBe(true) + expect(onDisabledClickFn).toHaveBeenCalledTimes(2) + }) it('calls `onClose()` when active dropper is closed', () => { - document.body.innerHTML = openDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const dropper = new Dropper(wrapper); - dropper.initialize(); + document.body.innerHTML = openDropperMarkup + const wrapper = document.querySelector('.generic-dropper-wrapper') + const dropper = new Dropper(wrapper) + dropper.initialize() - const onCloseFn = jest.spyOn(dropper, 'onClose'); + const onCloseFn = jest.spyOn(dropper, 'onClose') // Check initial state: - expect(dropper.isDropperOpen).toBe(true); - expect(onCloseFn).not.toHaveBeenCalled(); + expect(dropper.isDropperOpen).toBe(true) + expect(onCloseFn).not.toHaveBeenCalled() // Check state after closing: - dropper.closeDropper(); - expect(dropper.isDropperOpen).toBe(false); - expect(onCloseFn).toHaveBeenCalledTimes(1); + dropper.closeDropper() + expect(dropper.isDropperOpen).toBe(false) + expect(onCloseFn).toHaveBeenCalledTimes(1) // Check state after toggling open then closed: - dropper.toggleDropper(); - expect(dropper.isDropperOpen).toBe(true); - expect(onCloseFn).toHaveBeenCalledTimes(1); // Should not be called when dropper is closed + dropper.toggleDropper() + expect(dropper.isDropperOpen).toBe(true) + expect(onCloseFn).toHaveBeenCalledTimes(1) // Should not be called when dropper is closed - dropper.toggleDropper(); - expect(dropper.isDropperOpen).toBe(false); - expect(onCloseFn).toHaveBeenCalledTimes(2); - }); + dropper.toggleDropper() + expect(dropper.isDropperOpen).toBe(false) + expect(onCloseFn).toHaveBeenCalledTimes(2) + }) test('toggling dropper results in correct event method being called', () => { - document.body.innerHTML = closedDropperMarkup; - const wrapper = document.querySelector('.generic-dropper-wrapper'); - const dropper = new Dropper(wrapper); - dropper.initialize(); + document.body.innerHTML = closedDropperMarkup + const wrapper = document.querySelector('.generic-dropper-wrapper') + const dropper = new Dropper(wrapper) + dropper.initialize() - const onCloseFn = jest.spyOn(dropper, 'onClose'); - const onOpenFn = jest.spyOn(dropper, 'onOpen'); + const onCloseFn = jest.spyOn(dropper, 'onClose') + const onOpenFn = jest.spyOn(dropper, 'onOpen') // Check initial state: - expect(dropper.isDropperOpen).toBe(false); - expect(onCloseFn).not.toHaveBeenCalled(); - expect(onOpenFn).not.toHaveBeenCalled(); + expect(dropper.isDropperOpen).toBe(false) + expect(onCloseFn).not.toHaveBeenCalled() + expect(onOpenFn).not.toHaveBeenCalled() // Check after toggling open: - dropper.toggleDropper(); - expect(dropper.isDropperOpen).toBe(true); - expect(onCloseFn).toHaveBeenCalledTimes(0); - expect(onOpenFn).toHaveBeenCalledTimes(1); + dropper.toggleDropper() + expect(dropper.isDropperOpen).toBe(true) + expect(onCloseFn).toHaveBeenCalledTimes(0) + expect(onOpenFn).toHaveBeenCalledTimes(1) // Check after toggling closed: - dropper.toggleDropper(); - expect(dropper.isDropperOpen).toBe(false); - expect(onCloseFn).toHaveBeenCalledTimes(1); - expect(onOpenFn).toHaveBeenCalledTimes(1); - }); - }); -}); + dropper.toggleDropper() + expect(dropper.isDropperOpen).toBe(false) + expect(onCloseFn).toHaveBeenCalledTimes(1) + expect(onOpenFn).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/tests/unit/js/editionEditPageClassification.test.js b/tests/unit/js/editionEditPageClassification.test.js index de2d2f6fba6..88f66530c7f 100644 --- a/tests/unit/js/editionEditPageClassification.test.js +++ b/tests/unit/js/editionEditPageClassification.test.js @@ -1,8 +1,8 @@ -import sinon from 'sinon'; import { initClassificationValidation } from '../../../openlibrary/plugins/openlibrary/js/edit.js'; -import { init } from '../../../openlibrary/plugins/openlibrary/js/jquery.repeat'; -import { htmlquote } from '../../../openlibrary/plugins/openlibrary/js/jsdef'; +import sinon from 'sinon'; import * as testData from './html-test-data'; +import { htmlquote } from '../../../openlibrary/plugins/openlibrary/js/jsdef'; +import { init } from '../../../openlibrary/plugins/openlibrary/js/jquery.repeat'; let sandbox; @@ -22,31 +22,11 @@ beforeEach(() => { describe('initClassificationValidation', () => { test.each([ // format: [testName, selectValue, classificationValue, expectedDisplay] - [ - 'Can have a classification and any value', - 'lc_classifications', - 'anything at all', - 'none', - ], - [ - 'Cannot have both an empty classification and classification value', - '', - '', - 'block', - ], + ['Can have a classification and any value', 'lc_classifications', 'anything at all', 'none'], + ['Cannot have both an empty classification and classification value', '', '', 'block'], ['Cannot have an empty classification', '', 'Test', 'block'], - [ - 'Cannot have an empty classification value', - 'lc_classifications', - '', - 'block', - ], - [ - 'Cannot have --- as a classification WITHOUT a value', - '---', - 'test', - 'block', - ], + ['Cannot have an empty classification value', 'lc_classifications', '', 'block'], + ['Cannot have --- as a classification WITHOUT a value', '---', 'test', 'block'], ['Cannot have --- as a classification with a value', '---', '', 'block'], ])('Test: %s', (testName, selectValue, classificationValue, expectedDisplay) => { $('#select-classification').val(selectValue); diff --git a/tests/unit/js/editionsEditPage.test.js b/tests/unit/js/editionsEditPage.test.js index e8cabcbc092..d8cf34f61a1 100644 --- a/tests/unit/js/editionsEditPage.test.js +++ b/tests/unit/js/editionsEditPage.test.js @@ -1,8 +1,8 @@ -import sinon from 'sinon'; import { validateIdentifiers } from '../../../openlibrary/plugins/openlibrary/js/edit.js'; -import { init } from '../../../openlibrary/plugins/openlibrary/js/jquery.repeat'; -import { htmlquote } from '../../../openlibrary/plugins/openlibrary/js/jsdef'; +import sinon from 'sinon'; import * as testData from './html-test-data'; +import { htmlquote } from '../../../openlibrary/plugins/openlibrary/js/jsdef'; +import { init } from '../../../openlibrary/plugins/openlibrary/js/jquery.repeat'; let sandbox; @@ -35,8 +35,8 @@ beforeEach(() => { // setup the HTML $(document.body).html(testData.editionIdentifiersSample); $('#identifiers').repeat({ - vars: { prefix: 'edition--' }, - validate: (data) => validateIdentifiers(data), + vars: {prefix: 'edition--'}, + validate: function(data) {return validateIdentifiers(data)}, }); }); @@ -84,7 +84,7 @@ describe('initIdentifierValidation', () => { $('#do-not-add-isbn').trigger('click'); expect($('.repeat-item').length).toBe(5); const cssDisplay = $('#id-errors').css('display'); - expect(cssDisplay).toEqual('none'); + expect(cssDisplay).toEqual('none') }); it('does NOT add a duplicate ISBN 10', () => { @@ -102,7 +102,7 @@ describe('initIdentifierValidation', () => { $('#id-value').val('09- 8478---2869 '); $('.repeat-add').trigger('click'); expect($('.repeat-item').length).toBe(6); - }); + }) it('does count identical stripped and unstripped ISBN 10s as the same ISBN', () => { $('#select-id').val('isbn_10'); @@ -149,7 +149,7 @@ describe('initIdentifierValidation', () => { $('#do-not-add-isbn').trigger('click'); expect($('.repeat-item').length).toBe(5); const cssDisplay = $('#id-errors').css('display'); - expect(cssDisplay).toEqual('none'); + expect(cssDisplay).toEqual('none') }); it('does NOT add a duplicate ISBN 13', () => { @@ -167,7 +167,7 @@ describe('initIdentifierValidation', () => { $('#id-value').val('978-16172--95 980 '); $('.repeat-add').trigger('click'); expect($('.repeat-item').length).toBe(6); - }); + }) it('does count identical stripped and unstripped ISBN 13s as the same ISBN', () => { $('#select-id').val('isbn_13'); @@ -209,7 +209,7 @@ describe('initIdentifierValidation', () => { $('#id-value').val(' 75-425165//r75'); $('.repeat-add').trigger('click'); expect($('.repeat-item').length).toBe(6); - }); + }) it('does count identical normalized and non-normalized LCCNs as the same LCCN', () => { $('#select-id').val('lccn'); diff --git a/tests/unit/js/html-test-data.js b/tests/unit/js/html-test-data.js index 9aec3a671fc..3548c53842c 100644 --- a/tests/unit/js/html-test-data.js +++ b/tests/unit/js/html-test-data.js @@ -100,7 +100,7 @@ export const clamperSample = ` <a>orphans</a> <a>fantasy fiction</a> <a>England in fiction</a> - </span>`; + </span>` export const readClassification = ` <fieldset class="major" id="classifications" data-config="{"Please select a classification.": "Please select a classification.", "You need to give a value to CLASS.": "You need to give a value to CLASS."}"> @@ -193,4 +193,4 @@ export const readClassification = ` </div> </div> </fieldset> -`; +` diff --git a/tests/unit/js/idValidation.test.js b/tests/unit/js/idValidation.test.js index 983ccd6a730..82fa3364745 100644 --- a/tests/unit/js/idValidation.test.js +++ b/tests/unit/js/idValidation.test.js @@ -1,11 +1,11 @@ import { + parseIsbn, + parseLccn, isChecksumValidIsbn10, isChecksumValidIsbn13, isFormatValidIsbn10, isFormatValidIsbn13, - isValidLccn, - parseIsbn, - parseLccn, + isValidLccn } from '../../../openlibrary/plugins/openlibrary/js/idValidation.js'; describe('parseIsbn', () => { @@ -15,7 +15,7 @@ describe('parseIsbn', () => { it('correctly parses ISBN 13 with dashes', () => { expect(parseIsbn('978-0-553-38168-9')).toBe('9780553381689'); }); -}); +}) // testing from examples listed here: // https://www.loc.gov/marc/lccn-namespace.html @@ -44,7 +44,7 @@ describe('parseLccn', () => { it('correctly parses LCCN example 8', () => { expect(parseLccn(' 79139101 /AC/r932')).toBe('79139101'); }); -}); +}) describe('isChecksumValidIsbn10', () => { it('returns true with valid ISBN 10 (X check character)', () => { @@ -60,7 +60,7 @@ describe('isChecksumValidIsbn10', () => { it('returns false with an invalid ISBN 10', () => { expect(isChecksumValidIsbn10('1234567890')).toBe(false); }); -}); +}) describe('isChecksumValidIsbn13', () => { it('returns true with valid ISBN 13 (check 1)', () => { @@ -76,7 +76,7 @@ describe('isChecksumValidIsbn13', () => { it('returns false with an invalid ISBN 13 (check 2)', () => { expect(isChecksumValidIsbn13('9790000000000')).toBe(false); }); -}); +}) describe('isFormatValidIsbn10', () => { it('returns true with valid ISBN 10 (X check character)', () => { @@ -92,7 +92,7 @@ describe('isFormatValidIsbn10', () => { it('returns false with blank value', () => { expect(isFormatValidIsbn10('')).toBe(false); }); -}); +}) describe('isFormatValidIsbn13', () => { it('returns true with valid ISBN 13', () => { @@ -108,7 +108,7 @@ describe('isFormatValidIsbn13', () => { it('returns false with invalis ISBN 13 (non-numeric)', () => { expect(isFormatValidIsbn13('979a430918002')).toBe(false); }); -}); +}) // testing from examples listed here: // https://www.loc.gov/marc/lccn-namespace.html @@ -154,4 +154,4 @@ describe('isValidLccn', () => { it('returns false for LCCN of length 13', () => { expect(isValidLccn('1250000000003')).toBe(false); }); -}); +}) diff --git a/tests/unit/js/jquery.repeat.test.js b/tests/unit/js/jquery.repeat.test.js index ed7214e6765..94f3d3a86a7 100644 --- a/tests/unit/js/jquery.repeat.test.js +++ b/tests/unit/js/jquery.repeat.test.js @@ -1,7 +1,7 @@ import sinon from 'sinon'; -import { init } from '../../../openlibrary/plugins/openlibrary/js/jquery.repeat'; -import { htmlquote } from '../../../openlibrary/plugins/openlibrary/js/jsdef'; import * as testData from './html-test-data'; +import { htmlquote } from '../../../openlibrary/plugins/openlibrary/js/jsdef'; +import { init } from '../../../openlibrary/plugins/openlibrary/js/jquery.repeat'; let sandbox; @@ -20,9 +20,9 @@ test('identifiers of repeated elements are never the same.', () => { // turn on jQuery repeat $('#identifiers').repeat({ vars: { - prefix: 'edition--', + prefix: 'edition--' }, - validate: () => {}, + validate: () => {} }); expect($('.repeat-item').length).toBe(5); @@ -30,14 +30,12 @@ test('identifiers of repeated elements are never the same.', () => { $('#id-value').text('fo4rzdaHDAwC'); $('.repeat-add').trigger('click'); expect($('.repeat-item').length).toBe(6); - $('#identifiers--3 .repeat-remove').trigger('click'); + $('#identifiers--3 .repeat-remove').trigger('click') expect($('.repeat-item').length).toBe(5); $('#select-id').val('goodreads'); $('#id-value').text('44415839'); $('.repeat-add').trigger('click'); expect($('.repeat-item').length).toBe(6); - const ids = $('[id]') - .map((_, node) => node.getAttribute('id')) - .toArray(); + const ids = $('[id]').map((_, node) => node.getAttribute('id')).toArray(); expect(ids.length).toBe(new Set(ids).size); }); diff --git a/tests/unit/js/jsdef.test.js b/tests/unit/js/jsdef.test.js index 25ec37d76da..f1f69ed93ae 100644 --- a/tests/unit/js/jsdef.test.js +++ b/tests/unit/js/jsdef.test.js @@ -1,12 +1,5 @@ -import { - enumerate, - foreach, - htmlquote, - join, - len, - range, - websafe, -} from '../../../openlibrary/plugins/openlibrary/js/jsdef'; +import { foreach, range, join, len, htmlquote, enumerate, + websafe } from '../../../openlibrary/plugins/openlibrary/js/jsdef'; test('jsdef: python range function', () => { expect(range(2, 5)).toEqual([2, 3, 4]); @@ -18,7 +11,7 @@ test('jsdef: enumerate', () => { expect(enumerate([1, 2, 3])).toEqual([ ['0', 1], ['1', 2], - ['2', 3], + ['2', 3] ]); }); @@ -28,13 +21,13 @@ test('jsdef: foreach', () => { const listToLoop = [1, 2, 3]; expect.assertions(1); return new Promise((resolve) => { - foreach(listToLoop, loop, () => { + foreach(listToLoop, loop, function () { called += 1; if (called === 3) { expect(called).toBe(3); resolve(); } - }); + }) }); }); @@ -60,5 +53,5 @@ test('jsdef: websafe', () => { // not sure if these are really necessary, but they document the current behaviour expect(websafe(undefined)).toBe(''); expect(websafe(null)).toBe(''); - expect(websafe({ toString: undefined })).toBe(''); + expect(websafe({toString: undefined})).toBe(''); }); diff --git a/tests/unit/js/lists.test.js b/tests/unit/js/lists.test.js index 5574867bccf..7e76994b28d 100644 --- a/tests/unit/js/lists.test.js +++ b/tests/unit/js/lists.test.js @@ -1,269 +1,195 @@ -import { - createActiveShowcaseItem, - ShowcaseItem, -} from '../../../openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js'; -import { CreateListForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js'; -import { - activeListShowcase, - authorShowcase, - editionShowcase, - filledListCreationForm, - listCreationForm, - listsSectionShowcase, - showcaseI18nInput, - subjectShowcase, - workShowcase, -} from './sample-html/lists-test-data'; +import { createActiveShowcaseItem, ShowcaseItem } from '../../../openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js'; +import { CreateListForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js' +import { listCreationForm, filledListCreationForm, showcaseI18nInput, subjectShowcase, authorShowcase, workShowcase, editionShowcase, activeListShowcase, listsSectionShowcase } from './sample-html/lists-test-data' describe('CreateListForm class tests', () => { test('CreateListForm fields correctly set', () => { - document.body.innerHTML = listCreationForm; - const formElem = document.querySelector('form'); - const listForm = new CreateListForm(formElem); + document.body.innerHTML = listCreationForm + const formElem = document.querySelector('form') + const listForm = new CreateListForm(formElem) - const createListButton = document.querySelector('#create-list-button'); - expect(listForm.createListButton === createListButton).toBe(true); + const createListButton = document.querySelector('#create-list-button') + expect(listForm.createListButton === createListButton).toBe(true) - const listTitleInput = document.querySelector('#list_label'); - expect(listForm.listTitleInput === listTitleInput).toBe(true); + const listTitleInput = document.querySelector('#list_label') + expect(listForm.listTitleInput === listTitleInput).toBe(true) - const listDescriptionInput = document.querySelector('#list_desc'); - expect(listForm.listDescriptionInput === listDescriptionInput).toBe(true); - }); + const listDescriptionInput = document.querySelector('#list_desc') + expect(listForm.listDescriptionInput === listDescriptionInput).toBe(true) + }) test('`resetForm()` clears a filled form', () => { - document.body.innerHTML = listCreationForm; - const formElem = document.querySelector('form'); - const listForm = new CreateListForm(formElem); + document.body.innerHTML = listCreationForm + const formElem = document.querySelector('form') + const listForm = new CreateListForm(formElem) // Initial checks - expect(listForm.listTitleInput.value).not.toBeTruthy(); - expect(listForm.listDescriptionInput.value).not.toBeTruthy(); + expect(listForm.listTitleInput.value).not.toBeTruthy() + expect(listForm.listDescriptionInput.value).not.toBeTruthy() // After setting input values - listForm.listTitleInput.value = 'New List'; - listForm.listDescriptionInput.value = 'My new list.'; - expect(listForm.listTitleInput.value).toBeTruthy(); - expect(listForm.listDescriptionInput.value).toBeTruthy(); + listForm.listTitleInput.value = 'New List' + listForm.listDescriptionInput.value = 'My new list.' + expect(listForm.listTitleInput.value).toBeTruthy() + expect(listForm.listDescriptionInput.value).toBeTruthy() // After clearing the form: - listForm.resetForm(); - expect(listForm.listTitleInput.value).not.toBeTruthy(); - expect(listForm.listDescriptionInput.value).not.toBeTruthy(); - }); + listForm.resetForm() + expect(listForm.listTitleInput.value).not.toBeTruthy() + expect(listForm.listDescriptionInput.value).not.toBeTruthy() + }) it('should have empty inputs after instantiation', () => { - document.body.innerHTML = filledListCreationForm; - const formElem = document.querySelector('form'); - const titleInput = formElem.querySelector('#list_label'); - const descriptionInput = formElem.querySelector('#list_desc'); + document.body.innerHTML = filledListCreationForm + const formElem = document.querySelector('form') + const titleInput = formElem.querySelector('#list_label') + const descriptionInput = formElem.querySelector('#list_desc') // Form is initially filled - expect(titleInput.value).toBeTruthy(); - expect(descriptionInput.value).toBeTruthy(); + expect(titleInput.value).toBeTruthy() + expect(descriptionInput.value).toBeTruthy() // Creating new CreateListForm should clear the form // eslint-disable-next-line no-unused-vars - const listForm = new CreateListForm(formElem); - expect(titleInput.value).not.toBeTruthy(); - expect(descriptionInput.value).not.toBeTruthy(); - }); -}); + const listForm = new CreateListForm(formElem) + expect(titleInput.value).not.toBeTruthy() + expect(descriptionInput.value).not.toBeTruthy() + }) +}) describe('createActiveShowcaseItem() tests', () => { test('createActiveShowcaseItem() results are as expected', () => { - document.body.innerHTML = showcaseI18nInput; - const listKey = '/people/openlibrary/lists/OL1L'; - const seedKey = '/books/OL3421846M'; - const listTitle = 'My First List'; - const coverUrl = '/images/icons/avatar_book-sm.png'; - - const li = createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl); - const anchors = li.querySelectorAll('a'); - const [imageLink, titleLink, removeLink] = anchors; - const inputs = li.querySelectorAll('input'); - const [titleInput, seedKeyInput, seedTypeInput] = inputs; + document.body.innerHTML = showcaseI18nInput + const listKey = '/people/openlibrary/lists/OL1L' + const seedKey = '/books/OL3421846M' + const listTitle = 'My First List' + const coverUrl = '/images/icons/avatar_book-sm.png' + + const li = createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl) + const anchors = li.querySelectorAll('a') + const [imageLink, titleLink, removeLink] = anchors + const inputs = li.querySelectorAll('input') + const [titleInput, seedKeyInput, seedTypeInput] = inputs + // Must have `actionable-item` class - expect(li.classList.contains('actionable-item')).toBe(true); + expect(li.classList.contains('actionable-item')).toBe(true) // List key has been set - expect(removeLink.dataset.listKey === listKey).toBe(true); - expect(imageLink.href.endsWith(listKey)).toBe(true); - expect(titleLink.href.endsWith(listKey)).toBe(true); - expect(removeLink.href.endsWith(listKey)).toBe(true); + expect(removeLink.dataset.listKey === listKey).toBe(true) + expect(imageLink.href.endsWith(listKey)).toBe(true) + expect(titleLink.href.endsWith(listKey)).toBe(true) + expect(removeLink.href.endsWith(listKey)).toBe(true) // Seed key has been set - expect(seedKeyInput.value === seedKey).toBe(true); - expect(seedTypeInput.value === 'edition').toBe(true); + expect(seedKeyInput.value === seedKey).toBe(true) + expect(seedTypeInput.value === 'edition').toBe(true) // List title has been set - expect(titleLink.dataset.listTitle === listTitle).toBe(true); - expect(titleLink.textContent === listTitle).toBe(true); - expect(titleInput.value === listTitle).toBe(true); + expect(titleLink.dataset.listTitle === listTitle).toBe(true) + expect(titleLink.textContent === listTitle).toBe(true) + expect(titleInput.value === listTitle).toBe(true) // Cover URL has been set - expect(imageLink.children[0].src.endsWith(coverUrl)).toBe(true); - }); + expect(imageLink.children[0].src.endsWith(coverUrl)).toBe(true) + }) test('createActiveShowcaseItem() sets the correct seed type', () => { - const listKey = '/people/openlibrary/lists/OL1L'; - const listTitle = 'My First List'; - const coverUrl = '/images/icons/avatar_book-sm.png'; - - const editionKey = '/books/OL3421846M'; - const workKey = '/works/OL54120W'; - const authorKey = '/authors/OL18319A'; - const subjectKey = 'quotations'; - const bogusKey = '/bogus/OL38475839B'; - - const editionItem = createActiveShowcaseItem( - listKey, - editionKey, - listTitle, - coverUrl, - ); - expect(editionItem.querySelector('input[name=seed-type]').value).toBe( - 'edition', - ); - - const workItem = createActiveShowcaseItem( - listKey, - workKey, - listTitle, - coverUrl, - ); - expect(workItem.querySelector('input[name=seed-type]').value).toBe('work'); - - const authorItem = createActiveShowcaseItem( - listKey, - authorKey, - listTitle, - coverUrl, - ); - expect(authorItem.querySelector('input[name=seed-type]').value).toBe( - 'author', - ); - - const subjectItem = createActiveShowcaseItem( - listKey, - subjectKey, - listTitle, - coverUrl, - ); - expect(subjectItem.querySelector('input[name=seed-type]').value).toBe( - 'subject', - ); - - const bogusItem = createActiveShowcaseItem( - listKey, - bogusKey, - listTitle, - coverUrl, - ); - expect(bogusItem.querySelector('input[name=seed-type]').value).toBe( - 'undefined', - ); - }); + const listKey = '/people/openlibrary/lists/OL1L' + const listTitle = 'My First List' + const coverUrl = '/images/icons/avatar_book-sm.png' + + const editionKey = '/books/OL3421846M' + const workKey = '/works/OL54120W' + const authorKey = '/authors/OL18319A' + const subjectKey = 'quotations' + const bogusKey = '/bogus/OL38475839B' + + const editionItem = createActiveShowcaseItem(listKey, editionKey, listTitle, coverUrl) + expect(editionItem.querySelector('input[name=seed-type]').value).toBe('edition') + + const workItem = createActiveShowcaseItem(listKey, workKey, listTitle, coverUrl) + expect(workItem.querySelector('input[name=seed-type]').value).toBe('work') + + const authorItem = createActiveShowcaseItem(listKey, authorKey, listTitle, coverUrl) + expect(authorItem.querySelector('input[name=seed-type]').value).toBe('author') + + const subjectItem = createActiveShowcaseItem(listKey, subjectKey, listTitle, coverUrl) + expect(subjectItem.querySelector('input[name=seed-type]').value).toBe('subject') + + const bogusItem = createActiveShowcaseItem(listKey, bogusKey, listTitle, coverUrl) + expect(bogusItem.querySelector('input[name=seed-type]').value).toBe('undefined') + }) it('sets the correct default value for `coverUrl`', () => { - document.body.innerHTML = showcaseI18nInput; - const listKey = '/people/openlibrary/lists/OL1L'; - const seedKey = '/books/OL3421846M'; - const listTitle = 'My First List'; + document.body.innerHTML = showcaseI18nInput + const listKey = '/people/openlibrary/lists/OL1L' + const seedKey = '/books/OL3421846M' + const listTitle = 'My First List' - const li = createActiveShowcaseItem(listKey, seedKey, listTitle); - const coverImage = li.querySelector('img'); + const li = createActiveShowcaseItem(listKey, seedKey, listTitle) + const coverImage = li.querySelector('img') - const expectedCoverUrl = '/images/icons/avatar_book-sm.png'; - expect(coverImage.src.endsWith(expectedCoverUrl)).toBe(true); - }); -}); + const expectedCoverUrl = '/images/icons/avatar_book-sm.png' + expect(coverImage.src.endsWith(expectedCoverUrl)).toBe(true) + }) +}) describe('ShowcaseItem class tests', () => { test('ShowcaseItem fields correctly set', () => { - document.body.innerHTML = activeListShowcase; - const showcaseElem = document.querySelector('.actionable-item'); - const showcase = new ShowcaseItem(showcaseElem); - const removeAffordance = showcaseElem.querySelector('.remove-from-list'); - - expect(showcase.showcaseElem === showcaseElem).toBe(true); - expect(showcase.isActiveShowcase).toBe(true); - expect(showcase.removeFromListAffordance === removeAffordance).toBe(true); - expect(showcase.listKey === '/people/openlibrary/lists/OL1L').toBe(true); - expect(showcase.seedKey === '/works/OL54120W').toBe(true); - expect(showcase.type).toBe('work'); - expect(showcase.seed).toMatchObject({ key: '/works/OL54120W' }); - }); + document.body.innerHTML = activeListShowcase + const showcaseElem = document.querySelector('.actionable-item') + const showcase = new ShowcaseItem(showcaseElem) + const removeAffordance = showcaseElem.querySelector('.remove-from-list') + + expect(showcase.showcaseElem === showcaseElem).toBe(true) + expect(showcase.isActiveShowcase).toBe(true) + expect(showcase.removeFromListAffordance === removeAffordance).toBe(true) + expect(showcase.listKey === '/people/openlibrary/lists/OL1L').toBe(true) + expect(showcase.seedKey === '/works/OL54120W').toBe(true) + expect(showcase.type).toBe('work') + expect(showcase.seed).toMatchObject({key: '/works/OL54120W'}) + }) it('correctly infers if it is an active showcase', () => { - document.body.innerHTML = activeListShowcase + listsSectionShowcase; - const [activeShowcaseElem, otherShowcaseElem] = - document.querySelectorAll('.actionable-item'); - const activeShowcase = new ShowcaseItem(activeShowcaseElem); - const otherShowcase = new ShowcaseItem(otherShowcaseElem); + document.body.innerHTML = activeListShowcase + listsSectionShowcase + const [activeShowcaseElem, otherShowcaseElem] = document.querySelectorAll('.actionable-item') + const activeShowcase = new ShowcaseItem(activeShowcaseElem) + const otherShowcase = new ShowcaseItem(otherShowcaseElem) - expect(activeShowcase.isActiveShowcase).toBe(true); - expect(otherShowcase.isActiveShowcase).toBe(false); - }); + expect(activeShowcase.isActiveShowcase).toBe(true) + expect(otherShowcase.isActiveShowcase).toBe(false) + }) describe('Seed type inference', () => { const cases = [ - { - markup: subjectShowcase, - expectedType: 'subject', - expectedIsWorkValue: false, - expectedIsSubjectValue: true, - }, - { - markup: authorShowcase, - expectedType: 'author', - expectedIsWorkValue: false, - expectedIsSubjectValue: false, - }, - { - markup: workShowcase, - expectedType: 'work', - expectedIsWorkValue: true, - expectedIsSubjectValue: false, - }, - { - markup: editionShowcase, - expectedType: 'edition', - expectedIsWorkValue: false, - expectedIsSubjectValue: false, - }, - ]; - - test.each(cases)('Type is $expectedType', ({ markup, expectedType }) => { - document.body.innerHTML = markup; - const showcaseElem = document.querySelector('.actionable-item'); - const showcase = new ShowcaseItem(showcaseElem); - expect(showcase.type).toBe(expectedType); - }); - - test.each(cases)('`isWork` value expected to be $expectedIsWorkValue', ({ - markup, - expectedIsWorkValue, - }) => { - document.body.innerHTML = markup; - const showcaseElem = document.querySelector('.actionable-item'); - const showcase = new ShowcaseItem(showcaseElem); - expect(showcase.isWork).toBe(expectedIsWorkValue); - }); - - test.each( - cases, - )('`isSubject` value expected to be $expectedIsSubjectValue', ({ - markup, - expectedIsSubjectValue, - }) => { - document.body.innerHTML = markup; - const showcaseElem = document.querySelector('.actionable-item'); - const showcase = new ShowcaseItem(showcaseElem); - expect(showcase.isSubject).toBe(expectedIsSubjectValue); - }); - }); + {markup: subjectShowcase, expectedType: 'subject', expectedIsWorkValue: false, expectedIsSubjectValue: true}, + {markup: authorShowcase, expectedType: 'author', expectedIsWorkValue: false, expectedIsSubjectValue: false}, + {markup: workShowcase, expectedType: 'work', expectedIsWorkValue: true, expectedIsSubjectValue: false}, + {markup: editionShowcase, expectedType: 'edition', expectedIsWorkValue: false, expectedIsSubjectValue: false} + ] + + test.each(cases)('Type is $expectedType', ({markup, expectedType}) => { + document.body.innerHTML = markup + const showcaseElem = document.querySelector('.actionable-item') + const showcase = new ShowcaseItem(showcaseElem) + expect(showcase.type).toBe(expectedType) + }) + + test.each(cases)('`isWork` value expected to be $expectedIsWorkValue', ({markup, expectedIsWorkValue}) => { + document.body.innerHTML = markup + const showcaseElem = document.querySelector('.actionable-item') + const showcase = new ShowcaseItem(showcaseElem) + expect(showcase.isWork).toBe(expectedIsWorkValue) + }) + + test.each(cases)('`isSubject` value expected to be $expectedIsSubjectValue', ({markup, expectedIsSubjectValue}) => { + document.body.innerHTML = markup + const showcaseElem = document.querySelector('.actionable-item') + const showcase = new ShowcaseItem(showcaseElem) + expect(showcase.isSubject).toBe(expectedIsSubjectValue) + }) + }) // XXX : test : removeSelf() fails safely when myBooksStore has not been created? -}); +}) diff --git a/tests/unit/js/my-books.test.js b/tests/unit/js/my-books.test.js index 5fbf042b6cd..037d0e9ffb1 100644 --- a/tests/unit/js/my-books.test.js +++ b/tests/unit/js/my-books.test.js @@ -1,149 +1,151 @@ -import { CreateListForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/CreateListForm'; -import { CheckInForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents'; -import { checkInForm } from './sample-html/checkIns-test-data'; -import { listCreationForm } from './sample-html/lists-test-data'; +import { listCreationForm } from './sample-html/lists-test-data' +import { checkInForm } from './sample-html/checkIns-test-data' +import { CreateListForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/CreateListForm' +import { CheckInForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents' jest.mock('jquery-ui/ui/widgets/dialog', () => {}); describe('CreateListForm.js class', () => { - let form; - let formElem; + let form + let formElem beforeEach(() => { - document.body.innerHTML = listCreationForm; - formElem = document.querySelector('form'); - form = new CreateListForm(formElem); - }); + document.body.innerHTML = listCreationForm + formElem = document.querySelector('form') + form = new CreateListForm(formElem) + }) test('References are set correctly', () => { - const createListButton = formElem.querySelector('#create-list-button'); - const nameInput = formElem.querySelector('#list_label'); - const descriptionInput = formElem.querySelector('#list_desc'); + const createListButton = formElem.querySelector('#create-list-button') + const nameInput = formElem.querySelector('#list_label') + const descriptionInput = formElem.querySelector('#list_desc') - expect(createListButton === form.createListButton).toBe(true); - expect(nameInput === form.listTitleInput).toBe(true); - expect(descriptionInput === form.listDescriptionInput).toBe(true); - }); + expect(createListButton === form.createListButton).toBe(true) + expect(nameInput === form.listTitleInput).toBe(true) + expect(descriptionInput === form.listDescriptionInput).toBe(true) + }) it('it clears the form after a resetForm() call', () => { - const nameInput = document.querySelector('#list_label'); - const descriptionInput = document.querySelector('#list_desc'); + const nameInput = document.querySelector('#list_label') + const descriptionInput = document.querySelector('#list_desc') // Form should be empty initially - expect(nameInput.value.length).toBe(0); - expect(descriptionInput.value.length).toBe(0); + expect(nameInput.value.length).toBe(0) + expect(descriptionInput.value.length).toBe(0) // Add values to each input - nameInput.value = 'My New List'; - descriptionInput.value = 'The best list ever'; - expect(nameInput.value.length).toBeGreaterThan(0); - expect(descriptionInput.value.length).toBeGreaterThan(0); + nameInput.value = 'My New List' + descriptionInput.value = 'The best list ever' + expect(nameInput.value.length).toBeGreaterThan(0) + expect(descriptionInput.value.length).toBeGreaterThan(0) // After clearing the form - form.resetForm(); - expect(nameInput.value.length).toBe(0); - expect(descriptionInput.value.length).toBe(0); - }); -}); + form.resetForm() + expect(nameInput.value.length).toBe(0) + expect(descriptionInput.value.length).toBe(0) + }) +}) describe('CheckInForm class', () => { - let formElem; - let submitButton; - let yearSelect; - let monthSelect; - let daySelect; + let formElem = undefined + let submitButton = undefined + let yearSelect = undefined + let monthSelect = undefined + let daySelect = undefined + + const workOlid = 'OL123W' + const editionKey = '/books/OL456M' + - const workOlid = 'OL123W'; - const editionKey = '/books/OL456M'; beforeEach(() => { - document.body.innerHTML = checkInForm; - formElem = document.querySelector('form'); - submitButton = document.querySelector('.check-in__submit-btn'); - yearSelect = document.querySelector('select[name=year]'); - monthSelect = document.querySelector('select[name=month]'); - daySelect = document.querySelector('select[name=day]'); - }); + document.body.innerHTML = checkInForm + formElem = document.querySelector('form') + submitButton = document.querySelector('.check-in__submit-btn') + yearSelect = document.querySelector('select[name=year]') + monthSelect = document.querySelector('select[name=month]') + daySelect = document.querySelector('select[name=day]') + }) test('Submit button, month select, and day select are initially disabled when read date is absent', () => { - const form = new CheckInForm(formElem, workOlid, editionKey); - form.initialize(); - expect(submitButton.disabled).toBe(true); - expect(monthSelect.disabled).toBe(true); - expect(daySelect.disabled).toBe(true); + const form = new CheckInForm(formElem, workOlid, editionKey) + form.initialize() + expect(submitButton.disabled).toBe(true) + expect(monthSelect.disabled).toBe(true) + expect(daySelect.disabled).toBe(true) - expect(yearSelect.disabled).toBe(false); - expect(yearSelect.value).toBe(''); - }); + expect(yearSelect.disabled).toBe(false) + expect(yearSelect.value).toBe('') + }) it('Sets correct values and enables selects and submit button', () => { - const form = new CheckInForm(formElem, workOlid, editionKey); - form.initialize(); - form.updateSelectedDate(2022, 1, 31); - expect(submitButton.disabled).toBe(false); - expect(monthSelect.disabled).toBe(false); - expect(daySelect.disabled).toBe(false); - - expect(yearSelect.value).toBe('2022'); - expect(monthSelect.value).toBe('1'); - expect(daySelect.value).toBe('31'); - }); + const form = new CheckInForm(formElem, workOlid, editionKey) + form.initialize() + form.updateSelectedDate(2022, 1, 31) + expect(submitButton.disabled).toBe(false) + expect(monthSelect.disabled).toBe(false) + expect(daySelect.disabled).toBe(false) + + expect(yearSelect.value).toBe('2022') + expect(monthSelect.value).toBe('1') + expect(daySelect.value).toBe('31') + }) it('Hides impossible day options', () => { - const form = new CheckInForm(formElem, workOlid, editionKey); - form.initialize(); - form.updateSelectedDate(2022, 2, 20); + const form = new CheckInForm(formElem, workOlid, editionKey) + form.initialize() + form.updateSelectedDate(2022, 2, 20) // The 28th day should be visible: - expect(daySelect.options[28].classList.contains('hidden')).toBe(false); + expect(daySelect.options[28].classList.contains('hidden')).toBe(false) // Subsequent days should not be visible - expect(daySelect.options[29].classList.contains('hidden')).toBe(true); - expect(daySelect.options[30].classList.contains('hidden')).toBe(true); - expect(daySelect.options[31].classList.contains('hidden')).toBe(true); - }); + expect(daySelect.options[29].classList.contains('hidden')).toBe(true) + expect(daySelect.options[30].classList.contains('hidden')).toBe(true) + expect(daySelect.options[31].classList.contains('hidden')).toBe(true) + }) it('Shows 29 days in February when there is a leap year', () => { - const form = new CheckInForm(formElem, workOlid, editionKey); - form.initialize(); - form.updateSelectedDate(2020, 2, 1); + const form = new CheckInForm(formElem, workOlid, editionKey) + form.initialize() + form.updateSelectedDate(2020, 2, 1) - expect(daySelect.options[29].classList.contains('hidden')).toBe(false); - expect(daySelect.options[30].classList.contains('hidden')).toBe(true); - expect(daySelect.options[31].classList.contains('hidden')).toBe(true); - }); + expect(daySelect.options[29].classList.contains('hidden')).toBe(false) + expect(daySelect.options[30].classList.contains('hidden')).toBe(true) + expect(daySelect.options[31].classList.contains('hidden')).toBe(true) + }) it('Associates labels with select elements during initialization', () => { - const form = new CheckInForm(formElem, workOlid, editionKey); + const form = new CheckInForm(formElem, workOlid, editionKey) // Get reference to each label: - const yearLabel = formElem.querySelector('.check-in__year-label'); - const monthLabel = formElem.querySelector('.check-in__month-label'); - const dayLabel = formElem.querySelector('.check-in__day-label'); + const yearLabel = formElem.querySelector('.check-in__year-label') + const monthLabel = formElem.querySelector('.check-in__month-label') + const dayLabel = formElem.querySelector('.check-in__day-label') // Verify labels have no `for` initially: - expect(yearLabel.htmlFor).toBe(''); - expect(monthLabel.htmlFor).toBe(''); - expect(dayLabel.htmlFor).toBe(''); + expect(yearLabel.htmlFor).toBe('') + expect(monthLabel.htmlFor).toBe('') + expect(dayLabel.htmlFor).toBe('') // Verify select elements have no `id` initially: - expect(yearSelect.id).toBe(''); - expect(monthSelect.id).toBe(''); - expect(daySelect.id).toBe(''); + expect(yearSelect.id).toBe('') + expect(monthSelect.id).toBe('') + expect(daySelect.id).toBe('') // Verify labels associated with selects after initialization: - form.initialize(); + form.initialize() - const expectedYearId = `year-select-${workOlid}`; - const expectedMonthId = `month-select-${workOlid}`; - const expectedDayId = `day-select-${workOlid}`; + const expectedYearId = `year-select-${workOlid}` + const expectedMonthId = `month-select-${workOlid}` + const expectedDayId = `day-select-${workOlid}` - expect(yearLabel.htmlFor).toBe(expectedYearId); - expect(monthLabel.htmlFor).toBe(expectedMonthId); - expect(dayLabel.htmlFor).toBe(expectedDayId); + expect(yearLabel.htmlFor).toBe(expectedYearId) + expect(monthLabel.htmlFor).toBe(expectedMonthId) + expect(dayLabel.htmlFor).toBe(expectedDayId) - expect(yearSelect.id).toBe(expectedYearId); - expect(monthSelect.id).toBe(expectedMonthId); - expect(daySelect.id).toBe(expectedDayId); - }); -}); + expect(yearSelect.id).toBe(expectedYearId) + expect(monthSelect.id).toBe(expectedMonthId) + expect(daySelect.id).toBe(expectedDayId) + }) +}) diff --git a/tests/unit/js/python.test.js b/tests/unit/js/python.test.js index 929176715f7..f645cb0d064 100644 --- a/tests/unit/js/python.test.js +++ b/tests/unit/js/python.test.js @@ -1,14 +1,10 @@ -import { - commify, - slice, - urlencode, -} from '../../../openlibrary/plugins/openlibrary/js/python'; +import { commify, urlencode, slice } from '../../../openlibrary/plugins/openlibrary/js/python'; test('commify', () => { expect(commify('5443232')).toBe('5,443,232'); expect(commify('50')).toBe('50'); expect(commify('5000')).toBe('5,000'); - expect(commify(['1', '2', '3', '45'])).toBe('1,2,3,45'); + expect(commify(['1','2','3','45'])).toBe('1,2,3,45'); expect(commify([1, 20, 3])).toBe('1,20,3'); }); @@ -20,9 +16,7 @@ describe('urlencode', () => { expect(urlencode(['apple'])).toEqual('0=apple'); }); test('array of 3', () => { - expect(urlencode(['apple', 'grapes', 'orange'])).toEqual( - '0=apple&1=grapes&2=orange', - ); + expect(urlencode(['apple', 'grapes', 'orange'])).toEqual('0=apple&1=grapes&2=orange'); }); }); diff --git a/tests/unit/js/sample-html/checkIns-test-data.js b/tests/unit/js/sample-html/checkIns-test-data.js index a653c1b4db1..f2ab53499a1 100644 --- a/tests/unit/js/sample-html/checkIns-test-data.js +++ b/tests/unit/js/sample-html/checkIns-test-data.js @@ -83,4 +83,4 @@ export const checkInForm = ` </span> </form> </div> -`; +` diff --git a/tests/unit/js/sample-html/dropper-test-data.js b/tests/unit/js/sample-html/dropper-test-data.js index 080dc8dbefa..584c186269e 100644 --- a/tests/unit/js/sample-html/dropper-test-data.js +++ b/tests/unit/js/sample-html/dropper-test-data.js @@ -5,25 +5,25 @@ export const legacyBookDropperMarkup = ` <div class="arrow arrow-unactivated"></div> </a> </div> -`; +` -export const openDropperMarkup = generateDropperMarkup(true); +export const openDropperMarkup = generateDropperMarkup(true) -export const closedDropperMarkup = generateDropperMarkup(false); +export const closedDropperMarkup = generateDropperMarkup(false) -export const disabledDropperMarkup = generateDropperMarkup(false, true); +export const disabledDropperMarkup = generateDropperMarkup(false, true) -function generateDropperMarkup (isDropperOpen, isDropperDisabled = false) { - let wrapperClasses = 'generic-dropper-wrapper'; - let arrowClasses = 'arrow'; +function generateDropperMarkup(isDropperOpen, isDropperDisabled = false) { + let wrapperClasses = 'generic-dropper-wrapper' + let arrowClasses = 'arrow' if (isDropperOpen) { - wrapperClasses += ' generic-dropper-wrapper--active'; - arrowClasses += ' up'; + wrapperClasses += ' generic-dropper-wrapper--active' + arrowClasses += ' up' } if (isDropperDisabled) { - wrapperClasses += ' generic-dropper--disabled'; + wrapperClasses += ' generic-dropper--disabled' } return ` @@ -42,5 +42,5 @@ function generateDropperMarkup (isDropperOpen, isDropperDisabled = false) { </div> </div> </div> - `; + ` } diff --git a/tests/unit/js/sample-html/lists-test-data.js b/tests/unit/js/sample-html/lists-test-data.js index 37eff9d5690..d27076f3627 100644 --- a/tests/unit/js/sample-html/lists-test-data.js +++ b/tests/unit/js/sample-html/lists-test-data.js @@ -1,6 +1,6 @@ -function createListFormMarkup (isFilled) { - const listName = isFilled ? 'My New List' : ''; - const listDescription = isFilled ? 'A list for all of my books' : ''; +function createListFormMarkup(isFilled) { + const listName = isFilled ? 'My New List' : '' + const listDescription = isFilled ? 'A list for all of my books' : '' return ` <form method="post" class="floatform" name="new-list" id="new-list"> @@ -28,16 +28,15 @@ function createListFormMarkup (isFilled) { </div> </div> </form> - `; + ` } -export const listCreationForm = createListFormMarkup(false); -export const filledListCreationForm = createListFormMarkup(true); +export const listCreationForm = createListFormMarkup(false) +export const filledListCreationForm = createListFormMarkup(true) -export const showcaseI18nInput = - '<input type="hidden" name="list-i18n-strings" value="{"cover_of": "Cover of: ", "see_this_list": "See this list", "remove_from_list": "Remove from your list?", "from": "from", "you": "You"}"></input>'; +export const showcaseI18nInput = '<input type="hidden" name="list-i18n-strings" value="{"cover_of": "Cover of: ", "see_this_list": "See this list", "remove_from_list": "Remove from your list?", "from": "from", "you": "You"}"></input>' -const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; +const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png' /** * @typedef {Object} ShowcaseDetails @@ -52,13 +51,11 @@ const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; * @param {boolean} isActiveShowcase * @param {Array<ShowcaseDetails>} showcaseData */ -function createShowcaseMarkup (isActiveShowcase, showcaseData) { - const listId = isActiveShowcase ? 'already-lists' : 'list-lists'; - const listClasses = 'listLists'.concat( - isActiveShowcase ? ' already-lists' : '', - ); +function createShowcaseMarkup(isActiveShowcase, showcaseData) { + const listId = isActiveShowcase ? 'already-lists' : 'list-lists' + const listClasses = 'listLists'.concat(isActiveShowcase ? ' already-lists' : '') - let showcaseMarkup = ''; + let showcaseMarkup = '' for (const data of showcaseData) { showcaseMarkup += `<li class="actionable-item"> @@ -77,13 +74,13 @@ function createShowcaseMarkup (isActiveShowcase, showcaseData) { <span class="owner">from <a href="${data.listOwner}">You</a></span> </span> </li> - `; + ` } return `<ul id="${listId}" class="${listClasses}"> ${showcaseMarkup} </ul> - `; + ` } export const showcaseDetailsData = [ @@ -92,42 +89,42 @@ export const showcaseDetailsData = [ seedKey: '/works/OL54120W', listTitle: 'My First List', listOwner: '/people/openlibrary', - seedType: 'work', + seedType: 'work' }, { listKey: '/people/openlibrary/lists/OL1L', seedKey: '/books/OL3421846M', listTitle: 'My First List', listOwner: '/people/openlibrary', - seedType: 'edition', + seedType: 'edition' }, { listKey: '/people/openlibrary/lists/OL2L', seedKey: '/works/OL54120W', listTitle: 'Another List', listOwner: '/people/openlibrary', - seedType: 'work', + seedType: 'work' }, { listKey: '/people/openlibrary/lists/OL1L', seedKey: '/authors/OL18319A', listTitle: 'My First List', listOwner: '/people/openlibrary', - seedType: 'author', + seedType: 'author' }, { listKey: '/people/openlibrary/lists/OL1L', seedKey: 'quotations', listTitle: 'My First List', listOwner: '/people/openlibrary', - seedType: 'subject', + seedType: 'subject' }, -]; +] -export const multipleShowcasesOnPage = createShowcaseMarkup(true, [showcaseDetailsData[0], showcaseDetailsData[2]]) + createShowcaseMarkup(false, [showcaseDetailsData[0], showcaseDetailsData[1]]); -export const activeListShowcase = createShowcaseMarkup(true, [showcaseDetailsData[0]]); -export const listsSectionShowcase = createShowcaseMarkup(false, [showcaseDetailsData[0]]); -export const subjectShowcase = createShowcaseMarkup(false, [showcaseDetailsData[4]]); -export const authorShowcase = createShowcaseMarkup(false, [showcaseDetailsData[3]]); -export const workShowcase = createShowcaseMarkup(false, [showcaseDetailsData[0]]); -export const editionShowcase = createShowcaseMarkup(false, [showcaseDetailsData[1]]); +export const multipleShowcasesOnPage = createShowcaseMarkup(true, [showcaseDetailsData[0], showcaseDetailsData[2]]) + createShowcaseMarkup(false, [showcaseDetailsData[0], showcaseDetailsData[1]]) +export const activeListShowcase = createShowcaseMarkup(true, [showcaseDetailsData[0]]) +export const listsSectionShowcase = createShowcaseMarkup(false, [showcaseDetailsData[0]]) +export const subjectShowcase = createShowcaseMarkup(false, [showcaseDetailsData[4]]) +export const authorShowcase = createShowcaseMarkup(false, [showcaseDetailsData[3]]) +export const workShowcase = createShowcaseMarkup(false, [showcaseDetailsData[0]]) +export const editionShowcase = createShowcaseMarkup(false, [showcaseDetailsData[1]]) diff --git a/tests/unit/js/sample-html/utils-test-data.js b/tests/unit/js/sample-html/utils-test-data.js index 8426b2551e0..bc49363c86f 100644 --- a/tests/unit/js/sample-html/utils-test-data.js +++ b/tests/unit/js/sample-html/utils-test-data.js @@ -1,17 +1,17 @@ // removeChildren() test data: // Single element, no children -export const childlessElem = '<div class="remove-tests"></div>'; +export const childlessElem = '<div class="remove-tests"></div>' // Single element, multiple children export const multiChildElem = `<div class="remove-tests"> <div>Child one</div> <div>Child two</div> -</div>`; +</div>` // Single element, child with children export const elemWithDescendants = `<div class="remove-tests"> <div> <div>Ancestor</div> </div> -</div>`; +</div>` diff --git a/tests/unit/js/search.test.js b/tests/unit/js/search.test.js index ee97f4b4d76..b06dc134641 100644 --- a/tests/unit/js/search.test.js +++ b/tests/unit/js/search.test.js @@ -1,7 +1,4 @@ -import { - less, - more, -} from '../../../openlibrary/plugins/openlibrary/js/search.js'; +import { more, less } from '../../../openlibrary/plugins/openlibrary/js/search.js'; /** Creates a dummy search facets section with a list of 'facetEntry' element and a * 'facetMoreLess' section. @@ -11,18 +8,14 @@ import { * @param {Number} minVisibleFacet minimum number of visible facet * @return {String} HTML search facets section */ -function createSearchFacets ( - totalFacet = 2, - visibleFacet = 2, - minVisibleFacet = 2, -) { +function createSearchFacets(totalFacet = 2, visibleFacet = 2, minVisibleFacet = 2) { const divSearchFacets = document.createElement('DIV'); divSearchFacets.setAttribute('id', 'searchFacets'); divSearchFacets.innerHTML = ` <div class="facet test"> <h4 class="facetHead">Facet Label</h4> </div> - `; + ` const divTestFacet = divSearchFacets.querySelector('div.test'); for (let i = 0; i < totalFacet; i++) { @@ -66,7 +59,7 @@ function createSearchFacets ( * @param {Number} totalFacet total number of facet * @param {Number} expectedVisibleFacet expected number of visible facet */ -function checkFacetVisibility (totalFacet, expectedVisibleFacet) { +function checkFacetVisibility(totalFacet, expectedVisibleFacet) { const facetEntryList = document.getElementsByClassName('facetEntry'); test('facetEntry element number', () => { @@ -75,16 +68,12 @@ function checkFacetVisibility (totalFacet, expectedVisibleFacet) { for (let i = 0; i < totalFacet; i++) { if (i < expectedVisibleFacet) { - test(`element "facet_${i + 1}" displayed`, () => { - expect(facetEntryList[i].classList.contains('ui-helper-hidden')).toBe( - false, - ); + test(`element "facet_${i+1}" displayed`, () => { + expect(facetEntryList[i].classList.contains('ui-helper-hidden')).toBe(false); }); } else { - test(`element "facet_${i + 1}" hidden`, () => { - expect(facetEntryList[i].classList.contains('ui-helper-hidden')).toBe( - true, - ); + test(`element "facet_${i+1}" hidden`, () => { + expect(facetEntryList[i].classList.contains('ui-helper-hidden')).toBe(true); }); } } @@ -96,16 +85,10 @@ function checkFacetVisibility (totalFacet, expectedVisibleFacet) { * @param {Number} minVisibleFacet minimum visible facet number * @param {Number} expectedVisibleFacet expected number of visible facet */ -function checkFacetMoreLessVisibility ( - totalFacet, - minVisibleFacet, - expectedVisibleFacet, -) { +function checkFacetMoreLessVisibility(totalFacet, minVisibleFacet, expectedVisibleFacet) { if (expectedVisibleFacet <= minVisibleFacet) { test('element "test_more"', () => { - expect(document.getElementById('test_more').style.display).not.toBe( - 'none', - ); + expect(document.getElementById('test_more').style.display).not.toBe('none'); }); test('element "test_bull"', () => { expect(document.getElementById('test_bull').style.display).toBe('none'); @@ -121,25 +104,17 @@ function checkFacetMoreLessVisibility ( expect(document.getElementById('test_bull').style.display).toBe('none'); }); test('element "test_less"', () => { - expect(document.getElementById('test_less').style.display).not.toBe( - 'none', - ); + expect(document.getElementById('test_less').style.display).not.toBe('none'); }); } else { test('element "test_more"', () => { - expect(document.getElementById('test_more').style.display).not.toBe( - 'none', - ); + expect(document.getElementById('test_more').style.display).not.toBe('none'); }); test('element "test_bull"', () => { - expect(document.getElementById('test_bull').style.display).not.toBe( - 'none', - ); + expect(document.getElementById('test_bull').style.display).not.toBe('none'); }); test('element "test_less"', () => { - expect(document.getElementById('test_less').style.display).not.toBe( - 'none', - ); + expect(document.getElementById('test_less').style.display).not.toBe('none'); }); } } @@ -147,32 +122,27 @@ function checkFacetMoreLessVisibility ( const _originalGetClientRects = window.Element.prototype.getClientRects; // Stubbed getClientRects to enable jQuery ':hidden' selector used by 'more' and 'less' functions -const _stubbedGetClientRects = function () { +const _stubbedGetClientRects = function() { let node = this; while (node) { if (node === document) { break; } - if ( - !node.style || - node.style.display === 'none' || - node.style.visibility === 'hidden' || - node.classList.contains('ui-helper-hidden') - ) { + if (!node.style || node.style.display === 'none' || node.style.visibility === 'hidden' || node.classList.contains('ui-helper-hidden')) { return []; } node = node.parentNode; } - return [{ width: 1, height: 1 }]; + return [{width: 1, height: 1}]; }; describe('more', () => { [ - /*[ totalFacet, minVisibleFacet, facetInc, visibleFacet, expectedVisibleFacet ]*/ - [7, 2, 3, 2, 5], - [9, 2, 3, 5, 8], - [7, 2, 3, 5, 7], - [7, 2, 3, 7, 7], + /*[ totalFacet, minVisibleFacet, facetInc, visibleFacet, expectedVisibleFacet ]*/ + [ 7, 2, 3, 2, 5 ], + [ 9, 2, 3, 5, 8 ], + [ 7, 2, 3, 5, 7 ], + [ 7, 2, 3, 7, 7 ] ].forEach((test) => { const label = `Facet setup [total: ${test[0]}, visible: ${test[3]}, min: ${test[1]}]`; describe(label, () => { @@ -194,11 +164,11 @@ describe('more', () => { describe('less', () => { [ - /*[ totalFacet, minVisibleFacet, facetInc, visibleFacet, expectedVisibleFacet ]*/ - [5, 2, 3, 2, 2], - [7, 2, 3, 5, 2], - [9, 2, 3, 8, 5], - [7, 2, 3, 7, 5], + /*[ totalFacet, minVisibleFacet, facetInc, visibleFacet, expectedVisibleFacet ]*/ + [ 5, 2, 3, 2, 2 ], + [ 7, 2, 3, 5, 2 ], + [ 9, 2, 3, 8, 5 ], + [ 7, 2, 3, 7, 5 ] ].forEach((test) => { const label = `Facet setup [total: ${test[0]}, visible: ${test[3]}, min: ${test[1]}]`; describe(label, () => { diff --git a/tests/unit/js/service-worker-matchers.test.js b/tests/unit/js/service-worker-matchers.test.js index a2638c17472..93765655ce3 100644 --- a/tests/unit/js/service-worker-matchers.test.js +++ b/tests/unit/js/service-worker-matchers.test.js @@ -1,26 +1,16 @@ -import { - matchArchiveOrgImage, - matchLargeCovers, - matchMiscFiles, - matchSmallMediumCovers, - matchStaticBuild, - matchStaticImages, -} from '../../../openlibrary/plugins/openlibrary/js/service-worker-matchers'; +import { matchMiscFiles, matchSmallMediumCovers, matchLargeCovers, matchStaticImages, matchStaticBuild, matchArchiveOrgImage } from '../../../openlibrary/plugins/openlibrary/js/service-worker-matchers'; + // Helper function to create a URL object -function _u (url) { - return { url: new URL(url) }; +function _u(url) { + return { url: new URL(url) } } // Group related tests together describe('URL Matchers', () => { describe('matchMiscFiles', () => { test('matches miscellaneous files', () => { - expect(matchMiscFiles(_u('https://openlibrary.org/favicon.ico'))).toBe( - true, - ); - expect( - matchMiscFiles(_u('https://openlibrary.org/static/manifest.json')), - ).toBe(true); + expect(matchMiscFiles(_u('https://openlibrary.org/favicon.ico'))).toBe(true); + expect(matchMiscFiles(_u('https://openlibrary.org/static/manifest.json'))).toBe(true); }); test('does not match homepage', () => { @@ -30,140 +20,60 @@ describe('URL Matchers', () => { describe('matchSmallMediumCovers', () => { test('matches small and medium cover sizes', () => { - expect( - matchSmallMediumCovers( - _u('https://covers.openlibrary.org/a/id/6257045-M.jpg'), - ), - ).toBe(true); - expect( - matchSmallMediumCovers( - _u('https://covers.openlibrary.org/a/id/6257045-S.jpg'), - ), - ).toBe(true); - expect( - matchSmallMediumCovers( - _u('https://covers.openlibrary.org/b/id/1852327-M.jpg'), - ), - ).toBe(true); - expect( - matchSmallMediumCovers( - _u('https://covers.openlibrary.org/a/olid/OL2838765A-M.jpg'), - ), - ).toBe(true); + expect(matchSmallMediumCovers(_u('https://covers.openlibrary.org/a/id/6257045-M.jpg'))).toBe(true); + expect(matchSmallMediumCovers(_u('https://covers.openlibrary.org/a/id/6257045-S.jpg'))).toBe(true); + expect(matchSmallMediumCovers(_u('https://covers.openlibrary.org/b/id/1852327-M.jpg'))).toBe(true); + expect(matchSmallMediumCovers(_u('https://covers.openlibrary.org/a/olid/OL2838765A-M.jpg'))).toBe(true); }); test('does not match large covers', () => { - expect( - matchSmallMediumCovers( - _u('https://covers.openlibrary.org/a/id/6257045-L.jpg'), - ), - ).toBe(false); + expect(matchSmallMediumCovers(_u('https://covers.openlibrary.org/a/id/6257045-L.jpg'))).toBe(false); }); }); describe('matchLargeCovers', () => { test('matches large cover sizes', () => { - expect( - matchLargeCovers( - _u('https://covers.openlibrary.org/a/id/6257045-L.jpg'), - ), - ).toBe(true); + expect(matchLargeCovers(_u('https://covers.openlibrary.org/a/id/6257045-L.jpg'))).toBe(true); }); test('does not match small or medium covers', () => { - expect( - matchLargeCovers( - _u('https://covers.openlibrary.org/a/id/6257045-S.jpg'), - ), - ).toBe(false); - expect( - matchLargeCovers( - _u('https://covers.openlibrary.org/a/id/6257045-M.jpg'), - ), - ).toBe(false); - expect( - matchLargeCovers(_u('https://covers.openlibrary.org/a/id/6257045.jpg')), - ).toBe(false); + expect(matchLargeCovers(_u('https://covers.openlibrary.org/a/id/6257045-S.jpg'))).toBe(false); + expect(matchLargeCovers(_u('https://covers.openlibrary.org/a/id/6257045-M.jpg'))).toBe(false); + expect(matchLargeCovers(_u('https://covers.openlibrary.org/a/id/6257045.jpg'))).toBe(false); }); }); describe('matchStaticImages', () => { test('matches static images', () => { - expect( - matchStaticImages( - _u('https://openlibrary.org/static/images/down-arrow.png'), - ), - ).toBe(true); - expect( - matchStaticImages( - _u( - 'https://testing.openlibrary.org/static/images/icons/barcode_scanner.svg', - ), - ), - ).toBe(true); + expect(matchStaticImages(_u('https://openlibrary.org/static/images/down-arrow.png'))).toBe(true); + expect(matchStaticImages(_u('https://testing.openlibrary.org/static/images/icons/barcode_scanner.svg'))).toBe(true); }); test('does not match other URLs', () => { - expect( - matchStaticImages( - _u( - 'https://openlibrary.org/static/build/js/4290.a0ae80aacde14696d322.js', - ), - ), - ).toBe(false); - expect( - matchStaticImages( - _u('https://covers.openlibrary.org/w/id/14348537-L.jpg'), - ), - ).toBe(false); + expect(matchStaticImages(_u('https://openlibrary.org/static/build/js/4290.a0ae80aacde14696d322.js'))).toBe(false); + expect(matchStaticImages(_u('https://covers.openlibrary.org/w/id/14348537-L.jpg'))).toBe(false); }); }); describe('matchStaticBuild', () => { test('matches static build files', () => { - expect( - matchStaticBuild( - _u( - 'https://openlibrary.org/static/build/js/4290.a0ae80aacde14696d322.js', - ), - ), - ).toBe(true); - expect( - matchStaticBuild( - _u( - 'https://testing.openlibrary.org/static/build/css/page-book.css?v=097b69dc350c972d96da0c70cebe7b75', - ), - ), - ).toBe(true); + expect(matchStaticBuild(_u('https://openlibrary.org/static/build/js/4290.a0ae80aacde14696d322.js'))).toBe(true); + expect(matchStaticBuild(_u('https://testing.openlibrary.org/static/build/css/page-book.css?v=097b69dc350c972d96da0c70cebe7b75'))).toBe(true); }); test('does not match localhost URLs', () => { - expect( - matchStaticBuild( - _u( - 'http://localhost:8080/static/build/js/4290.a0ae80aacde14696d322.js', - ), - ), - ).toBe(false); + expect(matchStaticBuild(_u('http://localhost:8080/static/build/js/4290.a0ae80aacde14696d322.js'))).toBe(false); }); }); describe('matchArchiveOrgImage', () => { test('matches archive.org images', () => { - expect( - matchArchiveOrgImage(_u('https://archive.org/services/img/@raybb')), - ).toBe(true); - expect( - matchArchiveOrgImage( - _u('https://archive.org/services/img/courtofmistfury0000maas'), - ), - ).toBe(true); + expect(matchArchiveOrgImage(_u('https://archive.org/services/img/@raybb'))).toBe(true); + expect(matchArchiveOrgImage(_u('https://archive.org/services/img/courtofmistfury0000maas'))).toBe(true); }); test('does not match other URLs', () => { - expect(matchArchiveOrgImage(_u('https://archive.org/services/'))).toBe( - false, - ); + expect(matchArchiveOrgImage(_u('https://archive.org/services/'))).toBe(false); }); }); }); diff --git a/tests/unit/js/setup.js b/tests/unit/js/setup.js index a57332a9bba..b6e448350b6 100644 --- a/tests/unit/js/setup.js +++ b/tests/unit/js/setup.js @@ -1,6 +1,5 @@ // Make jQuery available globally for tests import $ from 'jquery'; - window.jQuery = $; window.$ = $; diff --git a/tests/unit/js/signup.test.js b/tests/unit/js/signup.test.js index 8bce04035b3..de61b41b07e 100644 --- a/tests/unit/js/signup.test.js +++ b/tests/unit/js/signup.test.js @@ -26,19 +26,19 @@ beforeEach(() => { }); describe('Email tests', () => { - let emailLabel, emailField; + let emailLabel, emailField beforeEach(() => { - // call the function + // call the function initSignupForm(); //declare the elements emailLabel = document.querySelector('label[for="emailAddr"]'); emailField = document.getElementById('emailAddr'); - }); + }) test('validateEmail should update elements correctly on success', () => { - // set the email value + // set the email value emailField.value = 'testemail@archive.org'; // Trigger the blur event on the email field @@ -50,7 +50,7 @@ describe('Email tests', () => { }); test('validateEmail should update elements correctly for empty fields', () => { - // set the email value + // set the email value emailField.value = ''; // Trigger the blur event on the email field @@ -62,7 +62,7 @@ describe('Email tests', () => { }); test('validateEmail should update elements correctly for emails with plus signs', () => { - // set the email value + // set the email value emailField.value = 'testemail+01@archive.org'; // Trigger the blur event on the email field @@ -74,7 +74,7 @@ describe('Email tests', () => { }); test('validateEmail should update elements correctly for emails with no punctuation', () => { - // set the password values + // set the password values emailField.value = 'testemail'; // Trigger the blur event on the email fields @@ -86,7 +86,7 @@ describe('Email tests', () => { }); test('validateEmail should update elements correctly for emails with invalid punctuation', () => { - // set the email values + // set the email values emailField.value = 'testemail@archive-org'; // Trigger the blur event on the email fields @@ -99,19 +99,19 @@ describe('Email tests', () => { }); describe('Username tests', () => { - let usernameLabel, usernameField; + let usernameLabel, usernameField beforeEach(() => { - // call the function + // call the function initSignupForm(); //declare the elements usernameLabel = document.querySelector('label[for="username"]'); usernameField = document.getElementById('username'); - }); + }) test('validateUsername should update elements correctly on success', () => { - // set the username value + // set the username value usernameField.value = 'username123'; // Trigger the blur event on the username field @@ -123,7 +123,7 @@ describe('Username tests', () => { }); test('validateUsername should update elements correctly for empty fields', () => { - // set the username value + // set the username value usernameField.value = ''; // Trigger the blur event on the username field @@ -135,7 +135,7 @@ describe('Username tests', () => { }); test('validateUsername should update elements correctly for usernames over 20 chars', () => { - // set the username values + // set the username values usernameField.value = 'username1234567891011'; // Trigger the blur event on the username fields @@ -147,7 +147,7 @@ describe('Username tests', () => { }); test('validateusername should update elements correctly for usernames under 3 chars', () => { - // set the username values + // set the username values usernameField.value = 'us'; // Trigger the blur event on the username fields @@ -159,20 +159,21 @@ describe('Username tests', () => { }); }); + describe('Password tests', () => { - let passwordLabel, passwordField; + let passwordLabel, passwordField beforeEach(() => { - // call the function + // call the function initSignupForm(); //declare the elements passwordLabel = document.querySelector('label[for="password"]'); passwordField = document.getElementById('password'); - }); + }) test('validatePassword should update elements correctly on success', () => { - // set the password value + // set the password value passwordField.value = 'password123'; // Trigger the blur event on the password field @@ -184,7 +185,7 @@ describe('Password tests', () => { }); test('validatePassword should update elements correctly for empty fields', () => { - // set the password value + // set the password value passwordField.value = ''; // Trigger the blur event on the password field @@ -196,7 +197,7 @@ describe('Password tests', () => { }); test('validatePassword should update elements correctly for passwords over 20 chars', () => { - // set the password values + // set the password values passwordField.value = 'password1234567891011'; // Trigger the blur event on the password fields @@ -208,7 +209,7 @@ describe('Password tests', () => { }); test('validatePassword should update elements correctly for passwords under 3 chars', () => { - // set the password values + // set the password values passwordField.value = 'pa'; // Trigger the blur event on the password fields @@ -227,16 +228,16 @@ describe('Print disability tests', () => { initSignupForm(); checkbox = document.querySelector('#pd-request'); - selector = document.querySelector('#pda-selector'); - }); + selector = document.querySelector('#pda-selector') + }) test('Qualifying authority selector only visible when PD checkbox is checked', () => { - checkbox.checked = false; + checkbox.checked = false checkbox.dispatchEvent(new Event('change', { bubbles: true })); expect(selector.classList.contains('hidden')).toBe(true); - checkbox.checked = true; + checkbox.checked = true checkbox.dispatchEvent(new Event('change', { bubbles: true })); expect(selector.classList.contains('hidden')).toBe(false); - }); -}); + }) +}) diff --git a/tests/unit/js/utils.test.js b/tests/unit/js/utils.test.js index b35e7eb6995..b9205bfdfd4 100644 --- a/tests/unit/js/utils.test.js +++ b/tests/unit/js/utils.test.js @@ -1,69 +1,65 @@ +import { childlessElem, multiChildElem, elemWithDescendants } from './sample-html/utils-test-data'; import { removeChildren } from '../../../openlibrary/plugins/openlibrary/js/utils'; -import { - childlessElem, - elemWithDescendants, - multiChildElem, -} from './sample-html/utils-test-data'; describe('`removeChildren()` tests', () => { it('changes nothing if element has no children', () => { - document.body.innerHTML = childlessElem; - const elem = document.querySelector('.remove-tests'); - const clonedElem = elem.cloneNode(true); + document.body.innerHTML = childlessElem + const elem = document.querySelector('.remove-tests') + const clonedElem = elem.cloneNode(true) // Initial checks - expect(elem.childElementCount).toBe(0); - expect(elem.isEqualNode(clonedElem)).toBe(true); + expect(elem.childElementCount).toBe(0) + expect(elem.isEqualNode(clonedElem)).toBe(true) // Element should be unchanged after function call - removeChildren(elem); - expect(elem.childElementCount).toBe(0); - expect(elem.isEqualNode(clonedElem)).toBe(true); - }); + removeChildren(elem) + expect(elem.childElementCount).toBe(0) + expect(elem.isEqualNode(clonedElem)).toBe(true) + }) it('removes all of an element\'s children', () => { - document.body.innerHTML = multiChildElem; - const elem = document.querySelector('.remove-tests'); - const clonedElem = elem.cloneNode(true); + document.body.innerHTML = multiChildElem + const elem = document.querySelector('.remove-tests') + const clonedElem = elem.cloneNode(true) // Initial checks - expect(elem.childElementCount).toBe(2); - expect(elem.isEqualNode(clonedElem)).toBe(true); + expect(elem.childElementCount).toBe(2) + expect(elem.isEqualNode(clonedElem)).toBe(true) // After removing children - removeChildren(elem); - expect(elem.childElementCount).toBe(0); - expect(elem.isEqualNode(clonedElem)).toBe(false); - }); + removeChildren(elem) + expect(elem.childElementCount).toBe(0) + expect(elem.isEqualNode(clonedElem)).toBe(false) + }) it('removes children if they have children of their own', () => { - document.body.innerHTML = elemWithDescendants; - const elem = document.querySelector('.remove-tests'); - const clonedElem = elem.cloneNode(true); + document.body.innerHTML = elemWithDescendants + const elem = document.querySelector('.remove-tests') + const clonedElem = elem.cloneNode(true) // Inital checks - expect(elem.childElementCount).toBe(1); - expect(elem.children[0].childElementCount).toBe(1); - expect(elem.isEqualNode(clonedElem)).toBe(true); + expect(elem.childElementCount).toBe(1) + expect(elem.children[0].childElementCount).toBe(1) + expect(elem.isEqualNode(clonedElem)).toBe(true) // After removing children - removeChildren(elem); - expect(elem.childElementCount).toBe(0); - expect(elem.isEqualNode(clonedElem)).toBe(false); - }); + removeChildren(elem) + expect(elem.childElementCount).toBe(0) + expect(elem.isEqualNode(clonedElem)).toBe(false) + }) it('handles multiple parameters correctly', () => { - document.body.innerHTML = elemWithDescendants + multiChildElem; - const elems = document.querySelectorAll('.remove-tests'); + document.body.innerHTML = elemWithDescendants + multiChildElem + const elems = document.querySelectorAll('.remove-tests') // Initial checks: - expect(elems.length).toBe(2); - expect(elems[0].childElementCount).toBe(1); - expect(elems[1].childElementCount).toBe(2); + expect(elems.length).toBe(2) + expect(elems[0].childElementCount).toBe(1) + expect(elems[1].childElementCount).toBe(2) // After removing children: - removeChildren(...elems); - expect(elems[0].childElementCount).toBe(0); - expect(elems[1].childElementCount).toBe(0); - }); -}); + removeChildren(...elems) + expect(elems[0].childElementCount).toBe(0) + expect(elems[1].childElementCount).toBe(0) + }) +}) diff --git a/vue.config.js b/vue.config.js index c97bc10ff9c..cc7a26868b5 100644 --- a/vue.config.js +++ b/vue.config.js @@ -1,4 +1,4 @@ module.exports = { - lintOnSave: false, - publicPath: '/static/components/', + lintOnSave: false, + publicPath: '/static/components/' }; diff --git a/webpack.config.css.js b/webpack.config.css.js index 77a12587a79..be614b7cdf9 100644 --- a/webpack.config.css.js +++ b/webpack.config.css.js @@ -11,10 +11,7 @@ const path = require('path'); const glob = require('glob'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); -const distDir = path.resolve( - __dirname, - process.env.BUILD_DIR || 'static/build/css', -); +const distDir = path.resolve(__dirname, process.env.BUILD_DIR || 'static/build/css'); // Find all CSS entry files matching static/css/page-*.css const cssFiles = glob.sync('./static/css/page-*.css'); @@ -23,7 +20,7 @@ const entries = { tokens: './static/css/tokens.css', }; -cssFiles.forEach((file) => { +cssFiles.forEach(file => { const name = path.basename(file, '.css'); entries[name] = file; }); @@ -47,12 +44,12 @@ module.exports = { loader: 'css-loader', options: { url: false, - import: true, // Enable @import resolution - }, - }, - ], - }, - ], + import: true // Enable @import resolution + } + } + ] + } + ] }, plugins: [ new MiniCssExtractPlugin({ @@ -61,40 +58,35 @@ module.exports = { // Inline plugin to remove intermediary JS assets { apply: (compiler) => { - compiler.hooks.thisCompilation.tap( - 'RemoveJSAssetsPlugin', - (compilation) => { - compilation.hooks.processAssets.tap( - { - name: 'RemoveJSAssetsPlugin', - stage: - compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, - }, - (assets) => { - Object.keys(assets) - .filter((asset) => asset.endsWith('.js')) - .forEach((asset) => { - compilation.deleteAsset(asset); - }); - }, - ); - }, - ); - }, - }, + compiler.hooks.thisCompilation.tap('RemoveJSAssetsPlugin', (compilation) => { + compilation.hooks.processAssets.tap( + { + name: 'RemoveJSAssetsPlugin', + stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, + }, + (assets) => { + Object.keys(assets) + .filter((asset) => asset.endsWith('.js')) + .forEach((asset) => { + compilation.deleteAsset(asset); + }); + } + ); + }); + } + } ], optimization: { - minimizer: [new CssMinimizerPlugin()], + minimizer: [ + new CssMinimizerPlugin(), + ], runtimeChunk: false, splitChunks: false, }, // Useful for developing in docker/windows, which doesn't support file watchers - watchOptions: - process.env.FORCE_POLLING === 'true' - ? { - poll: 1000, // Check for changes every second - aggregateTimeout: 300, // Delay before rebuilding - ignored: /node_modules/, - } - : undefined, + watchOptions: process.env.FORCE_POLLING === 'true' ? { + poll: 1000, // Check for changes every second + aggregateTimeout: 300, // Delay before rebuilding + ignored: /node_modules/ + } : undefined, }; From 30777d83e9388659b127545a0f381e72fb064316 Mon Sep 17 00:00:00 2001 From: RayBB <RayBB@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:13:28 -0700 Subject: [PATCH 12/15] undo package json changes --- package-lock.json | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/package-lock.json b/package-lock.json index a36663e7648..e71bdf1ddec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3331,6 +3331,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3348,6 +3351,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3365,6 +3371,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3382,6 +3391,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3399,6 +3411,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3416,6 +3431,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4024,6 +4042,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4038,6 +4059,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4052,6 +4076,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4066,6 +4093,9 @@ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4080,6 +4110,9 @@ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4094,6 +4127,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4108,6 +4144,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4122,6 +4161,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -10282,6 +10324,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -10303,6 +10348,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -10324,6 +10372,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -10345,6 +10396,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ From 6f1b726cb1e46e374bad12688ff9cd574ee496f0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:13:45 +0000 Subject: [PATCH 13/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../components/BulkSearch/utils/samples.js | 2 +- openlibrary/components/MergeUI.vue | 58 +-- .../components/MergeUI/AuthorRoleTable.vue | 4 +- .../components/MergeUI/EditionSnippet.vue | 18 +- .../components/MergeUI/ExcerptsTable.vue | 4 +- openlibrary/components/MergeUI/MergeRow.vue | 2 +- .../components/MergeUI/MergeRowField.vue | 2 +- .../components/MergeUI/MergeRowJointField.vue | 2 +- openlibrary/components/MergeUI/TextDiff.vue | 4 +- openlibrary/components/ObservationForm.vue | 34 +- .../ObservationForm/components/CardBody.vue | 26 +- .../ObservationForm/components/CardHeader.vue | 2 +- .../components/CategorySelector.vue | 20 +- .../ObservationForm/components/OLChip.vue | 16 +- .../ObservationForm/components/SavedTags.vue | 32 +- .../ObservationForm/components/ValueCard.vue | 6 +- openlibrary/components/dev/vite.config.js | 6 +- .../plugins/openlibrary/js/book-page-lists.js | 60 +-- .../openlibrary/js/breadcrumb_select/index.js | 4 +- .../js/bulk-tagger/BulkTagger/MenuOption.js | 82 +-- .../BulkTagger/SortedMenuOptionContainer.js | 62 +-- .../openlibrary/js/bulk-tagger/index.js | 4 +- .../openlibrary/js/bulk-tagger/models/Tag.js | 58 +-- .../plugins/openlibrary/js/clampers.js | 10 +- .../openlibrary/js/compact-title/index.js | 38 +- openlibrary/plugins/openlibrary/js/covers.js | 26 +- .../plugins/openlibrary/js/dropper/Dropper.js | 46 +- .../plugins/openlibrary/js/dropper/index.js | 34 +- .../js/edition-nav-bar/EditionNavBar.js | 68 +-- .../openlibrary/js/edition-nav-bar/index.js | 12 +- .../openlibrary/js/editions-table/index.js | 50 +- .../plugins/openlibrary/js/following.js | 2 +- .../js/fulltext-search-suggestion.js | 56 +-- .../plugins/openlibrary/js/go-back-links.js | 8 +- .../plugins/openlibrary/js/graphs/index.js | 8 +- .../plugins/openlibrary/js/graphs/plot.js | 20 +- .../openlibrary/js/ia_thirdparty_logins.js | 18 +- .../plugins/openlibrary/js/idValidation.js | 22 +- .../plugins/openlibrary/js/ile/utils/ol.js | 6 +- openlibrary/plugins/openlibrary/js/index.js | 170 +++---- .../plugins/openlibrary/js/interstitial.js | 20 +- .../plugins/openlibrary/js/isbnOverride.js | 8 +- .../plugins/openlibrary/js/jquery.repeat.js | 22 +- openlibrary/plugins/openlibrary/js/jsdef.js | 26 +- .../plugins/openlibrary/js/lazy-carousel.js | 70 +-- .../openlibrary/js/lazy-thing-preview.js | 12 +- .../js/librarian-dashboard/index.js | 94 ++-- .../plugins/openlibrary/js/list_books.js | 8 +- .../openlibrary/js/lists/ListService.js | 8 +- .../openlibrary/js/lists/ListViewBody.js | 26 +- .../openlibrary/js/lists/ShowcaseItem.js | 130 ++--- .../merge-request-table/MergeRequestTable.js | 28 +- .../MergeRequestTable/TableHeader.js | 64 +-- .../MergeRequestTable/TableRow.js | 120 ++--- .../js/merge-request-table/index.js | 6 +- .../openlibrary/js/my-books/CreateListForm.js | 64 +-- .../openlibrary/js/my-books/MyBooksDropper.js | 110 ++-- .../MyBooksDropper/CheckInComponents.js | 472 +++++++++--------- .../my-books/MyBooksDropper/ReadingLists.js | 198 ++++---- .../plugins/openlibrary/js/my-books/index.js | 86 ++-- .../openlibrary/js/my-books/store/index.js | 42 +- .../openlibrary/js/native-dialog/index.js | 26 +- .../plugins/openlibrary/js/nonjquery_utils.js | 6 +- .../plugins/openlibrary/js/offline-banner.js | 2 +- .../plugins/openlibrary/js/ol.analytics.js | 10 +- openlibrary/plugins/openlibrary/js/ol.js | 16 +- .../plugins/openlibrary/js/partner_ol_lib.js | 28 +- .../plugins/openlibrary/js/patron_exports.js | 10 +- .../plugins/openlibrary/js/private-button.js | 4 +- openlibrary/plugins/openlibrary/js/python.js | 6 +- .../openlibrary/js/reading-goals/index.js | 150 +++--- .../openlibrary/js/readinglog_stats.js | 12 +- .../openlibrary/js/return-form/index.js | 6 +- openlibrary/plugins/openlibrary/js/search.js | 78 +-- .../openlibrary/js/service-worker-matchers.js | 16 +- .../plugins/openlibrary/js/service-worker.js | 2 +- openlibrary/plugins/openlibrary/js/signup.js | 88 ++-- .../openlibrary/js/star-ratings/index.js | 26 +- .../plugins/openlibrary/js/stats/index.js | 26 +- openlibrary/plugins/openlibrary/js/tabs.js | 4 +- openlibrary/plugins/openlibrary/js/team.js | 2 +- .../plugins/openlibrary/js/template.js | 14 +- .../plugins/openlibrary/js/type_changer.js | 4 +- openlibrary/plugins/openlibrary/js/utils.js | 18 +- .../plugins/openlibrary/js/waitlist.js | 6 +- static/bookmarklets/import_webbook.js | 2 +- stories/.storybook/main.js | 2 +- stories/.storybook/preview.js | 2 +- stories/Button.stories.js | 24 +- tests/unit/js/Browser.test.js | 2 +- tests/unit/js/SearchBar.test.js | 2 +- tests/unit/js/SelectionManager.test.js | 8 +- tests/unit/js/autocomplete.test.js | 4 +- tests/unit/js/droppers.test.js | 342 ++++++------- tests/unit/js/editionsEditPage.test.js | 12 +- tests/unit/js/html-test-data.js | 4 +- tests/unit/js/idValidation.test.js | 14 +- tests/unit/js/jquery.repeat.test.js | 2 +- tests/unit/js/jsdef.test.js | 2 +- tests/unit/js/lists.test.js | 250 +++++----- tests/unit/js/my-books.test.js | 200 ++++---- tests/unit/js/python.test.js | 2 +- .../unit/js/sample-html/checkIns-test-data.js | 2 +- .../unit/js/sample-html/dropper-test-data.js | 22 +- tests/unit/js/sample-html/lists-test-data.js | 44 +- tests/unit/js/sample-html/utils-test-data.js | 6 +- tests/unit/js/search.test.js | 10 +- tests/unit/js/service-worker-matchers.test.js | 4 +- tests/unit/js/signup.test.js | 24 +- tests/unit/js/utils.test.js | 76 +-- 110 files changed, 2169 insertions(+), 2169 deletions(-) diff --git a/openlibrary/components/BulkSearch/utils/samples.js b/openlibrary/components/BulkSearch/utils/samples.js index e9c08adddf5..1d5f9531a85 100644 --- a/openlibrary/components/BulkSearch/utils/samples.js +++ b/openlibrary/components/BulkSearch/utils/samples.js @@ -19,4 +19,4 @@ export const sampleData = [ source: 'https://en.wikipedia.org/wiki/Bibliography_of_the_Holocaust#Historical_studies', text: 'Bauer, Yehuda (1994). Jews for Sale? Nazi-Jewish Negotiations 1933-1945. Yale University Press. ISBN 0-300-06852-2.\nBerenbaum, Michael (1990). A Mosaic of Victims: Non-Jews Persecuted and Murdered by the Nazis.\nBergen, Doris (2009). War and Genocide: Concise History of the Holocaust.\nBlack, Edwin (2010). The Farhud: The Arab-Nazi Alliance in the Holocaust. Dialog Press. ISBN 0-914153-14-5.\nBraham, Randolph (1994) [1981]. The Politics of Genocide: The Holocaust in Hungary. Columbia University Press. ISBN 0-88033-247-6.\nBraham, Randolph (2011). The Auschwitz Reports and the Holocaust in Hungary. Columbia University Press.\nChalmers, Beverley (2015). Birth, Sex and Abuse: Women\'s Voices Under Nazi Rule. Grosvenor House Publishing Ltd. ISBN 1781483531.\nDavies, Norman; Lukas, Richard C. (2001) [1996]. Forgotten Holocaust: The Poles Under German Occupation.\nDean, Martin (2008). Robbing the Jews: The Confiscation of Jewish Property in the Holocaust. Cambridge University Press.\nEvans, Suzanne (2004). Forgotten Crimes: The Holocaust and People with Disabilities.\nFriedländer, Saul (1998). The Years of Persecution: Nazi Germany and the Jews, 1933-1939. Vol. 1.\nGrau, Gunter; Shoppmann, Claudia (1995). The Hidden Holocaust?: Gay and Lesbian Persecution in Germany 1933-45.\nHedgepeth, Sonja; Saidel, Rochelle (2010). Sexual Violence against Jewish Women during the Holocaust.\nPeukert, Detlev (1994). "The Genesis of the \'Final Solution\' from the Spirit of Science". In Thomas Childers; Jane Caplan (eds.). Reevaluating the Third Reich. New York: Holmes & Meier. pp. 234–252. ISBN 0-8419-1178-9.\nRees, Laurence (2017). The Holocaust: A New History. London: Viking Press. ISBN 978-1610398442.\nAméry, Jean (1980). At the Mind\'s Limits: Contemplations by a Survivor on Auschwitz and its Realities.\nBlitz Konig, Nanette (2018). Holocaust Memoirs of a Bergen-Belsen Survivor and Classmate of Anne Frank. Amsterdam Publishers. ISBN 9789492371614.\nDittman, Anita (2005). Trapped in Hitler\'s Hell. ISBN 0-9721512-8-1.\nGerrard, Mady. Full Circle. KLPM. ISBN 978-0955865008.\nKogon, Eugen (1974). Der SS-Staat. Das System der deutschen Konzentrationslager (in German).\nRittner, Carol; Roth, John K. (1998). Different Voices: Women and the Holocaust.\nStojka, Ceija (1988). We Live in Seclusion: The Memories of a Romni.\nSteinberg, Manny (2014). Outcry: Holocaust Memoirs. Amsterdam Publishers. ISBN 978-9-082103137.\nWetzler, Alfréd (2007). Escape from Hell: The True Story of the Auschwitz Protocol. Berghahn Books.\nWinter, Walter (2004). Winter Time: Memoirs of a German Sinto who Survived Auschwitz.\nCzech, Danuta (1999). Auschwitz Chronicle: 1939-1945.\nDean, Martin (1999). Collaboration in the Holocaust: Crimes of the Local Police in Belorussia and Ukraine.\nFings, Karola; Kenrick, Donald, eds. (1999). The Gypsies During the Second World War.\nHogan, David J.; Aretha, David, eds. (2000). The Holocaust Chronicle: A History in Words and Pictures. Lincolnwood, IL: Publications International.\nPressac, Jean-Claude (1989). Auschwitz: Technique and operation of the gas chambers.\nAgamben, Giorgio (1999). Remnants of Auschwitz: The Witness and the Archive.\nBloxham, Donald (2009). The Final Solution: A Genocide.\nLawson, Tom (2010). Debates on the Holocaust. University of Manchester Press.\nLeff, Laurel (2005). Buried By The Times: The Holocaust And America\'s Most Important Newspaper. Cambridge University Press. ISBN 0-521-81287-9.\nMason, Timothy. "Intention and Explanation: A Current Controversy about the Interpretation of National Socialism". In Marrus, Michael R. (ed.). The Nazi Holocaust Part 3, The "Final Solution": The Implementation of Mass Murder. Vol. 1. Westpoint, CT: Mecler. pp. 3–20.\nNiewyk, Donald L. (1992). Holocaust: Problems & Perspective of Interpretation.' } -] +]; diff --git a/openlibrary/components/MergeUI.vue b/openlibrary/components/MergeUI.vue index ff56fcd9913..253bff74ae0 100644 --- a/openlibrary/components/MergeUI.vue +++ b/openlibrary/components/MergeUI.vue @@ -52,13 +52,13 @@ </template> <script> -import MergeTable from './MergeUI/MergeTable.vue' +import MergeTable from './MergeUI/MergeTable.vue'; import { do_merge, update_merge_request, createMergeRequest, DEFAULT_EDITION_LIMIT } from './MergeUI/utils.js'; -const DO_MERGE = 'Do Merge' -const REQUEST_MERGE = 'Request Merge' -const LOADING = 'Loading...' -const SAVING = 'Saving...' +const DO_MERGE = 'Do Merge'; +const REQUEST_MERGE = 'Request Merge'; +const LOADING = 'Loading...'; +const SAVING = 'Saving...'; export default { name: 'App', @@ -81,17 +81,17 @@ export default { default: 'true', } }, - data() { + data () { return { url: new URL(location.toString()), mergeStatus: LOADING, mergeOutput: null, show_diffs: false, comment: '' - } + }; }, computed: { - olids() { + olids () { const olidsString = this.url.searchParams.get('records'); if (!olidsString) return []; return olidsString @@ -100,20 +100,20 @@ export default { .filter(Boolean); }, - isSuperLibrarian() { - return this.canmerge === 'true' + isSuperLibrarian () { + return this.canmerge === 'true'; }, - isDisabled() { - return this.mergeStatus !== DO_MERGE && this.mergeStatus !== REQUEST_MERGE + isDisabled () { + return this.mergeStatus !== DO_MERGE && this.mergeStatus !== REQUEST_MERGE; }, - showRejectButton() { - return this.mrid && this.isSuperLibrarian + showRejectButton () { + return this.mrid && this.isSuperLibrarian; } }, - mounted() { - const readyCta = this.isSuperLibrarian ? DO_MERGE : REQUEST_MERGE + mounted () { + const readyCta = this.isSuperLibrarian ? DO_MERGE : REQUEST_MERGE; this.$watch( '$refs.mergeTable.merge', (new_value) => { @@ -122,7 +122,7 @@ export default { ); }, methods: { - async doMerge() { + async doMerge () { if (!this.$refs.mergeTable.merge) return; const { record: master, dupes, editions_to_move, unmergeable_works } = this.$refs.mergeTable.merge; @@ -141,10 +141,10 @@ export default { } this.mergeOutput = await r.json(); if (this.mrid) { - await update_merge_request(this.mrid, 'approve', this.comment) + await update_merge_request(this.mrid, 'approve', this.comment); } else { - const workIds = [master.key].concat(Array.from(dupes, item => item.key)) - await createMergeRequest(workIds) + const workIds = [master.key].concat(Array.from(dupes, item => item.key)); + await createMergeRequest(workIds); } } catch (e) { this.mergeOutput = e.message; @@ -153,25 +153,25 @@ export default { } } else { // Create a new merge request with "pending" status - const workIds = [master.key].concat(Array.from(dupes, item => item.key)) - const splitKey = master.key.split('/') - const primaryRecord = splitKey[splitKey.length - 1] + const workIds = [master.key].concat(Array.from(dupes, item => item.key)); + const splitKey = master.key.split('/'); + const primaryRecord = splitKey[splitKey.length - 1]; await createMergeRequest(workIds, primaryRecord, 'create-pending', this.comment) .then(response => response.json()) .then(data => { if (data.status === 'ok') { // Redirect to merge table on success: - window.location.replace(`/merges#mrid-${data.id}`) + window.location.replace(`/merges#mrid-${data.id}`); } - }) + }); } this.mergeStatus = 'Done'; }, - async rejectMerge() { + async rejectMerge () { try { - await update_merge_request(this.mrid, 'decline', this.comment) - this.mergeOutput = 'Merge request closed' + await update_merge_request(this.mrid, 'decline', this.comment); + this.mergeOutput = 'Merge request closed'; } catch (e) { this.mergeOutput = e.message; throw e; @@ -179,7 +179,7 @@ export default { this.mergeStatus = 'Reject Merge'; } } -} +}; </script> <style> diff --git a/openlibrary/components/MergeUI/AuthorRoleTable.vue b/openlibrary/components/MergeUI/AuthorRoleTable.vue index a057644f017..9efda1993f3 100644 --- a/openlibrary/components/MergeUI/AuthorRoleTable.vue +++ b/openlibrary/components/MergeUI/AuthorRoleTable.vue @@ -54,9 +54,9 @@ export default { roles: Array }, computed: { - fields() { + fields () { return _.uniq(_.flatMap(this.roles, Object.keys)).sort(); } } -} +}; </script> diff --git a/openlibrary/components/MergeUI/EditionSnippet.vue b/openlibrary/components/MergeUI/EditionSnippet.vue index 0de0a239f5c..270bd12b4c7 100644 --- a/openlibrary/components/MergeUI/EditionSnippet.vue +++ b/openlibrary/components/MergeUI/EditionSnippet.vue @@ -61,17 +61,17 @@ export default { edition: Object }, computed: { - publish_year() { + publish_year () { if (!this.edition.publish_date) return ''; const m = this.edition.publish_date.match(/\d{4}/); return m ? m[0] : null; }, - publishers() { + publishers () { return this.edition.publishers || []; }, - number_of_pages() { + number_of_pages () { if (this.edition.number_of_pages) { return this.edition.number_of_pages; } else if (this.edition.pagination) { @@ -82,17 +82,17 @@ export default { return '?'; }, - full_title() { + full_title () { let title = this.edition.title; if (this.edition.subtitle) title += `: ${this.edition.subtitle}`; return title; }, - cover_id() { + cover_id () { return this.edition.covers?.[0] ?? null; }, - cover_url() { + cover_url () { if (this.cover_id) return `https://covers.openlibrary.org/b/id/${this.cover_id}-M.jpg`; const ocaid = this.edition.ocaid; @@ -102,13 +102,13 @@ export default { return ''; }, - languages() { + languages () { if (!this.edition.languages) return '???'; const langs = this.edition.languages.map(lang => lang.key.split('/')[2]); return langs.join(', '); }, - asins() { + asins () { return _.uniq([ ...((this.edition.identifiers && this.edition.identifiers.amazon) || []), this.edition.isbn_10 && ISBN.asIsbn10(this.edition.isbn_10), @@ -118,7 +118,7 @@ export default { }, methods: { - openEnlargedCover() { + openEnlargedCover () { let url = ''; if (this.cover_id) { url = `https://covers.openlibrary.org/b/id/${this.cover_id}.jpg`; diff --git a/openlibrary/components/MergeUI/ExcerptsTable.vue b/openlibrary/components/MergeUI/ExcerptsTable.vue index 013f3ad0f0c..ddfecfd1baf 100644 --- a/openlibrary/components/MergeUI/ExcerptsTable.vue +++ b/openlibrary/components/MergeUI/ExcerptsTable.vue @@ -37,9 +37,9 @@ export default { excerpts: Array }, computed: { - fields() { + fields () { return _.uniq(_.flatMap(this.excerpts, Object.keys)); } } -} +}; </script> diff --git a/openlibrary/components/MergeUI/MergeRow.vue b/openlibrary/components/MergeUI/MergeRow.vue index 01df7bcbf21..6a17706ef11 100644 --- a/openlibrary/components/MergeUI/MergeRow.vue +++ b/openlibrary/components/MergeUI/MergeRow.vue @@ -88,7 +88,7 @@ export default { type: Boolean } }, - data() { + data () { return { master_key: null }; diff --git a/openlibrary/components/MergeUI/MergeRowField.vue b/openlibrary/components/MergeUI/MergeRowField.vue index 2742122f31c..dc9a2c9db13 100644 --- a/openlibrary/components/MergeUI/MergeRowField.vue +++ b/openlibrary/components/MergeUI/MergeRowField.vue @@ -157,7 +157,7 @@ export default { } }, computed: { - title() { + title () { let title = `.${this.field}`; if (this.value instanceof Array) { const length = this.value.length; diff --git a/openlibrary/components/MergeUI/MergeRowJointField.vue b/openlibrary/components/MergeUI/MergeRowJointField.vue index 502e7aa49f5..27e3cb9507e 100644 --- a/openlibrary/components/MergeUI/MergeRowJointField.vue +++ b/openlibrary/components/MergeUI/MergeRowJointField.vue @@ -40,7 +40,7 @@ export default { } }, computed: { - presentFields() { + presentFields () { return this.fields.filter(f => f in this.record); } } diff --git a/openlibrary/components/MergeUI/TextDiff.vue b/openlibrary/components/MergeUI/TextDiff.vue index 91773141e6b..0359a824c17 100644 --- a/openlibrary/components/MergeUI/TextDiff.vue +++ b/openlibrary/components/MergeUI/TextDiff.vue @@ -25,7 +25,7 @@ export default { } }, computed: { - diff() { + diff () { const fn = { char: diffChars, word: diffWordsWithSpace, @@ -33,7 +33,7 @@ export default { return fn[this.resolution](this.left, this.right); } } -} +}; </script> <style scoped> diff --git a/openlibrary/components/ObservationForm.vue b/openlibrary/components/ObservationForm.vue index 30754a9ba6a..f74214eb4c0 100644 --- a/openlibrary/components/ObservationForm.vue +++ b/openlibrary/components/ObservationForm.vue @@ -30,11 +30,11 @@ </template> <script> -import CategorySelector from './ObservationForm/components/CategorySelector.vue' -import SavedTags from './ObservationForm/components/SavedTags.vue' -import ValueCard from './ObservationForm/components/ValueCard.vue' +import CategorySelector from './ObservationForm/components/CategorySelector.vue'; +import SavedTags from './ObservationForm/components/SavedTags.vue'; +import ValueCard from './ObservationForm/components/ValueCard.vue'; -import { decodeAndParseJSON, resizeColorbox } from './ObservationForm/Utils' +import { decodeAndParseJSON, resizeColorbox } from './ObservationForm/Utils'; export default { name: 'ObservationForm', @@ -84,7 +84,7 @@ export default { required: true } }, - data: function() { + data: function () { return { /** * An object representing the currently selected tag type. @@ -113,7 +113,7 @@ export default { * An array containing all book tag types and values. */ observationsArray: null, - } + }; }, computed: { /** @@ -121,28 +121,28 @@ export default { * * @returns {Number|null} The ID of the selected observation, if one exists. */ - getSelectedId: function() { + getSelectedId: function () { if (this.selectedObservation) { return this.selectedObservation.id; } - return null + return null; } }, - created: function() { + created: function () { this.observationsArray = decodeAndParseJSON(this.schema)['observations']; this.allSelectedValues = decodeAndParseJSON(this.observations); this.selectRandomObservation(); }, - mounted: function() { + mounted: function () { this.observer = new ResizeObserver(() => { resizeColorbox(); }); - this.observer.observe(this.$refs.form) + this.observer.observe(this.$refs.form); }, - beforeUnmount: function() { + beforeUnmount: function () { if (this.observer) { - this.observer.disconnect() + this.observer.disconnect(); } }, methods: { @@ -151,18 +151,18 @@ export default { * * @param {Object | null} observation The new selected observation, or `null` if no type is selected. */ - updateSelected: function(observation) { - this.selectedObservation = observation + updateSelected: function (observation) { + this.selectedObservation = observation; }, /** * Randomly sets a selected observation. */ - selectRandomObservation: function() { + selectRandomObservation: function () { const randomNumber = Math.floor(Math.random() * 100000); this.selectedObservation = this.observationsArray[randomNumber % this.observationsArray.length]; } } -} +}; </script> <style scoped> diff --git a/openlibrary/components/ObservationForm/components/CardBody.vue b/openlibrary/components/ObservationForm/components/CardBody.vue index d268713cc92..3ba378e914b 100644 --- a/openlibrary/components/ObservationForm/components/CardBody.vue +++ b/openlibrary/components/ObservationForm/components/CardBody.vue @@ -14,9 +14,9 @@ </template> <script> -import OLChip from './OLChip.vue' +import OLChip from './OLChip.vue'; -import { updateObservation } from '../ObservationService' +import { updateObservation } from '../ObservationService'; export default { name: 'CardBody', @@ -81,8 +81,8 @@ export default { /** * Returns an array of all of this book tag type's currently selected values. */ - selectedValues: function() { - return this.allSelectedValues[this.type]?.length ? this.allSelectedValues[this.type] : [] + selectedValues: function () { + return this.allSelectedValues[this.type]?.length ? this.allSelectedValues[this.type] : []; } }, methods: { @@ -94,19 +94,19 @@ export default { * @param {boolean} isSelected `true` if a chip is selected, `false` otherwise. * @param {String} text The text that the updated chip is displaying. */ - updateSelected: function(isSelected, text) { - let updatedValues = this.allSelectedValues[this.type] ? this.allSelectedValues[this.type] : [] + updateSelected: function (isSelected, text) { + let updatedValues = this.allSelectedValues[this.type] ? this.allSelectedValues[this.type] : []; if (isSelected) { if (this.multiSelect) { - updatedValues.push(text) + updatedValues.push(text); updateObservation('add', this.type, text, this.workKey, this.username) .catch(() => { updatedValues.pop(); }) .finally(() => { this.allSelectedValues[this.type] = updatedValues; - }) + }); } else { if (updatedValues.length) { let deleteSuccessful = false; @@ -118,13 +118,13 @@ export default { if (deleteSuccessful) { updateObservation('add', this.type, text, this.workKey, this.username) .then(() => { - updatedValues = [text] + updatedValues = [text]; }) .finally(() => { this.allSelectedValues[this.type] = updatedValues; - }) + }); } - }) + }); } } } else { @@ -133,11 +133,11 @@ export default { updateObservation('delete', this.type, text, this.workKey, this.username) .catch(() => { updatedValues.push(text); - }) + }); } } } -} +}; </script> <style scoped> diff --git a/openlibrary/components/ObservationForm/components/CardHeader.vue b/openlibrary/components/ObservationForm/components/CardHeader.vue index 4fb3262e8bf..091553b6459 100644 --- a/openlibrary/components/ObservationForm/components/CardHeader.vue +++ b/openlibrary/components/ObservationForm/components/CardHeader.vue @@ -18,7 +18,7 @@ export default { required: true } }, -} +}; </script> <style scoped> diff --git a/openlibrary/components/ObservationForm/components/CategorySelector.vue b/openlibrary/components/ObservationForm/components/CategorySelector.vue index b9e541ac529..b89204a3cc3 100644 --- a/openlibrary/components/ObservationForm/components/CategorySelector.vue +++ b/openlibrary/components/ObservationForm/components/CategorySelector.vue @@ -27,7 +27,7 @@ </template> <script> -import OLChip from './OLChip.vue' +import OLChip from './OLChip.vue'; export default { name: 'CategorySelector', @@ -74,7 +74,7 @@ export default { default: 0 } }, - data: function() { + data: function () { return { /** * The ID of the selected book tag type. @@ -82,7 +82,7 @@ export default { * @type {number | null} */ selectedId: this.initialSelectedId, - } + }; }, methods: { /** @@ -91,20 +91,20 @@ export default { * @param {boolean} isSelected Whether or not a chip is currently selected. * @param {String} text The text displayed by a chip. */ - updateSelected: function(isSelected, text) { + updateSelected: function (isSelected, text) { if (isSelected) { // TODO: This for loop shouldn't be necessary for (let i = 0; i < this.observationsArray.length; ++i) { if (this.observationsArray[i].label === text) { this.selectedId = this.observationsArray[i].id; - this.$emit('update-selected', this.observationsArray[i]) + this.$emit('update-selected', this.observationsArray[i]); } } } else { this.selectedId = null; // Set ObservationForm's selected observation to null - this.$emit('update-selected', null) + this.$emit('update-selected', null); } }, /** @@ -112,8 +112,8 @@ export default { * * @param {number} id A chip's id. */ - isSelected: function(id) { - return this.selectedId === id + isSelected: function (id) { + return this.selectedId === id; }, /** * Returns an HTML code denoting what symbol to display in a book tag type chip. @@ -123,7 +123,7 @@ export default { * * @returns {String} An HTML code representing selections of a type. */ - displaySymbol: function(type) { + displaySymbol: function (type) { if (this.allSelectedValues[type] && this.allSelectedValues[type].length) { // ✔ - Heavy checkmark return '✔'; @@ -131,7 +131,7 @@ export default { return '•'; } } -} +}; </script> <style scoped> diff --git a/openlibrary/components/ObservationForm/components/OLChip.vue b/openlibrary/components/ObservationForm/components/OLChip.vue index e5f58967a2e..45cbfc180b7 100644 --- a/openlibrary/components/ObservationForm/components/OLChip.vue +++ b/openlibrary/components/ObservationForm/components/OLChip.vue @@ -51,7 +51,7 @@ export default { default: '' } }, - data: function() { + data: function () { return { /** * Tracks whether this chip is currently selected. @@ -59,7 +59,7 @@ export default { * @type {boolean} */ isSelected: this.selected - } + }; }, computed: { /** @@ -67,20 +67,20 @@ export default { * * @returns 'click' if this chip can be selected, otherwise `null` */ - canSelect: function() { + canSelect: function () { return this.selectable ? 'click' : null; } }, watch: { selected (newValue) { - this.isSelected = newValue + this.isSelected = newValue; } }, methods: { /** * Toggles the value of `isSelected` and fires an `update-selected` event. */ - onClick: function() { + onClick: function () { this.toggleSelected(); /** * Update selected event. @@ -88,16 +88,16 @@ export default { * @property {boolean} isSelected Selected status of this chip. * @property {String} text Main text displayed by this chip. */ - this.$emit('update-selected', this.isSelected, this.text) + this.$emit('update-selected', this.isSelected, this.text); }, /** * Toggles the state of `isSelected` */ - toggleSelected: function() { + toggleSelected: function () { this.isSelected = !this.isSelected; } } -} +}; </script> <style scoped> diff --git a/openlibrary/components/ObservationForm/components/SavedTags.vue b/openlibrary/components/ObservationForm/components/SavedTags.vue index 0d023c3d1a5..ecf79dba6dd 100644 --- a/openlibrary/components/ObservationForm/components/SavedTags.vue +++ b/openlibrary/components/ObservationForm/components/SavedTags.vue @@ -38,9 +38,9 @@ </template> <script> -import OLChip from './OLChip.vue' +import OLChip from './OLChip.vue'; -import { updateObservation } from '../ObservationService' +import { updateObservation } from '../ObservationService'; export default { @@ -80,7 +80,7 @@ export default { required: true } }, - data: function() { + data: function () { return { /** * Contains class strings for each selected book tag @@ -94,18 +94,18 @@ export default { * @type {Object} */ classLists: {} - } + }; }, computed: { /** * An array of a patron's book tags. */ - selectedValues: function() { + selectedValues: function () { const results = []; for (const type in this.allSelectedValues) { for (const value of this.allSelectedValues[type]) { - results.push(`${type}: ${value}`) + results.push(`${type}: ${value}`); } } @@ -118,8 +118,8 @@ export default { * * @param {String} chipText The text of the selected tag chip, in the form "<type>: <value>" */ - removeItem: function(chipText) { - const [type, value] = chipText.split(': ') + removeItem: function (chipText) { + const [type, value] = chipText.split(': '); const valueIndex = this.allSelectedValues[type].indexOf(value); const valueArr = this.allSelectedValues[type]; @@ -131,9 +131,9 @@ export default { }) .finally(() => { if (valueArr.length === 0) { - delete this.allSelectedValues[type] + delete this.allSelectedValues[type]; } - }) + }); // Remove hover class: this.removeHoverClass(chipText); @@ -143,7 +143,7 @@ export default { * * @param {String} value The chip's key. */ - addHoverClass: function(value) { + addHoverClass: function (value) { this.classLists[value] = 'hover'; }, /** @@ -151,8 +151,8 @@ export default { * * @param {String} value The chip's key. */ - removeHoverClass: function(value) { - this.classLists[value] = '' + removeHoverClass: function (value) { + this.classLists[value] = ''; }, /** * Returns the class list string for the chip with the given key. @@ -160,11 +160,11 @@ export default { * @param {String} value The chip's key * @returns The chip's class list string. */ - getClassList: function(value) { - return this.classLists[value] ? this.classLists[value] : '' + getClassList: function (value) { + return this.classLists[value] ? this.classLists[value] : ''; } } -} +}; </script> <style scoped> diff --git a/openlibrary/components/ObservationForm/components/ValueCard.vue b/openlibrary/components/ObservationForm/components/ValueCard.vue index 3a9ca41a81f..5fdded40018 100644 --- a/openlibrary/components/ObservationForm/components/ValueCard.vue +++ b/openlibrary/components/ObservationForm/components/ValueCard.vue @@ -16,7 +16,7 @@ </template> <script> -import CardBody from './CardBody.vue' +import CardBody from './CardBody.vue'; import CardHeader from './CardHeader.vue'; export default { @@ -54,7 +54,7 @@ export default { values: { type: Array, required: true, - validator: function(arr) { + validator: function (arr) { for (const item of arr) { if (typeof(item) !== 'string') { return false; @@ -94,7 +94,7 @@ export default { required: true } }, -} +}; </script> <style scoped> diff --git a/openlibrary/components/dev/vite.config.js b/openlibrary/components/dev/vite.config.js index ea39d477e95..33b433e4f2c 100644 --- a/openlibrary/components/dev/vite.config.js +++ b/openlibrary/components/dev/vite.config.js @@ -2,10 +2,10 @@ This is the config used for the dev server ala `npm run serve` This does not effect production builds */ -import { defineConfig } from 'vite' -import vue from '@vitejs/plugin-vue' +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; // https://vite.dev/config/ export default defineConfig({ plugins: [vue()], -}) +}); diff --git a/openlibrary/plugins/openlibrary/js/book-page-lists.js b/openlibrary/plugins/openlibrary/js/book-page-lists.js index eb16d7c0bf8..af8c44bb966 100644 --- a/openlibrary/plugins/openlibrary/js/book-page-lists.js +++ b/openlibrary/plugins/openlibrary/js/book-page-lists.js @@ -1,86 +1,86 @@ -import { buildPartialsUrl } from './utils' -import { initAsyncFollowing } from './following' +import { buildPartialsUrl } from './utils'; +import { initAsyncFollowing } from './following'; /** * Initializes lazy-loading the "Lists" section of Open Library book pages. * * @param elem {HTMLElement} Container for book page lists section */ -export function initListsSection(elem) { +export function initListsSection (elem) { // Show loading indicator - const loadingIndicator = elem.querySelector('.loadingIndicator') - loadingIndicator.classList.remove('hidden') + const loadingIndicator = elem.querySelector('.loadingIndicator'); + loadingIndicator.classList.remove('hidden'); - const ids = JSON.parse(elem.dataset.ids) + const ids = JSON.parse(elem.dataset.ids); const intersectionObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { // Unregister intersection listener - intersectionObserver.unobserve(entries[0].target) + intersectionObserver.unobserve(entries[0].target); fetchPartials(ids.work, ids.edition) .then((resp) => { // Check response code, continue if not 4XX or 5XX - return resp.json() + return resp.json(); }) .then((data) => { // Replace loading indicator with partials - const listSection = loadingIndicator.parentElement - const fragment = document.createDocumentFragment() + const listSection = loadingIndicator.parentElement; + const fragment = document.createDocumentFragment(); for (const htmlString of data.partials) { - const template = document.createElement('template') - template.innerHTML = htmlString - fragment.append(...template.content.childNodes) + const template = document.createElement('template'); + template.innerHTML = htmlString; + fragment.append(...template.content.childNodes); } - listSection.replaceChildren(fragment) + listSection.replaceChildren(fragment); // Show "See All" link if (data.hasLists) { - const showAllLink = elem.querySelector('.lists-heading a') + const showAllLink = elem.querySelector('.lists-heading a'); if (showAllLink) { - showAllLink.classList.remove('hidden') + showAllLink.classList.remove('hidden'); } } // Initialize private buttons after content is loaded - initPrivateButtonsAfterLoad(listSection) + initPrivateButtonsAfterLoad(listSection); const followForms = listSection.querySelectorAll('.follow-form'); - initAsyncFollowing(followForms) - }) + initAsyncFollowing(followForms); + }); } - }) + }); }, { root: null, rootMargin: '200px', threshold: 0 - }) + }); - intersectionObserver.observe(elem) + intersectionObserver.observe(elem); } /** * Initialize private buttons after the lists section has been loaded * @param {HTMLElement} container - The container that now has the loaded content */ -function initPrivateButtonsAfterLoad(container) { - const privateButtons = container.querySelectorAll('.list-follow-card__private-button') +function initPrivateButtonsAfterLoad (container) { + const privateButtons = container.querySelectorAll('.list-follow-card__private-button'); if (privateButtons.length > 0) { import(/* webpackChunkName: "private-buttons" */ './private-button') .then(module => { - module.initPrivateButtons(privateButtons) - }) + module.initPrivateButtons(privateButtons); + }); } } -async function fetchPartials(workId, editionId) { - const params = {} +async function fetchPartials (workId, editionId) { + const params = {}; if (workId) { - params.workId = workId + params.workId = workId; } if (editionId) { - params.editionId = editionId + params.editionId = editionId; } return fetch(buildPartialsUrl('BPListsSection', params)); diff --git a/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js b/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js index 35333ed4378..2126881bf4a 100644 --- a/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js +++ b/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js @@ -3,12 +3,12 @@ * * @param {NodeList<HTMLElement>} crumbs - NodeList of breadcrumb select elements. */ -export function initBreadcrumbSelect(crumbs) { +export function initBreadcrumbSelect (crumbs) { const allowedKeys = new Set(['Tab', 'Enter', ' ']); const preventedKeys = new Set(['ArrowUp', 'ArrowDown']); // watch crumbs for changes, // ensures it's a full value change, not a user exploring options via keyboard - function handleNavEvents(nav) { + function handleNavEvents (nav) { let ignoreChange = false; nav.addEventListener('change', () => { diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js index a2280150267..8f8a7e17bfd 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js @@ -7,7 +7,7 @@ const classTypeSuffixes = { subject_places: '--place', subject_times: '--time', collections: '--collection' -} +}; /** * @typedef OptionState @@ -25,7 +25,7 @@ export const MenuOptionState = { NONE_TAGGED: 0, SOME_TAGGED: 1, ALL_TAGGED: 2, -} +}; export class MenuOption { @@ -38,7 +38,7 @@ export class MenuOption { * @param {OptionState} optionState * @param {Number} taggedWorksCount Number of selected works which have the given tag */ - constructor(tag, optionState, taggedWorksCount) { + constructor (tag, optionState, taggedWorksCount) { /** * Reference to the root element of this MenuOption. * @@ -46,7 +46,7 @@ export class MenuOption { * @member {HTMLElement} * @see {initialize} */ - this.rootElement + this.rootElement; /** * Copy of the tag which is represented by this menu option. @@ -54,7 +54,7 @@ export class MenuOption { * @member {Tag} * @readonly */ - this.tag = tag + this.tag = tag; /** * Represents the amount of selected works that share this tag. @@ -64,14 +64,14 @@ export class MenuOption { * * @member {OptionState} */ - this.optionState = optionState + this.optionState = optionState; /** * Tracks number of selected works which have this tag. * * @member {Number} */ - this.taggedWorksCount = taggedWorksCount + this.taggedWorksCount = taggedWorksCount; } /** @@ -80,8 +80,8 @@ export class MenuOption { * Must be called before an event handler can be attached to * this menu option */ - initialize() { - this.createMenuOption() + initialize () { + this.createMenuOption(); } /** @@ -90,38 +90,38 @@ export class MenuOption { * Stores newly created element as `rootElement`. The new element is not * attached to the DOM, and does not yet have any attached event handlers. */ - createMenuOption() { - const parentElem = document.createElement('div') - parentElem.classList.add('selected-tag') + createMenuOption () { + const parentElem = document.createElement('div'); + parentElem.classList.add('selected-tag'); - let bemSuffix = '' + let bemSuffix = ''; switch (this.optionState) { case MenuOptionState.NONE_TAGGED: - bemSuffix = 'none-tagged' - break + bemSuffix = 'none-tagged'; + break; case MenuOptionState.SOME_TAGGED: - bemSuffix = 'some-tagged' - break + bemSuffix = 'some-tagged'; + break; case MenuOptionState.ALL_TAGGED: - bemSuffix = 'all-tagged' - break + bemSuffix = 'all-tagged'; + break; } const markup = `<span class="selected-tag__status selected-tag__status--${bemSuffix}"></span> <span class="selected-tag__name">${this.tag.tagName}</span> <span class="selected-tag__type-container"> <span class="selected-tag__type selected-tag__type${classTypeSuffixes[this.tag.tagType]}">${this.tag.displayType}</span> - </span>` + </span>`; - parentElem.innerHTML = markup - this.rootElement = parentElem + parentElem.innerHTML = markup; + this.rootElement = parentElem; } /** * Removes this MenuOption from the DOM. */ - remove() { - this.rootElement.remove() + remove () { + this.rootElement.remove(); } /** @@ -134,29 +134,29 @@ export class MenuOption { * @see {@link MenuOptionState} * @see {initialize} */ - updateMenuOptionState(menuOptionState) { + updateMenuOptionState (menuOptionState) { if (this.rootElement) { // `rootElement` not set until `initialize` is called - this.optionState = menuOptionState - const statusIndicator = this.rootElement.querySelector('.selected-tag__status') + this.optionState = menuOptionState; + const statusIndicator = this.rootElement.querySelector('.selected-tag__status'); switch (menuOptionState) { case MenuOptionState.NONE_TAGGED: - statusIndicator.classList.remove('selected-tag__status--all-tagged', 'selected-tag__status--some-tagged') - statusIndicator.classList.add('selected-tag__status--none-tagged') + statusIndicator.classList.remove('selected-tag__status--all-tagged', 'selected-tag__status--some-tagged'); + statusIndicator.classList.add('selected-tag__status--none-tagged'); break; case MenuOptionState.SOME_TAGGED: - statusIndicator.classList.remove('selected-tag__status--all-tagged', 'selected-tag__status--none-tagged') - statusIndicator.classList.add('selected-tag__status--some-tagged') + statusIndicator.classList.remove('selected-tag__status--all-tagged', 'selected-tag__status--none-tagged'); + statusIndicator.classList.add('selected-tag__status--some-tagged'); break; case MenuOptionState.ALL_TAGGED: - statusIndicator.classList.remove('selected-tag__status--none-tagged', 'selected-tag__status--some-tagged') - statusIndicator.classList.add('selected-tag__status--all-tagged') + statusIndicator.classList.remove('selected-tag__status--none-tagged', 'selected-tag__status--some-tagged'); + statusIndicator.classList.add('selected-tag__status--all-tagged'); break; default: // XXX : `optionState` is now incorrect - throw new Error('Unexpected value passed for menu option state.') + throw new Error('Unexpected value passed for menu option state.'); } } else { - throw new Error('MenuOption must be initialized before state can be updated.') + throw new Error('MenuOption must be initialized before state can be updated.'); } } @@ -165,22 +165,22 @@ export class MenuOption { * * Fires an `option-hidden` event when this is called. */ - hide() { - this.rootElement.classList.add('hidden') - this.rootElement.dispatchEvent(new CustomEvent('option-hidden')) + hide () { + this.rootElement.classList.add('hidden'); + this.rootElement.dispatchEvent(new CustomEvent('option-hidden')); } /** * Shows this menu option. */ - show() { - this.rootElement.classList.remove('hidden') + show () { + this.rootElement.classList.remove('hidden'); } /** * Stages the selected menu option. */ - stage() { + stage () { this.rootElement.classList.add('selected-tag--staged'); } } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js index 22ebcb29908..889b545626d 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js @@ -18,9 +18,9 @@ export class SortedMenuOptionContainer { * * @param {HTMLElement} element The container */ - constructor(element) { - this.rootElement = element - this.sortedMenuOptions = [] + constructor (element) { + this.rootElement = element; + this.sortedMenuOptions = []; } /** @@ -28,11 +28,11 @@ export class SortedMenuOptionContainer { * * @param {...MenuOption} menuOptions Menu options to be added to the container. */ - add(...menuOptions) { + add (...menuOptions) { for (const option of menuOptions) { - const index = this.findIndex(option) - this.sortedMenuOptions.splice(index, 0, option) - this.updateViewOnAdd(option, index) + const index = this.findIndex(option); + this.sortedMenuOptions.splice(index, 0, option); + this.updateViewOnAdd(option, index); } } @@ -42,12 +42,12 @@ export class SortedMenuOptionContainer { * @param {MenuOption} menuOption The option being attached to the DOM. * @param {Number} index The index where the given option will be inserted. */ - updateViewOnAdd(menuOption, index) { + updateViewOnAdd (menuOption, index) { if (index === 0) { - this.rootElement.prepend(menuOption.rootElement) + this.rootElement.prepend(menuOption.rootElement); } else { - const sibling = this.rootElement.children[index - 1] - sibling.insertAdjacentElement('afterend', menuOption.rootElement) + const sibling = this.rootElement.children[index - 1]; + sibling.insertAdjacentElement('afterend', menuOption.rootElement); } } @@ -56,11 +56,11 @@ export class SortedMenuOptionContainer { * * @param {...MenuOption} menuOptions Options that are to be removed from this container */ - remove(...menuOptions) { + remove (...menuOptions) { for (const option of menuOptions) { - const index = this.findIndex(option) - const removed = this.sortedMenuOptions.splice(index, 1) - removed.forEach((option) => option.remove()) + const index = this.findIndex(option); + const removed = this.sortedMenuOptions.splice(index, 1); + removed.forEach((option) => option.remove()); } } @@ -71,26 +71,26 @@ export class SortedMenuOptionContainer { * @param {MenuOption} menuOption * @returns {Number} Index where the given menu option should be inserted. */ - findIndex(menuOption) { - let index = 0 + findIndex (menuOption) { + let index = 0; // XXX : Binary search? while (index < this.sortedMenuOptions.length) { - const currentMenuOption = this.sortedMenuOptions[index] + const currentMenuOption = this.sortedMenuOptions[index]; if (currentMenuOption.tag.tagName.toLowerCase() === menuOption.tag.tagName.toLowerCase()) { // Compare types if (currentMenuOption.tag.tagType.toLowerCase() >= menuOption.tag.tagType.toLowerCase()) { - return index + return index; } } else if (currentMenuOption.tag.tagName.toLowerCase() > menuOption.tag.tagName.toLowerCase()) { - return index + return index; } - ++index + ++index; } - return index + return index; } /** @@ -99,8 +99,8 @@ export class SortedMenuOptionContainer { * @param {MenuOption} menuOption The object that we are searching for * @returns {boolean} `true` if a matching menu option exists in this container */ - contains(menuOption) { - return this.sortedMenuOptions.some((option) => menuOption.tag.equals(option.tag)) + contains (menuOption) { + return this.sortedMenuOptions.some((option) => menuOption.tag.equals(option.tag)); } /** @@ -109,8 +109,8 @@ export class SortedMenuOptionContainer { * @param {Tag} tag * @returns {boolean} `true` if a menu option which represents the given tag is in this container. */ - containsOptionWithTag(tag) { - return this.sortedMenuOptions.some((option) => tag.equals(option.tag)) + containsOptionWithTag (tag) { + return this.sortedMenuOptions.some((option) => tag.equals(option.tag)); } /** @@ -119,17 +119,17 @@ export class SortedMenuOptionContainer { * @param {Tag} tag * @returns {MenuOption|undefined} The first matching menu option, or `undefined` if none were found. */ - findByTag(tag) { - return this.sortedMenuOptions.find((option) => tag.equals(option.tag)) + findByTag (tag) { + return this.sortedMenuOptions.find((option) => tag.equals(option.tag)); } /** * Removes all menu options from this container. */ - clear() { + clear () { while (this.sortedMenuOptions.length > 0) { - this.sortedMenuOptions.pop() + this.sortedMenuOptions.pop(); } - this.rootElement.innerHTML = '' + this.rootElement.innerHTML = ''; } } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js index 6da43278fd3..bdffeaf38f8 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js @@ -3,7 +3,7 @@ * * @returns HTML for the bulk tagging form */ -export function renderBulkTagger() { +export function renderBulkTagger () { return `<form action="/tags/bulk_tag_works" method="post" class="bulk-tagging-form hidden"> <div class="form-header"> <p>Manage Subjects</p> @@ -36,5 +36,5 @@ export function renderBulkTagger() { <div class="submit-tags-section"> <button type="submit" class="bulk-tagging-submit cta-btn cta-btn--primary" disabled>Submit</button> </div> - </form>` + </form>`; } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js index aa3bb1887b3..ae35c571df3 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js @@ -8,7 +8,7 @@ const displayTypeMapping = { subject_places: 'place', subject_times: 'time', collections: 'collection', -} +}; /** * Maps UI-ready subject types to their corresponding @@ -20,7 +20,7 @@ export const subjectTypeMapping = { place: 'subject_places', time: 'subject_times', collection: 'collections' -} +}; /** * Compare function for determining the order of two tags. @@ -33,25 +33,25 @@ export const subjectTypeMapping = { * @returns {Number} * @see {Array.sort} */ -export function compare(tagA, tagB) { - const lowerA = createComparableTag(tagA) - const lowerB = createComparableTag(tagB) +export function compare (tagA, tagB) { + const lowerA = createComparableTag(tagA); + const lowerB = createComparableTag(tagB); if (lowerA.tagName < lowerB.tagName) { - return -1 + return -1; } else if (lowerA.tagName > lowerB.tagName) { - return 1 + return 1; } else { if (lowerA.tagType < lowerB.tagType) { - return -1 + return -1; } else if (lowerA.tagType > lowerB.tagtype) { - return 1 + return 1; } } - return 0 + return 0; } /** @@ -64,11 +64,11 @@ export function compare(tagA, tagB) { * @returns {Object} Tag-like object that is suitable to use for sorting comparisons. * @see {compare} */ -function createComparableTag(tag) { +function createComparableTag (tag) { return { tagName: tag.tagName.toLowerCase(), tagType: tag.tagType.toLowerCase() - } + }; } /** @@ -90,13 +90,13 @@ export class Tag { * * @throws Will throw an error if both `tagType` and `displayType` are falsey */ - constructor(tagName, tagType = null, displayType = null) { + constructor (tagName, tagType = null, displayType = null) { if (!(tagType || displayType)) { - throw new Error('Tag must have at least one type') + throw new Error('Tag must have at least one type'); } - this.tagName = tagName - this.tagType = tagType || this.convertToType(displayType) - this.displayType = displayType || this.convertToDisplayType(tagType) + this.tagName = tagName; + this.tagType = tagType || this.convertToType(displayType); + this.displayType = displayType || this.convertToDisplayType(tagType); } /** @@ -107,12 +107,12 @@ export class Tag { * @returns {String} The corresponding technical tag type * @throws Will throw an error if the given type is unrecognized. */ - convertToType(displayType) { - const result = subjectTypeMapping[displayType] + convertToType (displayType) { + const result = subjectTypeMapping[displayType]; if (!result) { - throw new Error('Unrecognized `displayType` value') + throw new Error('Unrecognized `displayType` value'); } - return result + return result; } /** @@ -123,12 +123,12 @@ export class Tag { * @returns {String} A type string that can be displayed in the UI * @throws Will throw an error if the given type is unrecognized */ - convertToDisplayType(tagType) { - const result = displayTypeMapping[tagType] + convertToDisplayType (tagType) { + const result = displayTypeMapping[tagType]; if (!result) { - throw new Error('Unrecognized `tagType` value') + throw new Error('Unrecognized `tagType` value'); } - return result + return result; } /** @@ -140,10 +140,10 @@ export class Tag { * @param {Tag} tag * @returns `true` if the given tag is considered equivalent to this tag. */ - equals(tag) { - const lowerSelf = createComparableTag(this) - const lowerTag = createComparableTag(tag) + equals (tag) { + const lowerSelf = createComparableTag(this); + const lowerTag = createComparableTag(tag); - return lowerSelf.tagName === lowerTag.tagName && lowerSelf.tagType === lowerTag.tagType + return lowerSelf.tagName === lowerTag.tagName && lowerSelf.tagType === lowerTag.tagType; } } diff --git a/openlibrary/plugins/openlibrary/js/clampers.js b/openlibrary/plugins/openlibrary/js/clampers.js index 26e1faadd65..673b4f5c48f 100644 --- a/openlibrary/plugins/openlibrary/js/clampers.js +++ b/openlibrary/plugins/openlibrary/js/clampers.js @@ -2,7 +2,7 @@ * @param {NodeListOf<Element>} clampers * */ -export function initClampers(clampers) { +export function initClampers (clampers) { for (const clamper of clampers) { if (clamper.clientHeight === clamper.scrollHeight) { clamper.classList.remove('clamp'); @@ -18,14 +18,14 @@ export function initClampers(clampers) { return; } - clamper.style.display = clamper.style.display === '-webkit-box' || clamper.style.display === '' ? 'unset' : '-webkit-box' + clamper.style.display = clamper.style.display === '-webkit-box' || clamper.style.display === '' ? 'unset' : '-webkit-box'; if (clamper.getAttribute('data-before') === '\u25BE ') { - clamper.setAttribute('data-before', '\u25B8 ') + clamper.setAttribute('data-before', '\u25B8 '); } else { - clamper.setAttribute('data-before', '\u25BE ') + clamper.setAttribute('data-before', '\u25BE '); } - }) + }); } } } diff --git a/openlibrary/plugins/openlibrary/js/compact-title/index.js b/openlibrary/plugins/openlibrary/js/compact-title/index.js index cbf8d1b7e0b..91039cb0ef5 100644 --- a/openlibrary/plugins/openlibrary/js/compact-title/index.js +++ b/openlibrary/plugins/openlibrary/js/compact-title/index.js @@ -25,13 +25,13 @@ let mainTitleElem; * @param {HTMLElement} navbar The book page navbar component * @param {HTMLElement} title The compact title component */ -export function initCompactTitle(navbar, title) { - mainTitleElem = document.querySelector('.work-title-and-author.desktop .work-title') +export function initCompactTitle (navbar, title) { + mainTitleElem = document.querySelector('.work-title-and-author.desktop .work-title'); // Show compact title on page reload: onScroll(navbar, title); // And update on scroll - window.addEventListener('scroll', function() { - onScroll(navbar, title) + window.addEventListener('scroll', function () { + onScroll(navbar, title); }); } @@ -44,37 +44,37 @@ export function initCompactTitle(navbar, title) { * @param {HTMLElement} navbar The book page navbar component * @param {HTMLElement} title The compact title component */ -function onScroll(navbar, title) { - const compactTitleBounds = title.getBoundingClientRect() - const navbarBounds = navbar.getBoundingClientRect() - const mainTitleBounds = mainTitleElem.getBoundingClientRect() +function onScroll (navbar, title) { + const compactTitleBounds = title.getBoundingClientRect(); + const navbarBounds = navbar.getBoundingClientRect(); + const mainTitleBounds = mainTitleElem.getBoundingClientRect(); if (mainTitleBounds.bottom < navbarBounds.bottom) { // The main title is off-screen if (!navbar.classList.contains('sticky--lowest')) { // Compact title not displayed // Display compact title - title.classList.remove('hidden') + title.classList.remove('hidden'); // Animate navbar $(navbar).addClass('nav-bar-wrapper--slidedown') .one('animationend', () => { - $(navbar).addClass('sticky--lowest') - $(navbar).removeClass('nav-bar-wrapper--slidedown') + $(navbar).addClass('sticky--lowest'); + $(navbar).removeClass('nav-bar-wrapper--slidedown'); // Ensure correct nav item is selected after compact title slides in: - updateSelectedNavItem() - }) + updateSelectedNavItem(); + }); } else { if (navbarBounds.top < compactTitleBounds.bottom) { // We've scrolled to the bottom of the container, and the navbar is unstuck - title.classList.add('hidden') + title.classList.add('hidden'); } else { - title.classList.remove('hidden') + title.classList.remove('hidden'); } } } else { // At least some of the main title is below the navbar if (!title.classList.contains('hidden')) { - title.classList.add('hidden') + title.classList.add('hidden'); $(navbar).addClass('nav-bar-wrapper--slideup') .one('animationend', () => { - $(navbar).removeClass('sticky--lowest') - $(navbar).removeClass('nav-bar-wrapper--slideup') - }) + $(navbar).removeClass('sticky--lowest'); + $(navbar).removeClass('nav-bar-wrapper--slideup'); + }); } } } diff --git a/openlibrary/plugins/openlibrary/js/covers.js b/openlibrary/plugins/openlibrary/js/covers.js index c208baa4bfc..7224649c1ce 100644 --- a/openlibrary/plugins/openlibrary/js/covers.js +++ b/openlibrary/plugins/openlibrary/js/covers.js @@ -8,7 +8,7 @@ import 'jquery-ui-touch-punch'; // this makes drag-to-reorder work on touch devi import { closePopup } from './utils'; //cover/change.html -export function initCoversChange() { +export function initCoversChange () { // Pull data from data-config of class "manageCovers" in covers/manage.html const data_config_json = $('.manageCovers').data('config'); const doc_type_key = data_config_json['key']; @@ -38,14 +38,14 @@ export function initCoversChange() { }); } -function add_iframe(selector, src) { +function add_iframe (selector, src) { $(selector) .append('<iframe frameborder="0" height="580" width="580" marginheight="0" marginwidth="0" scrolling="auto"></iframe>') .find('iframe') .attr('src', src); } -function showLoadingIndicator() { +function showLoadingIndicator () { const loadingIndicator = document.querySelector('.loadingIndicator'); const formDivs = document.querySelectorAll('.ol-cover-form, .imageIntro'); @@ -56,8 +56,8 @@ function showLoadingIndicator() { } // covers/manage.html and covers/add.html -export function initCoversAddManage() { - $('.ol-cover-form').on('submit', function() { +export function initCoversAddManage () { + $('.ol-cover-form').on('submit', function () { showLoadingIndicator(); }); @@ -73,7 +73,7 @@ export function initCoversAddManage() { // covers/saved.html // Uses parent.$ in place of $ where elements lie outside of the "saved" window -export function initCoversSaved() { +export function initCoversSaved () { // Save the new image // Pull data from data-config of class "imageSaved" in covers/saved.html const data_config_json = parent.$('.manageCovers').data('config'); @@ -117,7 +117,7 @@ export function initCoversSaved() { } // This function will be triggered when the user clicks the "Paste" button -async function pasteImage() { +async function pasteImage () { let formData = null; try { const clipboardItems = await navigator.clipboard.read(); @@ -131,9 +131,9 @@ async function pasteImage() { const blob = await item.getType(mimeType); const image = document.createElement('img'); image.src = URL.createObjectURL(blob); - image.alt = '' - const imageContainer = document.querySelector('.image-container') - imageContainer.replaceChildren(image) + image.alt = ''; + const imageContainer = document.querySelector('.image-container'); + imageContainer.replaceChildren(image); // Update the global formData with the new image blob formData = new FormData(); @@ -148,7 +148,7 @@ async function pasteImage() { // Show the upload button const uploadButton = document.getElementById('uploadButtonPaste'); - uploadButton.classList.remove('hidden') + uploadButton.classList.remove('hidden'); return formData; } @@ -158,13 +158,13 @@ async function pasteImage() { } } -export function initPasteForm(coverForm) { +export function initPasteForm (coverForm) { const pasteButton = coverForm.querySelector('#pasteButton'); let formData = null; pasteButton.addEventListener('click', async () => { formData = await pasteImage(coverForm); - pasteButton.textContent = 'Change Image' + pasteButton.textContent = 'Change Image'; }); coverForm.addEventListener('submit', (event) => { diff --git a/openlibrary/plugins/openlibrary/js/dropper/Dropper.js b/openlibrary/plugins/openlibrary/js/dropper/Dropper.js index e2caa919f1e..d841c0ecefa 100644 --- a/openlibrary/plugins/openlibrary/js/dropper/Dropper.js +++ b/openlibrary/plugins/openlibrary/js/dropper/Dropper.js @@ -29,13 +29,13 @@ export class Dropper { * * @param {HTMLElement} dropper Reference to the dropper's root element */ - constructor(dropper) { + constructor (dropper) { /** * References the root element of the dropper. * * @member {HTMLElement} */ - this.dropper = dropper + this.dropper = dropper; /** * jQuery object containing the root element of the dropper. @@ -46,7 +46,7 @@ export class Dropper { * * @member {JQuery<HTMLElement>} */ - this.$dropper = $(dropper) + this.$dropper = $(dropper); /** * Reference to the affordance that, when clicked, toggles @@ -54,14 +54,14 @@ export class Dropper { * * @member {HTMLElement} */ - this.dropClick = dropper.querySelector('.generic-dropper__dropclick') + this.dropClick = dropper.querySelector('.generic-dropper__dropclick'); /** * Tracks the current "Open" state of this dropper. * * @member {boolean} */ - this.isDropperOpen = dropper.classList.contains('generic-dropper-wrapper--active') + this.isDropperOpen = dropper.classList.contains('generic-dropper-wrapper--active'); /** * Tracks whether this dropper is disabled. @@ -70,16 +70,16 @@ export class Dropper { * * @member {boolean} */ - this.isDropperDisabled = dropper.classList.contains('generic-dropper--disabled') + this.isDropperDisabled = dropper.classList.contains('generic-dropper--disabled'); } /** * Adds click listener to dropper's toggle arrow. */ - initialize() { + initialize () { this.dropClick.addEventListener('click', () => { - this.toggleDropper() - }) + this.toggleDropper(); + }); } /** @@ -88,7 +88,7 @@ export class Dropper { * Subclasses of `Dropper` may override this to add * functionality that should occur on dropper open. */ - onOpen() {} + onOpen () {} /** * Function that is called after a dropper has closed. @@ -96,7 +96,7 @@ export class Dropper { * Subclasses of `Dropper` may override this to add * functionality that should occur on dropper close. */ - onClose() {} + onClose () {} /** * Function that is called when the drop-click affordance of @@ -104,7 +104,7 @@ export class Dropper { * * Subclasses of `Dropper` may override this as needed. */ - onDisabledClick() {} + onDisabledClick () {} /** * Closes dropper if opened; opens dropper if closed. @@ -115,19 +115,19 @@ export class Dropper { * Calls either `onOpen()` or `onClose()` after the dropper * has been toggled. */ - toggleDropper() { + toggleDropper () { if (this.isDropperDisabled) { this.onDisabledClick(); } else { this.$dropper.find('.generic-dropper__dropdown').slideToggle(25); - this.$dropper.find('.arrow').toggleClass('up') - this.$dropper.toggleClass('generic-dropper-wrapper--active') - this.isDropperOpen = !this.isDropperOpen + this.$dropper.find('.arrow').toggleClass('up'); + this.$dropper.toggleClass('generic-dropper-wrapper--active'); + this.isDropperOpen = !this.isDropperOpen; if (this.isDropperOpen) { - this.onOpen() + this.onOpen(); } else { - this.onClose() + this.onClose(); } } } @@ -140,16 +140,16 @@ export class Dropper { * Calls `onDisabledClick()` if this dropper is disabled. * Otherwise, closes dropper and calls `onClose()`. */ - closeDropper() { + closeDropper () { if (this.isDropperDisabled) { this.onDisabledClick(); } else { - this.$dropper.find('.generic-dropper__dropdown').slideUp(25) + this.$dropper.find('.generic-dropper__dropdown').slideUp(25); this.$dropper.find('.arrow').removeClass('up'); - this.$dropper.removeClass('generic-dropper-wrapper--active') - this.isDropperOpen = false + this.$dropper.removeClass('generic-dropper-wrapper--active'); + this.isDropperOpen = false; - this.onClose() + this.onClose(); } } } diff --git a/openlibrary/plugins/openlibrary/js/dropper/index.js b/openlibrary/plugins/openlibrary/js/dropper/index.js index 62ff4c52c59..eac18e6db8c 100644 --- a/openlibrary/plugins/openlibrary/js/dropper/index.js +++ b/openlibrary/plugins/openlibrary/js/dropper/index.js @@ -4,33 +4,33 @@ import { debounce } from '../nonjquery_utils'; * Holds references to each dropper on a page. * @type {Array<HTMLElement>} */ -const droppers = [] +const droppers = []; /** * Adds expand and collapse functionality to our droppers. * * @param {HTMLCollection<HTMLElement>} dropperElements */ -export function initDroppers(dropperElements) { +export function initDroppers (dropperElements) { for (const dropper of dropperElements) { - droppers.push(dropper) + droppers.push(dropper); - $(dropper).on('click', '.dropclick', debounce(function() { + $(dropper).on('click', '.dropclick', debounce(function () { $(this).next('.dropdown').slideToggle(25); $(this).parent().next('.dropdown').slideToggle(25); $(this).parent().find('.arrow').toggleClass('up'); - }, 300, false)) + }, 300, false)); - $(dropper).on('click', '.dropper__close', debounce(function() { - closeDropper($(dropper)) - }, 300, false)) + $(dropper).on('click', '.dropper__close', debounce(function () { + closeDropper($(dropper)); + }, 300, false)); } // Close any open dropdown list if the user clicks outside of component: - $(document).on('click', function(event) { + $(document).on('click', function (event) { for (const dropper of droppers) { if (!dropper.contains(event.target)) { - closeDropper($(dropper)) + closeDropper($(dropper)); } } }); @@ -40,11 +40,11 @@ export function initDroppers(dropperElements) { * close an open dropdown in a given container * @param {jQuery.Object} $container */ -function closeDropper($container) { +function closeDropper ($container) { $container.find('.dropdown').slideUp(25); // Legacy droppers - $container.find('.generic-dropper__dropdown').slideUp(25) // New generic droppers + $container.find('.generic-dropper__dropdown').slideUp(25); // New generic droppers $container.find('.arrow').removeClass('up'); - $container.removeClass('generic-dropper-wrapper--active') + $container.removeClass('generic-dropper-wrapper--active'); } /** @@ -56,14 +56,14 @@ function closeDropper($container) { * * @param {NodeList<HTMLElement>} dropperElements */ -export function initGenericDroppers(dropperElements) { - const genericDroppers = Array.from(dropperElements) +export function initGenericDroppers (dropperElements) { + const genericDroppers = Array.from(dropperElements); // Close any open dropdown if the user clicks outside of component: - $(document).on('click', function(event) { + $(document).on('click', function (event) { for (const dropper of genericDroppers) { if (!dropper.contains(event.target)) { - closeDropper($(dropper)) + closeDropper($(dropper)); } } }); diff --git a/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js b/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js index f83406c7cf2..89f135e3772 100644 --- a/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js +++ b/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js @@ -8,63 +8,63 @@ export default class EdtionNavBar { * * @param {HTMLElement} navbarWrapper */ - constructor(navbarWrapper) { + constructor (navbarWrapper) { /** * Reference to the parent element of the navbar. * @type {HTMLElement} */ - this.navbarWrapper = navbarWrapper + this.navbarWrapper = navbarWrapper; /** * The navbar * @type {HTMLElement} */ - this.navbarElem = navbarWrapper.querySelector('.work-menu') + this.navbarElem = navbarWrapper.querySelector('.work-menu'); /** * The mobile-only navigation arrow. Not guaranteed to exist. * @type {HTMLElement|null} */ - this.navArrowLeft = navbarWrapper.querySelector('.nav-arrow-left') + this.navArrowLeft = navbarWrapper.querySelector('.nav-arrow-left'); /** * The mobile-only navigation arrow. Not guaranteed to exist. * @type {HTMLElement|null} */ - this.navArrowRight = navbarWrapper.querySelector('.nav-arrow-right') + this.navArrowRight = navbarWrapper.querySelector('.nav-arrow-right'); /** * References each nav item in this navbar. * @type {Array<HTMLLIElement>} */ - this.navItems = Array.from(this.navbarElem.querySelectorAll('li')) + this.navItems = Array.from(this.navbarElem.querySelectorAll('li')); /** * Index of the currently selected nav item. * @type {number} */ - this.selectedIndex = 0 + this.selectedIndex = 0; /** * The nav items' target anchor elements. * @type {HTMLAnchorElement} */ - this.targetAnchors = [] + this.targetAnchors = []; - this.initialize() + this.initialize(); } /** * Adds the necessary event handlers to the navbar. */ - initialize() { + initialize () { // Add click listeners to navbar items: for (let i = 0; i < this.navItems.length; ++i) { this.navItems[i].addEventListener('click', () => { - this.selectedIndex = i - this.selectElement(this.navItems[i]) - }) + this.selectedIndex = i; + this.selectElement(this.navItems[i]); + }); // Add this nav item's target anchor to array: - this.targetAnchors.push(document.getElementById(this.navItems[i].children[0].hash.substring(1))) + this.targetAnchors.push(document.getElementById(this.navItems[i].children[0].hash.substring(1))); // Set selectedIndex to the correct value: if (this.navItems[i].classList.contains('selected')) { - this.selectedIndex = i + this.selectedIndex = i; } } @@ -72,43 +72,43 @@ export default class EdtionNavBar { if (this.navArrowLeft) { this.navArrowLeft.addEventListener('click', () => { if (this.selectedIndex > 0) { - --this.selectedIndex - this.navItems[this.selectedIndex].children[0].click() + --this.selectedIndex; + this.navItems[this.selectedIndex].children[0].click(); } - }) + }); } if (this.navArrowRight) { this.navArrowRight.addEventListener('click', () => { if (this.selectedIndex < this.navItems.length - 1) { // Simulate click on the next nav item: - ++this.selectedIndex - this.navItems[this.selectedIndex].children[0].click() + ++this.selectedIndex; + this.navItems[this.selectedIndex].children[0].click(); } - }) + }); } // Add scroll listener for position-aware nav item selection document.addEventListener('scroll', () => { - this.updateSelected() - }) + this.updateSelected(); + }); } /** * Determines this navbar's position on the page and updates the selected * nav item. */ - updateSelected() { - const navbarHeight = this.navbarWrapper.getBoundingClientRect().height + updateSelected () { + const navbarHeight = this.navbarWrapper.getBoundingClientRect().height; if (navbarHeight > 0) { - let i = this.navItems.length + let i = this.navItems.length; // 10 is for a little bit of padding while (--i > 0 && this.navbarWrapper.offsetTop + navbarHeight < (this.targetAnchors[i].offsetTop - 10)) { // Do nothing } if (i !== this.selectedIndex) { - this.selectedIndex = i - this.selectElement(this.navItems[i]) + this.selectedIndex = i; + this.selectElement(this.navItems[i]); } } } @@ -118,7 +118,7 @@ export default class EdtionNavBar { * * @param {HTMLElement} selectedItem Newly selected nav item */ - scrollNavbar(selectedItem) { + scrollNavbar (selectedItem) { // Note: We don't use the browser native scrollIntoView method because // that method scrolls _recursively_, so it also tries to scroll the // body to center the element on the screen, causing weird jitters. @@ -126,7 +126,7 @@ export default class EdtionNavBar { this.navbarElem.scrollTo({ left: selectedItem.offsetLeft - (this.navbarElem.clientWidth - selectedItem.offsetWidth) / 2, behavior: 'instant' - }) + }); } /** @@ -137,11 +137,11 @@ export default class EdtionNavBar { * * @param {HTMLLIElement} selectedElem Element corresponding to the 'selected' navbar item. */ - selectElement(selectedElem) { + selectElement (selectedElem) { for (const li of this.navItems) { - li.classList.remove('selected') + li.classList.remove('selected'); } - selectedElem.classList.add('selected') - this.scrollNavbar(selectedElem) + selectedElem.classList.add('selected'); + this.scrollNavbar(selectedElem); } } diff --git a/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js b/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js index dcccf4c477c..8b36a5766c1 100644 --- a/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js +++ b/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js @@ -4,17 +4,17 @@ import EdtionNavBar from './EditionNavBar'; * Holds references to each book page navbar. * @type {Array<EditionNavBar>} */ -const navbars = [] +const navbars = []; /** * Initializes and stores references to each book page navbar. * * @param {HTMLCollection<HTMLElement>} navbarWrappers Each navbar found on the book page */ -export function initNavbars(navbarWrappers) { +export function initNavbars (navbarWrappers) { for (const wrapper of navbarWrappers) { - const navbar = new EdtionNavBar(wrapper) - navbars.push(navbar) + const navbar = new EdtionNavBar(wrapper); + navbars.push(navbar); } } @@ -26,8 +26,8 @@ export function initNavbars(navbarWrappers) { * something other then a scroll event (e.g. when * stickied to a new position). */ -export function updateSelectedNavItem() { +export function updateSelectedNavItem () { for (const navbar of navbars) { - navbar.updateSelected() + navbar.updateSelected(); } } diff --git a/openlibrary/plugins/openlibrary/js/editions-table/index.js b/openlibrary/plugins/openlibrary/js/editions-table/index.js index 8b297e90999..1ff7a0b7999 100755 --- a/openlibrary/plugins/openlibrary/js/editions-table/index.js +++ b/openlibrary/plugins/openlibrary/js/editions-table/index.js @@ -4,33 +4,33 @@ import '../../../../../static/css/legacy-datatables.css'; const DEFAULT_LENGTH = 3; const LS_RESULTS_LENGTH_KEY = 'editions-table.resultsLength'; -export function initEditionsTable() { +export function initEditionsTable () { var rowCount; let currentLength; // Prevent reinitialization of the editions datatable if ($.fn.DataTable.isDataTable($('#editions'))) { return; } - $('#editions th.title').on('mouseover', function(){ + $('#editions th.title').on('mouseover', function (){ if ($(this).hasClass('sorting_asc')) { - $(this).attr('title','Sort latest to earliest'); + $(this).attr('title', 'Sort latest to earliest'); } else if ($(this).hasClass('sorting_desc')) { - $(this).attr('title','Sort earliest to latest'); + $(this).attr('title', 'Sort earliest to latest'); } else { - $(this).attr('title','Sort by publish date'); + $(this).attr('title', 'Sort by publish date'); } }); - $('#editions th.read').on('mouseover', function(){ + $('#editions th.read').on('mouseover', function (){ if ($(this).hasClass('sorting_asc')) { - $(this).attr('title','Push readable versions to the bottom'); + $(this).attr('title', 'Push readable versions to the bottom'); } else if ($(this).hasClass('sorting_desc')) { - $(this).attr('title','Sort by editions to read'); + $(this).attr('title', 'Sort by editions to read'); } else { - $(this).attr('title','Available to read'); + $(this).attr('title', 'Available to read'); } }); - function toggleSorting(e) { + function toggleSorting (e) { $('#editions th span').html(''); $(e).find('span').html(' ↑'); if ($(e).hasClass('sorting_asc')) { @@ -41,25 +41,25 @@ export function initEditionsTable() { } $('#editions th.read span').html(' ↑'); - $('#editions th').on('mouseup', function() { - toggleSorting(this) + $('#editions th').on('mouseup', function () { + toggleSorting(this); }); - $('#editions').on('length.dt', function(e, settings, length) { + $('#editions').on('length.dt', function (e, settings, length) { localStorage.setItem(LS_RESULTS_LENGTH_KEY, length); }); - $('#editions th').on('keydown', function(e) { + $('#editions th').on('keydown', function (e) { if (e.key === 'Enter') { toggleSorting(this); } - }) + }); rowCount = $('#editions tbody tr').length; if (rowCount < 4) { $('#editions').DataTable({ - aoColumns: [{sType: 'html'},null], - order: [ [1,'asc'] ], + aoColumns: [{sType: 'html'}, null], + order: [ [1, 'asc'] ], bPaginate: false, bInfo: false, bFilter: false, @@ -69,8 +69,8 @@ export function initEditionsTable() { } else { currentLength = Number(localStorage.getItem(LS_RESULTS_LENGTH_KEY)); $('#editions').DataTable({ - aoColumns: [{sType: 'html'},null], - order: [ [1,'asc'] ], + aoColumns: [{sType: 'html'}, null], + order: [ [1, 'asc'] ], lengthMenu: [ [3, 10, 25, 50, 100, -1], [3, 10, 25, 50, 100, 'All'] ], bPaginate: true, bInfo: true, @@ -79,16 +79,16 @@ export function initEditionsTable() { bStateSave: false, bAutoWidth: false, pageLength: currentLength ? currentLength : DEFAULT_LENGTH, - drawCallback: function() { + drawCallback: function () { if ($('#ile-toolbar')) { - const editionStorage = JSON.parse(sessionStorage.getItem('ile-items'))['edition'] + const editionStorage = JSON.parse(sessionStorage.getItem('ile-items'))['edition']; const matchEdition = (string) => { - return string.match(/OL[0-9]+[a-zA-Z]/) - } + return string.match(/OL[0-9]+[a-zA-Z]/); + }; for (const el of $('.ile-selected')) { const anchor = el.getElementsByTagName('a'); if (anchor.length) { - const edIdentifier = matchEdition(anchor[0].getAttribute('href')) + const edIdentifier = matchEdition(anchor[0].getAttribute('href')); if (!editionStorage.includes(edIdentifier[0])) { el.classList.remove('ile-selected'); } @@ -105,6 +105,6 @@ export function initEditionsTable() { } } } - }) + }); } } diff --git a/openlibrary/plugins/openlibrary/js/following.js b/openlibrary/plugins/openlibrary/js/following.js index 0930da0acbf..b5d618ba1a1 100644 --- a/openlibrary/plugins/openlibrary/js/following.js +++ b/openlibrary/plugins/openlibrary/js/following.js @@ -1,6 +1,6 @@ import { PersistentToast } from './Toast'; -export async function initAsyncFollowing(followForms) { +export async function initAsyncFollowing (followForms) { followForms.forEach(form => { form.addEventListener('submit', async (e) => { e.preventDefault(); diff --git a/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js b/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js index 1ac759942b1..9ac3934dbbf 100644 --- a/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js +++ b/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js @@ -1,55 +1,55 @@ -import { buildPartialsUrl } from './utils' +import { buildPartialsUrl } from './utils'; -export function initFulltextSearchSuggestion(fulltextSearchSuggestion) { - const isLoading = showLoadingIndicators(fulltextSearchSuggestion) +export function initFulltextSearchSuggestion (fulltextSearchSuggestion) { + const isLoading = showLoadingIndicators(fulltextSearchSuggestion); if (isLoading) { - const query = fulltextSearchSuggestion.dataset.query - getPartials(fulltextSearchSuggestion, query) + const query = fulltextSearchSuggestion.dataset.query; + getPartials(fulltextSearchSuggestion, query); } } -function showLoadingIndicators(fulltextSearchSuggestion) { - let isLoading = false - const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator') +function showLoadingIndicators (fulltextSearchSuggestion) { + let isLoading = false; + const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator'); if (loadingIndicator) { - isLoading = true - loadingIndicator.classList.remove('hidden') + isLoading = true; + loadingIndicator.classList.remove('hidden'); } - return isLoading + return isLoading; } -async function getPartials(fulltextSearchSuggestion, query) { +async function getPartials (fulltextSearchSuggestion, query) { return fetch(buildPartialsUrl('FulltextSearchSuggestion', {data: query})) .then((resp) => { if (resp.status !== 200) { - throw new Error(`Failed to fetch partials. Status code: ${resp.status}`) + throw new Error(`Failed to fetch partials. Status code: ${resp.status}`); } - return resp.json() + return resp.json(); }) .then((data) => { - fulltextSearchSuggestion.innerHTML += data['partials'] + fulltextSearchSuggestion.innerHTML += data['partials']; const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator'); if (loadingIndicator) { - loadingIndicator.classList.add('hidden') + loadingIndicator.classList.add('hidden'); } }) .catch(() => { - const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator') + const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator'); if (loadingIndicator) { - loadingIndicator.classList.add('hidden') + loadingIndicator.classList.add('hidden'); } - const existingRetryAffordance = fulltextSearchSuggestion.querySelector('.fulltext-suggestions__retry') + const existingRetryAffordance = fulltextSearchSuggestion.querySelector('.fulltext-suggestions__retry'); if (existingRetryAffordance) { - existingRetryAffordance.classList.remove('hidden') + existingRetryAffordance.classList.remove('hidden'); } else { - fulltextSearchSuggestion.insertAdjacentHTML('afterbegin', renderRetryLink()) - const retryAffordance = fulltextSearchSuggestion.querySelector('.fulltext-suggestions__retry') + fulltextSearchSuggestion.insertAdjacentHTML('afterbegin', renderRetryLink()); + const retryAffordance = fulltextSearchSuggestion.querySelector('.fulltext-suggestions__retry'); retryAffordance.addEventListener('click', () => { - retryAffordance.classList.add('hidden') - getPartials(fulltextSearchSuggestion, query) - }) + retryAffordance.classList.add('hidden'); + getPartials(fulltextSearchSuggestion, query); + }); } - }) + }); } /** @@ -57,6 +57,6 @@ async function getPartials(fulltextSearchSuggestion, query) { * * @returns {string} HTML for a retry link. */ -function renderRetryLink() { - return '<span class="fulltext-suggestions__retry">Failed to fetch fulltext search suggestions. <a href="javascript:;">Retry?</a></span>' +function renderRetryLink () { + return '<span class="fulltext-suggestions__retry">Failed to fetch fulltext search suggestions. <a href="javascript:;">Retry?</a></span>'; } diff --git a/openlibrary/plugins/openlibrary/js/go-back-links.js b/openlibrary/plugins/openlibrary/js/go-back-links.js index 9a02f46bd3b..92f99285853 100644 --- a/openlibrary/plugins/openlibrary/js/go-back-links.js +++ b/openlibrary/plugins/openlibrary/js/go-back-links.js @@ -4,14 +4,14 @@ * * @param {NodeList<HTMLElement>} goBackLinks */ -export function initGoBackLinks(goBackLinks) { +export function initGoBackLinks (goBackLinks) { for (const link of goBackLinks) { link.addEventListener('click', () => { if (history.length > 2) { - history.go(-1) + history.go(-1); } else { - window.location.href='/' + window.location.href='/'; } - }) + }); } } diff --git a/openlibrary/plugins/openlibrary/js/graphs/index.js b/openlibrary/plugins/openlibrary/js/graphs/index.js index 192926e9842..e6d698cf8a6 100644 --- a/openlibrary/plugins/openlibrary/js/graphs/index.js +++ b/openlibrary/plugins/openlibrary/js/graphs/index.js @@ -1,7 +1,7 @@ import { loadGraphIfExists, loadEditionsGraph } from './plot'; import options from './options.js'; -export function plotAdminGraphs() { +export function plotAdminGraphs () { loadGraphIfExists('editgraph', {}, 'edit(s) on'); loadGraphIfExists('membergraph', {}, 'new members(s) on'); loadGraphIfExists('works_minigraph', {}, ' works on '); @@ -13,7 +13,7 @@ export function plotAdminGraphs() { loadGraphIfExists('books-added-per-day', options.booksAdded); } -export function initHomepageGraphs() { +export function initHomepageGraphs () { loadGraphIfExists('visitors-graph', {}, 'unique visitors on', '#e44028'); loadGraphIfExists('members-graph', {}, 'new members on', '#748d36'); loadGraphIfExists('edits-graph', {}, 'catalog edits on', '#00636a'); @@ -21,13 +21,13 @@ export function initHomepageGraphs() { loadGraphIfExists('ebooks-graph', {}, 'ebooks borrowed on', '#35672e'); } -export function initPublishersGraph() { +export function initPublishersGraph () { if (document.getElementById('chartPubHistory')) { loadEditionsGraph('chartPubHistory', {}, 'editions in'); } } -export function init() { +export function init () { plotAdminGraphs(); initHomepageGraphs(); initPublishersGraph(); diff --git a/openlibrary/plugins/openlibrary/js/graphs/plot.js b/openlibrary/plugins/openlibrary/js/graphs/plot.js index b4034a2ea39..fd163244fc1 100644 --- a/openlibrary/plugins/openlibrary/js/graphs/plot.js +++ b/openlibrary/plugins/openlibrary/js/graphs/plot.js @@ -15,7 +15,7 @@ import 'flot/jquery.flot.time.js'; * - http://localhost:8080/subjects/fantasy#sort=date_published&ebooks=true * - http://localhost:8080/publishers/Barnes_&_Noble */ -export function loadEditionsGraph() { +export function loadEditionsGraph () { var data, options, placeholder, plot, dateFrom, dateTo, previousPoint; data = [{data: JSON.parse(document.getElementById('graph-json-chartPubHistory').textContent)}]; @@ -52,7 +52,7 @@ export function loadEditionsGraph() { }; placeholder = $('#chartPubHistory'); - function showTooltip(x, y, contents) { + function showTooltip (x, y, contents) { $(`<div id="chartLabel">${contents}</div>`).css({ position: 'absolute', display: 'none', @@ -100,7 +100,7 @@ export function loadEditionsGraph() { const yearFrom = item.datapoint[0].toFixed(0); applyDateFilter(yearFrom, yearFrom); - plot.highlight(item.series,item.datapoint); + plot.highlight(item.series, item.datapoint); } else { plot.unhighlight(); @@ -120,7 +120,7 @@ export function loadEditionsGraph() { applyDateFilter(yearFrom, yearTo); }); - function applyDateFilter(yearFrom, yearTo, hideSelector='.chartUnzoom', showSelector='.chartZoom') { + function applyDateFilter (yearFrom, yearTo, hideSelector='.chartUnzoom', showSelector='.chartZoom') { document.dispatchEvent(new CustomEvent('filter', { detail: { yearFrom: yearFrom, yearTo: yearTo } })); $(hideSelector).hide(); $(showSelector).removeClass('hidden').show(); @@ -130,7 +130,7 @@ export function loadEditionsGraph() { dateFrom = plot.getAxes().xaxis.min.toFixed(0); dateTo = plot.getAxes().xaxis.max.toFixed(0); - $('.resetSelection').on('click', function() { + $('.resetSelection').on('click', function () { plot = $.plot(placeholder, data, options); const yearFrom = plot.getAxes().xaxis.min.toFixed(0); @@ -147,7 +147,7 @@ export function loadEditionsGraph() { } } -export function plot_minigraph(node, data) { +export function plot_minigraph (node, data) { var options = { series: { lines: { @@ -168,7 +168,7 @@ export function plot_minigraph(node, data) { $.plot(node, [data], options); } -export function plot_tooltip_graph(node, data, tooltip_message, color='#748d36') { +export function plot_tooltip_graph (node, data, tooltip_message, color='#748d36') { var i, options, graph; // empty set of rows. Escape early. if (!data.length) { @@ -204,7 +204,7 @@ export function plot_tooltip_graph(node, data, tooltip_message, color='#748d36') graph = $.plot(node, [data], options); - function showTooltip(x, y, contents) { + function showTooltip (x, y, contents) { $(`<div id="chartLabelA">${contents}</div>`).css({ position: 'absolute', display: 'none', @@ -246,7 +246,7 @@ export function plot_tooltip_graph(node, data, tooltip_message, color='#748d36') * @param {string} [color] in hexidecimal to apply to the bars of a tooltip graph. * Ignored if options and no tooltip_message is passed. */ -export function loadGraph(id, options = {}, tooltip_message = '', color = null) { +export function loadGraph (id, options = {}, tooltip_message = '', color = null) { let data; const node = document.getElementById(id); const graphSelector = `graph-json-${id}`; @@ -282,7 +282,7 @@ export function loadGraph(id, options = {}, tooltip_message = '', color = null) * @param {string} [color] in hexidecimal to apply to the bars of a tooltip graph. * Ignored if options and no tooltip_message is passed. */ -export function loadGraphIfExists(id, options, tooltip_message, color) { +export function loadGraphIfExists (id, options, tooltip_message, color) { if ($(`#${id}`).length) { loadGraph(id, options, tooltip_message, color); } diff --git a/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js b/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js index ede17d1940c..2e8b4c0f18c 100644 --- a/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js +++ b/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js @@ -3,13 +3,13 @@ * * @param {*} element - The element to be modified by the handleMessageEvent function. */ -export function initMessageEventListener(element) { +export function initMessageEventListener (element) { /** * Handles messages from archive.org and performs actions based on the message type. * * @param {MessageEvent} e - The message event. */ - function handleMessageEvent(e) { + function handleMessageEvent (e) { if (!/[./]archive\.org$$/.test(e.origin)) return; if (e.data.type === 'resize') { @@ -17,14 +17,14 @@ export function initMessageEventListener(element) { if (e.data.height) element.style.height = `${e.data.height}px`; } else if (e.data.type === 's3-keys') { - const s3AccessInput = document.querySelector('#access') - const s3SecretInput = document.querySelector('#secret') - s3AccessInput.value = e.data.s3.access - s3SecretInput.value = e.data.s3.secret + const s3AccessInput = document.querySelector('#access'); + const s3SecretInput = document.querySelector('#secret'); + s3AccessInput.value = e.data.s3.access; + s3SecretInput.value = e.data.s3.secret; - const loginForm = document.querySelector('#register') - loginForm.action = '/account/login' - loginForm.submit() + const loginForm = document.querySelector('#register'); + loginForm.action = '/account/login'; + loginForm.submit(); } } diff --git a/openlibrary/plugins/openlibrary/js/idValidation.js b/openlibrary/plugins/openlibrary/js/idValidation.js index 23b9532fa42..35977660381 100644 --- a/openlibrary/plugins/openlibrary/js/idValidation.js +++ b/openlibrary/plugins/openlibrary/js/idValidation.js @@ -3,7 +3,7 @@ * @param {String} isbn ISBN string for parsing * @returns {String} parsed isbn string */ -export function parseIsbn(isbn) { +export function parseIsbn (isbn) { return isbn.replace(/[ -]/g, ''); } @@ -13,7 +13,7 @@ export function parseIsbn(isbn) { * @param {String} isbn ISBN string to check * @returns {boolean} true if the isbn has a valid format */ -export function isFormatValidIsbn10(isbn) { +export function isFormatValidIsbn10 (isbn) { const regex = /^[0-9]{9}[0-9X]$/; return regex.test(isbn); } @@ -24,7 +24,7 @@ export function isFormatValidIsbn10(isbn) { * @param {String} isbn ISBN string for validating * @returns {boolean} true if ISBN string is a valid ISBN 10 */ -export function isChecksumValidIsbn10(isbn) { +export function isChecksumValidIsbn10 (isbn) { const chars = isbn.replace('X', 'A').split(''); chars.reverse(); @@ -42,7 +42,7 @@ export function isChecksumValidIsbn10(isbn) { * @param {String} isbn ISBN string to check * @returns {boolean} true if the isbn has a valid format */ -export function isFormatValidIsbn13(isbn) { +export function isFormatValidIsbn13 (isbn) { const regex = /^[0-9]{13}$/; return regex.test(isbn); } @@ -53,7 +53,7 @@ export function isFormatValidIsbn13(isbn) { * @param {String} isbn ISBN string for validating * @returns {Boolean} true if ISBN string is a valid ISBN 13 */ -export function isChecksumValidIsbn13(isbn) { +export function isChecksumValidIsbn13 (isbn) { const chars = isbn.split(''); const sum = chars .map((char, idx) => ((idx % 2 * 2 + 1) * parseInt(char, 10))) @@ -69,7 +69,7 @@ export function isChecksumValidIsbn13(isbn) { * @param {String} lccn LCCN string for parsing * @returns {String} parsed LCCN string */ -export function parseLccn(lccn) { +export function parseLccn (lccn) { // cleaning initial lccn entry const parsed = lccn // any alpha characters need to be lowercase @@ -84,7 +84,7 @@ export function parseLccn(lccn) { .replace(/[/]+.*$/, ''); // splitting at hyphen and padding the right hand value with zeros up to 6 characters - const groups = parsed.match(/(.+)-+([0-9]+)/) + const groups = parsed.match(/(.+)-+([0-9]+)/); if (groups && groups.length === 3) { return groups[1] + groups[2].padStart(6, '0'); } @@ -97,7 +97,7 @@ export function parseLccn(lccn) { * @param {String} lccn LCCN string to test for valid syntax * @returns {boolean} true if given LCCN is valid syntax, false otherwise */ -export function isValidLccn(lccn) { +export function isValidLccn (lccn) { // matching parsed entry to regex representing valid lccn // regex taken from /openlibrary/utils/lccn.py const regex = /^([a-z]|[a-z]?([a-z]{2}|[0-9]{2})|[a-z]{2}[0-9]{2})?[0-9]{8}$/; @@ -109,7 +109,7 @@ export function isValidLccn(lccn) { * @param {String} oclc OCLC string for parsing * @returns {String} parsed OCLC string */ -export function parseOclc(oclc) { +export function parseOclc (oclc) { // cleaning initial oclc entry return oclc // remove any whitespace @@ -127,7 +127,7 @@ export function parseOclc(oclc) { * @param {String} oclc OCLC string to test for valid syntax * @returns {boolean} true if given OCLC is valid syntax, false otherwise */ -export function isValidOclc(oclc) { +export function isValidOclc (oclc) { // matching parsed entry to regex representing valid oclc const regex = /^[1-9][0-9]*$/; return regex.test(oclc); @@ -142,7 +142,7 @@ export function isValidOclc(oclc) { * @param {String} newId New identifier entry to be checked * @returns {boolean} true if the new identifier has already been entered */ -export function isIdDupe(idEntries, newId) { +export function isIdDupe (idEntries, newId) { // check each current entry value against new identifier return Array.from(idEntries).some( entry => entry['value'] === newId diff --git a/openlibrary/plugins/openlibrary/js/ile/utils/ol.js b/openlibrary/plugins/openlibrary/js/ile/utils/ol.js index 2727cc5faed..a38775a1b98 100644 --- a/openlibrary/plugins/openlibrary/js/ile/utils/ol.js +++ b/openlibrary/plugins/openlibrary/js/ile/utils/ol.js @@ -11,7 +11,7 @@ import uniqBy from 'lodash/uniqBy'; * @param {WorkOLID} old_work * @param {WorkOLID} new_work */ -export async function move_to_work(edition_ids, old_work, new_work) { +export async function move_to_work (edition_ids, old_work, new_work) { for (const olid of edition_ids) { const url = `/books/${olid}.json`; const record = await fetch(url).then(r => r.json()); @@ -30,7 +30,7 @@ export async function move_to_work(edition_ids, old_work, new_work) { * @param {AuthorOLID} old_author * @param {AuthorOLID} new_author */ -export async function move_to_author(work_ids, old_author, new_author) { +export async function move_to_author (work_ids, old_author, new_author) { for (const olid of work_ids) { const url = `/works/${olid}.json`; const record = await fetch(url).then(r => r.json()); @@ -45,7 +45,7 @@ export async function move_to_author(work_ids, old_author, new_author) { record._comment = 'move to correct author'; const r = await fetch(url, { method: 'PUT', body: JSON.stringify(record) }); // eslint-disable-next-line no-console - console.log(`moved ${olid}; ${r.status}`) + console.log(`moved ${olid}; ${r.status}`); } else { // eslint-disable-next-line no-console console.warn(`${old_author} not in ${url}!`); diff --git a/openlibrary/plugins/openlibrary/js/index.js b/openlibrary/plugins/openlibrary/js/index.js index 1a262393ba0..0ffd7ef4b82 100644 --- a/openlibrary/plugins/openlibrary/js/index.js +++ b/openlibrary/plugins/openlibrary/js/index.js @@ -2,7 +2,7 @@ import 'jquery'; import { exposeGlobally } from './jsdef'; import initAnalytics from './ol.analytics'; import init from './ol.js'; -import initServiceWorker from './service-worker-init.js' +import initServiceWorker from './service-worker-init.js'; import '../../../../static/css/js-all.css'; // polyfill Promise support for IE11 import Promise from 'promise-polyfill'; @@ -40,7 +40,7 @@ jQuery(function () { // Polyfill for .closest() if (!Element.prototype.closest) { - Element.prototype.closest = function(s) { + Element.prototype.closest = function (s) { let el = this; do { if (Element.prototype.matches.call(el, s)) return el; @@ -66,7 +66,7 @@ jQuery(function () { $('.no-img img').hide(); // disable save button after click - $('button[name=\'_save\']').on('submit', function() { + $('button[name=\'_save\']').on('submit', function () { $(this).attr('disabled', true); }); @@ -157,7 +157,7 @@ jQuery(function () { } // conditionally load for type changing input - const typeChanger = document.getElementById('type.key') + const typeChanger = document.getElementById('type.key'); if (typeChanger) { import(/* webpackChunkName: "type-changer" */ './type_changer.js') .then(module => module.initTypeChanger(typeChanger)); @@ -197,21 +197,21 @@ jQuery(function () { } // Enable any carousels in the page - const carouselElements = document.querySelectorAll('.carousel--progressively-enhanced') + const carouselElements = document.querySelectorAll('.carousel--progressively-enhanced'); if (carouselElements.length) { import(/* webpackChunkName: "carousel" */ './carousel') .then((module) => { - module.initialzeCarousels(carouselElements) - }) + module.initialzeCarousels(carouselElements); + }); } if ($('script[type="text/json+graph"]').length > 0) { import(/* webpackChunkName: "graphs" */ './graphs') .then((module) => module.init()); } - const readingLogCharts = document.querySelector('.readinglog-charts') + const readingLogCharts = document.querySelector('.readinglog-charts'); if (readingLogCharts) { - const readingLogConfig = JSON.parse(readingLogCharts.dataset.config) + const readingLogConfig = JSON.parse(readingLogCharts.dataset.config); import(/* webpackChunkName: "readinglog-stats" */ './readinglog_stats') .then(module => module.init(readingLogConfig)); } @@ -230,7 +230,7 @@ jQuery(function () { } // Disable data export buttons on form submit - const patronImportForms = document.querySelectorAll('.patron-export-form') + const patronImportForms = document.querySelectorAll('.patron-export-form'); if (patronImportForms.length) { import(/* webpackChunkName: "patron-exports" */ './patron_exports') .then(module => module.initPatronExportForms(patronImportForms)); @@ -253,7 +253,7 @@ jQuery(function () { module.addNotesPageButtonListeners(); } if ($shareModalLinks.length) { - module.initShareModal($shareModalLinks) + module.initShareModal($shareModalLinks); } }); } @@ -288,13 +288,13 @@ jQuery(function () { } if (document.getElementById('autofill-dev-credentials')) { - document.getElementById('username').value = 'openlibrary@example.com' - document.getElementById('password').value = 'admin123' - document.getElementById('remember').checked = true + document.getElementById('username').value = 'openlibrary@example.com'; + document.getElementById('password').value = 'admin123'; + document.getElementById('remember').checked = true; } - const anonymizationButton = document.querySelector('.account-anonymization-button') - const adminLinks = document.getElementById('adminLinks') - const confirmButtons = document.querySelectorAll('.do-confirm') + const anonymizationButton = document.querySelector('.account-anonymization-button'); + const adminLinks = document.getElementById('adminLinks'); + const confirmButtons = document.querySelectorAll('.do-confirm'); if (adminLinks || anonymizationButton || confirmButtons.length) { import(/* webpackChunkName: "admin" */ './admin') .then(module => { @@ -315,7 +315,7 @@ jQuery(function () { .then((module) => module.initOfflineBanner()); } - const searchFacets = document.getElementById('searchFacets') + const searchFacets = document.getElementById('searchFacets'); if (searchFacets) { import(/* webpackChunkName: "search" */ './search') .then((module) => module.initSearchFacets(searchFacets)); @@ -330,20 +330,20 @@ jQuery(function () { // Handle pencil clicks document.querySelectorAll('.edit-subject-btn').forEach(btn => { btn.addEventListener('click', (e) => { - e.preventDefault() - const workOlid = btn.dataset.workOlid + e.preventDefault(); + const workOlid = btn.dataset.workOlid; if (!window.ILE.selectionManager.selectedItems.work.includes(workOlid)) { - window.ILE.selectionManager.addSelectedItem(workOlid) - window.ILE.selectionManager.updateToolbar() + window.ILE.selectionManager.addSelectedItem(workOlid); + window.ILE.selectionManager.updateToolbar(); } - window.ILE.updateAndShowBulkTagger([workOlid], true) - }) - }) - }) + window.ILE.updateAndShowBulkTagger([workOlid], true); + }); + }); + }); // Import ile then the datatable to apply clickable classes to all listed editions if (document.getElementsByClassName('editions-table--progressively-enhanced').length) { import(/* webpackChunkName: "editions-table" */ './editions-table') - .then(module => module.initEditionsTable()) + .then(module => module.initEditionsTable()); } } // conditionally load functionality based on what's in the page @@ -361,68 +361,68 @@ jQuery(function () { $('#cboxSlideshow').attr({'aria-label': 'Slideshow button', 'aria-hidden': 'true'}); } - const droppers = document.querySelectorAll('.dropper') - const genericDroppers = document.querySelectorAll('.generic-dropper-wrapper') + const droppers = document.querySelectorAll('.dropper'); + const genericDroppers = document.querySelectorAll('.generic-dropper-wrapper'); if (droppers.length || genericDroppers.length) { import(/* webpackChunkName: "droppers" */ './dropper') .then((module) => { - module.initDroppers(droppers) - module.initGenericDroppers(genericDroppers) - }) + module.initDroppers(droppers); + module.initGenericDroppers(genericDroppers); + }); } // My Books Droppers (includes New List Form and Reading Check-Ins): - const myBooksDroppers = document.querySelectorAll('.my-books-dropper') + const myBooksDroppers = document.querySelectorAll('.my-books-dropper'); if (myBooksDroppers.length) { - const actionableListShowcases = document.querySelectorAll('.actionable-item') + const actionableListShowcases = document.querySelectorAll('.actionable-item'); import(/* webpackChunkName: "my-books" */ './my-books') .then((module) => { - module.initMyBooksAffordances(myBooksDroppers, actionableListShowcases) - }) + module.initMyBooksAffordances(myBooksDroppers, actionableListShowcases); + }); } // TODO: Make these selectors a consistent interface const $dialogs = $('.dialog--open,.dialog--close,#noMaster,#confirmMerge,#leave-waitinglist-dialog,#bookPreview'); if ($dialogs.length) { import(/* webpackChunkName: "dialog" */ './dialog') - .then(module => module.initDialogs()) + .then(module => module.initDialogs()); } const nativeDialogs = document.querySelectorAll('.native-dialog'); if (nativeDialogs.length) { import(/* webpackChunkName: "native-dialog" */ './native-dialog') - .then(module => module.initDialogs(nativeDialogs)) + .then(module => module.initDialogs(nativeDialogs)); } // Yearly reading goal functionality - const setGoalLinks = document.querySelectorAll('.set-reading-goal-link') - const goalEditLinks = document.querySelectorAll('.edit-reading-goal-link') - const goalSubmitButtons = document.querySelectorAll('.reading-goal-submit-button') - const yearElements = document.querySelectorAll('.use-local-year') + const setGoalLinks = document.querySelectorAll('.set-reading-goal-link'); + const goalEditLinks = document.querySelectorAll('.edit-reading-goal-link'); + const goalSubmitButtons = document.querySelectorAll('.reading-goal-submit-button'); + const yearElements = document.querySelectorAll('.use-local-year'); if (setGoalLinks.length || goalEditLinks.length || goalSubmitButtons.length || yearElements.length) { import(/* webpackChunkName: "reading-goals" */ './reading-goals') .then((module) => { if (setGoalLinks.length) { - module.initYearlyGoalPrompt(setGoalLinks) + module.initYearlyGoalPrompt(setGoalLinks); } if (goalEditLinks.length) { - module.initGoalEditLinks(goalEditLinks) + module.initGoalEditLinks(goalEditLinks); } if (goalSubmitButtons.length) { - module.initGoalSubmitButtons(goalSubmitButtons) + module.initGoalSubmitButtons(goalSubmitButtons); } if (yearElements.length) { - module.displayLocalYear(yearElements) + module.displayLocalYear(yearElements); } - }) + }); } $(document).on('click', '.slide-toggle', function () { $(`#${$(this).attr('aria-controls')}`).slideToggle(); }); - $('#wikiselect').on('focus', function(){$(this).trigger('select');}) + $('#wikiselect').on('focus', function (){$(this).trigger('select');}); $('.hamburger-component .mask-menu').on('click', function () { $('details[open]').not(this).removeAttr('open'); @@ -434,8 +434,8 @@ jQuery(function () { } }); - $('.dropdown-menu').each(function() { - $(this).find('a').last().on('focusout', function() { + $('.dropdown-menu').each(function () { + $(this).find('a').last().on('focusout', function () { $('.header-dropdown > details[open]').removeAttr('open'); }); }); @@ -450,67 +450,67 @@ jQuery(function () { }); // Prevent default star rating behavior: - const ratingForms = document.querySelectorAll('.star-rating-form') + const ratingForms = document.querySelectorAll('.star-rating-form'); if (ratingForms.length) { import(/* webpackChunkName: "star-ratings" */'./star-ratings') .then((module) => module.initRatingHandlers(ratingForms)); } // Book page navbar initialization: - const navbarWrappers = document.querySelectorAll('.nav-bar-wrapper') + const navbarWrappers = document.querySelectorAll('.nav-bar-wrapper'); if (navbarWrappers.length) { // Add JS for book page navbar: import(/* webpackChunkName: "nav-bar" */ './edition-nav-bar') .then((module) => { - module.initNavbars(navbarWrappers) + module.initNavbars(navbarWrappers); }); // Add sticky title component animations to desktop views: import(/* webpackChunkName: "compact-title" */ './compact-title') .then((module) => { - const compactTitle = document.querySelector('.compact-title') - const desktopNavbar = [...navbarWrappers].find(elem => elem.classList.contains('desktop-only')) - module.initCompactTitle(desktopNavbar, compactTitle) - }) + const compactTitle = document.querySelector('.compact-title'); + const desktopNavbar = [...navbarWrappers].find(elem => elem.classList.contains('desktop-only')); + module.initCompactTitle(desktopNavbar, compactTitle); + }); } // Add functionality for librarian merge request table: - const librarianQueue = document.querySelector('.librarian-queue-wrapper') + const librarianQueue = document.querySelector('.librarian-queue-wrapper'); if (librarianQueue) { import(/* webpackChunkName: "merge-request-table" */'./merge-request-table') .then(module => { - module.initLibrarianQueue(librarianQueue) - }) + module.initLibrarianQueue(librarianQueue); + }); } // Add functionality to the team page for filtering members: - const teamCards = document.querySelector('.teamCards_container') + const teamCards = document.querySelector('.teamCards_container'); if (teamCards) { import(/* webpackChunkName "team" */ './team') .then(module => { module.initTeamFilter(); - }) + }); } // Add new providers in edit edition view: - const addProviderRowLink = document.querySelector('#add-new-provider-row') + const addProviderRowLink = document.querySelector('#add-new-provider-row'); if (addProviderRowLink) { import(/* webpackChunkName "add-provider-link" */ './add_provider') - .then(module => module.initAddProviderRowLink(addProviderRowLink)) + .then(module => module.initAddProviderRowLink(addProviderRowLink)); } // Allow banner announcements to be dismissed by logged-in users: - const banners = document.querySelectorAll('.page-banner--dismissable') + const banners = document.querySelectorAll('.page-banner--dismissable'); if (banners.length) { import(/* webpackChunkName: "dismissible-banner" */ './banner') - .then(module => module.initDismissibleBanners(banners)) + .then(module => module.initDismissibleBanners(banners)); } - const returnForms = document.querySelectorAll('.return-form') + const returnForms = document.querySelectorAll('.return-form'); if (returnForms.length) { import(/* webpackChunkName: "return-form" */ './return-form') - .then(module => module.initReturnForms(returnForms)) + .then(module => module.initReturnForms(returnForms)); } const crumbs = document.querySelectorAll('.crumb select'); @@ -538,59 +538,59 @@ jQuery(function () { } // Password visibility toggle: - const passwordVisibilityToggle = document.querySelector('.password-visibility-toggle') + const passwordVisibilityToggle = document.querySelector('.password-visibility-toggle'); if (passwordVisibilityToggle) { import(/* webpackChunkName: "password-visibility-toggle" */ './password-toggle') - .then(module => module.initPasswordToggling(passwordVisibilityToggle)) + .then(module => module.initPasswordToggling(passwordVisibilityToggle)); } // Affiliate links: - const affiliateLinksSection = document.querySelectorAll('.affiliate-links-section') + const affiliateLinksSection = document.querySelectorAll('.affiliate-links-section'); if (affiliateLinksSection.length) { import(/* webpackChunkName: "affiliate-links" */ './affiliate-links') - .then(module => module.initAffiliateLinks(affiliateLinksSection)) + .then(module => module.initAffiliateLinks(affiliateLinksSection)); } // Fulltext search box: - const fulltextSearchSuggestion = document.querySelector('#fulltext-search-suggestion') + const fulltextSearchSuggestion = document.querySelector('#fulltext-search-suggestion'); if (fulltextSearchSuggestion) { import(/* webpackChunkName: "fulltext-search-suggestion" */ './fulltext-search-suggestion') - .then(module => module.initFulltextSearchSuggestion(fulltextSearchSuggestion)) + .then(module => module.initFulltextSearchSuggestion(fulltextSearchSuggestion)); } // Go back redirect: - const backLinks = document.querySelectorAll('.go-back-link') + const backLinks = document.querySelectorAll('.go-back-link'); if (backLinks.length) { import (/* webpackChunkName: "go-back-links" */ './go-back-links') - .then(module => module.initGoBackLinks(backLinks)) + .then(module => module.initGoBackLinks(backLinks)); } // Lazy-load book page lists section - const listSection = document.querySelector('.lists-section') + const listSection = document.querySelector('.lists-section'); if (listSection) { import(/* webpackChunkName: "book-page-lists" */ './book-page-lists') - .then(module => module.initListsSection(listSection)) + .then(module => module.initListsSection(listSection)); } // Initialize follow forms lazily const followForms = document.querySelectorAll('.follow-form'); if (followForms.length) { import(/* webpackChunkName: "following" */ './following') - .then(module => module.initAsyncFollowing(followForms)) + .then(module => module.initAsyncFollowing(followForms)); } // Generalized carousel lazy-loading - const lazyCarousels = document.querySelectorAll('.lazy-carousel') + const lazyCarousels = document.querySelectorAll('.lazy-carousel'); if (lazyCarousels.length) { import(/* webpackChunkName: "lazy-carousels" */ './lazy-carousel') - .then(module => module.initLazyCarousel(lazyCarousels)) + .then(module => module.initLazyCarousel(lazyCarousels)); } // Librarian Dashboard - const librarianDashboard = document.querySelector('.librarian-dashboard') + const librarianDashboard = document.querySelector('.librarian-dashboard'); if (librarianDashboard) { import(/* webpackChunkName: "librarian-dashboard" */ './librarian-dashboard') - .then(module => module.initLibrarianDashboard(librarianDashboard)) + .then(module => module.initLibrarianDashboard(librarianDashboard)); } // List books @@ -600,9 +600,9 @@ jQuery(function () { } // Stats page login counts - const monthlyLoginStats = document.querySelector('.monthly-login-counts') + const monthlyLoginStats = document.querySelector('.monthly-login-counts'); if (monthlyLoginStats) { import(/* webpackChunkName: "stats" */ './stats') - .then(module => module.initUniqueLoginCounts(monthlyLoginStats)) + .then(module => module.initUniqueLoginCounts(monthlyLoginStats)); } }); diff --git a/openlibrary/plugins/openlibrary/js/interstitial.js b/openlibrary/plugins/openlibrary/js/interstitial.js index 43542b622da..4d0dcfd03fb 100644 --- a/openlibrary/plugins/openlibrary/js/interstitial.js +++ b/openlibrary/plugins/openlibrary/js/interstitial.js @@ -1,21 +1,21 @@ -export function initInterstitial(elem) { - let seconds = elem.dataset.wait - const url = elem.dataset.url - const timerElement = elem.querySelector('#timer') +export function initInterstitial (elem) { + let seconds = elem.dataset.wait; + const url = elem.dataset.url; + const timerElement = elem.querySelector('#timer'); const countdown = setInterval(() => { - seconds-- - timerElement.textContent = seconds + seconds--; + timerElement.textContent = seconds; if (seconds === 0) { - clearInterval(countdown) - window.location.href = url + clearInterval(countdown); + window.location.href = url; } - }, 1000) // 1 second interval + }, 1000); // 1 second interval // Add cancel button handler const cancelButton = elem.querySelector('.close-window'); if (cancelButton) { cancelButton.addEventListener('click', () => { - window.close() + window.close(); }); } } diff --git a/openlibrary/plugins/openlibrary/js/isbnOverride.js b/openlibrary/plugins/openlibrary/js/isbnOverride.js index 23b4bf80b56..c50e875ced9 100644 --- a/openlibrary/plugins/openlibrary/js/isbnOverride.js +++ b/openlibrary/plugins/openlibrary/js/isbnOverride.js @@ -9,7 +9,7 @@ */ export const isbnOverride = { data: null, - set(isbnData) { this.data = isbnData }, - get() { return this.data }, - clear() { this.data = null }, -} + set (isbnData) { this.data = isbnData; }, + get () { return this.data; }, + clear () { this.data = null; }, +}; diff --git a/openlibrary/plugins/openlibrary/js/jquery.repeat.js b/openlibrary/plugins/openlibrary/js/jquery.repeat.js index 5e83377fd68..82b2f8416df 100644 --- a/openlibrary/plugins/openlibrary/js/jquery.repeat.js +++ b/openlibrary/plugins/openlibrary/js/jquery.repeat.js @@ -1,14 +1,14 @@ -import Template from './template' -import { isbnOverride } from '../../openlibrary/js/isbnOverride' +import Template from './template'; +import { isbnOverride } from '../../openlibrary/js/isbnOverride'; /** * jquery repeat: jquery plugin to handle repetitive inputs in a form. * * Used in addbook process. */ -export function init() { +export function init () { // used in books/edit/exercpt, books/edit/web and books/edit/edition - $.fn.repeat = function(options) { + $.fn.repeat = function (options) { var addSelector, removeSelector, id, elems, t, code, nextRowId; options = options || {}; @@ -20,9 +20,9 @@ export function init() { form: $(`${id}-form`), display: $(`${id}-display`), template: $(`${id}-template`) - } + }; - function createTemplate(selector) { + function createTemplate (selector) { code = $(selector).html() .replace(/%7B%7B/gi, '<%=') .replace(/%7D%7D/gi, '%>') @@ -38,9 +38,9 @@ export function init() { * object representing. * @return {object} data mapping names to values */ - function formdata() { + function formdata () { var data = {}; - $(':input', elems.form).each(function() { + $(':input', elems.form).each(function () { var $e = $(this), name = $e.attr('name'), type = $e.attr('type'), @@ -60,7 +60,7 @@ export function init() { * Creates a removable `repeat-item`. * @param {jQuery.Event} event */ - function onAdd(event) { + function onAdd (event) { var data, newid; const isbnOverrideData = isbnOverride.get(); event.preventDefault(); @@ -100,7 +100,7 @@ export function init() { elems._this.trigger('repeat-add'); } - function onRemove(event) { + function onRemove (event) { event.preventDefault(); $(this).parents('.repeat-item').eq(0).remove(); elems._this.trigger('repeat-remove'); @@ -110,5 +110,5 @@ export function init() { // Click handlers should apply to newly created add/remove selectors $(document).on('click', addSelector, onAdd); $(document).on('click', removeSelector, onRemove); - } + }; } diff --git a/openlibrary/plugins/openlibrary/js/jsdef.js b/openlibrary/plugins/openlibrary/js/jsdef.js index df0f7fda1bd..18cb47c4f31 100644 --- a/openlibrary/plugins/openlibrary/js/jsdef.js +++ b/openlibrary/plugins/openlibrary/js/jsdef.js @@ -21,7 +21,7 @@ import { truncate, cond } from './utils'; */ //used in templates/lib/pagination.html -export function range(begin, end, step) { +export function range (begin, end, step) { var r, i; step = step || 1; if (end === undefined) { @@ -42,7 +42,7 @@ export function range(begin, end, step) { * > " - ".join(["a", "b", "c"]) * a - b - c */ -export function join(items) { +export function join (items) { return items.join(this); } @@ -51,12 +51,12 @@ export function join(items) { */ // used in templates/admin/loans.html -export function len(array) { +export function len (array) { return array.length; } // used in templates/type/permission/edit.html -export function enumerate(a) { +export function enumerate (a) { var b = new Array(a.length); var i; for (i in a) { @@ -65,7 +65,7 @@ export function enumerate(a) { return b; } -export function ForLoop(parent, seq) { +export function ForLoop (parent, seq) { this.parent = parent; this.seq = seq; @@ -73,7 +73,7 @@ export function ForLoop(parent, seq) { this.index0 = -1; } -ForLoop.prototype.next = function() { +ForLoop.prototype.next = function () { var i = this.index0+1; this.index0 = i; @@ -88,10 +88,10 @@ ForLoop.prototype.next = function() { this.revindex0 = this.length - i; this.revindex = this.length - i + 1; -} +}; // used in plugins/upstream/jsdef.py -export function foreach(seq, parent_loop, callback) { +export function foreach (seq, parent_loop, callback) { var loop = new ForLoop(parent_loop, seq); var i, args, j; @@ -114,7 +114,7 @@ export function foreach(seq, parent_loop, callback) { } // used in templates/lists/widget.html -export function websafe(value) { +export function websafe (value) { // Safari 6 is failing with weird javascript error in this function. // Added try-catch to avoid it. try { @@ -135,7 +135,7 @@ export function websafe(value) { * Quote a string * @param {string|number} text to quote */ -export function htmlquote(text) { +export function htmlquote (text) { // This code exists for compatibility with template.js text = String(text); text = text.replace(/&/g, '&'); // Must be done first! @@ -146,7 +146,7 @@ export function htmlquote(text) { return text; } -export function is_jsdef() { +export function is_jsdef () { return true; } @@ -160,11 +160,11 @@ export function is_jsdef() { * @param {string} key - the key to get from the object * @param {any} def - the default value to return if the key isn't found */ -export function jsdef_get(obj, key, def=null) { +export function jsdef_get (obj, key, def=null) { return (key in obj) ? obj[key] : def; } -export function exposeGlobally() { +export function exposeGlobally () { // Extend existing prototypes String.prototype.join = join; diff --git a/openlibrary/plugins/openlibrary/js/lazy-carousel.js b/openlibrary/plugins/openlibrary/js/lazy-carousel.js index 603b10715e8..1aca14dcc44 100644 --- a/openlibrary/plugins/openlibrary/js/lazy-carousel.js +++ b/openlibrary/plugins/openlibrary/js/lazy-carousel.js @@ -7,25 +7,25 @@ import { buildPartialsUrl } from './utils'; * * @param elems {NodeList<HTMLElement>} Collection of placeholder carousel elements */ -export function initLazyCarousel(elems) { +export function initLazyCarousel (elems) { // Create intersection observer const intersectionObserver = new IntersectionObserver(intersectionCallback, { root: null, rootMargin: '200px', threshold: 0 - }) + }); elems.forEach(elem => { // Observe element for intersections - intersectionObserver.observe(elem) + intersectionObserver.observe(elem); // Add retry listener - const retryElem = elem.querySelector('.retry-btn') + const retryElem = elem.querySelector('.retry-btn'); retryElem.addEventListener('click', (e) => { - e.preventDefault() + e.preventDefault(); handleRetry(elem); - }) - }) + }); + }); } /** @@ -34,8 +34,8 @@ export function initLazyCarousel(elems) { * @param data {object} * @returns {Promise<Response>} */ -async function fetchPartials(data) { - return fetch(buildPartialsUrl('LazyCarousel', {...data})) +async function fetchPartials (data) { + return fetch(buildPartialsUrl('LazyCarousel', {...data})); } /** @@ -49,22 +49,22 @@ async function fetchPartials(data) { * * @param target {HTMLElement} A placeholder element for a carousel */ -function doFetchAndUpdate(target) { - const config = JSON.parse(target.dataset.config) - const loadingIndicator = target.querySelector('.loadingIndicator') +function doFetchAndUpdate (target) { + const config = JSON.parse(target.dataset.config); + const loadingIndicator = target.querySelector('.loadingIndicator'); fetchPartials(config) .then(resp => { if (!resp.ok) { - throw new Error('Failed to fetch partials from server') + throw new Error('Failed to fetch partials from server'); } - return resp.json() + return resp.json(); }) .then(data => { - const newElem = document.createElement('div') - newElem.innerHTML = data.partials.trim() - const carouselElements = newElem.querySelectorAll('.carousel--progressively-enhanced') - loadingIndicator.classList.add('hidden') + const newElem = document.createElement('div'); + newElem.innerHTML = data.partials.trim(); + const carouselElements = newElem.querySelectorAll('.carousel--progressively-enhanced'); + loadingIndicator.classList.add('hidden'); if (carouselElements.length === 0 && config.fallback) { // No results, disable filters @@ -77,16 +77,16 @@ function doFetchAndUpdate(target) { target.querySelector('.lazy-carousel-fallback').classList.remove('hidden'); } else { - target.parentNode.insertBefore(newElem, target) - target.remove() - initialzeCarousels(carouselElements) + target.parentNode.insertBefore(newElem, target); + target.remove(); + initialzeCarousels(carouselElements); } }) .catch(() => { loadingIndicator.classList.add('hidden'); - const retryElem = target.querySelector('.lazy-carousel-retry') - retryElem.classList.remove('hidden') - }) + const retryElem = target.querySelector('.lazy-carousel-retry'); + retryElem.classList.remove('hidden'); + }); } /** @@ -95,14 +95,14 @@ function doFetchAndUpdate(target) { * * @param target {Element} */ -function handleRetry(target) { - target.querySelector('.loadingIndicator').classList.remove('hidden') - target.querySelector('.lazy-carousel-retry').classList.add('hidden') - const carouselFallbackElem = target.querySelector('.lazy-carousel-fallback') +function handleRetry (target) { + target.querySelector('.loadingIndicator').classList.remove('hidden'); + target.querySelector('.lazy-carousel-retry').classList.add('hidden'); + const carouselFallbackElem = target.querySelector('.lazy-carousel-fallback'); if (carouselFallbackElem) { - carouselFallbackElem.classList.add('hidden') + carouselFallbackElem.classList.add('hidden'); } - doFetchAndUpdate(target) + doFetchAndUpdate(target); } /** @@ -113,12 +113,12 @@ function handleRetry(target) { * @param entries {Array<IntersectionObserverEntry>} * @param observer {IntersectionObserver} */ -function intersectionCallback(entries, observer) { +function intersectionCallback (entries, observer) { entries.forEach(entry => { if (entry.isIntersecting) { - const target = entry.target - observer.unobserve(target) - doFetchAndUpdate(target) + const target = entry.target; + observer.unobserve(target); + doFetchAndUpdate(target); } - }) + }); } diff --git a/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js b/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js index aca22bf8a28..d00cc8da961 100644 --- a/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js +++ b/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js @@ -18,7 +18,7 @@ import chunk from 'lodash/chunk'; * Currently only works with works, editions, and authors. */ export class LazyThingPreview { - constructor() { + constructor () { /** @type {Array<{key: string, render_fn: Function}>} */ this.queue = []; /** @type {Object<string, object>} */ @@ -27,7 +27,7 @@ export class LazyThingPreview { this.renderDebounced = debounce(this.render.bind(this), 100); } - init() { + init () { $('.lazy-thing-preview').each((i, el) => { this.push({ key: el.dataset.key, @@ -39,7 +39,7 @@ export class LazyThingPreview { /** * @param {{key: string, render_fn_name: string}} arg0 */ - push({key, render_fn_name}) { + push ({key, render_fn_name}) { const render_fn = window[render_fn_name]; if (this.cache[key]) { this.renderKey(key, render_fn, this.cache[key]); @@ -54,7 +54,7 @@ export class LazyThingPreview { * @param {Function} render_fn * @param {object} book */ - renderKey(key, render_fn, book) { + renderKey (key, render_fn, book) { const $el = $(`.lazy-thing-preview[data-key="${key}"]`); $el.html(render_fn(book)); } @@ -63,7 +63,7 @@ export class LazyThingPreview { * @param {string[]} keys * @returns {AsyncGenerator<object[]>} */ - async* getThings(keys) { + async* getThings (keys) { const workKeys = keys.filter(key => key.startsWith('/works/')); const editionKeys = keys.filter(key => key.startsWith('/books/')); const authorKeys = keys.filter(key => key.startsWith('/authors/')); @@ -100,7 +100,7 @@ export class LazyThingPreview { } } - async render() { + async render () { const keys = this.queue.map(({key}) => key); const render_fn_map = Object.fromEntries( this.queue.map(({key, render_fn}) => [key, render_fn]) diff --git a/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js b/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js index 73bb92ba585..8c76b293ae7 100644 --- a/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js +++ b/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js @@ -8,12 +8,12 @@ let i18nStrings; * * @param {HTMLDetailsElement} rootElement */ -export function initLibrarianDashboard(rootElement) { - i18nStrings = JSON.parse(rootElement.dataset.i18n) - const table = rootElement.querySelector('.dq-table') +export function initLibrarianDashboard (rootElement) { + i18nStrings = JSON.parse(rootElement.dataset.i18n); + const table = rootElement.querySelector('.dq-table'); rootElement.addEventListener('click', () => { - populateTable(table) - }, {once: true}) + populateTable(table); + }, {once: true}); } /** @@ -22,11 +22,11 @@ export function initLibrarianDashboard(rootElement) { * @param {HTMLTableElement} table * @returns {Promise<void>} */ -async function populateTable(table) { - const bookCount = Number(table.dataset.totalBooks) - const rows = table.querySelectorAll('.dq-table__row') +async function populateTable (table) { + const bookCount = Number(table.dataset.totalBooks); + const rows = table.querySelectorAll('.dq-table__row'); - await Promise.all([...rows].map(row => updateRow(row, bookCount))) + await Promise.all([...rows].map(row => updateRow(row, bookCount))); } /** @@ -36,45 +36,45 @@ async function populateTable(table) { * @param {number} totalCount Total number of search results * @returns {Promise<void>} */ -async function updateRow(row, totalCount) { - const queryFragment = row.dataset.queryFragment - const apiUrl = buildUrl(queryFragment, false) - const searchPageUrl = buildUrl(queryFragment) +async function updateRow (row, totalCount) { + const queryFragment = row.dataset.queryFragment; + const apiUrl = buildUrl(queryFragment, false); + const searchPageUrl = buildUrl(queryFragment); // Make query const data = await fetch(apiUrl) .then((resp) => { if (!resp.ok) { - throw new Error(`Data quality response status : ${resp.status}`) + throw new Error(`Data quality response status : ${resp.status}`); } - return resp.json() + return resp.json(); }) .catch(() => { return null; - }) + }); // Render status cell markup - let newCellMarkup + let newCellMarkup; if (data === null) { - newCellMarkup = renderErrorCell(searchPageUrl) + newCellMarkup = renderErrorCell(searchPageUrl); } else { - newCellMarkup = renderResultsCells(data, totalCount, searchPageUrl) + newCellMarkup = renderResultsCells(data, totalCount, searchPageUrl); } // Include retry affordance, regardless of result - newCellMarkup += renderRetryCell() + newCellMarkup += renderRetryCell(); - replaceStatusCells(row, newCellMarkup) + replaceStatusCells(row, newCellMarkup); // Add listener to retry affordance - const retryAffordance = row.querySelector('.dqs-run-again') + const retryAffordance = row.querySelector('.dqs-run-again'); retryAffordance.addEventListener('click', () => { // Update view to "pending" - replaceStatusCells(row, renderPendingCell()) + replaceStatusCells(row, renderPendingCell()); // Retry query - updateRow(row, totalCount) - }) + updateRow(row, totalCount); + }); } /** @@ -83,13 +83,13 @@ async function updateRow(row, totalCount) { * @param {string} queryFragment * @param {boolean} forUi */ -function buildUrl(queryFragment, forUi = true) { - const match = window.location.pathname.match(/authors\/(OL\d+A)/) - const queryParamString = match ? `?q=author_key:${match[1]}` : window.location.search +function buildUrl (queryFragment, forUi = true) { + const match = window.location.pathname.match(/authors\/(OL\d+A)/); + const queryParamString = match ? `?q=author_key:${match[1]}` : window.location.search; - const params = new URLSearchParams(queryParamString) - params.set('q', `${params.get('q')} ${queryFragment}`) - return `/search${forUi ? '' : '.json'}?${params.toString()}` + const params = new URLSearchParams(queryParamString); + params.set('q', `${params.get('q')} ${queryFragment}`); + return `/search${forUi ? '' : '.json'}?${params.toString()}`; } /** @@ -98,15 +98,15 @@ function buildUrl(queryFragment, forUi = true) { * @param {HTMLTableRowElement} row * @param {string} newCellMarkup Markup for the new status cells */ -function replaceStatusCells(row, newCellMarkup) { - const statusCells = row.querySelectorAll('td:not(.dq-table__criterion-cell)') +function replaceStatusCells (row, newCellMarkup) { + const statusCells = row.querySelectorAll('td:not(.dq-table__criterion-cell)'); for (const cell of statusCells) { - cell.remove() + cell.remove(); } - const template = document.createElement('template') - template.innerHTML = newCellMarkup - row.append(...template.content.children) + const template = document.createElement('template'); + template.innerHTML = newCellMarkup; + row.append(...template.content.children); } /** @@ -118,9 +118,9 @@ function replaceStatusCells(row, newCellMarkup) { * * @returns {string} HTML string */ -function renderResultsCells(results, totalCount, failingHref) { - const numFound = results.numFound - const percentage = Math.floor(((totalCount - numFound) / totalCount) * 100) +function renderResultsCells (results, totalCount, failingHref) { + const numFound = results.numFound; + const percentage = Math.floor(((totalCount - numFound) / totalCount) * 100); return `<td class="dq-table__results-cell"> <meter title="${numFound} of ${totalCount}" min="0" max="100" value="${percentage}"></meter> @@ -128,7 +128,7 @@ function renderResultsCells(results, totalCount, failingHref) { </td> <td style="text-align:right"> <a href="${failingHref}">${numFound} ${i18nStrings['failing']}</a> - </td>` + </td>`; } /** @@ -136,12 +136,12 @@ function renderResultsCells(results, totalCount, failingHref) { * * @returns {string} HTML string */ -function renderRetryCell() { +function renderRetryCell () { return `<td> <button class="dqs-run-again"> ${i18nStrings['reload']} </button> - </td>` + </td>`; } /** @@ -150,10 +150,10 @@ function renderRetryCell() { * @param {string} href Link to search page for failing query * @returns {string} */ -function renderErrorCell(href) { +function renderErrorCell (href) { return `<td colspan="2"> <a href="${href}">${i18nStrings['error']}</a> - </td>` + </td>`; } /** @@ -161,6 +161,6 @@ function renderErrorCell(href) { * * @returns {string} */ -function renderPendingCell() { - return `<td colspan="3">${i18nStrings['loading']}</td>` +function renderPendingCell () { + return `<td colspan="3">${i18nStrings['loading']}</td>`; } diff --git a/openlibrary/plugins/openlibrary/js/list_books.js b/openlibrary/plugins/openlibrary/js/list_books.js index ab4c6b6ef4f..9e0c77afcd1 100644 --- a/openlibrary/plugins/openlibrary/js/list_books.js +++ b/openlibrary/plugins/openlibrary/js/list_books.js @@ -3,21 +3,21 @@ export class ListBooks { * @param {HTMLElement} listBooks * @param {HTMLElement} layoutToolbar **/ - constructor(listBooks, layoutToolbar) { + constructor (listBooks, layoutToolbar) { this.listBooks = listBooks; this.layoutToolbar = layoutToolbar; this.activeLayout = this.layoutToolbar.querySelector('a.active'); } - attach() { + attach () { $(this.layoutToolbar).on('click', 'a', this.updateLayout.bind(this)); } /** * @param {MouseEvent} event */ - updateLayout(event) { + updateLayout (event) { event.preventDefault(); const layoutAnchor = event.target; this.layoutToolbar.querySelector('a.active').classList.remove('active'); @@ -27,7 +27,7 @@ export class ListBooks { document.cookie = `LBL=${layout}; path=/; max-age=31536000`; } - static init() { + static init () { // Assume only one list-books/layout per page new ListBooks( document.querySelector('.list-books'), diff --git a/openlibrary/plugins/openlibrary/js/lists/ListService.js b/openlibrary/plugins/openlibrary/js/lists/ListService.js index 7f501321c2c..7311c6e24f1 100644 --- a/openlibrary/plugins/openlibrary/js/lists/ListService.js +++ b/openlibrary/plugins/openlibrary/js/lists/ListService.js @@ -12,7 +12,7 @@ import { buildPartialsUrl } from '../utils'; * @param {object} data Object containing the new list's name, description, and seeds. * @returns {Promise<Response>} The results of the POST request */ -export async function createList(userKey, data) { +export async function createList (userKey, data) { return await fetch(`${userKey}/lists.json`, { method: 'post', headers: { @@ -30,7 +30,7 @@ export async function createList(userKey, data) { * @param {object} seed Object containing the new list's name, description, and seeds. * @returns {Promise<Response>} The result of the POST request */ -export async function addItem(listKey, seed) { +export async function addItem (listKey, seed) { const body = { add: [seed] }; return await fetch(`${listKey}/seeds.json`, { method: 'post', @@ -49,7 +49,7 @@ export async function addItem(listKey, seed) { * @param {string|{ key: string }} seed The item being removed from the list. * @returns {Promise<Response>} The POST response */ -export async function removeItem(listKey, seed) { +export async function removeItem (listKey, seed) { const body = { remove: [seed] }; return await fetch(`${listKey}/seeds.json`, { method: 'post', @@ -62,7 +62,7 @@ export async function removeItem(listKey, seed) { } // XXX : jsdoc -export async function getListPartials() { +export async function getListPartials () { return await fetch(buildPartialsUrl('MyBooksDropperLists'), { headers: { 'Content-Type': 'application/json', diff --git a/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js b/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js index 44e77eb245a..845eef9a51d 100644 --- a/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js +++ b/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js @@ -10,7 +10,7 @@ import 'jquery-ui/ui/widgets/dialog'; const itemsWithDeleteList = $('.deleteList .resultTitle'); if (itemsWithDeleteList.length) { const deleteListLink = $('.listDelete--myLists'); - itemsWithDeleteList.each(function() { + itemsWithDeleteList.each(function () { $(deleteListLink).clone().prependTo(this).removeClass('hidden'); }); @@ -24,7 +24,7 @@ if (itemsWithDeleteList.length) { const itemsWithDeleteSeed = $('.deleteSeed .resultTitle'); if (itemsWithDeleteSeed.length) { const deleteSeedLink = $('.seedDelete--myLists'); - itemsWithDeleteSeed.each(function() { + itemsWithDeleteSeed.each(function () { $(deleteSeedLink).clone().prependTo(this).removeClass('hidden'); }); @@ -38,9 +38,9 @@ if (itemsWithDeleteSeed.length) { * @param {string} seed - path to seed book being removed, ex: /books/OL23269118M * @param {function} success - click function */ -function remove_seed(list_key, seed, success) { +function remove_seed (list_key, seed, success) { if (seed[0] === '/') { - seed = {key: seed} + seed = {key: seed}; } $.ajax({ @@ -52,7 +52,7 @@ function remove_seed(list_key, seed, success) { }), dataType: 'json', - beforeSend: function(xhr) { + beforeSend: function (xhr) { xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Accept', 'application/json'); }, @@ -63,7 +63,7 @@ function remove_seed(list_key, seed, success) { /** * @returns {number} count of number of seed books in a list */ -function get_seed_count() { +function get_seed_count () { return $('ul#listResults').children().length; } @@ -85,7 +85,7 @@ const getConfirmButtonLabelText = () => { // Add listeners to each .listDelete link element // Sometimes .listDelete is dynamically added to the DOM, so we'll add the listener to a parent element -$('#listResults').on('click', '.listDelete a', function() { +$('#listResults').on('click', '.listDelete a', function () { if (get_seed_count() > 1 && !$(this).parent().hasClass('listDelete--myLists')) { $('#remove-seed-dialog') .data('seed-key', $(this).closest('[data-seed-key]').data('seed-key')) @@ -111,13 +111,13 @@ $('#remove-seed-dialog').dialog({ ConfirmRemoveSeed: { text: getConfirmButtonLabelText(), id: 'remove-seed-dialog--confirm', - click: function() { + click: function () { var list_key = $(this).data('list-key'); var seed_key = $(this).data('seed-key'); var _this = this; - remove_seed(list_key, seed_key, function() { + remove_seed(list_key, seed_key, function () { $(`[data-seed-key='${seed_key}']`).remove(); // update seed count $('#list-items-count').load(`${location.href} #list-items-count`); @@ -132,7 +132,7 @@ $('#remove-seed-dialog').dialog({ CancelRemoveSeed: { text: getCancelButtonLabelText(), id: 'remove-seed-dialog--cancel', - click: function() { + click: function () { $(this).dialog('close'); $('#remove-seed-dialog').addClass('hidden'); } @@ -150,11 +150,11 @@ $('#delete-list-dialog').dialog({ ConfirmDeleteList: { text: getConfirmButtonLabelText(), id: 'delete-list-dialog--confirm', - click: function() { + click: function () { var list_key = $(this).data('list-key'); var _this = this; - $.post(`${list_key}/delete.json`, function() { + $.post(`${list_key}/delete.json`, function () { $(_this).dialog('close'); window.location.reload(); }); @@ -163,7 +163,7 @@ $('#delete-list-dialog').dialog({ CancelDeleteList: { text: getCancelButtonLabelText(), id: 'delete-list-dialog--cancel', - click: function() { + click: function () { $(this).dialog('close'); } } diff --git a/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js b/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js index f20a633ec3c..716e9fdf235 100644 --- a/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js +++ b/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js @@ -1,8 +1,8 @@ /** * @module lists/ShowcaseItem.js */ -import { removeItem } from './ListService' -import myBooksStore from '../my-books/store' +import { removeItem } from './ListService'; +import myBooksStore from '../my-books/store'; /** * Represents an actionable list showcase item. @@ -32,64 +32,64 @@ export class ShowcaseItem { * * @param {HTMLElement} showcaseElem */ - constructor(showcaseElem) { + constructor (showcaseElem) { /** * Reference to the root element of this component. * @member {HTMLElement} */ - this.showcaseElem = showcaseElem + this.showcaseElem = showcaseElem; /** * `true` if this object represents the active lists showcase. * @member {boolean} */ - this.isActiveShowcase = showcaseElem.parentElement.classList.contains('already-lists') + this.isActiveShowcase = showcaseElem.parentElement.classList.contains('already-lists'); /** * Reference to the affordance which removes an item from this list. * @member {HTMLElement} */ - this.removeFromListAffordance = showcaseElem.querySelector('.remove-from-list') + this.removeFromListAffordance = showcaseElem.querySelector('.remove-from-list'); /** * Unique identifier for the showcased list. * @member {string} */ - this.listKey = this.removeFromListAffordance.dataset.listKey + this.listKey = this.removeFromListAffordance.dataset.listKey; /** * Unique identifier for the showcased list member. * @member {string} */ - this.seedKey = showcaseElem.querySelector('input[name=seed-key]').value + this.seedKey = showcaseElem.querySelector('input[name=seed-key]').value; /** * The list item's type. * @member {'subject'|'edition'|'work'|'author'} */ - this.type = showcaseElem.querySelector('input[name=seed-type]').value + this.type = showcaseElem.querySelector('input[name=seed-type]').value; /** * `true` if this list item is a subject. * @member {boolean} */ - this.isSubject = this.type === 'subject' + this.isSubject = this.type === 'subject'; /** * `true` if this list item is a work * @member {boolean} */ - this.isWork = !this.isSubject && this.seedKey.slice(-1) === 'W' + this.isWork = !this.isSubject && this.seedKey.slice(-1) === 'W'; /** * `POST` request-ready representation of the list's seed key. * @member {string|object} */ - this.seed + this.seed; if (this.isSubject) { - this.seed = this.seedKey + this.seed = this.seedKey; } else { - this.seed = { key: this.seedKey } + this.seed = { key: this.seedKey }; } } @@ -97,11 +97,11 @@ export class ShowcaseItem { * Attaches click listeners to the showcase item's "Remove from list" * affordance. */ - initialize() { + initialize () { this.removeFromListAffordance.addEventListener('click', (event) => { - event.preventDefault() - this.removeShowcaseItem() - }) + event.preventDefault(); + this.removeShowcaseItem(); + }); } /** @@ -110,28 +110,28 @@ export class ShowcaseItem { * Removes any affiliated showcase items from the DOM, and updates all * dropper list affordances. */ - async removeShowcaseItem() { + async removeShowcaseItem () { await removeItem(this.listKey, this.seed) .then(response => response.json()) .then(() => { - const showcases = myBooksStore.getShowcases() + const showcases = myBooksStore.getShowcases(); // Remove self: - this.removeSelf() + this.removeSelf(); // Remove other showcase items that are associated with the list and seed key: for (const showcase of showcases) { if (showcase.isShowcaseForListAndSeed(this.listKey, this.seedKey)) { - showcase.removeSelf() + showcase.removeSelf(); } } // Update droppers: - const droppers = myBooksStore.getDroppers() + const droppers = myBooksStore.getDroppers(); for (const dropper of droppers) { - dropper.readingLists.updateViewAfterModifyingList(this.listKey, this.isWork, false) + dropper.readingLists.updateViewAfterModifyingList(this.listKey, this.isWork, false); } - }) + }); } /** @@ -140,12 +140,12 @@ export class ShowcaseItem { * Removes self from the myBooksStore's showcase array * upon success. */ - removeSelf() { - const showcases = myBooksStore.getShowcases() - const thisIndex = showcases.indexOf(this) + removeSelf () { + const showcases = myBooksStore.getShowcases(); + const thisIndex = showcases.indexOf(this); if (thisIndex >= 0) { - this.showcaseElem.remove() - showcases.splice(thisIndex, 1) + this.showcaseElem.remove(); + showcases.splice(thisIndex, 1); } } @@ -160,19 +160,19 @@ export class ShowcaseItem { * * @param {boolean} showWorks `true` if only active showcase items related to works should be displayed */ - toggleVisibility(showWorks) { + toggleVisibility (showWorks) { if (this.isActiveShowcase) { if (showWorks) { if (this.isWork) { - this.showcaseElem.classList.remove('hidden') + this.showcaseElem.classList.remove('hidden'); } else { - this.showcaseElem.classList.add('hidden') + this.showcaseElem.classList.add('hidden'); } } else { if (this.isWork) { - this.showcaseElem.classList.add('hidden') + this.showcaseElem.classList.add('hidden'); } else { - this.showcaseElem.classList.remove('hidden') + this.showcaseElem.classList.remove('hidden'); } } } @@ -185,8 +185,8 @@ export class ShowcaseItem { * @param {string} seedKey * @return {boolean} `true` if the given keys match this item's keys */ - isShowcaseForListAndSeed(listKey, seedKey) { - return (this.listKey === listKey) && (this.seedKey === seedKey) + isShowcaseForListAndSeed (listKey, seedKey) { + return (this.listKey === listKey) && (this.seedKey === seedKey); } } @@ -195,9 +195,9 @@ export class ShowcaseItem { * showcase items. * @type {Record<string, string>} */ -let i18nStrings +let i18nStrings; -const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png' +const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; /** * Returns the inferred type of the given seed key. @@ -205,19 +205,19 @@ const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png' * @param {string} seed * @returns {string} Type of the given seed key. */ -function getSeedType(seed) { +function getSeedType (seed) { // XXX : validate input? if (seed[0] !== '/') { - return 'subject' + return 'subject'; } if (seed.endsWith('M')) { - return 'edition' + return 'edition'; } if (seed.endsWith('W')) { - return 'work' + return 'work'; } if (seed.endsWith('A')) { - return 'author' + return 'author'; } } @@ -233,15 +233,15 @@ function getSeedType(seed) { * @param {string} [coverUrl] * @returns {HTMLLIElement} */ -export function createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl = DEFAULT_COVER_URL) { +export function createActiveShowcaseItem (listKey, seedKey, listTitle, coverUrl = DEFAULT_COVER_URL) { if (!i18nStrings) { - const i18nInput = document.querySelector('input[name=list-i18n-strings]') - i18nStrings = JSON.parse(i18nInput.value) + const i18nInput = document.querySelector('input[name=list-i18n-strings]'); + i18nStrings = JSON.parse(i18nInput.value); } - const splitKey = listKey.split('/') - const userKey = `/${splitKey[1]}/${splitKey[2]}` - const seedType = getSeedType(seedKey) + const splitKey = listKey.split('/'); + const userKey = `/${splitKey[1]}/${splitKey[2]}`; + const seedType = getSeedType(seedKey); const itemMarkUp = `<span class="image"> <a href="${listKey}"><img src="${coverUrl}" alt="${i18nStrings['cover_of']}${listTitle}" title="${i18nStrings['cover_of']}${listTitle}"/></a> @@ -255,14 +255,14 @@ export function createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl = <a href="${listKey}" class="remove-from-list red smaller arial plain" data-list-key="${listKey}" title="${i18nStrings['remove_from_list']}">[X]</a> </span> <span class="owner">${i18nStrings['from']} <a href="${userKey}">${i18nStrings['you']}</a></span> - </span>` + </span>`; - const li = document.createElement('li') - li.classList.add('actionable-item') - li.dataset.listKey = listKey - li.innerHTML = itemMarkUp + const li = document.createElement('li'); + li.classList.add('actionable-item'); + li.dataset.listKey = listKey; + li.innerHTML = itemMarkUp; - return li + return li; } /** @@ -275,9 +275,9 @@ export function createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl = * * @param {boolean} showWorksOnly */ -export function toggleActiveShowcaseItems(showWorksOnly) { +export function toggleActiveShowcaseItems (showWorksOnly) { for (const item of myBooksStore.getShowcases()) { - item.toggleVisibility(showWorksOnly) + item.toggleVisibility(showWorksOnly); } } @@ -296,16 +296,16 @@ export function toggleActiveShowcaseItems(showWorksOnly) { * @param {string} listTitle * @param {string} [coverUrl] */ -export function attachNewActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl = DEFAULT_COVER_URL) { - const activeListsShowcase = document.querySelector('.already-lists') +export function attachNewActiveShowcaseItem (listKey, seedKey, listTitle, coverUrl = DEFAULT_COVER_URL) { + const activeListsShowcase = document.querySelector('.already-lists'); if (activeListsShowcase) { - const li = createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl) - activeListsShowcase.appendChild(li) + const li = createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl); + activeListsShowcase.appendChild(li); - const showcase = new ShowcaseItem(li) - showcase.initialize() + const showcase = new ShowcaseItem(li); + showcase.initialize(); - myBooksStore.getShowcases().push(showcase) + myBooksStore.getShowcases().push(showcase); } } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js index 7e850020557..8ea03ca21f0 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js @@ -5,8 +5,8 @@ * @module merge-request-table/MergeRequestTable */ -import TableHeader from './MergeRequestTable/TableHeader' -import { setI18nStrings, TableRow } from './MergeRequestTable/TableRow' +import TableHeader from './MergeRequestTable/TableHeader'; +import { setI18nStrings, TableRow } from './MergeRequestTable/TableRow'; /** * Class representing the librarian request table. @@ -20,42 +20,42 @@ export default class MergeRequestTable { * * @param {HTMLElement} mergeRequestTable */ - constructor(mergeRequestTable) { + constructor (mergeRequestTable) { /** * The `username` of the authenticated patron, or '' if logged out. * * @param {string} */ - this.username = mergeRequestTable.dataset.username + this.username = mergeRequestTable.dataset.username; - const localizedStrings = JSON.parse(mergeRequestTable.dataset.i18n) - setI18nStrings(localizedStrings) + const localizedStrings = JSON.parse(mergeRequestTable.dataset.i18n); + setI18nStrings(localizedStrings); /** * Reference to this table's header. * * @param {HTMLElement} */ - this.tableHeader = new TableHeader(mergeRequestTable.querySelector('.table-header')) + this.tableHeader = new TableHeader(mergeRequestTable.querySelector('.table-header')); /** * References to each row in the table. * * @param {Array<TableRow>} */ - this.tableRows = [] - const rowElements = mergeRequestTable.querySelectorAll('.mr-table-row') + this.tableRows = []; + const rowElements = mergeRequestTable.querySelectorAll('.mr-table-row'); for (const elem of rowElements) { - this.tableRows.push(new TableRow(elem, this.username)) + this.tableRows.push(new TableRow(elem, this.username)); } } /** * Hydrates the librarian request table. */ - initialize() { - this.tableHeader.initialize() - document.addEventListener('click', (event) => this.tableHeader.closeMenusIfClickOutside(event)) - this.tableRows.forEach(elem => elem.initialize()) + initialize () { + this.tableHeader.initialize(); + document.addEventListener('click', (event) => this.tableHeader.closeMenusIfClickOutside(event)); + this.tableRows.forEach(elem => elem.initialize()); } } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js index 39be68c2b3d..48d79d10e2d 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js @@ -19,7 +19,7 @@ export default class TableHeader { * * @param {HTMLElement} tableHeader */ - constructor(tableHeader) { + constructor (tableHeader) { /** * References to each select menu. These are always visible * in the header bar, and, when clicked, display a drop-down @@ -27,33 +27,33 @@ export default class TableHeader { * * @param {NodeList<HTMLElement>} */ - this.dropMenuButtons = tableHeader.querySelectorAll('.mr-dropdown') + this.dropMenuButtons = tableHeader.querySelectorAll('.mr-dropdown'); /** * References each drop-down filter option menu. * * @param {NodeList<HTMLElement>} */ - this.dropMenus = tableHeader.querySelectorAll('.mr-dropdown-menu') + this.dropMenus = tableHeader.querySelectorAll('.mr-dropdown-menu'); /** * References each drop-down menu "X" affordance, which closes * the appropriate drop-down menu. * * @param{NodeList<HTMLElement>} */ - this.closeButtons = tableHeader.querySelectorAll('.dropdown-close') + this.closeButtons = tableHeader.querySelectorAll('.dropdown-close'); /** * References each text input filter. * * @param{NodeList<HTMLElement>} */ - this.searchInputs = tableHeader.querySelectorAll('.filter') + this.searchInputs = tableHeader.querySelectorAll('.filter'); } /** * Hydrates the table header affordances. */ - initialize() { - this.initFilters() + initialize () { + this.initFilters(); } /** @@ -62,12 +62,12 @@ export default class TableHeader { * @param {Event} event * @param {string} menuButtonId */ - toggleAMenuWhileClosingOthers(event, menuButtonId) { + toggleAMenuWhileClosingOthers (event, menuButtonId) { // prevent closing of menu on bubbling unless click menuButton itself if (event.target.id === menuButtonId) { // close other open menus, then toggle selected menu - this.closeOtherMenus(menuButtonId) - event.target.firstElementChild.classList.toggle('hidden') + this.closeOtherMenus(menuButtonId); + event.target.firstElementChild.classList.toggle('hidden'); } } @@ -76,12 +76,12 @@ export default class TableHeader { * * @param {string} menuButtonId */ - closeOtherMenus(menuButtonId) { + closeOtherMenus (menuButtonId) { this.dropMenuButtons.forEach((menuButton) => { if (menuButton.id !== menuButtonId) { - menuButton.firstElementChild.classList.add('hidden') + menuButton.firstElementChild.classList.add('hidden'); } - }) + }); } /** @@ -89,14 +89,14 @@ export default class TableHeader { * * @param {Event} event */ - filterMenuItems(event) { - const input = document.getElementById(event.target.id) - const filter = input.value.toUpperCase() - const menu = input.closest('.mr-dropdown-menu') - const items = menu.getElementsByClassName('dropdown-item') + filterMenuItems (event) { + const input = document.getElementById(event.target.id); + const filter = input.value.toUpperCase(); + const menu = input.closest('.mr-dropdown-menu'); + const items = menu.getElementsByClassName('dropdown-item'); // skip first item in menu for (let i=1; i < items.length; i++) { - const text = items[i].textContent + const text = items[i].textContent; items[i].classList.toggle('hidden', text.toUpperCase().indexOf(filter) === -1); } } @@ -107,13 +107,13 @@ export default class TableHeader { * * @param {Event} event */ - closeMenusIfClickOutside(event) { + closeMenusIfClickOutside (event) { const menusClicked = Array.from(this.dropMenuButtons).filter((menuButton) => { - return menuButton.contains(event.target) - }) + return menuButton.contains(event.target); + }); // want to preserve clicking in a menu, i.e. when filtering for users if (!menusClicked.length) { - this.dropMenus.forEach((menu) => menu.classList.add('hidden')) + this.dropMenus.forEach((menu) => menu.classList.add('hidden')); } } @@ -121,19 +121,19 @@ export default class TableHeader { * Initialize events for merge queue filter dropdown menu functionality. * */ - initFilters() { + initFilters () { this.dropMenuButtons.forEach((menuButton) => { menuButton.addEventListener('click', (event) => { - this.toggleAMenuWhileClosingOthers(event, menuButton.id) - }) - }) + this.toggleAMenuWhileClosingOthers(event, menuButton.id); + }); + }); this.closeButtons.forEach((button) => { button.addEventListener('click', (event) => { - event.target.closest('.mr-dropdown-menu').classList.toggle('hidden') - }) - }) + event.target.closest('.mr-dropdown-menu').classList.toggle('hidden'); + }); + }); this.searchInputs.forEach((input) => { - input.addEventListener('keyup', (event) => this.filterMenuItems(event)) - }) + input.addEventListener('keyup', (event) => this.filterMenuItems(event)); + }); } } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js index b78e5988c56..76cb134d942 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js @@ -4,12 +4,12 @@ * @module merge-request-table/MergeRequestTable/TableRow */ -import { claimRequest, commentOnRequest, declineRequest, unassignRequest } from '../MergeRequestService' -import { FadingToast } from '../../Toast' +import { claimRequest, commentOnRequest, declineRequest, unassignRequest } from '../MergeRequestService'; +import { FadingToast } from '../../Toast'; let i18nStrings; -export function setI18nStrings(localizedStrings) { +export function setI18nStrings (localizedStrings) { i18nStrings = localizedStrings; } @@ -34,67 +34,67 @@ export class TableRow { * @param {HTMLElement} row Root element of a table row * @param {string} username `username` of logged-in patron. Empty if unauthenticated. */ - constructor(row, username) { + constructor (row, username) { /** * Reference to this row. * * @param {HTMLElement} */ - this.row = row + this.row = row; /** * `username` of authenticated patron, or '' if unauthenticated. * * @param {HTMLElement} */ - this.username = username + this.username = username; /** * Unique identifier for this row. * * @param {Number} */ - this.mrid = row.dataset.mrid + this.mrid = row.dataset.mrid; /** * Button used to toggle the full comments display's visibility. * * @param {HTMLElement} */ - this.toggleCommentButton = row.querySelector('.mr-comment-toggle__comment-expand') + this.toggleCommentButton = row.querySelector('.mr-comment-toggle__comment-expand'); /** * Element which displays this row's comment count. * * @param {HTMLElement} */ - this.commentCountDisplay = row.querySelector('.mr-comment-toggle__comment-count') + this.commentCountDisplay = row.querySelector('.mr-comment-toggle__comment-count'); /** * Element displaying the most recent comment on this request. * * @param {HTMLElement} */ - this.commentPreview = row.querySelector('.mr-details__comment-preview') + this.commentPreview = row.querySelector('.mr-details__comment-preview'); /** * Hidden comments display. Also contains reply inputs, if rendered. * * @param {HTMLElement} */ - this.fullCommentsPanel = row.querySelector('.comment-panel') + this.fullCommentsPanel = row.querySelector('.comment-panel'); /** * Element that displays all of the comments for this request. * * @param {HTMLElement} */ - this.commentsDisplay = this.fullCommentsPanel.querySelector('.comment-panel__comment-display') + this.commentsDisplay = this.fullCommentsPanel.querySelector('.comment-panel__comment-display'); /** * The comment text input. * * @param {HTMLElement|null} */ - this.commentReplyInput = this.fullCommentsPanel.querySelector('.comment-panel__reply-input') + this.commentReplyInput = this.fullCommentsPanel.querySelector('.comment-panel__reply-input'); /** * The comment reply button. * * @param {HTMLElement|null} */ - this.replyButton = this.fullCommentsPanel.querySelector('.comment-panel__reply-btn') + this.replyButton = this.fullCommentsPanel.querySelector('.comment-panel__reply-btn'); /** * Affordance which allows one to close their own request. * @@ -102,47 +102,47 @@ export class TableRow { * * @param {HTMLElement|null} */ - this.closeRequestButton = this.row.querySelector('.mr-close-link') + this.closeRequestButton = this.row.querySelector('.mr-close-link'); /** * Button used by super-librarians to claim a request. * * @param {HTMLElement} */ - this.reviewButton = this.row.querySelector('.mr-review-actions__review-btn') + this.reviewButton = this.row.querySelector('.mr-review-actions__review-btn'); /** * Reference to root element of the assignee display. * * @param {HTMLElement} */ - this.assigneeElement = this.row.querySelector('.mr-review-actions__assignee') + this.assigneeElement = this.row.querySelector('.mr-review-actions__assignee'); /** * Assignee display element which displays the assignee's name. * * @param {HTMLElement} */ - this.assigneeLabel = this.row.querySelector('.mr-review-actions__assignee-name') + this.assigneeLabel = this.row.querySelector('.mr-review-actions__assignee-name'); /** * Element that unassignees the current reviewer when clicked. * * @param {HTMLElement} */ - this.unassignReviewerButton = this.row.querySelector('.mr-review-actions__unassign') + this.unassignReviewerButton = this.row.querySelector('.mr-review-actions__unassign'); } /** * Hydrates interactive elements in this row. */ - initialize() { - this.toggleCommentButton.addEventListener('click', () => this.toggleComments()) + initialize () { + this.toggleCommentButton.addEventListener('click', () => this.toggleComments()); if (this.closeRequestButton) { - this.closeRequestButton.addEventListener('click', () => this.closeRequest()) + this.closeRequestButton.addEventListener('click', () => this.closeRequest()); } if (this.replyButton && this.commentReplyInput) { - this.replyButton.addEventListener('click', () => this.addComment()) + this.replyButton.addEventListener('click', () => this.addComment()); } - this.reviewButton.addEventListener('click', () => this.claimRequest()) + this.reviewButton.addEventListener('click', () => this.claimRequest()); if (this.unassignReviewerButton) { - this.unassignReviewerButton.addEventListener('click', () => this.unassignReviewer()) + this.unassignReviewerButton.addEventListener('click', () => this.unassignReviewer()); } } @@ -153,9 +153,9 @@ export class TableRow { * the full comments panel is hidden. This function toggles * each element's visibility. */ - toggleComments() { - this.commentPreview.classList.toggle('hidden') - this.fullCommentsPanel.classList.toggle('hidden') + toggleComments () { + this.commentPreview.classList.toggle('hidden'); + this.fullCommentsPanel.classList.toggle('hidden'); // Add depressed effect to toggle button: this.toggleCommentButton.classList.toggle('mr-comment-toggle__comment-expand--active'); @@ -165,20 +165,20 @@ export class TableRow { * Closes the request linked to this row, and removes this * row from the DOM. */ - async closeRequest() { - const comment = prompt(i18nStrings['close_request_comment_prompt']) + async closeRequest () { + const comment = prompt(i18nStrings['close_request_comment_prompt']); if (comment !== null) { // Comment will be `null` if "Cancel" button pressed await declineRequest(this.mrid, comment) .then(result => result.json()) .then(data => { if (data.status === 'ok') { - this.row.parentElement.removeChild(this.row) + this.row.parentElement.removeChild(this.row); } }) .catch(e => { // XXX : toast? - throw e - }) + throw e; + }); } } @@ -187,22 +187,22 @@ export class TableRow { * * Updates the view on success. */ - async addComment() { - const comment = this.commentReplyInput.value.trim() + async addComment () { + const comment = this.commentReplyInput.value.trim(); if (comment) { await commentOnRequest(this.mrid, comment) .then(result => result.json()) .then(data => { if (data.status === 'ok') { - this.updateCommentViews(comment) - this.commentReplyInput.value = '' + this.updateCommentViews(comment); + this.commentReplyInput.value = ''; } else { - new FadingToast(i18nStrings['comment_submission_failure_message']).show() + new FadingToast(i18nStrings['comment_submission_failure_message']).show(); } }) .catch(e => { - throw e - }) + throw e; + }); } } @@ -215,24 +215,24 @@ export class TableRow { * * @param {string} comment The newly added comment. */ - updateCommentViews(comment) { - const escapedComment = document.createTextNode(comment) + updateCommentViews (comment) { + const escapedComment = document.createTextNode(comment); // Update preview: - this.commentPreview.innerText = escapedComment.textContent + this.commentPreview.innerText = escapedComment.textContent; // Update full display: - const newComment = document.createElement('div') - newComment.classList.add('comment-panel__comment') - newComment.innerHTML = `<span class="commenter">@${this.username}</span> ` - newComment.appendChild(escapedComment) + const newComment = document.createElement('div'); + newComment.classList.add('comment-panel__comment'); + newComment.innerHTML = `<span class="commenter">@${this.username}</span> `; + newComment.appendChild(escapedComment); - this.commentsDisplay.appendChild(newComment) - this.commentsDisplay.scrollTop = this.commentsDisplay.scrollHeight + this.commentsDisplay.appendChild(newComment); + this.commentsDisplay.scrollTop = this.commentsDisplay.scrollHeight; // Update comment count: - const count = Number(this.commentCountDisplay.innerText) + 1 - this.commentCountDisplay.innerText = count + const count = Number(this.commentCountDisplay.innerText) + 1; + this.commentCountDisplay.innerText = count; } /** @@ -240,16 +240,16 @@ export class TableRow { * * Hides the review button, and shows the assignee display. */ - async claimRequest() { + async claimRequest () { await claimRequest(this.mrid) .then(result => result.json()) .then(data => { if (data.status === 'ok') { - this.assigneeLabel.innerText = `@${this.username}` - this.assigneeElement.classList.remove('hidden') - this.reviewButton.classList.add('hidden') + this.assigneeLabel.innerText = `@${this.username}`; + this.assigneeElement.classList.remove('hidden'); + this.reviewButton.classList.add('hidden'); } - }) + }); } /** @@ -257,14 +257,14 @@ export class TableRow { * * Hides the assignee display and shows the review button on success. */ - async unassignReviewer() { + async unassignReviewer () { await unassignRequest(this.mrid) .then(result => result.json()) .then(data => { if (data.status === 'ok') { - this.assigneeElement.classList.add('hidden') - this.reviewButton.classList.remove('hidden') + this.assigneeElement.classList.add('hidden'); + this.reviewButton.classList.remove('hidden'); } - }) + }); } } diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/index.js b/openlibrary/plugins/openlibrary/js/merge-request-table/index.js index a98cff2c265..113dfdf5daf 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/index.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/index.js @@ -5,7 +5,7 @@ import MergeRequestTable from './MergeRequestTable'; * * @param {HTMLElement} elem Reference to the queue's root element. */ -export function initLibrarianQueue(elem) { - const librarianQueue = new MergeRequestTable(elem) - librarianQueue.initialize() +export function initLibrarianQueue (elem) { + const librarianQueue = new MergeRequestTable(elem); + librarianQueue.initialize(); } diff --git a/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js b/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js index 89660d454c7..b92a747fdc3 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js +++ b/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js @@ -4,10 +4,10 @@ * @module my-books/CreateListForm.js */ import 'jquery-colorbox'; -import myBooksStore from './store' -import { websafe } from '../jsdef' -import { createList } from '../lists/ListService' -import { attachNewActiveShowcaseItem } from '../lists/ShowcaseItem' +import myBooksStore from './store'; +import { websafe } from '../jsdef'; +import { createList } from '../lists/ListService'; +import { attachNewActiveShowcaseItem } from '../lists/ShowcaseItem'; /** * Represents the list creation form displayed when a patron @@ -28,40 +28,40 @@ export class CreateListForm { * * @param {HTMLElement} form */ - constructor(form) { + constructor (form) { /** * References this form's "Create List" button. * * @member {HTMLElement} */ - this.createListButton = form.querySelector('#create-list-button') + this.createListButton = form.querySelector('#create-list-button'); /** * References the form's list title input field. * * @member {HTMLElement} */ - this.listTitleInput = form.querySelector('#list_label') + this.listTitleInput = form.querySelector('#list_label'); /** * References the form's list description input field. * * @member {HTMLElement} */ - this.listDescriptionInput = form.querySelector('#list_desc') + this.listDescriptionInput = form.querySelector('#list_desc'); // Clear form on page refresh: - this.resetForm() + this.resetForm(); } /** * Attaches click listener to the "Create List" button. */ - initialize() { + initialize () { this.createListButton.addEventListener('click', (event) =>{ - event.preventDefault() - this.createNewList() - }) + event.preventDefault(); + this.createNewList(); + }); } /** @@ -79,37 +79,37 @@ export class CreateListForm { * * @async */ - async createNewList() { + async createNewList () { // Construct seed object for first list item: - const listTitle = websafe(this.listTitleInput.value) - const listDescription = websafe(this.listDescriptionInput.value) + const listTitle = websafe(this.listTitleInput.value); + const listDescription = websafe(this.listDescriptionInput.value); - const openDropper = myBooksStore.getOpenDropper() - const seed = openDropper.readingLists.getSeed() + const openDropper = myBooksStore.getOpenDropper(); + const seed = openDropper.readingLists.getSeed(); const postData = { name: listTitle, description: listDescription, seeds: [seed] - } + }; // Call list creation service with seed object: await createList(myBooksStore.getUserKey(), postData) .then(response => response.json()) .then((data) => { // Update active lists showcase: - attachNewActiveShowcaseItem(data['key'], seed, listTitle, data['key']) + attachNewActiveShowcaseItem(data['key'], seed, listTitle, data['key']); // Update all droppers with new list data - this.updateDroppersOnListCreation(data['key'], listTitle, data['key']) + this.updateDroppersOnListCreation(data['key'], listTitle, data['key']); // Clear list creation form fields, nullify seed - this.resetForm() + this.resetForm(); }) .finally(() => { // Close the modal - $.colorbox.close() - }) + $.colorbox.close(); + }); } /** @@ -118,21 +118,21 @@ export class CreateListForm { * @param {string} listKey Key of the newly created list * @param {string} listTitle Title of the new list */ - updateDroppersOnListCreation(listKey, listTitle, coverUrl) { - const droppers = myBooksStore.getDroppers() - const openDropper = myBooksStore.getOpenDropper() + updateDroppersOnListCreation (listKey, listTitle, coverUrl) { + const droppers = myBooksStore.getDroppers(); + const openDropper = myBooksStore.getOpenDropper(); for (const dropper of droppers) { - const isActive = dropper === openDropper - dropper.readingLists.onListCreationSuccess(listKey, listTitle, isActive, coverUrl) + const isActive = dropper === openDropper; + dropper.readingLists.onListCreationSuccess(listKey, listTitle, isActive, coverUrl); } } /** * Clears the list title and desciption fields in the form. */ - resetForm() { - this.listTitleInput.value = '' - this.listDescriptionInput.value = '' + resetForm () { + this.listTitleInput.value = ''; + this.listDescriptionInput.value = ''; } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js index d01ec785bbe..b705a92f539 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js @@ -2,12 +2,12 @@ * Defines functionality related to Open Library's My Books dropper components. * @module my-books/MyBooksDropper */ -import myBooksStore from './store' -import { CheckInComponents } from './MyBooksDropper/CheckInComponents' -import { ReadingLists } from './MyBooksDropper/ReadingLists' -import {ReadingLogForms, ReadingLogShelves} from './MyBooksDropper/ReadingLogForms' -import { Dropper } from '../dropper/Dropper' -import { removeChildren } from '../utils' +import myBooksStore from './store'; +import { CheckInComponents } from './MyBooksDropper/CheckInComponents'; +import { ReadingLists } from './MyBooksDropper/ReadingLists'; +import {ReadingLogForms, ReadingLogShelves} from './MyBooksDropper/ReadingLogForms'; +import { Dropper } from '../dropper/Dropper'; +import { removeChildren } from '../utils'; /** * Represents a single My Books Dropper. @@ -32,19 +32,19 @@ export class MyBooksDropper extends Dropper { * * @param {HTMLElement} dropper */ - constructor(dropper) { - super(dropper) + constructor (dropper) { + super(dropper); const dropperActionCallbacks = { closeDropper: this.closeDropper.bind(this), toggleDropper: this.toggleDropper.bind(this) - } + }; /** * Reference to this dropper's list content. * @member {ReadingLists} */ - this.readingLists = new ReadingLists(dropper, dropperActionCallbacks) + this.readingLists = new ReadingLists(dropper, dropperActionCallbacks); /** * Reference to the dropper's list loading indicator. @@ -52,49 +52,49 @@ export class MyBooksDropper extends Dropper { * This is only rendered when the patron is logged in. * @member {HTMLElement|null} */ - this.loadingIndicator = dropper.querySelector('.list-loading-indicator') + this.loadingIndicator = dropper.querySelector('.list-loading-indicator'); /** * Reference to the interval ID of the animation `setInterval` call. * @member {NodeJS.Timer|undefined} */ - this.loadingAnimationId + this.loadingAnimationId; /** * The work key associated with this dropper, if any. * * @member {string|undefined} */ - this.workKey = this.dropper.dataset.workKey + this.workKey = this.dropper.dataset.workKey; - const splitKey = this.workKey ? this.workKey.split('/') : [''] - const workOlid = splitKey[splitKey.length - 1] + const splitKey = this.workKey ? this.workKey.split('/') : ['']; + const workOlid = splitKey[splitKey.length - 1]; /** * @type {CheckInComponents|null} */ - this.checkInComponents = workOlid ? new CheckInComponents(document.querySelector(`#check-in-container-${workOlid}`)) : null + this.checkInComponents = workOlid ? new CheckInComponents(document.querySelector(`#check-in-container-${workOlid}`)) : null; /** * References this dropper's reading log buttons. * @member {ReadingLogForms} */ - this.readingLogForms = new ReadingLogForms(dropper, this.checkInComponents, dropperActionCallbacks) + this.readingLogForms = new ReadingLogForms(dropper, this.checkInComponents, dropperActionCallbacks); } /** * Hydrates dropper contents and loads patron's lists. */ - initialize() { - super.initialize() + initialize () { + super.initialize(); - this.readingLogForms.initialize() - this.readingLists.initialize() + this.readingLogForms.initialize(); + this.readingLists.initialize(); if (this.checkInComponents) { - this.checkInComponents.initialize() + this.checkInComponents.initialize(); } - this.loadingAnimationId = this.initLoadingAnimation(this.dropper.querySelector('.loading-ellipsis')) + this.loadingAnimationId = this.initLoadingAnimation(this.dropper.querySelector('.loading-ellipsis')); } /** @@ -103,18 +103,18 @@ export class MyBooksDropper extends Dropper { * @param {HTMLElement} loadingIndicator * @returns {NodeJS.Timer} */ - initLoadingAnimation(loadingIndicator) { - let count = 0 - const intervalId = setInterval(function() { - let ellipsis = '' + initLoadingAnimation (loadingIndicator) { + let count = 0; + const intervalId = setInterval(function () { + let ellipsis = ''; for (let i = 0; i < count % 4; ++i) { - ellipsis += '.' + ellipsis += '.'; } - loadingIndicator.innerText = ellipsis - ++count - }, 1500) + loadingIndicator.innerText = ellipsis; + ++count; + }, 1500); - return intervalId + return intervalId; } /** @@ -123,9 +123,9 @@ export class MyBooksDropper extends Dropper { * * @param {string} partialHtml */ - updateReadingLists(partialHtml) { - clearInterval(this.loadingAnimationId) - this.replaceLoadingIndicators(this.loadingIndicator, partialHtml) + updateReadingLists (partialHtml) { + clearInterval(this.loadingAnimationId); + this.replaceLoadingIndicators(this.loadingIndicator, partialHtml); } /** @@ -137,12 +137,12 @@ export class MyBooksDropper extends Dropper { * * @returns {Array<string>} */ - getSeedKeys() { - const results = [this.readingLists.seedKey] + getSeedKeys () { + const results = [this.readingLists.seedKey]; if (this.readingLists.workKey) { - results.push(this.readingLists.workKey) + results.push(this.readingLists.workKey); } - return results + return results; } /** @@ -158,15 +158,15 @@ export class MyBooksDropper extends Dropper { * @param {HTMLElement} dropperListsPlaceholder Loading indicator found inside of the dropdown content * @param {ListPartials} partials */ - replaceLoadingIndicators(dropperListsPlaceholder, partialHTML) { - const dropperParent = dropperListsPlaceholder ? dropperListsPlaceholder.parentElement : null + replaceLoadingIndicators (dropperListsPlaceholder, partialHTML) { + const dropperParent = dropperListsPlaceholder ? dropperListsPlaceholder.parentElement : null; if (dropperParent) { - removeChildren(dropperParent) - dropperParent.insertAdjacentHTML('afterbegin', partialHTML) + removeChildren(dropperParent); + dropperParent.insertAdjacentHTML('afterbegin', partialHTML); - const anchors = this.dropper.querySelectorAll('.modify-list') - this.readingLists.initModifyListAffordances(anchors) + const anchors = this.dropper.querySelectorAll('.modify-list'); + this.readingLists.initModifyListAffordances(anchors); } } @@ -179,16 +179,16 @@ export class MyBooksDropper extends Dropper { * * @param shelf {ReadingLogShelf} */ - updateShelfDisplay(shelf) { - this.readingLogForms.updateActivatedStatus(true) - this.readingLogForms.updatePrimaryBookshelfId(Number(shelf)) - this.readingLogForms.updatePrimaryButtonText(this.readingLogForms.getDisplayString(shelf)) + updateShelfDisplay (shelf) { + this.readingLogForms.updateActivatedStatus(true); + this.readingLogForms.updatePrimaryBookshelfId(Number(shelf)); + this.readingLogForms.updatePrimaryButtonText(this.readingLogForms.getDisplayString(shelf)); if (this.checkInComponents) { if (!this.checkInComponents.hasReadDate() && shelf === ReadingLogShelves.ALREADY_READ) { - this.checkInComponents.showCheckInDisplay() + this.checkInComponents.showCheckInDisplay(); } else { - this.checkInComponents.hideCheckInPrompt() + this.checkInComponents.hideCheckInPrompt(); } } } @@ -199,8 +199,8 @@ export class MyBooksDropper extends Dropper { * * @override */ - onOpen() { - myBooksStore.setOpenDropper(this) + onOpen () { + myBooksStore.setOpenDropper(this); } /** @@ -210,7 +210,7 @@ export class MyBooksDropper extends Dropper { * * @override */ - onDisabledClick() { - window.location = `/account/login?redirect=${location.pathname}` + onDisabledClick () { + window.location = `/account/login?redirect=${location.pathname}`; } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js index c7318194397..b2aed540a3f 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js @@ -3,7 +3,7 @@ * @module my-books/MyBooksDropper/CheckInComponents */ import { initDialogClosers } from '../../dialog'; -import { PersistentToast } from '../../Toast' +import { PersistentToast } from '../../Toast'; /** * Array of days for each month, listed in order starting with January. @@ -12,7 +12,7 @@ import { PersistentToast } from '../../Toast' * @readonly * @type {array<number>} */ -const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] +const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; /** * Determines if the given year is a leap year. @@ -20,8 +20,8 @@ const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] * @param {Number} year * @returns `true` if the given year is a leap year. */ -function isLeapYear(year) { - return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) +function isLeapYear (year) { + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); } /** @@ -40,12 +40,12 @@ export class CheckInComponents { /** * @param checkInContainer */ - constructor(checkInContainer) { + constructor (checkInContainer) { // HTML for the check-in components is not rendered if // the patron is unauthenticated, or if the dropper // is for an orphaned edition. if (!checkInContainer) { - return + return; } /** @@ -58,19 +58,19 @@ export class CheckInComponents { /** * @type {ReadDateConfig} */ - this.config = JSON.parse(checkInContainer.dataset.config) + this.config = JSON.parse(checkInContainer.dataset.config); - const checkInPromptElem = checkInContainer.querySelector('.check-in-prompt') + const checkInPromptElem = checkInContainer.querySelector('.check-in-prompt'); /** * @type {CheckInPrompt} */ - this.checkInPrompt = new CheckInPrompt(checkInPromptElem) + this.checkInPrompt = new CheckInPrompt(checkInPromptElem); - const checkInDisplayElem = checkInContainer.querySelector('.last-read-date') + const checkInDisplayElem = checkInContainer.querySelector('.last-read-date'); /** * @type {CheckInDisplay} */ - this.checkInDisplay = new CheckInDisplay(checkInDisplayElem) + this.checkInDisplay = new CheckInDisplay(checkInDisplayElem); /** * References element that will be displayed in last read date form modal. @@ -78,93 +78,93 @@ export class CheckInComponents { * * @type {HTMLElement|undefined} */ - this.modalContent = undefined + this.modalContent = undefined; /** * @type {CheckInForm|undefined} */ - this.checkInForm = undefined + this.checkInForm = undefined; } - initialize() { - this.checkInPrompt.initialize() + initialize () { + this.checkInPrompt.initialize(); this.checkInPrompt.getRootElement().addEventListener('submit-check-in', (event) => { - const year = event.detail.year - const month = event.detail.month - const day = event.detail.day + const year = event.detail.year; + const month = event.detail.month; + const day = event.detail.day; - const eventData = this.prepareEventRequest(year, month, day) + const eventData = this.prepareEventRequest(year, month, day); this.postCheckIn(eventData, this.checkInForm.getFormAction()) .then((resp) => { if (!resp.ok) { - throw Error(`Check-in request failed. Status: ${resp.status}`) + throw Error(`Check-in request failed. Status: ${resp.status}`); } - this.updateDateAndShowDisplay(year, month, day) + this.updateDateAndShowDisplay(year, month, day); }) .catch(() => { - new PersistentToast('Failed to submit check-in. Please try again in a few moments.').show() - }) - }) + new PersistentToast('Failed to submit check-in. Please try again in a few moments.').show(); + }); + }); - let hiddenModalContentContainer = document.querySelector('#hidden-modal-content-container') + let hiddenModalContentContainer = document.querySelector('#hidden-modal-content-container'); if (!hiddenModalContentContainer) { - hiddenModalContentContainer = document.createElement('div') - hiddenModalContentContainer.classList.add('hidden') - hiddenModalContentContainer.id = 'hidden-modal-content-container' - document.body.appendChild(hiddenModalContentContainer) + hiddenModalContentContainer = document.createElement('div'); + hiddenModalContentContainer.classList.add('hidden'); + hiddenModalContentContainer.id = 'hidden-modal-content-container'; + document.body.appendChild(hiddenModalContentContainer); } - const modalContent = this.createModalContentFromTemplate() - hiddenModalContentContainer.appendChild(modalContent) + const modalContent = this.createModalContentFromTemplate(); + hiddenModalContentContainer.appendChild(modalContent); - this.modalContent = hiddenModalContentContainer.querySelector(`#modal-content-${this.config.workOlid}`) + this.modalContent = hiddenModalContentContainer.querySelector(`#modal-content-${this.config.workOlid}`); - const formElem = this.modalContent.querySelector('form') - this.checkInForm = new CheckInForm(formElem, this.config.workOlid, this.config.editionKey || '', this.config.lastReadDate || '', this.config.eventId) - this.checkInForm.initialize() + const formElem = this.modalContent.querySelector('form'); + this.checkInForm = new CheckInForm(formElem, this.config.workOlid, this.config.editionKey || '', this.config.lastReadDate || '', this.config.eventId); + this.checkInForm.initialize(); this.checkInForm.getRootElement().addEventListener('delete-check-in', () => { this.deleteCheckIn(this.checkInForm.getEventId()) .then(resp => { if (!resp.ok) { - throw Error(`Check-in delete request failed. Status: ${resp.status}`) + throw Error(`Check-in delete request failed. Status: ${resp.status}`); } - this.checkInForm.resetForm() - this.checkInDisplay.hide() - this.checkInPrompt.show() + this.checkInForm.resetForm(); + this.checkInDisplay.hide(); + this.checkInPrompt.show(); }) .catch(() => { // TODO : Use localized strings - new PersistentToast('Failed to delete check-in. Please try again in a few moments.').show() + new PersistentToast('Failed to delete check-in. Please try again in a few moments.').show(); }) .finally(() => { - this.closeModal() - }) - }) + this.closeModal(); + }); + }); this.checkInForm.getRootElement().addEventListener('submit-check-in', (event) => { - const year = event.detail.year - const month = event.detail.month - const day = event.detail.day + const year = event.detail.year; + const month = event.detail.month; + const day = event.detail.day; - const eventData = this.prepareEventRequest(year, month, day) + const eventData = this.prepareEventRequest(year, month, day); this.postCheckIn(eventData, this.checkInForm.getFormAction()) .then((resp) => { if (!resp.ok) { - throw Error(`Check-in request failed. Status: ${resp.status}`) + throw Error(`Check-in request failed. Status: ${resp.status}`); } - this.updateDateAndShowDisplay(year, month, day) + this.updateDateAndShowDisplay(year, month, day); }) .catch(() => { // TODO : Use localized strings - new PersistentToast('Failed to submit check-in. Please try again in a few moments.').show() + new PersistentToast('Failed to submit check-in. Please try again in a few moments.').show(); }) .finally(() => { - this.closeModal() - }) - }) + this.closeModal(); + }); + }); - const closeModalElements = this.modalContent.querySelectorAll('.dialog--close') - initDialogClosers(closeModalElements) + const closeModalElements = this.modalContent.querySelectorAll('.dialog--close'); + initDialogClosers(closeModalElements); } /** @@ -172,14 +172,14 @@ export class CheckInComponents { * * @returns {HTMLElement} */ - createModalContentFromTemplate() { - const templateElem = document.createElement('template') - const modalContentTemplate = document.querySelector('#check-in-form-modal') - templateElem.innerHTML = modalContentTemplate.outerHTML - const modalContent = templateElem.content.firstElementChild - modalContent.id = `modal-content-${this.config.workOlid}` + createModalContentFromTemplate () { + const templateElem = document.createElement('template'); + const modalContentTemplate = document.querySelector('#check-in-form-modal'); + templateElem.innerHTML = modalContentTemplate.outerHTML; + const modalContent = templateElem.content.firstElementChild; + modalContent.id = `modal-content-${this.config.workOlid}`; - return modalContent + return modalContent; } /** @@ -189,24 +189,24 @@ export class CheckInComponents { * @param {number|null} month * @param {number|null} day */ - updateDateAndShowDisplay(year, month = null, day = null) { + updateDateAndShowDisplay (year, month = null, day = null) { // Update last read date display - let dateString = String(year) + let dateString = String(year); if (month) { - dateString += `-${String(month).padStart(2, '0')}` + dateString += `-${String(month).padStart(2, '0')}`; if (day) { - dateString += `-${String(day).padStart(2, '0')}` + dateString += `-${String(day).padStart(2, '0')}`; } } - this.checkInDisplay.updateDateDisplay(dateString) + this.checkInDisplay.updateDateDisplay(dateString); // Update component visibility - this.checkInPrompt.hide() - this.checkInDisplay.show() + this.checkInPrompt.hide(); + this.checkInDisplay.show(); // Update submission form - this.checkInForm.updateSelectedDate(year, month, day) - this.checkInForm.showDeleteButton() + this.checkInForm.updateSelectedDate(year, month, day); + this.checkInForm.showDeleteButton(); } @@ -226,7 +226,7 @@ export class CheckInComponents { * @param {string} url * @returns {Promise<Response>} */ - postCheckIn(eventData, url) { + postCheckIn (eventData, url) { return fetch(url, { method: 'POST', headers: { @@ -234,7 +234,7 @@ export class CheckInComponents { accept: 'application/json' }, body: JSON.stringify(eventData) - }) + }); } /** @@ -243,10 +243,10 @@ export class CheckInComponents { * @param {string} eventId * @returns {Promise<Response>} */ - async deleteCheckIn(eventId) { + async deleteCheckIn (eventId) { return fetch(`/check-ins/${eventId}`, { method: 'DELETE' - }) + }); } /** @@ -257,12 +257,12 @@ export class CheckInComponents { * @param {number|null} day * @returns {CheckInEventPostRequestData} */ - prepareEventRequest(year, month = null, day = null) { + prepareEventRequest (year, month = null, day = null) { // Get event id - const eventId = this.checkInForm.getEventId() + const eventId = this.checkInForm.getEventId(); // Get event type - const eventType = this.checkInForm.getEventType() + const eventType = this.checkInForm.getEventType(); const eventRequest = { event_id: eventId ? Number(eventId) : null, @@ -270,14 +270,14 @@ export class CheckInComponents { year: year, month: month, day: day - } + }; - const editionKey = this.checkInForm.getEditionKey() || null + const editionKey = this.checkInForm.getEditionKey() || null; if (editionKey) { - eventRequest.edition_key = editionKey + eventRequest.edition_key = editionKey; } - return eventRequest + return eventRequest; } /** @@ -285,50 +285,50 @@ export class CheckInComponents { * * @returns {boolean} */ - hasReadDate() { - return !this.checkInDisplay.getRootElement().classList.contains('hidden') + hasReadDate () { + return !this.checkInDisplay.getRootElement().classList.contains('hidden'); } /** * Resets the check-in form. */ - resetForm() { - this.checkInForm.resetForm() + resetForm () { + this.checkInForm.resetForm(); } /** * Show the check-in display. */ - showCheckInDisplay() { - this.checkInDisplay.show() + showCheckInDisplay () { + this.checkInDisplay.show(); } /** * Hide the check-in display. */ - hideCheckInDisplay() { - this.checkInDisplay.hide() + hideCheckInDisplay () { + this.checkInDisplay.hide(); } /** * Show the check-in prompt. */ - showCheckInPrompt() { - this.checkInPrompt.show() + showCheckInPrompt () { + this.checkInPrompt.show(); } /** * Hide the check-in prompt. */ - hideCheckInPrompt() { - this.checkInPrompt.hide() + hideCheckInPrompt () { + this.checkInPrompt.hide(); } /** * Closes the opened `colorbox` modal. */ - closeModal() { - $.colorbox.close() + closeModal () { + $.colorbox.close(); } } @@ -342,29 +342,29 @@ class CheckInPrompt { /** * @param {HTMLElement} checkInPrompt */ - constructor(checkInPrompt) { - this.rootElem = checkInPrompt + constructor (checkInPrompt) { + this.rootElem = checkInPrompt; } - initialize() { - const yearLink = this.rootElem.querySelector('.prompt-current-year') + initialize () { + const yearLink = this.rootElem.querySelector('.prompt-current-year'); yearLink.addEventListener('click', () => { // Get the current year - const year = new Date().getFullYear() + const year = new Date().getFullYear(); - this.dispatchCheckInSubmission(year) - }) + this.dispatchCheckInSubmission(year); + }); - const todayLink = this.rootElem.querySelector('.prompt-today') + const todayLink = this.rootElem.querySelector('.prompt-today'); todayLink.addEventListener('click', () => { // Get today's date - const now = new Date() - const year = now.getFullYear() - const month = now.getMonth() + 1 - const day = now.getDate() + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth() + 1; + const day = now.getDate(); - this.dispatchCheckInSubmission(year, month, day) - }) + this.dispatchCheckInSubmission(year, month, day); + }); } /** @@ -374,37 +374,37 @@ class CheckInPrompt { * @param {number|null} month * @param {number|null} day */ - dispatchCheckInSubmission(year, month = null, day = null) { + dispatchCheckInSubmission (year, month = null, day = null) { const submitEvent = new CustomEvent('submit-check-in', { detail: { year: year, month: month, day: day } - }) - this.rootElem.dispatchEvent(submitEvent) + }); + this.rootElem.dispatchEvent(submitEvent); } /** * Hides this check-in prompt. */ - hide() { - this.rootElem.classList.add('hidden') + hide () { + this.rootElem.classList.add('hidden'); } /** * Shows this check-in prompt. */ - show() { - this.rootElem.classList.remove('hidden') + show () { + this.rootElem.classList.remove('hidden'); } /** * Returns reference to the root element of this check-in prompt. * @returns {HTMLElement} */ - getRootElement() { - return this.rootElem + getRootElement () { + return this.rootElem; } } @@ -417,9 +417,9 @@ class CheckInDisplay { /** * @param {HTMLElement} checkInDisplay */ - constructor(checkInDisplay) { - this.rootElem = checkInDisplay - this.dateDisplayElem = this.rootElem.querySelector('.check-in-date') + constructor (checkInDisplay) { + this.rootElem = checkInDisplay; + this.dateDisplayElem = this.rootElem.querySelector('.check-in-date'); } /** @@ -427,29 +427,29 @@ class CheckInDisplay { * * @param {string} date */ - updateDateDisplay(date) { - this.dateDisplayElem.textContent = date + updateDateDisplay (date) { + this.dateDisplayElem.textContent = date; } /** * Hides this date display. */ - hide() { - this.rootElem.classList.add('hidden') + hide () { + this.rootElem.classList.add('hidden'); } /** * Shows this date display. */ - show() { - this.rootElem.classList.remove('hidden') + show () { + this.rootElem.classList.remove('hidden'); } /** * @returns {HTMLElement} */ - getRootElement() { - return this.rootElem + getRootElement () { + return this.rootElem; } } @@ -471,152 +471,152 @@ export class CheckInForm { * @param {string|null} lastReadDate * @param {number|null} eventId */ - constructor(formElem, workOlid, editionKey = null, lastReadDate = null, eventId = null) { - this.rootElem = formElem - this.workOlid = workOlid - this.editionKey = editionKey - this.lastReadDate = lastReadDate - this.eventId = eventId + constructor (formElem, workOlid, editionKey = null, lastReadDate = null, eventId = null) { + this.rootElem = formElem; + this.workOlid = workOlid; + this.editionKey = editionKey; + this.lastReadDate = lastReadDate; + this.eventId = eventId; /** * Reference to hidden `event_type` form input. * * @type {HTMLInputElement|undefined} */ - this.eventTypeInput = this.rootElem.querySelector('input[name=event_type]') + this.eventTypeInput = this.rootElem.querySelector('input[name=event_type]'); /** * Reference to hidden `event_id` form input. * * @type {HTMLInputElement|undefined} */ - this.eventIdInput = this.rootElem.querySelector('input[name=event_id]') + this.eventIdInput = this.rootElem.querySelector('input[name=event_id]'); /** * Reference to hidden `edition_key` form input. * * @type {HTMLInputElement} */ - this.editionKeyInput = this.rootElem.querySelector('input[name=edition_key]') + this.editionKeyInput = this.rootElem.querySelector('input[name=edition_key]'); /** * Reference to the form's year `select` element. * * @type {HTMLSelectElement} */ - this.yearSelect = this.rootElem.querySelector('select[name=year]') + this.yearSelect = this.rootElem.querySelector('select[name=year]'); /** * Reference to the form's month `select` element. * * @type {HTMLSelectElement} */ - this.monthSelect = this.rootElem.querySelector('select[name=month]') + this.monthSelect = this.rootElem.querySelector('select[name=month]'); /** * Reference to the form's day `select` element. * * @type {HTMLSelectElement} */ - this.daySelect = this.rootElem.querySelector('select[name=day]') + this.daySelect = this.rootElem.querySelector('select[name=day]'); /** * Reference to the form's submit button. * @type {HTMLButtonElement} */ - this.submitButton = this.rootElem.querySelector('.check-in__submit-btn') + this.submitButton = this.rootElem.querySelector('.check-in__submit-btn'); /** * Reference to the form's delete button. * * @type {HTMLButtonElement} */ - this.deleteButton = this.rootElem.querySelector('.check-in__delete-btn') + this.deleteButton = this.rootElem.querySelector('.check-in__delete-btn'); } - initialize() { + initialize () { // Set form's action - this.rootElem.action = `/works/${this.workOlid}/check-ins.json` + this.rootElem.action = `/works/${this.workOlid}/check-ins.json`; // Set form's event ID if (this.eventId) { - this.setEventId(this.eventId) - this.showDeleteButton() + this.setEventId(this.eventId); + this.showDeleteButton(); } // Set form's edition_key if (this.editionKey) { - this.editionKeyInput.value = this.editionKey + this.editionKeyInput.value = this.editionKey; } // Set date select elements to the last read date - const [yearString, monthString, dayString] = this.lastReadDate ? this.lastReadDate.split('-') : [null, null, null] - this.updateSelectedDate(Number(yearString), Number(monthString), Number(dayString)) + const [yearString, monthString, dayString] = this.lastReadDate ? this.lastReadDate.split('-') : [null, null, null]; + this.updateSelectedDate(Number(yearString), Number(monthString), Number(dayString)); // Update form for new years day const currentYear = new Date().getFullYear(); - const hiddenYear = this.yearSelect.querySelector('.show-if-local-year') + const hiddenYear = this.yearSelect.querySelector('.show-if-local-year'); // The year select element has a hidden option for next year. This // option is shown on 1 January if the client's local year is different // from the server's local year. if (Number(hiddenYear.value) === currentYear) { - hiddenYear.classList.remove('hidden') + hiddenYear.classList.remove('hidden'); } // Associate labels with select elements - const yearLabel = this.rootElem.querySelector('.check-in__year-label') - const yearSelectId = `year-select-${this.workOlid}` - this.yearSelect.id = yearSelectId - yearLabel.htmlFor = yearSelectId + const yearLabel = this.rootElem.querySelector('.check-in__year-label'); + const yearSelectId = `year-select-${this.workOlid}`; + this.yearSelect.id = yearSelectId; + yearLabel.htmlFor = yearSelectId; - const monthLabel = this.rootElem.querySelector('.check-in__month-label') - const monthSelectId = `month-select-${this.workOlid}` - this.monthSelect.id = monthSelectId - monthLabel.htmlFor = monthSelectId + const monthLabel = this.rootElem.querySelector('.check-in__month-label'); + const monthSelectId = `month-select-${this.workOlid}`; + this.monthSelect.id = monthSelectId; + monthLabel.htmlFor = monthSelectId; - const dayLabel = this.rootElem.querySelector('.check-in__day-label') - const daySelectId = `day-select-${this.workOlid}` - this.daySelect.id = daySelectId - dayLabel.htmlFor = daySelectId + const dayLabel = this.rootElem.querySelector('.check-in__day-label'); + const daySelectId = `day-select-${this.workOlid}`; + this.daySelect.id = daySelectId; + dayLabel.htmlFor = daySelectId; // Add listeners to form elements: this.yearSelect.addEventListener('change', () => { - this.onDateSelectionChange() - }) + this.onDateSelectionChange(); + }); this.monthSelect.addEventListener('change', () => { - this.onDateSelectionChange() - }) + this.onDateSelectionChange(); + }); this.deleteButton.addEventListener('click', (event) => { - event.preventDefault() - const deleteEvent = new CustomEvent('delete-check-in') - this.rootElem.dispatchEvent(deleteEvent) - }) + event.preventDefault(); + const deleteEvent = new CustomEvent('delete-check-in'); + this.rootElem.dispatchEvent(deleteEvent); + }); this.submitButton.addEventListener('click', (event) => { - event.preventDefault() + event.preventDefault(); const submitEvent = new CustomEvent('submit-check-in', { detail: { year: this.getSelectedYear(), month: this.getSelectedMonth(), day: this.getSelectedDay() } - }) - this.rootElem.dispatchEvent(submitEvent) - }) - const todayLink = this.rootElem.querySelector('.check-in__today') + }); + this.rootElem.dispatchEvent(submitEvent); + }); + const todayLink = this.rootElem.querySelector('.check-in__today'); todayLink.addEventListener('click', () => { // Get today's date - const now = new Date() - const year = now.getFullYear() - const month = now.getMonth() + 1 - const day = now.getDate() + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth() + 1; + const day = now.getDate(); - this.updateSelectedDate(year, month, day) - }) + this.updateSelectedDate(year, month, day); + }); } /** * Gets currently selected date, then updates the form. */ - onDateSelectionChange() { - const year = this.yearSelect.selectedIndex ? Number(this.yearSelect.value) : null - this.updateSelectedDate(year, this.monthSelect.selectedIndex, this.daySelect.selectedIndex) + onDateSelectionChange () { + const year = this.yearSelect.selectedIndex ? Number(this.yearSelect.value) : null; + this.updateSelectedDate(year, this.monthSelect.selectedIndex, this.daySelect.selectedIndex); } /** @@ -626,43 +626,43 @@ export class CheckInForm { * @param {number|null} month * @param {number|null} day */ - updateSelectedDate(year = null, month = null, day = null) { + updateSelectedDate (year = null, month = null, day = null) { if (!month) { - day = null + day = null; } if (!year) { - month = null - day = null + month = null; + day = null; } if (year) { - this.yearSelect.value = year || '' - this.monthSelect.disabled = false - this.submitButton.disabled = false + this.yearSelect.value = year || ''; + this.monthSelect.disabled = false; + this.submitButton.disabled = false; } else { - this.yearSelect.selectedIndex = 0 - this.monthSelect.disabled = true - this.submitButton.disabled = true + this.yearSelect.selectedIndex = 0; + this.monthSelect.disabled = true; + this.submitButton.disabled = true; } if (month) { - this.monthSelect.value = month || '' - this.daySelect.disabled = false + this.monthSelect.value = month || ''; + this.daySelect.disabled = false; // Update daySelect options for month/leap year - let daysInMonth = DAYS_IN_MONTH[month - 1] + let daysInMonth = DAYS_IN_MONTH[month - 1]; if (month === 2 && isLeapYear(year)) { - ++daysInMonth + ++daysInMonth; } - this.updateDayOptions(daysInMonth) + this.updateDayOptions(daysInMonth); } else { - this.monthSelect.selectedIndex = 0 - this.daySelect.disabled = true + this.monthSelect.selectedIndex = 0; + this.daySelect.disabled = true; } if (day) { - const daysInMonth = DAYS_IN_MONTH[this.monthSelect.selectedIndex - 1] - this.daySelect.selectedIndex = day > daysInMonth ? 0 : day + const daysInMonth = DAYS_IN_MONTH[this.monthSelect.selectedIndex - 1]; + this.daySelect.selectedIndex = day > daysInMonth ? 0 : day; } else { - this.daySelect.selectedIndex = 0 + this.daySelect.selectedIndex = 0; } } @@ -671,12 +671,12 @@ export class CheckInForm { * * @param {number} daysInMonth */ - updateDayOptions(daysInMonth) { + updateDayOptions (daysInMonth) { for (let i = 0; i < this.daySelect.options.length; ++i) { if (i <= daysInMonth) { - this.daySelect.options[i].classList.remove('hidden') + this.daySelect.options[i].classList.remove('hidden'); } else { - this.daySelect.options[i].classList.add('hidden') + this.daySelect.options[i].classList.add('hidden'); } } } @@ -687,24 +687,24 @@ export class CheckInForm { * Unsets the `event_id` input value, hides the delete button, and * resets the date select elements to their default values. */ - resetForm() { - this.setEventId('') - this.updateSelectedDate() - this.hideDeleteButton() + resetForm () { + this.setEventId(''); + this.updateSelectedDate(); + this.hideDeleteButton(); } /** * Shows this form's delete button. */ - showDeleteButton() { - this.deleteButton.classList.remove('invisible') + showDeleteButton () { + this.deleteButton.classList.remove('invisible'); } /** * Hides this form's delete button. */ - hideDeleteButton() { - this.deleteButton.classList.add('invisible') + hideDeleteButton () { + this.deleteButton.classList.add('invisible'); } /** @@ -712,8 +712,8 @@ export class CheckInForm { * * @returns {number|null} The selected year, or `null` if none selected */ - getSelectedYear() { - return this.yearSelect.selectedIndex ? Number(this.yearSelect.value) : null + getSelectedYear () { + return this.yearSelect.selectedIndex ? Number(this.yearSelect.value) : null; } /** @@ -721,8 +721,8 @@ export class CheckInForm { * * @returns {number|null} The selected month, or `null` if none selected */ - getSelectedMonth() { - return this.monthSelect.selectedIndex || null + getSelectedMonth () { + return this.monthSelect.selectedIndex || null; } /** @@ -730,8 +730,8 @@ export class CheckInForm { * * @returns {number|null} The selected day, or `null` if none selected */ - getSelectedDay() { - return this.daySelect.selectedIndex || null + getSelectedDay () { + return this.daySelect.selectedIndex || null; } /** @@ -739,8 +739,8 @@ export class CheckInForm { * * @returns {string} */ - getEventId() { - return this.eventIdInput.value + getEventId () { + return this.eventIdInput.value; } /** @@ -748,8 +748,8 @@ export class CheckInForm { * * @param value */ - setEventId(value) { - this.eventIdInput.value = value + setEventId (value) { + this.eventIdInput.value = value; } /** @@ -757,8 +757,8 @@ export class CheckInForm { * * @returns {string} */ - getEventType() { - return this.eventTypeInput.value + getEventType () { + return this.eventTypeInput.value; } /** @@ -766,8 +766,8 @@ export class CheckInForm { * * @returns {string} */ - getEditionKey() { - return this.editionKeyInput.value + getEditionKey () { + return this.editionKeyInput.value; } /** @@ -775,8 +775,8 @@ export class CheckInForm { * * @returns {string} */ - getFormAction() { - return this.rootElem.action + getFormAction () { + return this.rootElem.action; } /** @@ -784,7 +784,7 @@ export class CheckInForm { * * @returns {HTMLFormElement} */ - getRootElement() { - return this.rootElem + getRootElement () { + return this.rootElem; } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js index c4ebbf1b2ed..b44bc6c9958 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js @@ -3,13 +3,13 @@ * @module my-books/MyBooksDropper/ReadingLists */ import 'jquery-colorbox'; -import myBooksStore from '../store' +import myBooksStore from '../store'; -import { addItem, removeItem } from '../../lists/ListService' -import { attachNewActiveShowcaseItem, toggleActiveShowcaseItems } from '../../lists/ShowcaseItem' -import { FadingToast } from '../../Toast' +import { addItem, removeItem } from '../../lists/ListService'; +import { attachNewActiveShowcaseItem, toggleActiveShowcaseItems } from '../../lists/ShowcaseItem'; +import { FadingToast } from '../../Toast'; -const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png' +const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; /** * Represents a single My Books dropper's list affordances, and defines their @@ -22,23 +22,23 @@ export class ReadingLists { * Adds functionality to the given dropper's list affordances. * @param {HTMLElement} dropper */ - constructor(dropper) { + constructor (dropper) { /** * References the given My Books Dropper root element. * * @member {HTMLElement} */ - this.dropper = dropper + this.dropper = dropper; /** * Reference to the "Use work" checkbox. * * @member {HTMLElement|null} */ - this.workCheckBox = dropper.querySelector('.work-checkbox') + this.workCheckBox = dropper.querySelector('.work-checkbox'); if (this.workCheckBox) { // Uncheck "Use work" checkbox on page refresh - this.workCheckBox.checked = false + this.workCheckBox.checked = false; } /** @@ -46,14 +46,14 @@ export class ReadingLists { * * @member {HTMLElement} */ - this.dropperListsElement = dropper.querySelector('.my-lists') + this.dropperListsElement = dropper.querySelector('.my-lists'); /** * Key of the document that will be added to or removed from a list. * * @member {string} */ - this.seedKey = this.dropperListsElement.dataset.seedKey + this.seedKey = this.dropperListsElement.dataset.seedKey; /** * Key of the work associated with this dropper. Will be an empty @@ -61,14 +61,14 @@ export class ReadingLists { * * @member {string} */ - this.workKey = this.dropperListsElement.dataset.workKey + this.workKey = this.dropperListsElement.dataset.workKey; /** * The patron's user key. * * @member {string} */ - this.userKey = this.dropperListsElement.dataset.userKey + this.userKey = this.dropperListsElement.dataset.userKey; /** * Stores information about a single list. @@ -86,41 +86,41 @@ export class ReadingLists { * * @member {Record<string, ActiveListData>} */ - this.patronLists = {} + this.patronLists = {}; } /** * Adds functionality to all of the dropper's list affordances. */ - initialize() { - this.initModifyListAffordances(this.dropper.querySelectorAll('.modify-list')) + initialize () { + this.initModifyListAffordances(this.dropper.querySelectorAll('.modify-list')); - const openListModalButton = this.dropper.querySelector('.create-new-list') + const openListModalButton = this.dropper.querySelector('.create-new-list'); if (openListModalButton) { - this.addOpenListModalClickListener(openListModalButton) + this.addOpenListModalClickListener(openListModalButton); } if (this.workCheckBox) { this.workCheckBox.addEventListener('click', () => { - this.updateListDisplays() - toggleActiveShowcaseItems(this.workCheckBox.checked) - }) + this.updateListDisplays(); + toggleActiveShowcaseItems(this.workCheckBox.checked); + }); } } /** * Updates dropdown list affordances when an update occurs. */ - updateListDisplays() { - const isWorkSelected = this.workCheckBox && this.workCheckBox.checked + updateListDisplays () { + const isWorkSelected = this.workCheckBox && this.workCheckBox.checked; for (const key of Object.keys(this.patronLists)) { - const listData = this.patronLists[key] + const listData = this.patronLists[key]; if (isWorkSelected) { - this.toggleDisplayedType(listData.workOnList, key) + this.toggleDisplayedType(listData.workOnList, key); } else { - this.toggleDisplayedType(listData.itemOnList, key) + this.toggleDisplayedType(listData.itemOnList, key); } } } @@ -135,13 +135,13 @@ export class ReadingLists { * @param {boolean} isListMember True if the item is on the list * @param {string} listKey Unique identifier for a list */ - toggleDisplayedType(isListMember, listKey) { - const listData = this.patronLists[listKey] + toggleDisplayedType (isListMember, listKey) { + const listData = this.patronLists[listKey]; if (isListMember) { - listData.dropperListAffordance.classList.add('list--active') + listData.dropperListAffordance.classList.add('list--active'); } else { - listData.dropperListAffordance.classList.remove('list--active') + listData.dropperListAffordance.classList.remove('list--active'); } } @@ -153,47 +153,47 @@ export class ReadingLists { * * @param {NodeList<HTMLElement>} modifyListElements */ - initModifyListAffordances(modifyListElements) { + initModifyListAffordances (modifyListElements) { for (const elem of modifyListElements) { - const listItemKeys = elem.dataset.listItems - const listKey = elem.dataset.listKey - const itemOnList = listItemKeys.includes(this.seedKey) - const elemParent = elem.parentElement + const listItemKeys = elem.dataset.listItems; + const listKey = elem.dataset.listKey; + const itemOnList = listItemKeys.includes(this.seedKey); + const elemParent = elem.parentElement; this.patronLists[listKey] = { title: elem.innerText, coverUrl: elem.dataset.listCoverUrl, itemOnList: itemOnList, dropperListAffordance: elemParent, // The .list element - } + }; if (!this.patronLists[listKey].coverUrl) { - this.patronLists[listKey].coverUrl = DEFAULT_COVER_URL + this.patronLists[listKey].coverUrl = DEFAULT_COVER_URL; } if (this.workCheckBox) { // Check for work key membership: - const workOnList = listItemKeys.includes(this.workKey) - this.patronLists[listKey].workOnList = workOnList + const workOnList = listItemKeys.includes(this.workKey); + this.patronLists[listKey].workOnList = workOnList; if (this.workCheckBox.checked) { if (workOnList) { - elemParent.classList.add('list--active') + elemParent.classList.add('list--active'); } } else { if (itemOnList) { - elemParent.classList.add('list--active') + elemParent.classList.add('list--active'); } } } else { if (itemOnList) { - elemParent.classList.add('list--active') + elemParent.classList.add('list--active'); } } elem.addEventListener('click', (event) => { - event.preventDefault() - const isAddingItem = !this.patronLists[listKey].dropperListAffordance.classList.contains('list--active') - this.modifyList(listKey, isAddingItem) - }) + event.preventDefault(); + const isAddingItem = !this.patronLists[listKey].dropperListAffordance.classList.contains('list--active'); + this.modifyList(listKey, isAddingItem); + }); } } @@ -204,57 +204,57 @@ export class ReadingLists { * @param {string} listKey Unique key for list * @param {boolean} isAddingItem `true` if an item is being added to a list */ - async modifyList(listKey, isAddingItem) { - let seed - const isWork = this.workCheckBox && this.workCheckBox.checked + async modifyList (listKey, isAddingItem) { + let seed; + const isWork = this.workCheckBox && this.workCheckBox.checked; // Seed will be a string if its type is 'subject' - const isSubjectSeed = this.seedKey[0] !== '/' + const isSubjectSeed = this.seedKey[0] !== '/'; if (isWork) { - seed = { key: this.workKey } + seed = { key: this.workKey }; } else if (isSubjectSeed) { - seed = this.seedKey + seed = this.seedKey; } else { - seed = { key: this.seedKey } + seed = { key: this.seedKey }; } - const makeChange = isAddingItem ? addItem : removeItem - this.patronLists[listKey].dropperListAffordance.classList.remove('list--active') - this.patronLists[listKey].dropperListAffordance.classList.add('list--pending') + const makeChange = isAddingItem ? addItem : removeItem; + this.patronLists[listKey].dropperListAffordance.classList.remove('list--active'); + this.patronLists[listKey].dropperListAffordance.classList.add('list--pending'); await makeChange(listKey, seed) .then((response) => { if (response.status >= 400) { - throw new Error('List update failed') + throw new Error('List update failed'); } - response.json() + response.json(); }) .then(() => { - this.updateViewAfterModifyingList(listKey, isWork, isAddingItem) + this.updateViewAfterModifyingList(listKey, isWork, isAddingItem); - const seedKey = isWork ? this.workKey : this.seedKey + const seedKey = isWork ? this.workKey : this.seedKey; if (isAddingItem) { // make new active showcase item - const listTitle = this.patronLists[listKey].title - attachNewActiveShowcaseItem(listKey, seedKey, listTitle, this.patronLists[listKey].coverUrl) + const listTitle = this.patronLists[listKey].title; + attachNewActiveShowcaseItem(listKey, seedKey, listTitle, this.patronLists[listKey].coverUrl); } else { // remove existing showcase items - const showcases = myBooksStore.getShowcases() - const matchingShowcases = showcases.filter((item) => item.listKey === listKey && item.seedKey === seedKey) + const showcases = myBooksStore.getShowcases(); + const matchingShowcases = showcases.filter((item) => item.listKey === listKey && item.seedKey === seedKey); for (const item of matchingShowcases) { - item.removeSelf() + item.removeSelf(); } } }) .catch(() => { if (!isAddingItem) { // Replace check mark if patron was removing an item from a list - this.patronLists[listKey].dropperListAffordance.classList.add('list--active') + this.patronLists[listKey].dropperListAffordance.classList.add('list--active'); } - new FadingToast('Could not update list. Please try again later.').show() + new FadingToast('Could not update list. Please try again later.').show(); }) - .finally(() => this.patronLists[listKey].dropperListAffordance.classList.remove('list--pending')) + .finally(() => this.patronLists[listKey].dropperListAffordance.classList.remove('list--pending')); } /** @@ -265,14 +265,14 @@ export class ReadingLists { * @param {boolean} isWork `true` if a work was added or removed * @param {boolean} wasItemAdded `true` if item was added to list */ - updateViewAfterModifyingList(listKey, isWork, wasItemAdded) { + updateViewAfterModifyingList (listKey, isWork, wasItemAdded) { if (isWork) { - this.patronLists[listKey].workOnList = wasItemAdded + this.patronLists[listKey].workOnList = wasItemAdded; } else { - this.patronLists[listKey].itemOnList = wasItemAdded + this.patronLists[listKey].itemOnList = wasItemAdded; } - this.updateListDisplays() + this.updateListDisplays(); } /** @@ -283,16 +283,16 @@ export class ReadingLists { * * @param {HTMLElement} openListModalButton */ - addOpenListModalClickListener(openListModalButton) { + addOpenListModalClickListener (openListModalButton) { openListModalButton.addEventListener('click', (event) => { - event.preventDefault() + event.preventDefault(); $.colorbox({ inline: true, opacity: '0.5', href: '#addList' - }) - }) + }); + }); } /** @@ -305,22 +305,22 @@ export class ReadingLists { * @param {boolean} isActive `True` if this dropper's seed is on the list * @param {string} coverUrl URL for the list's cover image */ - onListCreationSuccess(listKey, listTitle, isActive, coverUrl) { - const dropperListAffordance = this.createDropdownListAffordance(listKey, listTitle, isActive) + onListCreationSuccess (listKey, listTitle, isActive, coverUrl) { + const dropperListAffordance = this.createDropdownListAffordance(listKey, listTitle, isActive); this.patronLists[listKey] = { title: listTitle, coverUrl: coverUrl, dropperListAffordance: dropperListAffordance - } + }; if (isActive) { if (this.workCheckBox && this.workCheckBox.checked) { - this.patronLists[listKey].itemOnList = false - this.patronLists[listKey].workOnList = true + this.patronLists[listKey].itemOnList = false; + this.patronLists[listKey].workOnList = true; } else { - this.patronLists[listKey].itemOnList = true - this.patronLists[listKey].workOnList = false + this.patronLists[listKey].itemOnList = true; + this.patronLists[listKey].workOnList = false; } } } @@ -333,26 +333,26 @@ export class ReadingLists { * @param {boolean} isActive `true` if the seed is on this list * @returns {HTMLElement} Reference to the newly created element */ - createDropdownListAffordance(listKey, listTitle, isActive) { + createDropdownListAffordance (listKey, listTitle, isActive) { const itemMarkUp = `<span class="list__status-indicator"></span> <a href="${listKey}" class="modify-list dropper__close" data-list-cover-url="${listKey}" data-list-key="${listKey}">${listTitle}</a> - ` - const p = document.createElement('p') - p.classList.add('list') + `; + const p = document.createElement('p'); + p.classList.add('list'); if (isActive) { - p.classList.add('list--active') + p.classList.add('list--active'); } - p.innerHTML = itemMarkUp - this.dropperListsElement.appendChild(p) - const listAffordance = p.querySelector('.modify-list') + p.innerHTML = itemMarkUp; + this.dropperListsElement.appendChild(p); + const listAffordance = p.querySelector('.modify-list'); listAffordance.addEventListener('click', (event) => { - event.preventDefault() - const isAddingItem = !this.patronLists[listKey].dropperListAffordance.classList.contains('list--active') - this.modifyList(listKey, isAddingItem) - }) + event.preventDefault(); + const isAddingItem = !this.patronLists[listKey].dropperListAffordance.classList.contains('list--active'); + this.modifyList(listKey, isAddingItem); + }); - return p + return p; } /** @@ -360,12 +360,12 @@ export class ReadingLists { * * @returns {string} The seed key */ - getSeed() { + getSeed () { if (this.workCheckBox && this.workCheckBox.checked) { // seed is the work key: - return this.workKey + return this.workKey; } - return this.seedKey + return this.seedKey; } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/index.js b/openlibrary/plugins/openlibrary/js/my-books/index.js index 1640be09742..c1e50d9314d 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/index.js +++ b/openlibrary/plugins/openlibrary/js/my-books/index.js @@ -1,88 +1,88 @@ -import { CreateListForm } from './CreateListForm' -import { MyBooksDropper } from './MyBooksDropper' -import myBooksStore from './store' -import { getListPartials } from '../lists/ListService' -import { ShowcaseItem, createActiveShowcaseItem, toggleActiveShowcaseItems } from '../lists/ShowcaseItem' -import { removeChildren } from '../utils' +import { CreateListForm } from './CreateListForm'; +import { MyBooksDropper } from './MyBooksDropper'; +import myBooksStore from './store'; +import { getListPartials } from '../lists/ListService'; +import { ShowcaseItem, createActiveShowcaseItem, toggleActiveShowcaseItems } from '../lists/ShowcaseItem'; +import { removeChildren } from '../utils'; // XXX : jsdoc // XXX : decompose -export function initMyBooksAffordances(dropperElements, showcaseElements) { - const showcases = [] +export function initMyBooksAffordances (dropperElements, showcaseElements) { + const showcases = []; for (const elem of showcaseElements) { - const showcase = new ShowcaseItem(elem) - showcase.initialize() + const showcase = new ShowcaseItem(elem); + showcase.initialize(); - showcases.push(showcase) + showcases.push(showcase); } - myBooksStore.setShowcases(showcases) + myBooksStore.setShowcases(showcases); - const form = document.querySelector('#create-list-form') - const createListForm = new CreateListForm(form) - createListForm.initialize() + const form = document.querySelector('#create-list-form'); + const createListForm = new CreateListForm(form); + createListForm.initialize(); - const droppers = [] - const seedKeys = [] + const droppers = []; + const seedKeys = []; for (const dropper of dropperElements) { - const myBooksDropper = new MyBooksDropper(dropper) - myBooksDropper.initialize() + const myBooksDropper = new MyBooksDropper(dropper); + myBooksDropper.initialize(); - droppers.push(myBooksDropper) - seedKeys.push(...myBooksDropper.getSeedKeys()) + droppers.push(myBooksDropper); + seedKeys.push(...myBooksDropper.getSeedKeys()); } // Remove duplicate keys: - const seedKeySet = new Set(seedKeys) + const seedKeySet = new Set(seedKeys); // Get user key from first Dropper and add to store: - const userKey = droppers[0].readingLists.userKey - myBooksStore.setUserKey(userKey) - myBooksStore.setDroppers(droppers) + const userKey = droppers[0].readingLists.userKey; + myBooksStore.setUserKey(userKey); + myBooksStore.setDroppers(droppers); getListPartials() .then(response => response.json()) .then((data) => { // XXX : convert this block to one or two function calls - const listData = data.listData - const activeShowcaseItems = [] + const listData = data.listData; + const activeShowcaseItems = []; for (const listKey in listData) { // Check for matches between seed keys and list members // If match, create new active showcase item for (const seedKey of listData[listKey].members) { if (seedKeySet.has(seedKey)) { - const key = listData[listKey].members[0] - const coverID = key.slice(key.indexOf('OL')) - const cover = `https://covers.openlibrary.org/b/olid/${coverID}-S.jpg` + const key = listData[listKey].members[0]; + const coverID = key.slice(key.indexOf('OL')); + const cover = `https://covers.openlibrary.org/b/olid/${coverID}-S.jpg`; - activeShowcaseItems.push(createActiveShowcaseItem(listKey, seedKey, listData[listKey].listName, cover)) + activeShowcaseItems.push(createActiveShowcaseItem(listKey, seedKey, listData[listKey].listName, cover)); } } } - const activeListsShowcaseElem = document.querySelector('.already-lists') + const activeListsShowcaseElem = document.querySelector('.already-lists'); if (activeListsShowcaseElem) { // Remove the loading indicator: - removeChildren(activeListsShowcaseElem) + removeChildren(activeListsShowcaseElem); for (const li of activeShowcaseItems) { - activeListsShowcaseElem.appendChild(li) + activeListsShowcaseElem.appendChild(li); - const showcase = new ShowcaseItem(li) - showcase.initialize() + const showcase = new ShowcaseItem(li); + showcase.initialize(); - showcases.push(showcase) + showcases.push(showcase); } - toggleActiveShowcaseItems(false) + toggleActiveShowcaseItems(false); } // Update dropper content: for (const dropper of droppers) { - dropper.updateReadingLists(data['dropper']) + dropper.updateReadingLists(data['dropper']); } - }) + }); } /** @@ -91,8 +91,8 @@ export function initMyBooksAffordances(dropperElements, showcaseElements) { * @param workKey {string} * @returns {MyBooksDropper|undefined} */ -export function findDropperForWork(workKey) { +export function findDropperForWork (workKey) { return myBooksStore.getDroppers().find(dropper => { - return workKey === dropper.workKey - }) + return workKey === dropper.workKey; + }); } diff --git a/openlibrary/plugins/openlibrary/js/my-books/store/index.js b/openlibrary/plugins/openlibrary/js/my-books/store/index.js index a867b9f7659..59756cdf267 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/store/index.js +++ b/openlibrary/plugins/openlibrary/js/my-books/store/index.js @@ -11,73 +11,73 @@ class MyBooksStore { /** * Initializes the store. */ - constructor() { + constructor () { this._store = { droppers: [], showcases: [], userkey: '', openDropper: null - } + }; } /** * @returns {Array<MyBooksDropper>} */ - getDroppers() { - return this._store.droppers + getDroppers () { + return this._store.droppers; } /** * @param {Array<MyBooksDropper>} droppers */ - setDroppers(droppers) { - this._store.droppers = droppers + setDroppers (droppers) { + this._store.droppers = droppers; } /** * @returns {Array<ShowcaseItem>} */ - getShowcases() { - return this._store.showcases + getShowcases () { + return this._store.showcases; } /** * @param {Array<ShowcaseItem>} showcases */ - setShowcases(showcases) { - this._store.showcases = showcases + setShowcases (showcases) { + this._store.showcases = showcases; } /** * @returns {string} */ - getUserKey() { - return this._store.userKey + getUserKey () { + return this._store.userKey; } /** * @param {string} userKey */ - setUserKey(userKey) { - this._store.userKey = userKey + setUserKey (userKey) { + this._store.userKey = userKey; } /** * @returns {MyBooksDropper} */ - getOpenDropper() { - return this._store.openDropper + getOpenDropper () { + return this._store.openDropper; } /** * @param {MyBooksDropper} dropper */ - setOpenDropper(dropper) { - this._store.openDropper = dropper + setOpenDropper (dropper) { + this._store.openDropper = dropper; } } -const myBooksStore = new MyBooksStore() -Object.freeze(myBooksStore) +const myBooksStore = new MyBooksStore(); +Object.freeze(myBooksStore); -export default myBooksStore +export default myBooksStore; diff --git a/openlibrary/plugins/openlibrary/js/native-dialog/index.js b/openlibrary/plugins/openlibrary/js/native-dialog/index.js index dffbd21ff6a..d39d96130cc 100644 --- a/openlibrary/plugins/openlibrary/js/native-dialog/index.js +++ b/openlibrary/plugins/openlibrary/js/native-dialog/index.js @@ -6,23 +6,23 @@ * 2. The dialog receives a `close-dialog` event. * @param {HTMLCollection<HTMLDialogElement>} elems */ -export function initDialogs(elems) { +export function initDialogs (elems) { for (const elem of elems) { - elem.addEventListener('click', function(event) { + elem.addEventListener('click', function (event) { // Event target exclusions needed for FireFox, which sets mouse positions to zero on // <select> and <option> clicks if (isOutOfBounds(event, elem) && event.target.nodeName !== 'SELECT' && event.target.nodeName !== 'OPTION') { - elem.close() + elem.close(); } - }) - elem.addEventListener('close-dialog', function() { - elem.close() - }) - const closeIcon = elem.querySelector('.native-dialog--close') - closeIcon.addEventListener('click', function() { - elem.close() - }) + }); + elem.addEventListener('close-dialog', function () { + elem.close(); + }); + const closeIcon = elem.querySelector('.native-dialog--close'); + closeIcon.addEventListener('click', function () { + elem.close(); + }); } } @@ -33,8 +33,8 @@ export function initDialogs(elems) { * @param {HTMLDialogElement} dialog * @returns `true` if the click was out of bounds. */ -function isOutOfBounds(event, dialog) { - const rect = dialog.getBoundingClientRect() +function isOutOfBounds (event, dialog) { + const rect = dialog.getBoundingClientRect(); return ( event.clientX < rect.left || event.clientX > rect.right || diff --git a/openlibrary/plugins/openlibrary/js/nonjquery_utils.js b/openlibrary/plugins/openlibrary/js/nonjquery_utils.js index 15d080a9ac6..3c93e07cedc 100644 --- a/openlibrary/plugins/openlibrary/js/nonjquery_utils.js +++ b/openlibrary/plugins/openlibrary/js/nonjquery_utils.js @@ -12,11 +12,11 @@ * @param {Boolean} [execAsap] * @returns {Function} */ -export function debounce(func, threshold=100, execAsap=false) { +export function debounce (func, threshold=100, execAsap=false) { let timeout; - return function debounced() { + return function debounced () { const obj = this, args = arguments; - function delayed() { + function delayed () { if (!execAsap) func.apply(obj, args); timeout = null; diff --git a/openlibrary/plugins/openlibrary/js/offline-banner.js b/openlibrary/plugins/openlibrary/js/offline-banner.js index 917579fe51a..87bef872a97 100644 --- a/openlibrary/plugins/openlibrary/js/offline-banner.js +++ b/openlibrary/plugins/openlibrary/js/offline-banner.js @@ -1,4 +1,4 @@ -export function initOfflineBanner() { +export function initOfflineBanner () { window.addEventListener('offline', () => { $('#offline-info').slideDown(); diff --git a/openlibrary/plugins/openlibrary/js/ol.analytics.js b/openlibrary/plugins/openlibrary/js/ol.analytics.js index e9f4e1c820a..78c76651912 100644 --- a/openlibrary/plugins/openlibrary/js/ol.analytics.js +++ b/openlibrary/plugins/openlibrary/js/ol.analytics.js @@ -5,14 +5,14 @@ * */ -export default function initAnalytics() { +export default function initAnalytics () { var vs, i; var startTime = new Date(); if (window.archive_analytics) { // Setup analytics, depends on script loaded from CDN window.archive_analytics.set_up_event_tracking(); - window.archive_analytics.ol_send_event_ping = function(values) { + window.archive_analytics.ol_send_event_ping = function (values) { var endTime = new Date(); window.archive_analytics.send_ping({ service: 'ol', @@ -24,7 +24,7 @@ export default function initAnalytics() { loadtime: (endTime.getTime() - startTime.getTime()), cache_bust: Math.random() }); - } + }; vs = window.archive_analytics.get_data_packets(); for (i in vs) { @@ -36,7 +36,7 @@ export default function initAnalytics() { if (window.flights){ window.flights.init(); } - $(document).on('click', '[data-ol-link-track]', function() { + $(document).on('click', '[data-ol-link-track]', function () { var category_action = $(this).attr('data-ol-link-track').split('|'); // for testing, // console.log(category_action[0], category_action[1]); @@ -50,7 +50,7 @@ export default function initAnalytics() { window.vs = vs; // NOTE: This might cause issues if this script is made async #4474 - window.addEventListener('DOMContentLoaded', function send_analytics_pageview() { + window.addEventListener('DOMContentLoaded', function send_analytics_pageview () { window.archive_analytics.send_pageview({}); }); } diff --git a/openlibrary/plugins/openlibrary/js/ol.js b/openlibrary/plugins/openlibrary/js/ol.js index 92b42878f22..e9be0e3ee45 100644 --- a/openlibrary/plugins/openlibrary/js/ol.js +++ b/openlibrary/plugins/openlibrary/js/ol.js @@ -6,11 +6,11 @@ import { SearchModeSelector, mode as searchMode } from './SearchUtils'; /* Sets the key in the website cookie to the specified value */ -function setValueInCookie(key, value) { +function setValueInCookie (key, value) { document.cookie = `${key}=${value};path=/`; } -export default function init() { +export default function init () { const urlParams = getJsonFromUrl(location.search); if (urlParams.mode) { searchMode.write(urlParams.mode); @@ -26,17 +26,17 @@ export default function init() { initWebsiteTranslationOptions(); } -export function initBorrowAndReadLinks() { +export function initBorrowAndReadLinks () { // LOADING ONCLICK FUNCTIONS FOR BORROW AND READ LINKS // used in openlibrary/macros/AvailabilityButton.html and openlibrary/macros/LoanStatus.html - $(function(){ - $('.cta-btn--ia.cta-btn--borrow,.cta-btn--ia.cta-btn--read').on('click', function(){ + $(function (){ + $('.cta-btn--ia.cta-btn--borrow,.cta-btn--ia.cta-btn--read').on('click', function (){ $(this).removeClass('cta-btn cta-btn--available').addClass('cta-btn cta-btn--available--load'); }); }); - $(function(){ - $('#waitlist_ebook').on('click', function(){ + $(function (){ + $('#waitlist_ebook').on('click', function (){ $(this).removeClass('cta-btn cta-btn--unavailable').addClass('cta-btn cta-btn--unavailable--load'); }); }); @@ -44,7 +44,7 @@ export function initBorrowAndReadLinks() { } -export function initWebsiteTranslationOptions() { +export function initWebsiteTranslationOptions () { $('.locale-options li a').on('click', function (event) { event.preventDefault(); const locale = $(this).data('lang-id'); diff --git a/openlibrary/plugins/openlibrary/js/partner_ol_lib.js b/openlibrary/plugins/openlibrary/js/partner_ol_lib.js index 5748aedfd32..9642d851f0b 100644 --- a/openlibrary/plugins/openlibrary/js/partner_ol_lib.js +++ b/openlibrary/plugins/openlibrary/js/partner_ol_lib.js @@ -1,7 +1,7 @@ /** * @param {string} container */ -function getIsbnToElementMap(container) { +function getIsbnToElementMap (container) { const reISBN = /(978)?[0-9]{9}[0-9X]/i; const elements = Array.from(document.querySelectorAll(container)); const isbnElementMap = {}; @@ -10,7 +10,7 @@ function getIsbnToElementMap(container) { if (isbnMatches) { isbnElementMap[isbnMatches[0]] = e; } - }) + }); return isbnElementMap; } @@ -18,7 +18,7 @@ function getIsbnToElementMap(container) { * @param {string[]} isbnList * @returns {Promise<Array>} */ -async function getAvailabilityDataFromOpenLibrary(isbnList) { +async function getAvailabilityDataFromOpenLibrary (isbnList) { const apiBaseUrl = 'https://openlibrary.org/search.json'; const apiUrl = `${apiBaseUrl}?fields=*,availability&q=isbn:${isbnList.join('+OR+')}`; const response = await fetch(apiUrl); @@ -47,27 +47,27 @@ async function getAvailabilityDataFromOpenLibrary(isbnList) { * textOnBtn: "Open Library!" * }); */ -async function addOpenLibraryButtons(options) { - const {bookContainer, selectorToPlaceBtnIn, textOnBtn} = options +async function addOpenLibraryButtons (options) { + const {bookContainer, selectorToPlaceBtnIn, textOnBtn} = options; if (bookContainer === undefined) { throw Error( 'book container must be specified in options for open library buttons to populate!' - ) + ); } const foundIsbnElementsMap = getIsbnToElementMap(bookContainer); - const availabilityResults = await getAvailabilityDataFromOpenLibrary(Object.keys(foundIsbnElementsMap)) + const availabilityResults = await getAvailabilityDataFromOpenLibrary(Object.keys(foundIsbnElementsMap)); Object.keys(foundIsbnElementsMap).map((isbn) => { - const availability = availabilityResults[isbn] + const availability = availabilityResults[isbn]; if (availability && availability.status !== 'error') { - const e = foundIsbnElementsMap[isbn] + const e = foundIsbnElementsMap[isbn]; const buttons = selectorToPlaceBtnIn ? e.querySelector(selectorToPlaceBtnIn) : e; - const openLibraryBtnLink = document.createElement('a') - openLibraryBtnLink.href = `https://openlibrary.org/works/${availability.openlibrary_work}` - openLibraryBtnLink.text = textOnBtn || 'Open Library' - openLibraryBtnLink.classList.add('openlibrary-btn') + const openLibraryBtnLink = document.createElement('a'); + openLibraryBtnLink.href = `https://openlibrary.org/works/${availability.openlibrary_work}`; + openLibraryBtnLink.text = textOnBtn || 'Open Library'; + openLibraryBtnLink.classList.add('openlibrary-btn'); buttons.append(openLibraryBtnLink); } - }) + }); } // Expose globally so clients can use this method diff --git a/openlibrary/plugins/openlibrary/js/patron_exports.js b/openlibrary/plugins/openlibrary/js/patron_exports.js index 9315fa07ba4..a4c41c20f8d 100644 --- a/openlibrary/plugins/openlibrary/js/patron_exports.js +++ b/openlibrary/plugins/openlibrary/js/patron_exports.js @@ -3,7 +3,7 @@ * * @param {HTMLElement} buttonElement */ -function disableButton(buttonElement) { +function disableButton (buttonElement) { buttonElement.setAttribute('disabled', 'true'); buttonElement.setAttribute('aria-disabled', 'true'); } @@ -16,11 +16,11 @@ function disableButton(buttonElement) { * * @param {NodeList<HTMLFormElement>} elems */ -export function initPatronExportForms(elems) { +export function initPatronExportForms (elems) { elems.forEach((form) => { - const submitButton = form.querySelector('input[type=submit]') + const submitButton = form.querySelector('input[type=submit]'); form.addEventListener('submit', () => { disableButton(submitButton); - }) - }) + }); + }); } diff --git a/openlibrary/plugins/openlibrary/js/private-button.js b/openlibrary/plugins/openlibrary/js/private-button.js index 4ba965a02b7..a2053f0691f 100644 --- a/openlibrary/plugins/openlibrary/js/private-button.js +++ b/openlibrary/plugins/openlibrary/js/private-button.js @@ -1,6 +1,6 @@ -import { FadingToast } from './Toast' +import { FadingToast } from './Toast'; -export function initPrivateButtons(buttons) { +export function initPrivateButtons (buttons) { buttons.forEach(button => { button.addEventListener('click', (event) => { event.preventDefault(); diff --git a/openlibrary/plugins/openlibrary/js/python.js b/openlibrary/plugins/openlibrary/js/python.js index 393e6b7fe88..13a1b0c81d5 100644 --- a/openlibrary/plugins/openlibrary/js/python.js +++ b/openlibrary/plugins/openlibrary/js/python.js @@ -7,7 +7,7 @@ * @param {mixed} n * @return {string} */ -export function commify(n) { +export function commify (n) { var text = n.toString(); var re = /(\d+)(\d{3})/; @@ -19,7 +19,7 @@ export function commify(n) { } // Implementation of Python urllib.urlencode in Javascript. -export function urlencode(query) { +export function urlencode (query) { var parts = []; var k; for (k in query) { @@ -28,7 +28,7 @@ export function urlencode(query) { return parts.join('&'); } -export function slice(array, begin, end) { +export function slice (array, begin, end) { var a = []; var i; for (i=begin; i < Math.min(array.length, end); i++) { diff --git a/openlibrary/plugins/openlibrary/js/reading-goals/index.js b/openlibrary/plugins/openlibrary/js/reading-goals/index.js index f992a545132..fbc291a67bc 100644 --- a/openlibrary/plugins/openlibrary/js/reading-goals/index.js +++ b/openlibrary/plugins/openlibrary/js/reading-goals/index.js @@ -1,15 +1,15 @@ -import { initDialogs } from '../native-dialog' -import { buildPartialsUrl } from '../utils' +import { initDialogs } from '../native-dialog'; +import { buildPartialsUrl } from '../utils'; /** * Adds listener to open reading goal modal. * * @param {HTMLCollection<HTMLElement>} links Prompts for adding a reading goal */ -export function initYearlyGoalPrompt(links) { +export function initYearlyGoalPrompt (links) { for (const link of links) { if (!link.classList.contains('goal-set')) { - link.addEventListener('click', onYearlyGoalClick) + link.addEventListener('click', onYearlyGoalClick); } } } @@ -17,9 +17,9 @@ export function initYearlyGoalPrompt(links) { /** * Finds and shows the yearly goal modal. */ -function onYearlyGoalClick() { - const yearlyGoalModal = document.querySelector('#yearly-goal-modal') - yearlyGoalModal.showModal() +function onYearlyGoalClick () { + const yearlyGoalModal = document.querySelector('#yearly-goal-modal'); + yearlyGoalModal.showModal(); } /** @@ -33,12 +33,12 @@ function onYearlyGoalClick() { * * @param {HTMLCollection<HTMLElement>} elems ELements which display only the current year */ -export function displayLocalYear(elems) { - const localYear = new Date().getFullYear() +export function displayLocalYear (elems) { + const localYear = new Date().getFullYear(); for (const elem of elems) { - const serverYear = Number(elem.dataset.serverYear) + const serverYear = Number(elem.dataset.serverYear); if (localYear !== serverYear) { - elem.textContent = localYear + elem.textContent = localYear; } } } @@ -48,11 +48,11 @@ export function displayLocalYear(elems) { * * @param {HTMLCollection<HTMLElement>} editLinks Edit goal links */ -export function initGoalEditLinks(editLinks) { +export function initGoalEditLinks (editLinks) { for (const link of editLinks) { - const parent = link.closest('.reading-goal-progress') - const modal = parent.querySelector('dialog') - addGoalEditClickListener(link, modal) + const parent = link.closest('.reading-goal-progress'); + const modal = parent.querySelector('dialog'); + addGoalEditClickListener(link, modal); } } @@ -64,10 +64,10 @@ export function initGoalEditLinks(editLinks) { * @param {HTMLElement} editLink An edit goal link * @param {HTMLDialogElement} modal The modal that will be shown */ -function addGoalEditClickListener(editLink, modal) { - editLink.addEventListener('click', function() { - modal.showModal() - }) +function addGoalEditClickListener (editLink, modal) { + editLink.addEventListener('click', function () { + modal.showModal(); + }); } /** @@ -76,9 +76,9 @@ function addGoalEditClickListener(editLink, modal) { * * @param {HTMLCollection<HTMLElement>} submitButtons Submit goal buttons */ -export function initGoalSubmitButtons(submitButtons) { +export function initGoalSubmitButtons (submitButtons) { for (const button of submitButtons) { - addGoalSubmissionListener(button) + addGoalSubmissionListener(button); } } @@ -89,17 +89,17 @@ export function initGoalSubmitButtons(submitButtons) { * the action set a new goal, or updated an existing goal. * @param {HTMLELement} submitButton Reading goal form submit button */ -function addGoalSubmissionListener(submitButton) { - submitButton.addEventListener('click', function(event) { - event.preventDefault() +function addGoalSubmissionListener (submitButton) { + submitButton.addEventListener('click', function (event) { + event.preventDefault(); - const form = submitButton.closest('form') + const form = submitButton.closest('form'); if (!form.checkValidity()) { - form.reportValidity() - throw new Error('Form invalid') + form.reportValidity(); + throw new Error('Form invalid'); } - const formData = new FormData(form) + const formData = new FormData(form); fetch(form.action, { method: 'POST', @@ -110,48 +110,48 @@ function addGoalSubmissionListener(submitButton) { }) .then((response) => { if (!response.ok) { - throw new Error('Failed to set reading goal') + throw new Error('Failed to set reading goal'); } - const modal = form.closest('dialog') + const modal = form.closest('dialog'); if (modal) { - modal.close() + modal.close(); } - const yearlyGoalSections = document.querySelectorAll('.yearly-goal-section') + const yearlyGoalSections = document.querySelectorAll('.yearly-goal-section'); if (formData.get('is_update')) { // Progress component exists on page yearlyGoalSections.forEach((yearlyGoalSection) => { - const goalInput = form.querySelector('input[name=goal]') - const isDeleted = Number(goalInput.value) === 0 + const goalInput = form.querySelector('input[name=goal]'); + const isDeleted = Number(goalInput.value) === 0; if (isDeleted) { - const chipGroup = yearlyGoalSection.querySelector('.chip-group') - const goalContainer = yearlyGoalSection.querySelector('#reading-goal-container') + const chipGroup = yearlyGoalSection.querySelector('.chip-group'); + const goalContainer = yearlyGoalSection.querySelector('#reading-goal-container'); if (chipGroup) { - chipGroup.classList.remove('hidden') + chipGroup.classList.remove('hidden'); } if (goalContainer) { - goalContainer.remove() + goalContainer.remove(); } // Restore "Set reading goal" link hidden when goal was first set - const setGoalLink = yearlyGoalSection.querySelector('.set-reading-goal-link') + const setGoalLink = yearlyGoalSection.querySelector('.set-reading-goal-link'); if (setGoalLink) { - setGoalLink.classList.remove('hidden') + setGoalLink.classList.remove('hidden'); } } else { - const progressComponent = modal.closest('.reading-goal-progress') - updateProgressComponent(progressComponent, Number(formData.get('goal'))) + const progressComponent = modal.closest('.reading-goal-progress'); + updateProgressComponent(progressComponent, Number(formData.get('goal'))); } - }) + }); } else { - const goalYear = formData.get('year') - fetchProgressAndUpdateViews(yearlyGoalSections, goalYear) - const banner = document.querySelector('.page-banner-mybooks') + const goalYear = formData.get('year'); + fetchProgressAndUpdateViews(yearlyGoalSections, goalYear); + const banner = document.querySelector('.page-banner-mybooks'); if (banner) { - banner.remove() + banner.remove(); } } - }) - }) + }); + }); } /** @@ -161,17 +161,17 @@ function addGoalSubmissionListener(submitButton) { * @param {HTMLElement} elem A reading goal progress component * @param {Number} goal The new reading goal */ -function updateProgressComponent(elem, goal) { +function updateProgressComponent (elem, goal) { // Calculate new percentage: - const booksReadSpan = elem.querySelector('.reading-goal-progress__books-read') - const booksRead = Number(booksReadSpan.textContent) - const percentComplete = Math.floor((booksRead / goal) * 100) + const booksReadSpan = elem.querySelector('.reading-goal-progress__books-read'); + const booksRead = Number(booksReadSpan.textContent); + const percentComplete = Math.floor((booksRead / goal) * 100); // Update view: - const goalSpan = elem.querySelector('.reading-goal-progress__goal') - const completedBar = elem.querySelector('.reading-goal-progress__completed') - goalSpan.textContent = goal - completedBar.style.width = `${Math.min(100, percentComplete)}%` + const goalSpan = elem.querySelector('.reading-goal-progress__goal'); + const completedBar = elem.querySelector('.reading-goal-progress__completed'); + goalSpan.textContent = goal; + completedBar.style.width = `${Math.min(100, percentComplete)}%`; } /** @@ -183,39 +183,39 @@ function updateProgressComponent(elem, goal) { * @param {NodeList} yearlyGoalElems Containers for progress components and reading goal links. * @param {string} goalYear Year that the goal is set for. */ -function fetchProgressAndUpdateViews(yearlyGoalElems, goalYear) { +function fetchProgressAndUpdateViews (yearlyGoalElems, goalYear) { fetch(buildPartialsUrl('ReadingGoalProgress', {year: goalYear})) .then((response) => { if (!response.ok) { - throw new Error('Failed to fetch progress element') + throw new Error('Failed to fetch progress element'); } - return response.json() + return response.json(); }) - .then(function(data) { - const html = data['partials'] + .then(function (data) { + const html = data['partials']; yearlyGoalElems.forEach((yearlyGoalElem) => { - const progress = document.createElement('SPAN') - progress.id = 'reading-goal-container' - progress.innerHTML = html - yearlyGoalElem.appendChild(progress) + const progress = document.createElement('SPAN'); + progress.id = 'reading-goal-container'; + progress.innerHTML = html; + yearlyGoalElem.appendChild(progress); const link = yearlyGoalElem.querySelector('.set-reading-goal-link'); if (link) { if (link.classList.contains('li-title-desktop')) { // Remove click listener in mobile views - link.removeEventListener('click', onYearlyGoalClick) + link.removeEventListener('click', onYearlyGoalClick); } else { // Hide desktop "set 20XX reading goal" link link.classList.add('hidden'); } } - const progressEditLink = progress.querySelector('.edit-reading-goal-link') - const updateModal = progress.querySelector('dialog') - initDialogs([updateModal]) - addGoalEditClickListener(progressEditLink, updateModal) - const submitButton = updateModal.querySelector('.reading-goal-submit-button') - addGoalSubmissionListener(submitButton) - }) - }) + const progressEditLink = progress.querySelector('.edit-reading-goal-link'); + const updateModal = progress.querySelector('dialog'); + initDialogs([updateModal]); + addGoalEditClickListener(progressEditLink, updateModal); + const submitButton = updateModal.querySelector('.reading-goal-submit-button'); + addGoalSubmissionListener(submitButton); + }); + }); } diff --git a/openlibrary/plugins/openlibrary/js/readinglog_stats.js b/openlibrary/plugins/openlibrary/js/readinglog_stats.js index c3e1f33714d..293c06bef9c 100644 --- a/openlibrary/plugins/openlibrary/js/readinglog_stats.js +++ b/openlibrary/plugins/openlibrary/js/readinglog_stats.js @@ -39,7 +39,7 @@ import 'chartjs-plugin-datalabels'; /** * @param {Config} config */ -export function init(config) { +export function init (config) { Chart.scaleService.updateScaleDefaults('linear', { ticks: { beginAtZero: true, stepSize: 1 } }); const authors_by_id = fromPairs(config.authors.map(a => [a.key, a])); @@ -50,7 +50,7 @@ export function init(config) { * @param {Element} container * @param {HTMLCanvasElement} canvas */ - function createWorkChart(config, chartConfig, container, canvas) { + function createWorkChart (config, chartConfig, container, canvas) { /** @type {{[key: string]: Work[]}} */ const grouped = {}; /** @type {Work[]} */ @@ -137,7 +137,7 @@ export function init(config) { }, ]; - function buildSparql(authors) { + function buildSparql (authors) { return ` SELECT DISTINCT ?x ?xLabel ?olid ${ @@ -178,7 +178,7 @@ export function init(config) { bindings .filter(x => x[name]) .map(x => ({ [name]: x[name], [`${name}Label`]: x[`${name}Label`] })), - x => x[name].value) + x => x[name].value); record[name] = deduped.map(x => x[name]); record[`${name}Label`] = deduped.map(x => x[`${name}Label`]); } else { @@ -220,13 +220,13 @@ export function init(config) { * @param {string} key * @return {any} */ -function getPath(obj, key) { +function getPath (obj, key) { /** * @param {object} obj * @param {string[]} param1 * @return {any} */ - function main(obj, [head, ...rest]) { + function main (obj, [head, ...rest]) { if (typeof(obj) === 'undefined') return undefined; if (!head) return obj; if (head.endsWith('[]')) return obj[head.slice(0, -2)].flatMap(x => main(x, rest)); diff --git a/openlibrary/plugins/openlibrary/js/return-form/index.js b/openlibrary/plugins/openlibrary/js/return-form/index.js index 50b083737dd..c6b08b6d00f 100644 --- a/openlibrary/plugins/openlibrary/js/return-form/index.js +++ b/openlibrary/plugins/openlibrary/js/return-form/index.js @@ -4,13 +4,13 @@ * * @param {NodeList<HTMLElement>} returnForms */ -export function initReturnForms(returnForms) { +export function initReturnForms (returnForms) { for (const form of returnForms) { - const i18nStrings = JSON.parse(form.dataset.i18n) + const i18nStrings = JSON.parse(form.dataset.i18n); form.addEventListener('submit', (event) => { if (!confirm(i18nStrings['confirm_return'])) { event.preventDefault(); } - }) + }); } } diff --git a/openlibrary/plugins/openlibrary/js/search.js b/openlibrary/plugins/openlibrary/js/search.js index 6e11db26415..e0b36ecf20c 100644 --- a/openlibrary/plugins/openlibrary/js/search.js +++ b/openlibrary/plugins/openlibrary/js/search.js @@ -11,10 +11,10 @@ import { buildPartialsUrl } from './utils'; * @param {Number} start_facet_count initial number of displayed facets * @param {Number} facet_inc number of hidden facets to be displayed */ -export function more(header, start_facet_count, facet_inc) { - const facetEntry = `div.${header} div.facetEntry` - const shown = $(`${facetEntry}:not(:hidden)`).length - const total = $(facetEntry).length +export function more (header, start_facet_count, facet_inc) { + const facetEntry = `div.${header} div.facetEntry`; + const shown = $(`${facetEntry}:not(:hidden)`).length; + const total = $(facetEntry).length; if (shown === start_facet_count) { $(`#${header}_less`).show(); $(`#${header}_bull`).show(); @@ -33,10 +33,10 @@ export function more(header, start_facet_count, facet_inc) { * @param {Number} start_facet_count initial number of displayed facets * @param {Number} facet_inc number of displayed facets to be hidden */ -export function less(header, start_facet_count, facet_inc) { - const facetEntry = `div.${header} div.facetEntry` - const shown = $(`${facetEntry}:not(:hidden)`).length - const total = $(facetEntry).length +export function less (header, start_facet_count, facet_inc) { + const facetEntry = `div.${header} div.facetEntry`; + const shown = $(`${facetEntry}:not(:hidden)`).length; + const total = $(facetEntry).length; const increment_extra = (shown - start_facet_count) % facet_inc; const facet_dec = (increment_extra === 0) ? facet_inc:increment_extra; const next_shown = Math.max(start_facet_count, shown - facet_dec); @@ -64,31 +64,31 @@ export function less(header, start_facet_count, facet_inc) { * * @param {HTMLElement} facetsElem Root element of the search facets sidebar component */ -export async function initSearchFacets(facetsElem) { - const asyncLoad = facetsElem.dataset.asyncLoad +export async function initSearchFacets (facetsElem) { + const asyncLoad = facetsElem.dataset.asyncLoad; if (asyncLoad) { - const param = JSON.parse(facetsElem.dataset.param) + const param = JSON.parse(facetsElem.dataset.param); await whenVisible(facetsElem); fetchPartials(param) .then((data) => { if (data.activeFacets) { - const activeFacetsElem = createElementFromMarkup(data.activeFacets) - const activeFacetsContainer = document.querySelector('.selected-search-facets-container') - activeFacetsContainer.replaceChildren(activeFacetsElem) + const activeFacetsElem = createElementFromMarkup(data.activeFacets); + const activeFacetsContainer = document.querySelector('.selected-search-facets-container'); + activeFacetsContainer.replaceChildren(activeFacetsElem); } - const newFacetsElem = createElementFromMarkup(data.sidebar) - facetsElem.replaceWith(newFacetsElem) - hydrateFacets() + const newFacetsElem = createElementFromMarkup(data.sidebar); + facetsElem.replaceWith(newFacetsElem); + hydrateFacets(); - document.title = data.title + document.title = data.title; }) .catch(() => { // XXX : Handle case where `/partials` response is not `2XX` here - }) + }); } else { - hydrateFacets() + hydrateFacets(); } } @@ -96,16 +96,16 @@ export async function initSearchFacets(facetsElem) { /** * Adds click listeners to the "show more" and "show less" facet affordances. */ -function hydrateFacets() { +function hydrateFacets () { const data_config_json = $('#searchFacets').data('config'); const start_facet_count = data_config_json['start_facet_count']; const facet_inc = data_config_json['facet_inc']; $('.header_bull').hide(); - $('.header_more').on('click', function(){ + $('.header_more').on('click', function (){ more($(this).data('header'), start_facet_count, facet_inc); }); - $('.header_less').on('click', function(){ + $('.header_less').on('click', function (){ less($(this).data('header'), start_facet_count, facet_inc); }); } @@ -127,20 +127,20 @@ function hydrateFacets() { * * @throws Error when `/partials` response is not in 200-299 range. */ -function fetchPartials(param) { +function fetchPartials (param) { const data = { param: param, path: location.pathname, query: location.search - } + }; return fetch(buildPartialsUrl('SearchFacets', {data: JSON.stringify(data)})) .then((resp) => { if (!resp.ok) { - throw new Error(`Failed to fetch partials. Status code: ${resp.status}`) + throw new Error(`Failed to fetch partials. Status code: ${resp.status}`); } - return resp.json() - }) + return resp.json(); + }); } /** @@ -152,10 +152,10 @@ function fetchPartials(param) { * @param {string} markup HTML markup for a single element * @returns {HTMLElement} */ -function createElementFromMarkup(markup) { - const template = document.createElement('template') - template.innerHTML = markup - return template.content.children[0] +function createElementFromMarkup (markup) { + const template = document.createElement('template'); + template.innerHTML = markup; + return template.content.children[0]; } @@ -166,27 +166,27 @@ function createElementFromMarkup(markup) { * @param {IntersectionObserverInit} options * @returns {Promise<void>} */ -async function whenVisible(elem, options = {}) { +async function whenVisible (elem, options = {}) { return new Promise((resolve) => { const intersectionObserver = new IntersectionObserver( (entries, observer) => { entries.forEach(entry => { if (!entry.isIntersecting) { - return + return; } // Stop observing once the element is visible - observer.unobserve(entry.target) - observer.disconnect() - resolve() - }) + observer.unobserve(entry.target); + observer.disconnect(); + resolve(); + }); }, Object.assign({ root: null, rootMargin: '200px', threshold: 0 }, options) - ) + ); intersectionObserver.observe(elem); }); diff --git a/openlibrary/plugins/openlibrary/js/service-worker-matchers.js b/openlibrary/plugins/openlibrary/js/service-worker-matchers.js index 672e6fa4c5a..4385198e5b3 100644 --- a/openlibrary/plugins/openlibrary/js/service-worker-matchers.js +++ b/openlibrary/plugins/openlibrary/js/service-worker-matchers.js @@ -7,34 +7,34 @@ It is in a is a separate file to avoid this error when writing tests: */ -export function matchMiscFiles({ url }) { +export function matchMiscFiles ({ url }) { const miscFiles = ['/favicon.ico', '/static/manifest.json', '/cdn/archive.org/athena.js', - '/cdn/archive.org/donate.js'] + '/cdn/archive.org/donate.js']; return miscFiles.includes(url.pathname); } -export function matchSmallMediumCovers({ url }) { +export function matchSmallMediumCovers ({ url }) { const regex = /-[SM].jpg$/; return regex.test(url.pathname); } -export function matchLargeCovers({ url }) { +export function matchLargeCovers ({ url }) { const regex = /-L.jpg$/; return regex.test(url.pathname); } -export function matchStaticImages({ url }) { +export function matchStaticImages ({ url }) { const regex = /^\/images\/|^\/static\/images\//; return regex.test(url.pathname); } -export function matchStaticBuild({ url }) { +export function matchStaticBuild ({ url }) { const regex = /^\/static\/build\/.*(\.js|\.css)/; - const localhost = url.origin.includes('localhost') + const localhost = url.origin.includes('localhost'); return !localhost && regex.test(url.pathname); } -export function matchArchiveOrgImage({ url }) { +export function matchArchiveOrgImage ({ url }) { // most importantly, to cache your profile picture from loading every time // also caches some covers return url.href.startsWith('https://archive.org/services/img/'); diff --git a/openlibrary/plugins/openlibrary/js/service-worker.js b/openlibrary/plugins/openlibrary/js/service-worker.js index 8c3e992c191..b182481a147 100644 --- a/openlibrary/plugins/openlibrary/js/service-worker.js +++ b/openlibrary/plugins/openlibrary/js/service-worker.js @@ -70,7 +70,7 @@ registerRoute( cacheableResponses ], }) -) +); registerRoute( matchSmallMediumCovers, diff --git a/openlibrary/plugins/openlibrary/js/signup.js b/openlibrary/plugins/openlibrary/js/signup.js index fb0b7b5e821..81ab015815d 100644 --- a/openlibrary/plugins/openlibrary/js/signup.js +++ b/openlibrary/plugins/openlibrary/js/signup.js @@ -1,11 +1,11 @@ import { debounce } from './nonjquery_utils.js'; -export function initSignupForm() { +export function initSignupForm () { const signupForm = document.querySelector('form[name=signup]'); const submitBtn = document.querySelector('button[name=signup]'); - const rpdCheckbox = document.querySelector('#pd-request') - const pdaSelectorContainer = document.querySelector('#pda-selector') - const pdaSelector = document.querySelector('#pd_program') + const rpdCheckbox = document.querySelector('#pd-request'); + const pdaSelectorContainer = document.querySelector('#pda-selector'); + const pdaSelector = document.querySelector('#pd_program'); const i18nStrings = JSON.parse(signupForm.dataset.i18n); const emailLoadingIcon = $('.ol-signup-form__input--emailAddr .ol-signup-form__icon--loading'); const usernameLoadingIcon = $('.ol-signup-form__input--username .ol-signup-form__icon--loading'); @@ -21,16 +21,16 @@ export function initSignupForm() { const USERNAME_MAXLENGTH = 20; // Callback that is called when grecaptcha.execute() is successful - function submitCreateAccountForm() { + function submitCreateAccountForm () { signupForm.submit(); } - window.submitCreateAccountForm = submitCreateAccountForm + window.submitCreateAccountForm = submitCreateAccountForm; // Checks whether reportValidity exists for cross-browser compatibility // Includes invalid input count to account for checks not covered by reportValidity - $(signupForm).on('submit', function(e) { + $(signupForm).on('submit', function (e) { e.preventDefault(); - validatePDSelection() + validatePDSelection(); const numInvalidInputs = signupForm.querySelectorAll('.invalid').length; const isFormattingValid = !signupForm.reportValidity || signupForm.reportValidity(); if (numInvalidInputs === 0 && isFormattingValid && window.grecaptcha) { @@ -39,9 +39,9 @@ export function initSignupForm() { } }); - $('#username').on('keyup', function(){ + $('#username').on('keyup', function (){ const value = $(this).val(); - $('#userUrl').addClass('darkgreen').text(value).css('font-weight','700'); + $('#userUrl').addClass('darkgreen').text(value).css('font-weight', '700'); }); /** @@ -51,7 +51,7 @@ export function initSignupForm() { * @param {string} errorDiv The ID (incl #) of the div where the error msg will be rendered * @param {string} errorMsg The error message text */ - function renderError(inputId, errorDiv, errorMsg) { + function renderError (inputId, errorDiv, errorMsg) { $(inputId).addClass('invalid'); $(`label[for=${inputId.slice(1)}]`).addClass('invalid'); $(errorDiv).text(errorMsg); @@ -63,13 +63,13 @@ export function initSignupForm() { * @param {string} inputId The ID (incl #) of the input the error relates to * @param {string} errorDiv The ID (incl #) of the div where the error msg is currently rendered */ - function clearError(inputId, errorDiv) { + function clearError (inputId, errorDiv) { $(inputId).removeClass('invalid'); $(`label[for=${inputId.slice(1)}]`).removeClass('invalid'); $(errorDiv).text(''); } - function validateUsername() { + function validateUsername () { const value_username = $('#username').val(); usernameSuccessIcon.hide(); @@ -95,7 +95,7 @@ export function initSignupForm() { url: '/account/validate', data: { username: value_username }, type: 'GET', - success: function(errors) { + success: function (errors) { usernameLoadingIcon.hide(); if (errors.username) { @@ -108,7 +108,7 @@ export function initSignupForm() { }); } - function validateEmail() { + function validateEmail () { const value_email = $('#emailAddr').val(); emailSuccessIcon.hide(); @@ -129,7 +129,7 @@ export function initSignupForm() { url: '/account/validate', data: { email: value_email }, type: 'GET', - success: function(errors) { + success: function (errors) { emailLoadingIcon.hide(); if (errors.email) { @@ -142,7 +142,7 @@ export function initSignupForm() { }); } - function validatePassword() { + function validatePassword () { const value_password = $('#password').val(); if (value_password === '') { @@ -158,24 +158,24 @@ export function initSignupForm() { clearError('#password', '#passwordMessage'); } - function validatePDSelection() { + function validatePDSelection () { if (!rpdCheckbox.checked) { - clearError('#pd_program', '#pd_programMessage') + clearError('#pd_program', '#pd_programMessage'); pdaSelector.setAttribute('aria-invalid', 'false'); - return + return; } if (pdaSelector.value === '') { - renderError('#pd_program', '#pd_programMessage', i18nStrings['missing_pda_err']) + renderError('#pd_program', '#pd_programMessage', i18nStrings['missing_pda_err']); pdaSelector.setAttribute('aria-invalid', 'true'); - return + return; } - clearError('#pd_program', '#pd_programMessage') + clearError('#pd_program', '#pd_programMessage'); pdaSelector.setAttribute('aria-invalid', 'false'); } // Maps input ID attribute to corresponding validation function - function validateInput(input) { + function validateInput (input) { const id = $(input).attr('id'); if (id === 'emailAddr') { validateEmail(); @@ -188,53 +188,53 @@ export function initSignupForm() { } } - const $nonCheckboxInputs = $('form[name=signup] input:not([type="checkbox"])') + const $nonCheckboxInputs = $('form[name=signup] input:not([type="checkbox"])'); // Validates input fields already marked as invalid on value change - $nonCheckboxInputs.on('input', debounce(function(){ + $nonCheckboxInputs.on('input', debounce(function (){ if ($(this).hasClass('invalid')) { validateInput(this); } }, 50)); // Validates all other input fields (i.e. not already marked as invalid) on blur - $nonCheckboxInputs.on('blur', function() { + $nonCheckboxInputs.on('blur', function () { if (!$(this).hasClass('invalid')) { validateInput(this); } }); // Validates the print-disability authority selection when the selection changes - $('form[name=signup] select').on('change', function() { - validatePDSelection() - }) + $('form[name=signup] select').on('change', function () { + validatePDSelection(); + }); - function updateSelectorVisibility() { + function updateSelectorVisibility () { if (rpdCheckbox.checked) { - pdaSelectorContainer.classList.remove('hidden') - rpdCheckbox.setAttribute('aria-expanded','true') - pdaSelectorContainer.setAttribute('aria-hidden','false') - pdaSelector.setAttribute('aria-required', 'true') + pdaSelectorContainer.classList.remove('hidden'); + rpdCheckbox.setAttribute('aria-expanded', 'true'); + pdaSelectorContainer.setAttribute('aria-hidden', 'false'); + pdaSelector.setAttribute('aria-required', 'true'); } else { - pdaSelectorContainer.classList.add('hidden') - rpdCheckbox.setAttribute('aria-expanded','false') - pdaSelectorContainer.setAttribute('aria-hidden','true') - pdaSelector.setAttribute('aria-required', 'false') + pdaSelectorContainer.classList.add('hidden'); + rpdCheckbox.setAttribute('aria-expanded', 'false'); + pdaSelectorContainer.setAttribute('aria-hidden', 'true'); + pdaSelector.setAttribute('aria-required', 'false'); } } - rpdCheckbox.addEventListener('change', updateSelectorVisibility) + rpdCheckbox.addEventListener('change', updateSelectorVisibility); // On page reload, display PD program options and validate selection - updateSelectorVisibility() - validatePDSelection() + updateSelectorVisibility(); + validatePDSelection(); } -export function initLoginForm() { +export function initLoginForm () { const loginForm = $('form[name=login]'); const loadingText = loginForm.data('i18n')['loading_text']; loginForm.on('submit', () => { $('button[type=submit]').prop('disabled', true).text(loadingText); - }) + }); } diff --git a/openlibrary/plugins/openlibrary/js/star-ratings/index.js b/openlibrary/plugins/openlibrary/js/star-ratings/index.js index 08356d3935e..a782b722838 100644 --- a/openlibrary/plugins/openlibrary/js/star-ratings/index.js +++ b/openlibrary/plugins/openlibrary/js/star-ratings/index.js @@ -2,15 +2,15 @@ import { FadingToast } from '../Toast.js'; import { findDropperForWork } from '../my-books'; import { ReadingLogShelves } from '../my-books/MyBooksDropper/ReadingLogForms'; -export function initRatingHandlers(ratingForms) { +export function initRatingHandlers (ratingForms) { for (const form of ratingForms) { - form.addEventListener('submit', function(e) { + form.addEventListener('submit', function (e) { handleRatingSubmission(e, form); - }) + }); } } -function handleRatingSubmission(event, form) { +function handleRatingSubmission (event, form) { event.preventDefault(); // Continue only if selected star is different from previous rating if (!event.submitter.classList.contains('star-selected')) { @@ -19,8 +19,8 @@ function handleRatingSubmission(event, form) { const formData = new FormData(form); let rating; if (event.submitter.value) { - rating = Number(event.submitter.value) - formData.append('rating', event.submitter.value) + rating = Number(event.submitter.value); + formData.append('rating', event.submitter.value); } // Make AJAX call @@ -36,7 +36,7 @@ function handleRatingSubmission(event, form) { throw new Error('You must be logged in to rate books'); } if (!response.ok) { - throw new Error('Ratings update failed') + throw new Error('Ratings update failed'); } // Update view to deselect all stars form.querySelectorAll('.star-selected').forEach((elem) => { @@ -44,7 +44,7 @@ function handleRatingSubmission(event, form) { if (elem.hasAttribute('property')) { elem.removeAttribute('property'); } - }) + }); const clearButton = form.querySelector('.star-messaging'); if (rating) { // A rating was added or updated @@ -53,14 +53,14 @@ function handleRatingSubmission(event, form) { form.querySelectorAll(`.star-${rating}`).forEach((elem) => { elem.classList.add('star-selected'); if (elem.tagName === 'LABEL') { - elem.setAttribute('property', 'ratingValue') + elem.setAttribute('property', 'ratingValue'); } - }) + }); // Find dropper that is associated with this star rating affordance: - const dropper = findDropperForWork(form.dataset.workKey) + const dropper = findDropperForWork(form.dataset.workKey); if (dropper) { - dropper.updateShelfDisplay(ReadingLogShelves.ALREADY_READ) + dropper.updateShelfDisplay(ReadingLogShelves.ALREADY_READ); } } else { // A rating was deleted clearButton.classList.add('hidden'); @@ -68,6 +68,6 @@ function handleRatingSubmission(event, form) { }) .catch((error) => { new FadingToast(error.message).show(); - }) + }); } } diff --git a/openlibrary/plugins/openlibrary/js/stats/index.js b/openlibrary/plugins/openlibrary/js/stats/index.js index b68b22d021a..f07055317d2 100644 --- a/openlibrary/plugins/openlibrary/js/stats/index.js +++ b/openlibrary/plugins/openlibrary/js/stats/index.js @@ -5,23 +5,23 @@ * @returns {Promise<void>} * @see /openlibrary/templates/admin/index.html */ -export async function initUniqueLoginCounts(containerElem) { - const loadingIndicator = containerElem.querySelector('.loadingIndicator') - const i18nStrings = JSON.parse(containerElem.dataset.i18n) +export async function initUniqueLoginCounts (containerElem) { + const loadingIndicator = containerElem.querySelector('.loadingIndicator'); + const i18nStrings = JSON.parse(containerElem.dataset.i18n); const counts = await fetchCounts() .then((resp) => { if (resp.status !== 200) { - throw new Error(`Failed to fetch partials. Status code: ${resp.status}`) + throw new Error(`Failed to fetch partials. Status code: ${resp.status}`); } - return resp.json() - }) + return resp.json(); + }); - const countDiv = document.createElement('DIV') - countDiv.innerHTML = i18nStrings.uniqueLoginsCopy - const countSpan = countDiv.querySelector('.login-counts') - countSpan.textContent = counts.loginCount - loadingIndicator.replaceWith(countDiv) + const countDiv = document.createElement('DIV'); + countDiv.innerHTML = i18nStrings.uniqueLoginsCopy; + const countSpan = countDiv.querySelector('.login-counts'); + countSpan.textContent = counts.loginCount; + loadingIndicator.replaceWith(countDiv); } /** @@ -30,6 +30,6 @@ export async function initUniqueLoginCounts(containerElem) { * @returns {Promise<Response>} * @see `monthly_logins` class in /openlibrary/plugins/openlibrary/api.py */ -async function fetchCounts() { - return fetch('/api/monthly_logins.json') +async function fetchCounts () { + return fetch('/api/monthly_logins.json'); } diff --git a/openlibrary/plugins/openlibrary/js/tabs.js b/openlibrary/plugins/openlibrary/js/tabs.js index 83c7335af28..06a53fbd04d 100644 --- a/openlibrary/plugins/openlibrary/js/tabs.js +++ b/openlibrary/plugins/openlibrary/js/tabs.js @@ -1,9 +1,9 @@ const TABS_OPTIONS = { fx: { opacity: 'toggle' } }; import 'jquery-ui/ui/widgets/tabs'; -export function initTabs($node) { +export function initTabs ($node) { $node.tabs(TABS_OPTIONS); - $node.filter('.autohash').on('tabsselect', function(event, ui) { + $node.filter('.autohash').on('tabsselect', function (event, ui) { document.location.hash = ui.panel.id; }); } diff --git a/openlibrary/plugins/openlibrary/js/team.js b/openlibrary/plugins/openlibrary/js/team.js index ba1084a76df..674bf34d5f9 100644 --- a/openlibrary/plugins/openlibrary/js/team.js +++ b/openlibrary/plugins/openlibrary/js/team.js @@ -1,6 +1,6 @@ import team from '../../../templates/about/team.json'; import { updateURLParameters } from './utils'; -export function initTeamFilter() { +export function initTeamFilter () { const currentYear = new Date().getFullYear().toString(); // Photos const default_profile_image = diff --git a/openlibrary/plugins/openlibrary/js/template.js b/openlibrary/plugins/openlibrary/js/template.js index ff1a151d308..5e2eded2786 100644 --- a/openlibrary/plugins/openlibrary/js/template.js +++ b/openlibrary/plugins/openlibrary/js/template.js @@ -2,18 +2,18 @@ // // Inspired by http://ejohn.org/blog/javascript-micro-templating/ -export default function Template(tmpl_text) { +export default function Template (tmpl_text) { var s = []; var js = ['var _p=[];', 'with(env) {']; var tokens, i, t, f, g; - function addCode(text) { + function addCode (text) { js.push(text); } - function addExpr(text) { + function addExpr (text) { js.push(`_p.push(htmlquote(${text}));`); } - function addText(text) { + function addText (text) { js.push(`_p.push(__s[${s.length}]);`); s.push(text); } @@ -35,10 +35,10 @@ export default function Template(tmpl_text) { js.push('}', 'return _p.join(\'\');'); f = new Function(['__s', 'env'], js.join('\n')); - g = function(env) { + g = function (env) { return f(s, env); }; - g.toString = function() { return tmpl_text; }; - g.toCode = function() { return f.toString(); }; + g.toString = function () { return tmpl_text; }; + g.toCode = function () { return f.toString(); }; return g; } diff --git a/openlibrary/plugins/openlibrary/js/type_changer.js b/openlibrary/plugins/openlibrary/js/type_changer.js index 650cc6ae546..542082ef876 100644 --- a/openlibrary/plugins/openlibrary/js/type_changer.js +++ b/openlibrary/plugins/openlibrary/js/type_changer.js @@ -2,10 +2,10 @@ * Functionality for TypeChanger.html */ -export function initTypeChanger(elem) { +export function initTypeChanger (elem) { // /about?m=edit - where this code is run - function changeTemplate() { + function changeTemplate () { // Change the template of the page based on the selected value const searchParams = new URLSearchParams(window.location.search); const t = elem.value; diff --git a/openlibrary/plugins/openlibrary/js/utils.js b/openlibrary/plugins/openlibrary/js/utils.js index 423cdcfa4bd..8f76f3a5c10 100644 --- a/openlibrary/plugins/openlibrary/js/utils.js +++ b/openlibrary/plugins/openlibrary/js/utils.js @@ -5,13 +5,13 @@ See: https://github.com/internetarchive/openlibrary/pull/9180#issuecomment-21079 */ // closes active popup -export function closePopup() { +export function closePopup () { // Note we don't import colorbox here, since it's on the parent parent.jQuery.fn.colorbox.close(); } // used in templates/admin/imports.html -export function truncate(text, limit) { +export function truncate (text, limit) { if (text.length > limit) { return `${text.substr(0, limit)}...`; } else { @@ -20,7 +20,7 @@ export function truncate(text, limit) { } // used in openlibrary/templates/books/edit/excerpts.html -export function cond(predicate, true_value, false_value) { +export function cond (predicate, true_value, false_value) { if (predicate) { return true_value; } @@ -34,18 +34,18 @@ export function cond(predicate, true_value, false_value) { * * @param {...HTMLElement} elements */ -export function removeChildren(...elements) { +export function removeChildren (...elements) { for (const elem of elements) { if (elem) { while (elem.firstChild) { - elem.removeChild(elem.firstChild) + elem.removeChild(elem.firstChild); } } } } // Function to add or update multiple query parameters -export function updateURLParameters(params) { +export function updateURLParameters (params) { // Get the current URL const url = new URL(window.location.href); @@ -64,16 +64,16 @@ export function updateURLParameters(params) { * Remove leading/trailing empty space on field deselect. * @param string a value for document.querySelectorAll() */ -export function trimInputValues(param) { +export function trimInputValues (param) { const inputs = document.querySelectorAll(param); inputs.forEach(input => { - input.addEventListener('blur', function() { + input.addEventListener('blur', function () { this.value = this.value.trim(); }); }); } -export function buildPartialsUrl(component, params = {}) { +export function buildPartialsUrl (component, params = {}) { const curUrl = new URL(window.location.href); const url = new URL(`${location.origin}/partials/${component}.json`); diff --git a/openlibrary/plugins/openlibrary/js/waitlist.js b/openlibrary/plugins/openlibrary/js/waitlist.js index 1fb8d17c5b1..bf992e30c10 100644 --- a/openlibrary/plugins/openlibrary/js/waitlist.js +++ b/openlibrary/plugins/openlibrary/js/waitlist.js @@ -5,10 +5,10 @@ import 'jquery-ui/ui/widgets/dialog'; * * @param {NodeList<HTMLElement>} leaveWaitlistLinks - NodeList of leave waitlist links */ -export function initLeaveWaitlist(leaveWaitlistLinks) { +export function initLeaveWaitlist (leaveWaitlistLinks) { for (const link of leaveWaitlistLinks) { link.addEventListener('click', () => { - const $link = $(link) + const $link = $(link); const title = $link.parents('tr').find('.book').text(); $('#leave-waitinglist-dialog strong').text(title); // We remove the hidden class here because otherwise it flashes for a moment on page load @@ -16,6 +16,6 @@ export function initLeaveWaitlist(leaveWaitlistLinks) { $('#leave-waitinglist-dialog') .data('origin', $link) .dialog('open'); - }) + }); } } diff --git a/static/bookmarklets/import_webbook.js b/static/bookmarklets/import_webbook.js index e1103664991..a698c978e07 100644 --- a/static/bookmarklets/import_webbook.js +++ b/static/bookmarklets/import_webbook.js @@ -1,5 +1,5 @@ /* eslint-disable no-unused-labels */ -javascript:(async()=> { +javascript:(async ()=> { const url = prompt('Enter the book URL you want to import:'); if (!url) return; const promptText = `You are an expert book metadata librarian and research assistant. Your role is to search the web and collect accurate data to produce the most useful, factual, complete, and patron-oriented book page possible for the URL: diff --git a/stories/.storybook/main.js b/stories/.storybook/main.js index 36475e47e22..87d9d43833a 100644 --- a/stories/.storybook/main.js +++ b/stories/.storybook/main.js @@ -21,4 +21,4 @@ module.exports = { addons: [ '@storybook/addon-essentials' ], -} +}; diff --git a/stories/.storybook/preview.js b/stories/.storybook/preview.js index 2eaf63f831e..85cd3d12a8e 100644 --- a/stories/.storybook/preview.js +++ b/stories/.storybook/preview.js @@ -1,4 +1,4 @@ export const parameters = { actions: { argTypesRegex: '^on[A-Z].*' }, -} +}; diff --git a/stories/Button.stories.js b/stories/Button.stories.js index 9e77fab571d..51584dcf128 100644 --- a/stories/Button.stories.js +++ b/stories/Button.stories.js @@ -7,60 +7,60 @@ export default { const ButtonTemplate = (buttonType, text, badgeCount=null) => `<div class="cta-btn${ButtonTypes[buttonType]}">${text}${badgeCount ? BadgeTemplate(badgeCount) : ''}</div>`; -const BadgeTemplate = (badgeCount) => ` <span class="cta-btn__badge">${badgeCount}</span>` +const BadgeTemplate = (badgeCount) => ` <span class="cta-btn__badge">${badgeCount}</span>`; const ButtonTypes = { default: '', unavailable: ' cta-btn--unavailable', available: ' cta-btn--available', preview: ' cta-btn--shell cta-btn--preview' -} +}; -export const CtaBtn = () => ButtonTemplate('default','Leave waitlist'); +export const CtaBtn = () => ButtonTemplate('default', 'Leave waitlist'); CtaBtn.parameters = { docs: { source: { code: ButtonTemplate('default', 'Leave waitlist') } } -} +}; -export const CtaBtnUnavailable = () => ButtonTemplate('unavailable','Join waitlist'); +export const CtaBtnUnavailable = () => ButtonTemplate('unavailable', 'Join waitlist'); CtaBtnUnavailable.parameters = { docs: { source: { code: ButtonTemplate('unavailable', 'Join waitlist') } } -} +}; -export const CtaBtnAvailable = () => ButtonTemplate('available','Borrow'); +export const CtaBtnAvailable = () => ButtonTemplate('available', 'Borrow'); CtaBtnAvailable.parameters = { docs: { source: { code: ButtonTemplate('available', 'Borrow') } } -} +}; -export const CtaBtnPreview = () => ButtonTemplate('preview','Preview'); +export const CtaBtnPreview = () => ButtonTemplate('preview', 'Preview'); CtaBtnPreview.parameters = { docs: { source: { code: ButtonTemplate('preview', 'Preview') } } -} +}; export const CtaBtnWithBadge = () => - ButtonTemplate('unavailable','Join waiting list',4); + ButtonTemplate('unavailable', 'Join waiting list', 4); CtaBtnWithBadge.parameters = { docs: { source: { code: ButtonTemplate('unavailable', 'Join waiting list', 4) } } -} +}; export const CtaBtnGroup = () => `<div class="cta-button-group"> <a href="/borrow/ia/sevenhabitsofhi00cove?ref=ol" title="Borrow ebook from Internet Archive" id="borrow_ebook" data-ol-link-track="CTAClick|Borrow" class="cta-btn cta-btn--available">Borrow</a> diff --git a/tests/unit/js/Browser.test.js b/tests/unit/js/Browser.test.js index ac5e84f51ed..eb02c2b2d6f 100644 --- a/tests/unit/js/Browser.test.js +++ b/tests/unit/js/Browser.test.js @@ -22,7 +22,7 @@ describe('removeURLParameter', () => { test('URL with multiple occurences of param', () => { expect(fn('http://foo.com?x=3&x=4&x=5', 'x')).toBe('http://foo.com'); expect(fn('http://foo.com?x=3&x=4&z=5', 'x')).toBe('http://foo.com?z=5'); - }) + }); }); describe('getJsonFromUrl', () => { diff --git a/tests/unit/js/SearchBar.test.js b/tests/unit/js/SearchBar.test.js index e52be87166a..65f6edb9c68 100644 --- a/tests/unit/js/SearchBar.test.js +++ b/tests/unit/js/SearchBar.test.js @@ -78,7 +78,7 @@ describe('SearchBar', () => { }); test('Special inputs are added to the form on submit', () => { - const spy = sinon.spy(SearchUtils, 'addModeInputsToForm') + const spy = sinon.spy(SearchUtils, 'addModeInputsToForm'); sb.submitForm(); expect(spy.callCount).toBe(1); }); diff --git a/tests/unit/js/SelectionManager.test.js b/tests/unit/js/SelectionManager.test.js index 6331ed9d5f4..88b81e73b09 100644 --- a/tests/unit/js/SelectionManager.test.js +++ b/tests/unit/js/SelectionManager.test.js @@ -1,6 +1,6 @@ import SelectionManager from '../../../openlibrary/plugins/openlibrary/js/ile/utils/SelectionManager/SelectionManager.js'; -function createTestElementsForProcessClick() { +function createTestElementsForProcessClick () { const listItem = document.createElement('li'); listItem.classList.add('searchResultItem', 'ile-selectable'); @@ -15,10 +15,10 @@ function createTestElementsForProcessClick() { listItem.appendChild(bookTitle); - return {listItem,link}; + return {listItem, link}; } -function setupSelectionManager() { +function setupSelectionManager () { const sm = new SelectionManager(null, '/search'); sm.ile = { $statusImages: { append: jest.fn() } }; sm.selectedItems = { work: [] }; @@ -55,7 +55,7 @@ describe('SelectionManager', () => { test('processClick - clicking on a link or button', () => { const sm = setupSelectionManager(); - const { listItem,link } = createTestElementsForProcessClick(); + const { listItem, link } = createTestElementsForProcessClick(); link.addEventListener('click', () => { sm.processClick({ target: link, currentTarget: listItem }); diff --git a/tests/unit/js/autocomplete.test.js b/tests/unit/js/autocomplete.test.js index 65774407426..c3621c1df51 100644 --- a/tests/unit/js/autocomplete.test.js +++ b/tests/unit/js/autocomplete.test.js @@ -18,7 +18,7 @@ describe('highlight', () => { const highlightedText = highlight(test[0], test[1]); expect(highlightedText).toStrictEqual(test[2]); }); - }) + }); }); @@ -62,5 +62,5 @@ describe('mapApiResultsToAutocompleteSuggestions', () => { value: 'Add new item' } ); - }) + }); }); diff --git a/tests/unit/js/droppers.test.js b/tests/unit/js/droppers.test.js index 620ef58e70d..1cec462798d 100644 --- a/tests/unit/js/droppers.test.js +++ b/tests/unit/js/droppers.test.js @@ -1,7 +1,7 @@ import sinon from 'sinon'; import { initDroppers, initGenericDroppers } from '../../../openlibrary/plugins/openlibrary/js/dropper'; -import { Dropper } from '../../../openlibrary/plugins/openlibrary/js/dropper/Dropper' -import { legacyBookDropperMarkup, openDropperMarkup, closedDropperMarkup, disabledDropperMarkup } from './sample-html/dropper-test-data' +import { Dropper } from '../../../openlibrary/plugins/openlibrary/js/dropper/Dropper'; +import { legacyBookDropperMarkup, openDropperMarkup, closedDropperMarkup, disabledDropperMarkup } from './sample-html/dropper-test-data'; import * as nonjquery_utils from '../../../openlibrary/plugins/openlibrary/js/nonjquery_utils.js'; @@ -30,276 +30,276 @@ describe('initDroppers', () => { describe('Generic Droppers', () => { test('Clicking dropclick element toggles the dropper', () => { // Setup - document.body.innerHTML = closedDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const dropper = new Dropper(wrapper) - dropper.initialize() + document.body.innerHTML = closedDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + dropper.initialize(); - const dropClick = document.querySelector('.generic-dropper__dropclick') - const arrow = dropClick.querySelector('.arrow') + const dropClick = document.querySelector('.generic-dropper__dropclick'); + const arrow = dropClick.querySelector('.arrow'); // Dropper should be closed at the start - expect(arrow.classList.contains('up')).toBe(false) - expect(wrapper.classList.contains('dropper-wrapper--active')).toBe(false) + expect(arrow.classList.contains('up')).toBe(false); + expect(wrapper.classList.contains('dropper-wrapper--active')).toBe(false); // Open dropper - dropClick.click() - expect(arrow.classList.contains('up')).toBe(true) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true) + dropClick.click(); + expect(arrow.classList.contains('up')).toBe(true); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true); // Close dropper - dropClick.click() - expect(arrow.classList.contains('up')).toBe(false) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - }) + dropClick.click(); + expect(arrow.classList.contains('up')).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false); + }); test('Opened droppers close if they are not the target of a click', () => { // Setup - document.body.innerHTML = openDropperMarkup.concat(openDropperMarkup, openDropperMarkup) - const wrappers = document.querySelectorAll('.generic-dropper-wrapper') - initGenericDroppers(wrappers) + document.body.innerHTML = openDropperMarkup.concat(openDropperMarkup, openDropperMarkup); + const wrappers = document.querySelectorAll('.generic-dropper-wrapper'); + initGenericDroppers(wrappers); // Ensure that all three droppers are open - expect(wrappers.length).toBe(3) + expect(wrappers.length).toBe(3); for (const wrapper of wrappers) { - const arrow = wrapper.querySelector('.arrow') - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true) - expect(arrow.classList.contains('up')).toBe(true) + const arrow = wrapper.querySelector('.arrow'); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true); + expect(arrow.classList.contains('up')).toBe(true); } // After clicking the dropdown content of the first dropper: - const dropdownContent = wrappers[0].querySelector('.generic-dropper__dropdown') - dropdownContent.click() + const dropdownContent = wrappers[0].querySelector('.generic-dropper__dropdown'); + dropdownContent.click(); // First dropper should be open - expect(wrappers[0].classList.contains('generic-dropper-wrapper--active')).toBe(true) - expect(wrappers[0].querySelector('.arrow').classList.contains('up')).toBe(true) + expect(wrappers[0].classList.contains('generic-dropper-wrapper--active')).toBe(true); + expect(wrappers[0].querySelector('.arrow').classList.contains('up')).toBe(true); // ...while other droppers should be closed for (let i = 1; i < wrappers.length; ++i) { - const arrow = wrappers[i].querySelector('.arrow') - expect(wrappers[i].classList.contains('generic-dropper-wrapper--active')).toBe(false) - expect(arrow.classList.contains('up')).toBe(false) + const arrow = wrappers[i].querySelector('.arrow'); + expect(wrappers[i].classList.contains('generic-dropper-wrapper--active')).toBe(false); + expect(arrow.classList.contains('up')).toBe(false); } - }) + }); test('Disabled droppers cannot be opened nor closed', () => { - document.body.innerHTML = disabledDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const dropper = new Dropper(wrapper) - dropper.initialize() - const dropclick = wrapper.querySelector('.generic-dropper__dropclick') - const arrow = wrapper.querySelector('.arrow') + document.body.innerHTML = disabledDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + dropper.initialize(); + const dropclick = wrapper.querySelector('.generic-dropper__dropclick'); + const arrow = wrapper.querySelector('.arrow'); // Sanity checks - expect(wrapper.classList.contains('generic-dropper--disabled')).toBe(true) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - expect(arrow.classList.contains('up')).toBe(false) + expect(wrapper.classList.contains('generic-dropper--disabled')).toBe(true); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false); + expect(arrow.classList.contains('up')).toBe(false); // Click on the dropclick: - dropclick.click() + dropclick.click(); - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - expect(arrow.classList.contains('up')).toBe(false) - }) -}) + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false); + expect(arrow.classList.contains('up')).toBe(false); + }); +}); describe('Dropper.js class', () => { test('Dropper references set correctly on instantiation', () => { - document.body.innerHTML = closedDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const dropper = new Dropper(wrapper) + document.body.innerHTML = closedDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); // Reference to component root stored - expect(dropper.dropper === wrapper).toBe(true) + expect(dropper.dropper === wrapper).toBe(true); // Dropclick reference stored - const dropClick = wrapper.querySelector('.generic-dropper__dropclick') - expect(dropper.dropClick === dropClick).toBe(true) + const dropClick = wrapper.querySelector('.generic-dropper__dropclick'); + expect(dropper.dropClick === dropClick).toBe(true); // Dropper is closed - expect(dropper.isDropperOpen).toBe(false) + expect(dropper.isDropperOpen).toBe(false); // This dropper is not disabled - expect(dropper.isDropperDisabled).toBe(false) - }) + expect(dropper.isDropperDisabled).toBe(false); + }); it('is not functional until initialize() is called', () => { - document.body.innerHTML = closedDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const dropClick = wrapper.querySelector('.generic-dropper__dropclick') - const arrow = wrapper.querySelector('.arrow') + document.body.innerHTML = closedDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropClick = wrapper.querySelector('.generic-dropper__dropclick'); + const arrow = wrapper.querySelector('.arrow'); - const dropper = new Dropper(wrapper) - const spy = jest.spyOn(dropper, 'toggleDropper') + const dropper = new Dropper(wrapper); + const spy = jest.spyOn(dropper, 'toggleDropper'); // Dropper should be closed initially: - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - expect(arrow.classList.contains('up')).toBe(false) + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false); + expect(arrow.classList.contains('up')).toBe(false); // Clicking should not do anything yet: - dropClick.click() - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - expect(arrow.classList.contains('up')).toBe(false) - expect(spy).not.toHaveBeenCalled() + dropClick.click(); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false); + expect(arrow.classList.contains('up')).toBe(false); + expect(spy).not.toHaveBeenCalled(); // Test again after initialization: - dropper.initialize() - dropClick.click() - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true) - expect(arrow.classList.contains('up')).toBe(true) - expect(spy).toHaveBeenCalled() + dropper.initialize(); + dropClick.click(); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true); + expect(arrow.classList.contains('up')).toBe(true); + expect(spy).toHaveBeenCalled(); - jest.restoreAllMocks() - }) + jest.restoreAllMocks(); + }); it('can be closed if not disabled', () => { - document.body.innerHTML = openDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const arrow = wrapper.querySelector('.arrow') + document.body.innerHTML = openDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const arrow = wrapper.querySelector('.arrow'); - const dropper = new Dropper(wrapper) - dropper.initialize() + const dropper = new Dropper(wrapper); + dropper.initialize(); // Check initial state: - expect(dropper.isDropperDisabled).toBe(false) - expect(dropper.isDropperOpen).toBe(true) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true) - expect(arrow.classList.contains('up')).toBe(true) + expect(dropper.isDropperDisabled).toBe(false); + expect(dropper.isDropperOpen).toBe(true); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true); + expect(arrow.classList.contains('up')).toBe(true); // Check again after closing: - dropper.closeDropper() - expect(dropper.isDropperOpen).toBe(false) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - expect(arrow.classList.contains('up')).toBe(false) - }) + dropper.closeDropper(); + expect(dropper.isDropperOpen).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false); + expect(arrow.classList.contains('up')).toBe(false); + }); it('can be toggled if not disabled', () => { - document.body.innerHTML = closedDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const arrow = wrapper.querySelector('.arrow') + document.body.innerHTML = closedDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const arrow = wrapper.querySelector('.arrow'); - const dropper = new Dropper(wrapper) - dropper.initialize() + const dropper = new Dropper(wrapper); + dropper.initialize(); // Check initial state: - expect(dropper.isDropperDisabled).toBe(false) - expect(dropper.isDropperOpen).toBe(false) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - expect(arrow.classList.contains('up')).toBe(false) + expect(dropper.isDropperDisabled).toBe(false); + expect(dropper.isDropperOpen).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false); + expect(arrow.classList.contains('up')).toBe(false); // Check after toggling open: - dropper.toggleDropper() - expect(dropper.isDropperOpen).toBe(true) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true) - expect(arrow.classList.contains('up')).toBe(true) + dropper.toggleDropper(); + expect(dropper.isDropperOpen).toBe(true); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(true); + expect(arrow.classList.contains('up')).toBe(true); // Check after toggling once more: - dropper.toggleDropper() - expect(dropper.isDropperOpen).toBe(false) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - expect(arrow.classList.contains('up')).toBe(false) - }) + dropper.toggleDropper(); + expect(dropper.isDropperOpen).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false); + expect(arrow.classList.contains('up')).toBe(false); + }); it('cannot be opened while disabled', () => { - document.body.innerHTML = disabledDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const dropper = new Dropper(wrapper) - dropper.initialize() - const arrow = wrapper.querySelector('.arrow') + document.body.innerHTML = disabledDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + dropper.initialize(); + const arrow = wrapper.querySelector('.arrow'); // Check initial state: - expect(dropper.isDropperDisabled).toBe(true) - expect(arrow.classList.contains('up')).toBe(false) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) + expect(dropper.isDropperDisabled).toBe(true); + expect(arrow.classList.contains('up')).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false); // Check state after toggling: - dropper.toggleDropper() - expect(arrow.classList.contains('up')).toBe(false) - expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false) - }) + dropper.toggleDropper(); + expect(arrow.classList.contains('up')).toBe(false); + expect(wrapper.classList.contains('generic-dropper-wrapper--active')).toBe(false); + }); describe('Dropper event methods', () => { afterEach(() => { - jest.clearAllMocks() - }) + jest.clearAllMocks(); + }); it('calls `onDisabledClick()` when dropper is clicked while disabled', () => { - document.body.innerHTML = disabledDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const dropper = new Dropper(wrapper) - dropper.initialize() + document.body.innerHTML = disabledDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + dropper.initialize(); - const onDisabledClickFn = jest.spyOn(dropper, 'onDisabledClick') + const onDisabledClickFn = jest.spyOn(dropper, 'onDisabledClick'); // Check initial state: - expect(dropper.isDropperDisabled).toBe(true) - expect(onDisabledClickFn).not.toHaveBeenCalled() + expect(dropper.isDropperDisabled).toBe(true); + expect(onDisabledClickFn).not.toHaveBeenCalled(); // Check state after toggling: - dropper.toggleDropper() - expect(dropper.isDropperDisabled).toBe(true) - expect(onDisabledClickFn).toHaveBeenCalledTimes(1) + dropper.toggleDropper(); + expect(dropper.isDropperDisabled).toBe(true); + expect(onDisabledClickFn).toHaveBeenCalledTimes(1); // Check state after closing: - dropper.closeDropper() - expect(dropper.isDropperDisabled).toBe(true) - expect(onDisabledClickFn).toHaveBeenCalledTimes(2) - }) + dropper.closeDropper(); + expect(dropper.isDropperDisabled).toBe(true); + expect(onDisabledClickFn).toHaveBeenCalledTimes(2); + }); it('calls `onClose()` when active dropper is closed', () => { - document.body.innerHTML = openDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const dropper = new Dropper(wrapper) - dropper.initialize() + document.body.innerHTML = openDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + dropper.initialize(); - const onCloseFn = jest.spyOn(dropper, 'onClose') + const onCloseFn = jest.spyOn(dropper, 'onClose'); // Check initial state: - expect(dropper.isDropperOpen).toBe(true) - expect(onCloseFn).not.toHaveBeenCalled() + expect(dropper.isDropperOpen).toBe(true); + expect(onCloseFn).not.toHaveBeenCalled(); // Check state after closing: - dropper.closeDropper() - expect(dropper.isDropperOpen).toBe(false) - expect(onCloseFn).toHaveBeenCalledTimes(1) + dropper.closeDropper(); + expect(dropper.isDropperOpen).toBe(false); + expect(onCloseFn).toHaveBeenCalledTimes(1); // Check state after toggling open then closed: - dropper.toggleDropper() - expect(dropper.isDropperOpen).toBe(true) - expect(onCloseFn).toHaveBeenCalledTimes(1) // Should not be called when dropper is closed + dropper.toggleDropper(); + expect(dropper.isDropperOpen).toBe(true); + expect(onCloseFn).toHaveBeenCalledTimes(1); // Should not be called when dropper is closed - dropper.toggleDropper() - expect(dropper.isDropperOpen).toBe(false) - expect(onCloseFn).toHaveBeenCalledTimes(2) - }) + dropper.toggleDropper(); + expect(dropper.isDropperOpen).toBe(false); + expect(onCloseFn).toHaveBeenCalledTimes(2); + }); test('toggling dropper results in correct event method being called', () => { - document.body.innerHTML = closedDropperMarkup - const wrapper = document.querySelector('.generic-dropper-wrapper') - const dropper = new Dropper(wrapper) - dropper.initialize() + document.body.innerHTML = closedDropperMarkup; + const wrapper = document.querySelector('.generic-dropper-wrapper'); + const dropper = new Dropper(wrapper); + dropper.initialize(); - const onCloseFn = jest.spyOn(dropper, 'onClose') - const onOpenFn = jest.spyOn(dropper, 'onOpen') + const onCloseFn = jest.spyOn(dropper, 'onClose'); + const onOpenFn = jest.spyOn(dropper, 'onOpen'); // Check initial state: - expect(dropper.isDropperOpen).toBe(false) - expect(onCloseFn).not.toHaveBeenCalled() - expect(onOpenFn).not.toHaveBeenCalled() + expect(dropper.isDropperOpen).toBe(false); + expect(onCloseFn).not.toHaveBeenCalled(); + expect(onOpenFn).not.toHaveBeenCalled(); // Check after toggling open: - dropper.toggleDropper() - expect(dropper.isDropperOpen).toBe(true) - expect(onCloseFn).toHaveBeenCalledTimes(0) - expect(onOpenFn).toHaveBeenCalledTimes(1) + dropper.toggleDropper(); + expect(dropper.isDropperOpen).toBe(true); + expect(onCloseFn).toHaveBeenCalledTimes(0); + expect(onOpenFn).toHaveBeenCalledTimes(1); // Check after toggling closed: - dropper.toggleDropper() - expect(dropper.isDropperOpen).toBe(false) - expect(onCloseFn).toHaveBeenCalledTimes(1) - expect(onOpenFn).toHaveBeenCalledTimes(1) - }) - }) -}) + dropper.toggleDropper(); + expect(dropper.isDropperOpen).toBe(false); + expect(onCloseFn).toHaveBeenCalledTimes(1); + expect(onOpenFn).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/tests/unit/js/editionsEditPage.test.js b/tests/unit/js/editionsEditPage.test.js index d8cf34f61a1..b9ccb9b0e26 100644 --- a/tests/unit/js/editionsEditPage.test.js +++ b/tests/unit/js/editionsEditPage.test.js @@ -36,7 +36,7 @@ beforeEach(() => { $(document.body).html(testData.editionIdentifiersSample); $('#identifiers').repeat({ vars: {prefix: 'edition--'}, - validate: function(data) {return validateIdentifiers(data)}, + validate: function (data) {return validateIdentifiers(data);}, }); }); @@ -84,7 +84,7 @@ describe('initIdentifierValidation', () => { $('#do-not-add-isbn').trigger('click'); expect($('.repeat-item').length).toBe(5); const cssDisplay = $('#id-errors').css('display'); - expect(cssDisplay).toEqual('none') + expect(cssDisplay).toEqual('none'); }); it('does NOT add a duplicate ISBN 10', () => { @@ -102,7 +102,7 @@ describe('initIdentifierValidation', () => { $('#id-value').val('09- 8478---2869 '); $('.repeat-add').trigger('click'); expect($('.repeat-item').length).toBe(6); - }) + }); it('does count identical stripped and unstripped ISBN 10s as the same ISBN', () => { $('#select-id').val('isbn_10'); @@ -149,7 +149,7 @@ describe('initIdentifierValidation', () => { $('#do-not-add-isbn').trigger('click'); expect($('.repeat-item').length).toBe(5); const cssDisplay = $('#id-errors').css('display'); - expect(cssDisplay).toEqual('none') + expect(cssDisplay).toEqual('none'); }); it('does NOT add a duplicate ISBN 13', () => { @@ -167,7 +167,7 @@ describe('initIdentifierValidation', () => { $('#id-value').val('978-16172--95 980 '); $('.repeat-add').trigger('click'); expect($('.repeat-item').length).toBe(6); - }) + }); it('does count identical stripped and unstripped ISBN 13s as the same ISBN', () => { $('#select-id').val('isbn_13'); @@ -209,7 +209,7 @@ describe('initIdentifierValidation', () => { $('#id-value').val(' 75-425165//r75'); $('.repeat-add').trigger('click'); expect($('.repeat-item').length).toBe(6); - }) + }); it('does count identical normalized and non-normalized LCCNs as the same LCCN', () => { $('#select-id').val('lccn'); diff --git a/tests/unit/js/html-test-data.js b/tests/unit/js/html-test-data.js index 3548c53842c..9aec3a671fc 100644 --- a/tests/unit/js/html-test-data.js +++ b/tests/unit/js/html-test-data.js @@ -100,7 +100,7 @@ export const clamperSample = ` <a>orphans</a> <a>fantasy fiction</a> <a>England in fiction</a> - </span>` + </span>`; export const readClassification = ` <fieldset class="major" id="classifications" data-config="{"Please select a classification.": "Please select a classification.", "You need to give a value to CLASS.": "You need to give a value to CLASS."}"> @@ -193,4 +193,4 @@ export const readClassification = ` </div> </div> </fieldset> -` +`; diff --git a/tests/unit/js/idValidation.test.js b/tests/unit/js/idValidation.test.js index 82fa3364745..9819b39b515 100644 --- a/tests/unit/js/idValidation.test.js +++ b/tests/unit/js/idValidation.test.js @@ -15,7 +15,7 @@ describe('parseIsbn', () => { it('correctly parses ISBN 13 with dashes', () => { expect(parseIsbn('978-0-553-38168-9')).toBe('9780553381689'); }); -}) +}); // testing from examples listed here: // https://www.loc.gov/marc/lccn-namespace.html @@ -44,7 +44,7 @@ describe('parseLccn', () => { it('correctly parses LCCN example 8', () => { expect(parseLccn(' 79139101 /AC/r932')).toBe('79139101'); }); -}) +}); describe('isChecksumValidIsbn10', () => { it('returns true with valid ISBN 10 (X check character)', () => { @@ -60,7 +60,7 @@ describe('isChecksumValidIsbn10', () => { it('returns false with an invalid ISBN 10', () => { expect(isChecksumValidIsbn10('1234567890')).toBe(false); }); -}) +}); describe('isChecksumValidIsbn13', () => { it('returns true with valid ISBN 13 (check 1)', () => { @@ -76,7 +76,7 @@ describe('isChecksumValidIsbn13', () => { it('returns false with an invalid ISBN 13 (check 2)', () => { expect(isChecksumValidIsbn13('9790000000000')).toBe(false); }); -}) +}); describe('isFormatValidIsbn10', () => { it('returns true with valid ISBN 10 (X check character)', () => { @@ -92,7 +92,7 @@ describe('isFormatValidIsbn10', () => { it('returns false with blank value', () => { expect(isFormatValidIsbn10('')).toBe(false); }); -}) +}); describe('isFormatValidIsbn13', () => { it('returns true with valid ISBN 13', () => { @@ -108,7 +108,7 @@ describe('isFormatValidIsbn13', () => { it('returns false with invalis ISBN 13 (non-numeric)', () => { expect(isFormatValidIsbn13('979a430918002')).toBe(false); }); -}) +}); // testing from examples listed here: // https://www.loc.gov/marc/lccn-namespace.html @@ -154,4 +154,4 @@ describe('isValidLccn', () => { it('returns false for LCCN of length 13', () => { expect(isValidLccn('1250000000003')).toBe(false); }); -}) +}); diff --git a/tests/unit/js/jquery.repeat.test.js b/tests/unit/js/jquery.repeat.test.js index 94f3d3a86a7..dd7cb9787dd 100644 --- a/tests/unit/js/jquery.repeat.test.js +++ b/tests/unit/js/jquery.repeat.test.js @@ -30,7 +30,7 @@ test('identifiers of repeated elements are never the same.', () => { $('#id-value').text('fo4rzdaHDAwC'); $('.repeat-add').trigger('click'); expect($('.repeat-item').length).toBe(6); - $('#identifiers--3 .repeat-remove').trigger('click') + $('#identifiers--3 .repeat-remove').trigger('click'); expect($('.repeat-item').length).toBe(5); $('#select-id').val('goodreads'); $('#id-value').text('44415839'); diff --git a/tests/unit/js/jsdef.test.js b/tests/unit/js/jsdef.test.js index f1f69ed93ae..54fd843d9f1 100644 --- a/tests/unit/js/jsdef.test.js +++ b/tests/unit/js/jsdef.test.js @@ -27,7 +27,7 @@ test('jsdef: foreach', () => { expect(called).toBe(3); resolve(); } - }) + }); }); }); diff --git a/tests/unit/js/lists.test.js b/tests/unit/js/lists.test.js index 7e76994b28d..fc59f4977af 100644 --- a/tests/unit/js/lists.test.js +++ b/tests/unit/js/lists.test.js @@ -1,165 +1,165 @@ import { createActiveShowcaseItem, ShowcaseItem } from '../../../openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js'; -import { CreateListForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js' -import { listCreationForm, filledListCreationForm, showcaseI18nInput, subjectShowcase, authorShowcase, workShowcase, editionShowcase, activeListShowcase, listsSectionShowcase } from './sample-html/lists-test-data' +import { CreateListForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js'; +import { listCreationForm, filledListCreationForm, showcaseI18nInput, subjectShowcase, authorShowcase, workShowcase, editionShowcase, activeListShowcase, listsSectionShowcase } from './sample-html/lists-test-data'; describe('CreateListForm class tests', () => { test('CreateListForm fields correctly set', () => { - document.body.innerHTML = listCreationForm - const formElem = document.querySelector('form') - const listForm = new CreateListForm(formElem) + document.body.innerHTML = listCreationForm; + const formElem = document.querySelector('form'); + const listForm = new CreateListForm(formElem); - const createListButton = document.querySelector('#create-list-button') - expect(listForm.createListButton === createListButton).toBe(true) + const createListButton = document.querySelector('#create-list-button'); + expect(listForm.createListButton === createListButton).toBe(true); - const listTitleInput = document.querySelector('#list_label') - expect(listForm.listTitleInput === listTitleInput).toBe(true) + const listTitleInput = document.querySelector('#list_label'); + expect(listForm.listTitleInput === listTitleInput).toBe(true); - const listDescriptionInput = document.querySelector('#list_desc') - expect(listForm.listDescriptionInput === listDescriptionInput).toBe(true) - }) + const listDescriptionInput = document.querySelector('#list_desc'); + expect(listForm.listDescriptionInput === listDescriptionInput).toBe(true); + }); test('`resetForm()` clears a filled form', () => { - document.body.innerHTML = listCreationForm - const formElem = document.querySelector('form') - const listForm = new CreateListForm(formElem) + document.body.innerHTML = listCreationForm; + const formElem = document.querySelector('form'); + const listForm = new CreateListForm(formElem); // Initial checks - expect(listForm.listTitleInput.value).not.toBeTruthy() - expect(listForm.listDescriptionInput.value).not.toBeTruthy() + expect(listForm.listTitleInput.value).not.toBeTruthy(); + expect(listForm.listDescriptionInput.value).not.toBeTruthy(); // After setting input values - listForm.listTitleInput.value = 'New List' - listForm.listDescriptionInput.value = 'My new list.' - expect(listForm.listTitleInput.value).toBeTruthy() - expect(listForm.listDescriptionInput.value).toBeTruthy() + listForm.listTitleInput.value = 'New List'; + listForm.listDescriptionInput.value = 'My new list.'; + expect(listForm.listTitleInput.value).toBeTruthy(); + expect(listForm.listDescriptionInput.value).toBeTruthy(); // After clearing the form: - listForm.resetForm() - expect(listForm.listTitleInput.value).not.toBeTruthy() - expect(listForm.listDescriptionInput.value).not.toBeTruthy() - }) + listForm.resetForm(); + expect(listForm.listTitleInput.value).not.toBeTruthy(); + expect(listForm.listDescriptionInput.value).not.toBeTruthy(); + }); it('should have empty inputs after instantiation', () => { - document.body.innerHTML = filledListCreationForm - const formElem = document.querySelector('form') - const titleInput = formElem.querySelector('#list_label') - const descriptionInput = formElem.querySelector('#list_desc') + document.body.innerHTML = filledListCreationForm; + const formElem = document.querySelector('form'); + const titleInput = formElem.querySelector('#list_label'); + const descriptionInput = formElem.querySelector('#list_desc'); // Form is initially filled - expect(titleInput.value).toBeTruthy() - expect(descriptionInput.value).toBeTruthy() + expect(titleInput.value).toBeTruthy(); + expect(descriptionInput.value).toBeTruthy(); // Creating new CreateListForm should clear the form // eslint-disable-next-line no-unused-vars - const listForm = new CreateListForm(formElem) - expect(titleInput.value).not.toBeTruthy() - expect(descriptionInput.value).not.toBeTruthy() - }) -}) + const listForm = new CreateListForm(formElem); + expect(titleInput.value).not.toBeTruthy(); + expect(descriptionInput.value).not.toBeTruthy(); + }); +}); describe('createActiveShowcaseItem() tests', () => { test('createActiveShowcaseItem() results are as expected', () => { - document.body.innerHTML = showcaseI18nInput - const listKey = '/people/openlibrary/lists/OL1L' - const seedKey = '/books/OL3421846M' - const listTitle = 'My First List' - const coverUrl = '/images/icons/avatar_book-sm.png' + document.body.innerHTML = showcaseI18nInput; + const listKey = '/people/openlibrary/lists/OL1L'; + const seedKey = '/books/OL3421846M'; + const listTitle = 'My First List'; + const coverUrl = '/images/icons/avatar_book-sm.png'; - const li = createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl) - const anchors = li.querySelectorAll('a') - const [imageLink, titleLink, removeLink] = anchors - const inputs = li.querySelectorAll('input') - const [titleInput, seedKeyInput, seedTypeInput] = inputs + const li = createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl); + const anchors = li.querySelectorAll('a'); + const [imageLink, titleLink, removeLink] = anchors; + const inputs = li.querySelectorAll('input'); + const [titleInput, seedKeyInput, seedTypeInput] = inputs; // Must have `actionable-item` class - expect(li.classList.contains('actionable-item')).toBe(true) + expect(li.classList.contains('actionable-item')).toBe(true); // List key has been set - expect(removeLink.dataset.listKey === listKey).toBe(true) - expect(imageLink.href.endsWith(listKey)).toBe(true) - expect(titleLink.href.endsWith(listKey)).toBe(true) - expect(removeLink.href.endsWith(listKey)).toBe(true) + expect(removeLink.dataset.listKey === listKey).toBe(true); + expect(imageLink.href.endsWith(listKey)).toBe(true); + expect(titleLink.href.endsWith(listKey)).toBe(true); + expect(removeLink.href.endsWith(listKey)).toBe(true); // Seed key has been set - expect(seedKeyInput.value === seedKey).toBe(true) - expect(seedTypeInput.value === 'edition').toBe(true) + expect(seedKeyInput.value === seedKey).toBe(true); + expect(seedTypeInput.value === 'edition').toBe(true); // List title has been set - expect(titleLink.dataset.listTitle === listTitle).toBe(true) - expect(titleLink.textContent === listTitle).toBe(true) - expect(titleInput.value === listTitle).toBe(true) + expect(titleLink.dataset.listTitle === listTitle).toBe(true); + expect(titleLink.textContent === listTitle).toBe(true); + expect(titleInput.value === listTitle).toBe(true); // Cover URL has been set - expect(imageLink.children[0].src.endsWith(coverUrl)).toBe(true) - }) + expect(imageLink.children[0].src.endsWith(coverUrl)).toBe(true); + }); test('createActiveShowcaseItem() sets the correct seed type', () => { - const listKey = '/people/openlibrary/lists/OL1L' - const listTitle = 'My First List' - const coverUrl = '/images/icons/avatar_book-sm.png' + const listKey = '/people/openlibrary/lists/OL1L'; + const listTitle = 'My First List'; + const coverUrl = '/images/icons/avatar_book-sm.png'; - const editionKey = '/books/OL3421846M' - const workKey = '/works/OL54120W' - const authorKey = '/authors/OL18319A' - const subjectKey = 'quotations' - const bogusKey = '/bogus/OL38475839B' + const editionKey = '/books/OL3421846M'; + const workKey = '/works/OL54120W'; + const authorKey = '/authors/OL18319A'; + const subjectKey = 'quotations'; + const bogusKey = '/bogus/OL38475839B'; - const editionItem = createActiveShowcaseItem(listKey, editionKey, listTitle, coverUrl) - expect(editionItem.querySelector('input[name=seed-type]').value).toBe('edition') + const editionItem = createActiveShowcaseItem(listKey, editionKey, listTitle, coverUrl); + expect(editionItem.querySelector('input[name=seed-type]').value).toBe('edition'); - const workItem = createActiveShowcaseItem(listKey, workKey, listTitle, coverUrl) - expect(workItem.querySelector('input[name=seed-type]').value).toBe('work') + const workItem = createActiveShowcaseItem(listKey, workKey, listTitle, coverUrl); + expect(workItem.querySelector('input[name=seed-type]').value).toBe('work'); - const authorItem = createActiveShowcaseItem(listKey, authorKey, listTitle, coverUrl) - expect(authorItem.querySelector('input[name=seed-type]').value).toBe('author') + const authorItem = createActiveShowcaseItem(listKey, authorKey, listTitle, coverUrl); + expect(authorItem.querySelector('input[name=seed-type]').value).toBe('author'); - const subjectItem = createActiveShowcaseItem(listKey, subjectKey, listTitle, coverUrl) - expect(subjectItem.querySelector('input[name=seed-type]').value).toBe('subject') + const subjectItem = createActiveShowcaseItem(listKey, subjectKey, listTitle, coverUrl); + expect(subjectItem.querySelector('input[name=seed-type]').value).toBe('subject'); - const bogusItem = createActiveShowcaseItem(listKey, bogusKey, listTitle, coverUrl) - expect(bogusItem.querySelector('input[name=seed-type]').value).toBe('undefined') - }) + const bogusItem = createActiveShowcaseItem(listKey, bogusKey, listTitle, coverUrl); + expect(bogusItem.querySelector('input[name=seed-type]').value).toBe('undefined'); + }); it('sets the correct default value for `coverUrl`', () => { - document.body.innerHTML = showcaseI18nInput - const listKey = '/people/openlibrary/lists/OL1L' - const seedKey = '/books/OL3421846M' - const listTitle = 'My First List' + document.body.innerHTML = showcaseI18nInput; + const listKey = '/people/openlibrary/lists/OL1L'; + const seedKey = '/books/OL3421846M'; + const listTitle = 'My First List'; - const li = createActiveShowcaseItem(listKey, seedKey, listTitle) - const coverImage = li.querySelector('img') + const li = createActiveShowcaseItem(listKey, seedKey, listTitle); + const coverImage = li.querySelector('img'); - const expectedCoverUrl = '/images/icons/avatar_book-sm.png' - expect(coverImage.src.endsWith(expectedCoverUrl)).toBe(true) - }) -}) + const expectedCoverUrl = '/images/icons/avatar_book-sm.png'; + expect(coverImage.src.endsWith(expectedCoverUrl)).toBe(true); + }); +}); describe('ShowcaseItem class tests', () => { test('ShowcaseItem fields correctly set', () => { - document.body.innerHTML = activeListShowcase - const showcaseElem = document.querySelector('.actionable-item') - const showcase = new ShowcaseItem(showcaseElem) - const removeAffordance = showcaseElem.querySelector('.remove-from-list') - - expect(showcase.showcaseElem === showcaseElem).toBe(true) - expect(showcase.isActiveShowcase).toBe(true) - expect(showcase.removeFromListAffordance === removeAffordance).toBe(true) - expect(showcase.listKey === '/people/openlibrary/lists/OL1L').toBe(true) - expect(showcase.seedKey === '/works/OL54120W').toBe(true) - expect(showcase.type).toBe('work') - expect(showcase.seed).toMatchObject({key: '/works/OL54120W'}) - }) + document.body.innerHTML = activeListShowcase; + const showcaseElem = document.querySelector('.actionable-item'); + const showcase = new ShowcaseItem(showcaseElem); + const removeAffordance = showcaseElem.querySelector('.remove-from-list'); + + expect(showcase.showcaseElem === showcaseElem).toBe(true); + expect(showcase.isActiveShowcase).toBe(true); + expect(showcase.removeFromListAffordance === removeAffordance).toBe(true); + expect(showcase.listKey === '/people/openlibrary/lists/OL1L').toBe(true); + expect(showcase.seedKey === '/works/OL54120W').toBe(true); + expect(showcase.type).toBe('work'); + expect(showcase.seed).toMatchObject({key: '/works/OL54120W'}); + }); it('correctly infers if it is an active showcase', () => { - document.body.innerHTML = activeListShowcase + listsSectionShowcase - const [activeShowcaseElem, otherShowcaseElem] = document.querySelectorAll('.actionable-item') - const activeShowcase = new ShowcaseItem(activeShowcaseElem) - const otherShowcase = new ShowcaseItem(otherShowcaseElem) + document.body.innerHTML = activeListShowcase + listsSectionShowcase; + const [activeShowcaseElem, otherShowcaseElem] = document.querySelectorAll('.actionable-item'); + const activeShowcase = new ShowcaseItem(activeShowcaseElem); + const otherShowcase = new ShowcaseItem(otherShowcaseElem); - expect(activeShowcase.isActiveShowcase).toBe(true) - expect(otherShowcase.isActiveShowcase).toBe(false) - }) + expect(activeShowcase.isActiveShowcase).toBe(true); + expect(otherShowcase.isActiveShowcase).toBe(false); + }); describe('Seed type inference', () => { const cases = [ @@ -167,29 +167,29 @@ describe('ShowcaseItem class tests', () => { {markup: authorShowcase, expectedType: 'author', expectedIsWorkValue: false, expectedIsSubjectValue: false}, {markup: workShowcase, expectedType: 'work', expectedIsWorkValue: true, expectedIsSubjectValue: false}, {markup: editionShowcase, expectedType: 'edition', expectedIsWorkValue: false, expectedIsSubjectValue: false} - ] + ]; test.each(cases)('Type is $expectedType', ({markup, expectedType}) => { - document.body.innerHTML = markup - const showcaseElem = document.querySelector('.actionable-item') - const showcase = new ShowcaseItem(showcaseElem) - expect(showcase.type).toBe(expectedType) - }) + document.body.innerHTML = markup; + const showcaseElem = document.querySelector('.actionable-item'); + const showcase = new ShowcaseItem(showcaseElem); + expect(showcase.type).toBe(expectedType); + }); test.each(cases)('`isWork` value expected to be $expectedIsWorkValue', ({markup, expectedIsWorkValue}) => { - document.body.innerHTML = markup - const showcaseElem = document.querySelector('.actionable-item') - const showcase = new ShowcaseItem(showcaseElem) - expect(showcase.isWork).toBe(expectedIsWorkValue) - }) + document.body.innerHTML = markup; + const showcaseElem = document.querySelector('.actionable-item'); + const showcase = new ShowcaseItem(showcaseElem); + expect(showcase.isWork).toBe(expectedIsWorkValue); + }); test.each(cases)('`isSubject` value expected to be $expectedIsSubjectValue', ({markup, expectedIsSubjectValue}) => { - document.body.innerHTML = markup - const showcaseElem = document.querySelector('.actionable-item') - const showcase = new ShowcaseItem(showcaseElem) - expect(showcase.isSubject).toBe(expectedIsSubjectValue) - }) - }) + document.body.innerHTML = markup; + const showcaseElem = document.querySelector('.actionable-item'); + const showcase = new ShowcaseItem(showcaseElem); + expect(showcase.isSubject).toBe(expectedIsSubjectValue); + }); + }); // XXX : test : removeSelf() fails safely when myBooksStore has not been created? -}) +}); diff --git a/tests/unit/js/my-books.test.js b/tests/unit/js/my-books.test.js index 037d0e9ffb1..4130f33c511 100644 --- a/tests/unit/js/my-books.test.js +++ b/tests/unit/js/my-books.test.js @@ -1,151 +1,151 @@ -import { listCreationForm } from './sample-html/lists-test-data' -import { checkInForm } from './sample-html/checkIns-test-data' -import { CreateListForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/CreateListForm' -import { CheckInForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents' +import { listCreationForm } from './sample-html/lists-test-data'; +import { checkInForm } from './sample-html/checkIns-test-data'; +import { CreateListForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/CreateListForm'; +import { CheckInForm } from '../../../openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents'; jest.mock('jquery-ui/ui/widgets/dialog', () => {}); describe('CreateListForm.js class', () => { - let form - let formElem + let form; + let formElem; beforeEach(() => { - document.body.innerHTML = listCreationForm - formElem = document.querySelector('form') - form = new CreateListForm(formElem) - }) + document.body.innerHTML = listCreationForm; + formElem = document.querySelector('form'); + form = new CreateListForm(formElem); + }); test('References are set correctly', () => { - const createListButton = formElem.querySelector('#create-list-button') - const nameInput = formElem.querySelector('#list_label') - const descriptionInput = formElem.querySelector('#list_desc') + const createListButton = formElem.querySelector('#create-list-button'); + const nameInput = formElem.querySelector('#list_label'); + const descriptionInput = formElem.querySelector('#list_desc'); - expect(createListButton === form.createListButton).toBe(true) - expect(nameInput === form.listTitleInput).toBe(true) - expect(descriptionInput === form.listDescriptionInput).toBe(true) - }) + expect(createListButton === form.createListButton).toBe(true); + expect(nameInput === form.listTitleInput).toBe(true); + expect(descriptionInput === form.listDescriptionInput).toBe(true); + }); it('it clears the form after a resetForm() call', () => { - const nameInput = document.querySelector('#list_label') - const descriptionInput = document.querySelector('#list_desc') + const nameInput = document.querySelector('#list_label'); + const descriptionInput = document.querySelector('#list_desc'); // Form should be empty initially - expect(nameInput.value.length).toBe(0) - expect(descriptionInput.value.length).toBe(0) + expect(nameInput.value.length).toBe(0); + expect(descriptionInput.value.length).toBe(0); // Add values to each input - nameInput.value = 'My New List' - descriptionInput.value = 'The best list ever' - expect(nameInput.value.length).toBeGreaterThan(0) - expect(descriptionInput.value.length).toBeGreaterThan(0) + nameInput.value = 'My New List'; + descriptionInput.value = 'The best list ever'; + expect(nameInput.value.length).toBeGreaterThan(0); + expect(descriptionInput.value.length).toBeGreaterThan(0); // After clearing the form - form.resetForm() - expect(nameInput.value.length).toBe(0) - expect(descriptionInput.value.length).toBe(0) - }) -}) + form.resetForm(); + expect(nameInput.value.length).toBe(0); + expect(descriptionInput.value.length).toBe(0); + }); +}); describe('CheckInForm class', () => { - let formElem = undefined - let submitButton = undefined - let yearSelect = undefined - let monthSelect = undefined - let daySelect = undefined + let formElem = undefined; + let submitButton = undefined; + let yearSelect = undefined; + let monthSelect = undefined; + let daySelect = undefined; - const workOlid = 'OL123W' - const editionKey = '/books/OL456M' + const workOlid = 'OL123W'; + const editionKey = '/books/OL456M'; beforeEach(() => { - document.body.innerHTML = checkInForm - formElem = document.querySelector('form') - submitButton = document.querySelector('.check-in__submit-btn') - yearSelect = document.querySelector('select[name=year]') - monthSelect = document.querySelector('select[name=month]') - daySelect = document.querySelector('select[name=day]') - }) + document.body.innerHTML = checkInForm; + formElem = document.querySelector('form'); + submitButton = document.querySelector('.check-in__submit-btn'); + yearSelect = document.querySelector('select[name=year]'); + monthSelect = document.querySelector('select[name=month]'); + daySelect = document.querySelector('select[name=day]'); + }); test('Submit button, month select, and day select are initially disabled when read date is absent', () => { - const form = new CheckInForm(formElem, workOlid, editionKey) - form.initialize() - expect(submitButton.disabled).toBe(true) - expect(monthSelect.disabled).toBe(true) - expect(daySelect.disabled).toBe(true) + const form = new CheckInForm(formElem, workOlid, editionKey); + form.initialize(); + expect(submitButton.disabled).toBe(true); + expect(monthSelect.disabled).toBe(true); + expect(daySelect.disabled).toBe(true); - expect(yearSelect.disabled).toBe(false) - expect(yearSelect.value).toBe('') - }) + expect(yearSelect.disabled).toBe(false); + expect(yearSelect.value).toBe(''); + }); it('Sets correct values and enables selects and submit button', () => { - const form = new CheckInForm(formElem, workOlid, editionKey) - form.initialize() - form.updateSelectedDate(2022, 1, 31) - expect(submitButton.disabled).toBe(false) - expect(monthSelect.disabled).toBe(false) - expect(daySelect.disabled).toBe(false) - - expect(yearSelect.value).toBe('2022') - expect(monthSelect.value).toBe('1') - expect(daySelect.value).toBe('31') - }) + const form = new CheckInForm(formElem, workOlid, editionKey); + form.initialize(); + form.updateSelectedDate(2022, 1, 31); + expect(submitButton.disabled).toBe(false); + expect(monthSelect.disabled).toBe(false); + expect(daySelect.disabled).toBe(false); + + expect(yearSelect.value).toBe('2022'); + expect(monthSelect.value).toBe('1'); + expect(daySelect.value).toBe('31'); + }); it('Hides impossible day options', () => { - const form = new CheckInForm(formElem, workOlid, editionKey) - form.initialize() - form.updateSelectedDate(2022, 2, 20) + const form = new CheckInForm(formElem, workOlid, editionKey); + form.initialize(); + form.updateSelectedDate(2022, 2, 20); // The 28th day should be visible: - expect(daySelect.options[28].classList.contains('hidden')).toBe(false) + expect(daySelect.options[28].classList.contains('hidden')).toBe(false); // Subsequent days should not be visible - expect(daySelect.options[29].classList.contains('hidden')).toBe(true) - expect(daySelect.options[30].classList.contains('hidden')).toBe(true) - expect(daySelect.options[31].classList.contains('hidden')).toBe(true) - }) + expect(daySelect.options[29].classList.contains('hidden')).toBe(true); + expect(daySelect.options[30].classList.contains('hidden')).toBe(true); + expect(daySelect.options[31].classList.contains('hidden')).toBe(true); + }); it('Shows 29 days in February when there is a leap year', () => { - const form = new CheckInForm(formElem, workOlid, editionKey) - form.initialize() - form.updateSelectedDate(2020, 2, 1) + const form = new CheckInForm(formElem, workOlid, editionKey); + form.initialize(); + form.updateSelectedDate(2020, 2, 1); - expect(daySelect.options[29].classList.contains('hidden')).toBe(false) - expect(daySelect.options[30].classList.contains('hidden')).toBe(true) - expect(daySelect.options[31].classList.contains('hidden')).toBe(true) - }) + expect(daySelect.options[29].classList.contains('hidden')).toBe(false); + expect(daySelect.options[30].classList.contains('hidden')).toBe(true); + expect(daySelect.options[31].classList.contains('hidden')).toBe(true); + }); it('Associates labels with select elements during initialization', () => { - const form = new CheckInForm(formElem, workOlid, editionKey) + const form = new CheckInForm(formElem, workOlid, editionKey); // Get reference to each label: - const yearLabel = formElem.querySelector('.check-in__year-label') - const monthLabel = formElem.querySelector('.check-in__month-label') - const dayLabel = formElem.querySelector('.check-in__day-label') + const yearLabel = formElem.querySelector('.check-in__year-label'); + const monthLabel = formElem.querySelector('.check-in__month-label'); + const dayLabel = formElem.querySelector('.check-in__day-label'); // Verify labels have no `for` initially: - expect(yearLabel.htmlFor).toBe('') - expect(monthLabel.htmlFor).toBe('') - expect(dayLabel.htmlFor).toBe('') + expect(yearLabel.htmlFor).toBe(''); + expect(monthLabel.htmlFor).toBe(''); + expect(dayLabel.htmlFor).toBe(''); // Verify select elements have no `id` initially: - expect(yearSelect.id).toBe('') - expect(monthSelect.id).toBe('') - expect(daySelect.id).toBe('') + expect(yearSelect.id).toBe(''); + expect(monthSelect.id).toBe(''); + expect(daySelect.id).toBe(''); // Verify labels associated with selects after initialization: - form.initialize() + form.initialize(); - const expectedYearId = `year-select-${workOlid}` - const expectedMonthId = `month-select-${workOlid}` - const expectedDayId = `day-select-${workOlid}` + const expectedYearId = `year-select-${workOlid}`; + const expectedMonthId = `month-select-${workOlid}`; + const expectedDayId = `day-select-${workOlid}`; - expect(yearLabel.htmlFor).toBe(expectedYearId) - expect(monthLabel.htmlFor).toBe(expectedMonthId) - expect(dayLabel.htmlFor).toBe(expectedDayId) + expect(yearLabel.htmlFor).toBe(expectedYearId); + expect(monthLabel.htmlFor).toBe(expectedMonthId); + expect(dayLabel.htmlFor).toBe(expectedDayId); - expect(yearSelect.id).toBe(expectedYearId) - expect(monthSelect.id).toBe(expectedMonthId) - expect(daySelect.id).toBe(expectedDayId) - }) -}) + expect(yearSelect.id).toBe(expectedYearId); + expect(monthSelect.id).toBe(expectedMonthId); + expect(daySelect.id).toBe(expectedDayId); + }); +}); diff --git a/tests/unit/js/python.test.js b/tests/unit/js/python.test.js index f645cb0d064..3ccb2c308a5 100644 --- a/tests/unit/js/python.test.js +++ b/tests/unit/js/python.test.js @@ -4,7 +4,7 @@ test('commify', () => { expect(commify('5443232')).toBe('5,443,232'); expect(commify('50')).toBe('50'); expect(commify('5000')).toBe('5,000'); - expect(commify(['1','2','3','45'])).toBe('1,2,3,45'); + expect(commify(['1', '2', '3', '45'])).toBe('1,2,3,45'); expect(commify([1, 20, 3])).toBe('1,20,3'); }); diff --git a/tests/unit/js/sample-html/checkIns-test-data.js b/tests/unit/js/sample-html/checkIns-test-data.js index f2ab53499a1..a653c1b4db1 100644 --- a/tests/unit/js/sample-html/checkIns-test-data.js +++ b/tests/unit/js/sample-html/checkIns-test-data.js @@ -83,4 +83,4 @@ export const checkInForm = ` </span> </form> </div> -` +`; diff --git a/tests/unit/js/sample-html/dropper-test-data.js b/tests/unit/js/sample-html/dropper-test-data.js index 584c186269e..080dc8dbefa 100644 --- a/tests/unit/js/sample-html/dropper-test-data.js +++ b/tests/unit/js/sample-html/dropper-test-data.js @@ -5,25 +5,25 @@ export const legacyBookDropperMarkup = ` <div class="arrow arrow-unactivated"></div> </a> </div> -` +`; -export const openDropperMarkup = generateDropperMarkup(true) +export const openDropperMarkup = generateDropperMarkup(true); -export const closedDropperMarkup = generateDropperMarkup(false) +export const closedDropperMarkup = generateDropperMarkup(false); -export const disabledDropperMarkup = generateDropperMarkup(false, true) +export const disabledDropperMarkup = generateDropperMarkup(false, true); -function generateDropperMarkup(isDropperOpen, isDropperDisabled = false) { - let wrapperClasses = 'generic-dropper-wrapper' - let arrowClasses = 'arrow' +function generateDropperMarkup (isDropperOpen, isDropperDisabled = false) { + let wrapperClasses = 'generic-dropper-wrapper'; + let arrowClasses = 'arrow'; if (isDropperOpen) { - wrapperClasses += ' generic-dropper-wrapper--active' - arrowClasses += ' up' + wrapperClasses += ' generic-dropper-wrapper--active'; + arrowClasses += ' up'; } if (isDropperDisabled) { - wrapperClasses += ' generic-dropper--disabled' + wrapperClasses += ' generic-dropper--disabled'; } return ` @@ -42,5 +42,5 @@ function generateDropperMarkup(isDropperOpen, isDropperDisabled = false) { </div> </div> </div> - ` + `; } diff --git a/tests/unit/js/sample-html/lists-test-data.js b/tests/unit/js/sample-html/lists-test-data.js index d27076f3627..43df855010b 100644 --- a/tests/unit/js/sample-html/lists-test-data.js +++ b/tests/unit/js/sample-html/lists-test-data.js @@ -1,6 +1,6 @@ -function createListFormMarkup(isFilled) { - const listName = isFilled ? 'My New List' : '' - const listDescription = isFilled ? 'A list for all of my books' : '' +function createListFormMarkup (isFilled) { + const listName = isFilled ? 'My New List' : ''; + const listDescription = isFilled ? 'A list for all of my books' : ''; return ` <form method="post" class="floatform" name="new-list" id="new-list"> @@ -28,15 +28,15 @@ function createListFormMarkup(isFilled) { </div> </div> </form> - ` + `; } -export const listCreationForm = createListFormMarkup(false) -export const filledListCreationForm = createListFormMarkup(true) +export const listCreationForm = createListFormMarkup(false); +export const filledListCreationForm = createListFormMarkup(true); -export const showcaseI18nInput = '<input type="hidden" name="list-i18n-strings" value="{"cover_of": "Cover of: ", "see_this_list": "See this list", "remove_from_list": "Remove from your list?", "from": "from", "you": "You"}"></input>' +export const showcaseI18nInput = '<input type="hidden" name="list-i18n-strings" value="{"cover_of": "Cover of: ", "see_this_list": "See this list", "remove_from_list": "Remove from your list?", "from": "from", "you": "You"}"></input>'; -const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png' +const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; /** * @typedef {Object} ShowcaseDetails @@ -51,11 +51,11 @@ const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png' * @param {boolean} isActiveShowcase * @param {Array<ShowcaseDetails>} showcaseData */ -function createShowcaseMarkup(isActiveShowcase, showcaseData) { - const listId = isActiveShowcase ? 'already-lists' : 'list-lists' - const listClasses = 'listLists'.concat(isActiveShowcase ? ' already-lists' : '') +function createShowcaseMarkup (isActiveShowcase, showcaseData) { + const listId = isActiveShowcase ? 'already-lists' : 'list-lists'; + const listClasses = 'listLists'.concat(isActiveShowcase ? ' already-lists' : ''); - let showcaseMarkup = '' + let showcaseMarkup = ''; for (const data of showcaseData) { showcaseMarkup += `<li class="actionable-item"> @@ -74,13 +74,13 @@ function createShowcaseMarkup(isActiveShowcase, showcaseData) { <span class="owner">from <a href="${data.listOwner}">You</a></span> </span> </li> - ` + `; } return `<ul id="${listId}" class="${listClasses}"> ${showcaseMarkup} </ul> - ` + `; } export const showcaseDetailsData = [ @@ -119,12 +119,12 @@ export const showcaseDetailsData = [ listOwner: '/people/openlibrary', seedType: 'subject' }, -] +]; -export const multipleShowcasesOnPage = createShowcaseMarkup(true, [showcaseDetailsData[0], showcaseDetailsData[2]]) + createShowcaseMarkup(false, [showcaseDetailsData[0], showcaseDetailsData[1]]) -export const activeListShowcase = createShowcaseMarkup(true, [showcaseDetailsData[0]]) -export const listsSectionShowcase = createShowcaseMarkup(false, [showcaseDetailsData[0]]) -export const subjectShowcase = createShowcaseMarkup(false, [showcaseDetailsData[4]]) -export const authorShowcase = createShowcaseMarkup(false, [showcaseDetailsData[3]]) -export const workShowcase = createShowcaseMarkup(false, [showcaseDetailsData[0]]) -export const editionShowcase = createShowcaseMarkup(false, [showcaseDetailsData[1]]) +export const multipleShowcasesOnPage = createShowcaseMarkup(true, [showcaseDetailsData[0], showcaseDetailsData[2]]) + createShowcaseMarkup(false, [showcaseDetailsData[0], showcaseDetailsData[1]]); +export const activeListShowcase = createShowcaseMarkup(true, [showcaseDetailsData[0]]); +export const listsSectionShowcase = createShowcaseMarkup(false, [showcaseDetailsData[0]]); +export const subjectShowcase = createShowcaseMarkup(false, [showcaseDetailsData[4]]); +export const authorShowcase = createShowcaseMarkup(false, [showcaseDetailsData[3]]); +export const workShowcase = createShowcaseMarkup(false, [showcaseDetailsData[0]]); +export const editionShowcase = createShowcaseMarkup(false, [showcaseDetailsData[1]]); diff --git a/tests/unit/js/sample-html/utils-test-data.js b/tests/unit/js/sample-html/utils-test-data.js index bc49363c86f..8426b2551e0 100644 --- a/tests/unit/js/sample-html/utils-test-data.js +++ b/tests/unit/js/sample-html/utils-test-data.js @@ -1,17 +1,17 @@ // removeChildren() test data: // Single element, no children -export const childlessElem = '<div class="remove-tests"></div>' +export const childlessElem = '<div class="remove-tests"></div>'; // Single element, multiple children export const multiChildElem = `<div class="remove-tests"> <div>Child one</div> <div>Child two</div> -</div>` +</div>`; // Single element, child with children export const elemWithDescendants = `<div class="remove-tests"> <div> <div>Ancestor</div> </div> -</div>` +</div>`; diff --git a/tests/unit/js/search.test.js b/tests/unit/js/search.test.js index b06dc134641..2f05cbb99e5 100644 --- a/tests/unit/js/search.test.js +++ b/tests/unit/js/search.test.js @@ -8,14 +8,14 @@ import { more, less } from '../../../openlibrary/plugins/openlibrary/js/search.j * @param {Number} minVisibleFacet minimum number of visible facet * @return {String} HTML search facets section */ -function createSearchFacets(totalFacet = 2, visibleFacet = 2, minVisibleFacet = 2) { +function createSearchFacets (totalFacet = 2, visibleFacet = 2, minVisibleFacet = 2) { const divSearchFacets = document.createElement('DIV'); divSearchFacets.setAttribute('id', 'searchFacets'); divSearchFacets.innerHTML = ` <div class="facet test"> <h4 class="facetHead">Facet Label</h4> </div> - ` + `; const divTestFacet = divSearchFacets.querySelector('div.test'); for (let i = 0; i < totalFacet; i++) { @@ -59,7 +59,7 @@ function createSearchFacets(totalFacet = 2, visibleFacet = 2, minVisibleFacet = * @param {Number} totalFacet total number of facet * @param {Number} expectedVisibleFacet expected number of visible facet */ -function checkFacetVisibility(totalFacet, expectedVisibleFacet) { +function checkFacetVisibility (totalFacet, expectedVisibleFacet) { const facetEntryList = document.getElementsByClassName('facetEntry'); test('facetEntry element number', () => { @@ -85,7 +85,7 @@ function checkFacetVisibility(totalFacet, expectedVisibleFacet) { * @param {Number} minVisibleFacet minimum visible facet number * @param {Number} expectedVisibleFacet expected number of visible facet */ -function checkFacetMoreLessVisibility(totalFacet, minVisibleFacet, expectedVisibleFacet) { +function checkFacetMoreLessVisibility (totalFacet, minVisibleFacet, expectedVisibleFacet) { if (expectedVisibleFacet <= minVisibleFacet) { test('element "test_more"', () => { expect(document.getElementById('test_more').style.display).not.toBe('none'); @@ -122,7 +122,7 @@ function checkFacetMoreLessVisibility(totalFacet, minVisibleFacet, expectedVisib const _originalGetClientRects = window.Element.prototype.getClientRects; // Stubbed getClientRects to enable jQuery ':hidden' selector used by 'more' and 'less' functions -const _stubbedGetClientRects = function() { +const _stubbedGetClientRects = function () { let node = this; while (node) { if (node === document) { diff --git a/tests/unit/js/service-worker-matchers.test.js b/tests/unit/js/service-worker-matchers.test.js index 93765655ce3..f1da4ddb08c 100644 --- a/tests/unit/js/service-worker-matchers.test.js +++ b/tests/unit/js/service-worker-matchers.test.js @@ -2,8 +2,8 @@ import { matchMiscFiles, matchSmallMediumCovers, matchLargeCovers, matchStaticIm // Helper function to create a URL object -function _u(url) { - return { url: new URL(url) } +function _u (url) { + return { url: new URL(url) }; } // Group related tests together describe('URL Matchers', () => { diff --git a/tests/unit/js/signup.test.js b/tests/unit/js/signup.test.js index de61b41b07e..941f90ca377 100644 --- a/tests/unit/js/signup.test.js +++ b/tests/unit/js/signup.test.js @@ -26,7 +26,7 @@ beforeEach(() => { }); describe('Email tests', () => { - let emailLabel, emailField + let emailLabel, emailField; beforeEach(() => { // call the function @@ -35,7 +35,7 @@ describe('Email tests', () => { //declare the elements emailLabel = document.querySelector('label[for="emailAddr"]'); emailField = document.getElementById('emailAddr'); - }) + }); test('validateEmail should update elements correctly on success', () => { // set the email value @@ -99,7 +99,7 @@ describe('Email tests', () => { }); describe('Username tests', () => { - let usernameLabel, usernameField + let usernameLabel, usernameField; beforeEach(() => { // call the function @@ -108,7 +108,7 @@ describe('Username tests', () => { //declare the elements usernameLabel = document.querySelector('label[for="username"]'); usernameField = document.getElementById('username'); - }) + }); test('validateUsername should update elements correctly on success', () => { // set the username value @@ -161,7 +161,7 @@ describe('Username tests', () => { describe('Password tests', () => { - let passwordLabel, passwordField + let passwordLabel, passwordField; beforeEach(() => { // call the function @@ -170,7 +170,7 @@ describe('Password tests', () => { //declare the elements passwordLabel = document.querySelector('label[for="password"]'); passwordField = document.getElementById('password'); - }) + }); test('validatePassword should update elements correctly on success', () => { // set the password value @@ -228,16 +228,16 @@ describe('Print disability tests', () => { initSignupForm(); checkbox = document.querySelector('#pd-request'); - selector = document.querySelector('#pda-selector') - }) + selector = document.querySelector('#pda-selector'); + }); test('Qualifying authority selector only visible when PD checkbox is checked', () => { - checkbox.checked = false + checkbox.checked = false; checkbox.dispatchEvent(new Event('change', { bubbles: true })); expect(selector.classList.contains('hidden')).toBe(true); - checkbox.checked = true + checkbox.checked = true; checkbox.dispatchEvent(new Event('change', { bubbles: true })); expect(selector.classList.contains('hidden')).toBe(false); - }) -}) + }); +}); diff --git a/tests/unit/js/utils.test.js b/tests/unit/js/utils.test.js index b9205bfdfd4..f3a7204fe7d 100644 --- a/tests/unit/js/utils.test.js +++ b/tests/unit/js/utils.test.js @@ -3,63 +3,63 @@ import { removeChildren } from '../../../openlibrary/plugins/openlibrary/js/util describe('`removeChildren()` tests', () => { it('changes nothing if element has no children', () => { - document.body.innerHTML = childlessElem - const elem = document.querySelector('.remove-tests') - const clonedElem = elem.cloneNode(true) + document.body.innerHTML = childlessElem; + const elem = document.querySelector('.remove-tests'); + const clonedElem = elem.cloneNode(true); // Initial checks - expect(elem.childElementCount).toBe(0) - expect(elem.isEqualNode(clonedElem)).toBe(true) + expect(elem.childElementCount).toBe(0); + expect(elem.isEqualNode(clonedElem)).toBe(true); // Element should be unchanged after function call - removeChildren(elem) - expect(elem.childElementCount).toBe(0) - expect(elem.isEqualNode(clonedElem)).toBe(true) - }) + removeChildren(elem); + expect(elem.childElementCount).toBe(0); + expect(elem.isEqualNode(clonedElem)).toBe(true); + }); it('removes all of an element\'s children', () => { - document.body.innerHTML = multiChildElem - const elem = document.querySelector('.remove-tests') - const clonedElem = elem.cloneNode(true) + document.body.innerHTML = multiChildElem; + const elem = document.querySelector('.remove-tests'); + const clonedElem = elem.cloneNode(true); // Initial checks - expect(elem.childElementCount).toBe(2) - expect(elem.isEqualNode(clonedElem)).toBe(true) + expect(elem.childElementCount).toBe(2); + expect(elem.isEqualNode(clonedElem)).toBe(true); // After removing children - removeChildren(elem) - expect(elem.childElementCount).toBe(0) - expect(elem.isEqualNode(clonedElem)).toBe(false) - }) + removeChildren(elem); + expect(elem.childElementCount).toBe(0); + expect(elem.isEqualNode(clonedElem)).toBe(false); + }); it('removes children if they have children of their own', () => { - document.body.innerHTML = elemWithDescendants - const elem = document.querySelector('.remove-tests') - const clonedElem = elem.cloneNode(true) + document.body.innerHTML = elemWithDescendants; + const elem = document.querySelector('.remove-tests'); + const clonedElem = elem.cloneNode(true); // Inital checks - expect(elem.childElementCount).toBe(1) - expect(elem.children[0].childElementCount).toBe(1) - expect(elem.isEqualNode(clonedElem)).toBe(true) + expect(elem.childElementCount).toBe(1); + expect(elem.children[0].childElementCount).toBe(1); + expect(elem.isEqualNode(clonedElem)).toBe(true); // After removing children - removeChildren(elem) - expect(elem.childElementCount).toBe(0) - expect(elem.isEqualNode(clonedElem)).toBe(false) - }) + removeChildren(elem); + expect(elem.childElementCount).toBe(0); + expect(elem.isEqualNode(clonedElem)).toBe(false); + }); it('handles multiple parameters correctly', () => { - document.body.innerHTML = elemWithDescendants + multiChildElem - const elems = document.querySelectorAll('.remove-tests') + document.body.innerHTML = elemWithDescendants + multiChildElem; + const elems = document.querySelectorAll('.remove-tests'); // Initial checks: - expect(elems.length).toBe(2) - expect(elems[0].childElementCount).toBe(1) - expect(elems[1].childElementCount).toBe(2) + expect(elems.length).toBe(2); + expect(elems[0].childElementCount).toBe(1); + expect(elems[1].childElementCount).toBe(2); // After removing children: - removeChildren(...elems) - expect(elems[0].childElementCount).toBe(0) - expect(elems[1].childElementCount).toBe(0) - }) -}) + removeChildren(...elems); + expect(elems[0].childElementCount).toBe(0); + expect(elems[1].childElementCount).toBe(0); + }); +}); From 2450eef18f08d0f1399e2dddff55b014dc6e9e9c Mon Sep 17 00:00:00 2001 From: RayBB <RayBB@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:17:55 -0700 Subject: [PATCH 14/15] space-before-function-paren to never --- eslint.config.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.cjs b/eslint.config.cjs index 351315a26a2..92bdc286c07 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -138,7 +138,7 @@ module.exports = [ // GLOBALLY ENFORCED FORMATTING RULES "semi": ["error", "always"], - "space-before-function-paren": ["error", "always"], + "space-before-function-paren": ["error", "never"], "comma-spacing": ["error", { "before": false, "after": true }], "vue/no-mutating-props": "off", From b73efa4ec4ef9b45441b4b415daf95ef0dcaf5a1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:18:53 +0000 Subject: [PATCH 15/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- openlibrary/components/MergeUI.vue | 16 ++-- .../components/MergeUI/AuthorRoleTable.vue | 2 +- .../components/MergeUI/EditionSnippet.vue | 18 ++-- .../components/MergeUI/ExcerptsTable.vue | 2 +- openlibrary/components/MergeUI/MergeRow.vue | 2 +- .../components/MergeUI/MergeRowField.vue | 2 +- .../components/MergeUI/MergeRowJointField.vue | 2 +- openlibrary/components/MergeUI/TextDiff.vue | 2 +- openlibrary/components/ObservationForm.vue | 14 +-- .../ObservationForm/components/CardBody.vue | 4 +- .../components/CategorySelector.vue | 8 +- .../ObservationForm/components/OLChip.vue | 10 +-- .../ObservationForm/components/SavedTags.vue | 12 +-- .../ObservationForm/components/ValueCard.vue | 2 +- .../plugins/openlibrary/js/book-page-lists.js | 6 +- .../openlibrary/js/breadcrumb_select/index.js | 4 +- .../js/bulk-tagger/BulkTagger/MenuOption.js | 16 ++-- .../BulkTagger/SortedMenuOptionContainer.js | 18 ++-- .../openlibrary/js/bulk-tagger/index.js | 2 +- .../openlibrary/js/bulk-tagger/models/Tag.js | 12 +-- .../plugins/openlibrary/js/clampers.js | 2 +- .../openlibrary/js/compact-title/index.js | 6 +- openlibrary/plugins/openlibrary/js/covers.js | 24 +++--- .../plugins/openlibrary/js/dropper/Dropper.js | 14 +-- .../plugins/openlibrary/js/dropper/index.js | 14 +-- .../js/edition-nav-bar/EditionNavBar.js | 10 +-- .../openlibrary/js/edition-nav-bar/index.js | 4 +- .../openlibrary/js/editions-table/index.js | 16 ++-- .../plugins/openlibrary/js/following.js | 4 +- .../js/fulltext-search-suggestion.js | 8 +- .../plugins/openlibrary/js/go-back-links.js | 2 +- .../plugins/openlibrary/js/graphs/index.js | 8 +- .../plugins/openlibrary/js/graphs/plot.js | 26 +++--- .../openlibrary/js/ia_thirdparty_logins.js | 4 +- .../plugins/openlibrary/js/idValidation.js | 20 ++--- .../plugins/openlibrary/js/ile/utils/ol.js | 4 +- openlibrary/plugins/openlibrary/js/index.js | 20 ++--- .../plugins/openlibrary/js/interstitial.js | 2 +- .../plugins/openlibrary/js/isbnOverride.js | 6 +- .../plugins/openlibrary/js/jquery.repeat.js | 14 +-- openlibrary/plugins/openlibrary/js/jsdef.js | 24 +++--- .../plugins/openlibrary/js/lazy-carousel.js | 10 +-- .../openlibrary/js/lazy-thing-preview.js | 12 +-- .../js/librarian-dashboard/index.js | 18 ++-- .../plugins/openlibrary/js/list_books.js | 8 +- .../openlibrary/js/lists/ListService.js | 8 +- .../openlibrary/js/lists/ListViewBody.js | 24 +++--- .../openlibrary/js/lists/ShowcaseItem.js | 20 ++--- .../merge-request-table/MergeRequestTable.js | 4 +- .../MergeRequestTable/TableHeader.js | 14 +-- .../MergeRequestTable/TableRow.js | 18 ++-- .../js/merge-request-table/index.js | 2 +- .../openlibrary/js/my-books/CreateListForm.js | 10 +-- .../openlibrary/js/my-books/MyBooksDropper.js | 20 ++--- .../MyBooksDropper/CheckInComponents.js | 86 +++++++++---------- .../my-books/MyBooksDropper/ReadingLists.js | 22 ++--- .../plugins/openlibrary/js/my-books/index.js | 4 +- .../openlibrary/js/my-books/store/index.js | 18 ++-- .../openlibrary/js/native-dialog/index.js | 10 +-- .../plugins/openlibrary/js/nonjquery_utils.js | 6 +- .../plugins/openlibrary/js/offline-banner.js | 2 +- .../plugins/openlibrary/js/ol.analytics.js | 8 +- openlibrary/plugins/openlibrary/js/ol.js | 18 ++-- .../plugins/openlibrary/js/partner_ol_lib.js | 6 +- .../plugins/openlibrary/js/patron_exports.js | 4 +- .../plugins/openlibrary/js/private-button.js | 2 +- openlibrary/plugins/openlibrary/js/python.js | 6 +- .../openlibrary/js/reading-goals/index.js | 24 +++--- .../openlibrary/js/readinglog_stats.js | 10 +-- .../openlibrary/js/return-form/index.js | 2 +- openlibrary/plugins/openlibrary/js/search.js | 18 ++-- .../openlibrary/js/service-worker-matchers.js | 12 +-- openlibrary/plugins/openlibrary/js/signup.js | 36 ++++---- .../openlibrary/js/star-ratings/index.js | 6 +- .../plugins/openlibrary/js/stats/index.js | 4 +- openlibrary/plugins/openlibrary/js/tabs.js | 4 +- openlibrary/plugins/openlibrary/js/team.js | 2 +- .../plugins/openlibrary/js/template.js | 14 +-- .../plugins/openlibrary/js/type_changer.js | 4 +- openlibrary/plugins/openlibrary/js/utils.js | 16 ++-- .../plugins/openlibrary/js/waitlist.js | 2 +- static/bookmarklets/import_webbook.js | 2 +- stories/.storybook/main.js | 2 +- tests/unit/js/SelectionManager.test.js | 4 +- tests/unit/js/editionsEditPage.test.js | 2 +- tests/unit/js/jsdef.test.js | 2 +- .../unit/js/sample-html/dropper-test-data.js | 2 +- tests/unit/js/sample-html/lists-test-data.js | 4 +- tests/unit/js/search.test.js | 8 +- tests/unit/js/service-worker-matchers.test.js | 2 +- 90 files changed, 464 insertions(+), 464 deletions(-) diff --git a/openlibrary/components/MergeUI.vue b/openlibrary/components/MergeUI.vue index 253bff74ae0..0a0ced8c1c8 100644 --- a/openlibrary/components/MergeUI.vue +++ b/openlibrary/components/MergeUI.vue @@ -81,7 +81,7 @@ export default { default: 'true', } }, - data () { + data() { return { url: new URL(location.toString()), mergeStatus: LOADING, @@ -91,7 +91,7 @@ export default { }; }, computed: { - olids () { + olids() { const olidsString = this.url.searchParams.get('records'); if (!olidsString) return []; return olidsString @@ -100,19 +100,19 @@ export default { .filter(Boolean); }, - isSuperLibrarian () { + isSuperLibrarian() { return this.canmerge === 'true'; }, - isDisabled () { + isDisabled() { return this.mergeStatus !== DO_MERGE && this.mergeStatus !== REQUEST_MERGE; }, - showRejectButton () { + showRejectButton() { return this.mrid && this.isSuperLibrarian; } }, - mounted () { + mounted() { const readyCta = this.isSuperLibrarian ? DO_MERGE : REQUEST_MERGE; this.$watch( '$refs.mergeTable.merge', @@ -122,7 +122,7 @@ export default { ); }, methods: { - async doMerge () { + async doMerge() { if (!this.$refs.mergeTable.merge) return; const { record: master, dupes, editions_to_move, unmergeable_works } = this.$refs.mergeTable.merge; @@ -168,7 +168,7 @@ export default { this.mergeStatus = 'Done'; }, - async rejectMerge () { + async rejectMerge() { try { await update_merge_request(this.mrid, 'decline', this.comment); this.mergeOutput = 'Merge request closed'; diff --git a/openlibrary/components/MergeUI/AuthorRoleTable.vue b/openlibrary/components/MergeUI/AuthorRoleTable.vue index 9efda1993f3..11123d90e15 100644 --- a/openlibrary/components/MergeUI/AuthorRoleTable.vue +++ b/openlibrary/components/MergeUI/AuthorRoleTable.vue @@ -54,7 +54,7 @@ export default { roles: Array }, computed: { - fields () { + fields() { return _.uniq(_.flatMap(this.roles, Object.keys)).sort(); } } diff --git a/openlibrary/components/MergeUI/EditionSnippet.vue b/openlibrary/components/MergeUI/EditionSnippet.vue index 270bd12b4c7..0de0a239f5c 100644 --- a/openlibrary/components/MergeUI/EditionSnippet.vue +++ b/openlibrary/components/MergeUI/EditionSnippet.vue @@ -61,17 +61,17 @@ export default { edition: Object }, computed: { - publish_year () { + publish_year() { if (!this.edition.publish_date) return ''; const m = this.edition.publish_date.match(/\d{4}/); return m ? m[0] : null; }, - publishers () { + publishers() { return this.edition.publishers || []; }, - number_of_pages () { + number_of_pages() { if (this.edition.number_of_pages) { return this.edition.number_of_pages; } else if (this.edition.pagination) { @@ -82,17 +82,17 @@ export default { return '?'; }, - full_title () { + full_title() { let title = this.edition.title; if (this.edition.subtitle) title += `: ${this.edition.subtitle}`; return title; }, - cover_id () { + cover_id() { return this.edition.covers?.[0] ?? null; }, - cover_url () { + cover_url() { if (this.cover_id) return `https://covers.openlibrary.org/b/id/${this.cover_id}-M.jpg`; const ocaid = this.edition.ocaid; @@ -102,13 +102,13 @@ export default { return ''; }, - languages () { + languages() { if (!this.edition.languages) return '???'; const langs = this.edition.languages.map(lang => lang.key.split('/')[2]); return langs.join(', '); }, - asins () { + asins() { return _.uniq([ ...((this.edition.identifiers && this.edition.identifiers.amazon) || []), this.edition.isbn_10 && ISBN.asIsbn10(this.edition.isbn_10), @@ -118,7 +118,7 @@ export default { }, methods: { - openEnlargedCover () { + openEnlargedCover() { let url = ''; if (this.cover_id) { url = `https://covers.openlibrary.org/b/id/${this.cover_id}.jpg`; diff --git a/openlibrary/components/MergeUI/ExcerptsTable.vue b/openlibrary/components/MergeUI/ExcerptsTable.vue index ddfecfd1baf..ea3309dabc3 100644 --- a/openlibrary/components/MergeUI/ExcerptsTable.vue +++ b/openlibrary/components/MergeUI/ExcerptsTable.vue @@ -37,7 +37,7 @@ export default { excerpts: Array }, computed: { - fields () { + fields() { return _.uniq(_.flatMap(this.excerpts, Object.keys)); } } diff --git a/openlibrary/components/MergeUI/MergeRow.vue b/openlibrary/components/MergeUI/MergeRow.vue index 6a17706ef11..01df7bcbf21 100644 --- a/openlibrary/components/MergeUI/MergeRow.vue +++ b/openlibrary/components/MergeUI/MergeRow.vue @@ -88,7 +88,7 @@ export default { type: Boolean } }, - data () { + data() { return { master_key: null }; diff --git a/openlibrary/components/MergeUI/MergeRowField.vue b/openlibrary/components/MergeUI/MergeRowField.vue index dc9a2c9db13..2742122f31c 100644 --- a/openlibrary/components/MergeUI/MergeRowField.vue +++ b/openlibrary/components/MergeUI/MergeRowField.vue @@ -157,7 +157,7 @@ export default { } }, computed: { - title () { + title() { let title = `.${this.field}`; if (this.value instanceof Array) { const length = this.value.length; diff --git a/openlibrary/components/MergeUI/MergeRowJointField.vue b/openlibrary/components/MergeUI/MergeRowJointField.vue index 27e3cb9507e..502e7aa49f5 100644 --- a/openlibrary/components/MergeUI/MergeRowJointField.vue +++ b/openlibrary/components/MergeUI/MergeRowJointField.vue @@ -40,7 +40,7 @@ export default { } }, computed: { - presentFields () { + presentFields() { return this.fields.filter(f => f in this.record); } } diff --git a/openlibrary/components/MergeUI/TextDiff.vue b/openlibrary/components/MergeUI/TextDiff.vue index 0359a824c17..b5c60bf340d 100644 --- a/openlibrary/components/MergeUI/TextDiff.vue +++ b/openlibrary/components/MergeUI/TextDiff.vue @@ -25,7 +25,7 @@ export default { } }, computed: { - diff () { + diff() { const fn = { char: diffChars, word: diffWordsWithSpace, diff --git a/openlibrary/components/ObservationForm.vue b/openlibrary/components/ObservationForm.vue index f74214eb4c0..67762001b55 100644 --- a/openlibrary/components/ObservationForm.vue +++ b/openlibrary/components/ObservationForm.vue @@ -84,7 +84,7 @@ export default { required: true } }, - data: function () { + data: function() { return { /** * An object representing the currently selected tag type. @@ -121,26 +121,26 @@ export default { * * @returns {Number|null} The ID of the selected observation, if one exists. */ - getSelectedId: function () { + getSelectedId: function() { if (this.selectedObservation) { return this.selectedObservation.id; } return null; } }, - created: function () { + created: function() { this.observationsArray = decodeAndParseJSON(this.schema)['observations']; this.allSelectedValues = decodeAndParseJSON(this.observations); this.selectRandomObservation(); }, - mounted: function () { + mounted: function() { this.observer = new ResizeObserver(() => { resizeColorbox(); }); this.observer.observe(this.$refs.form); }, - beforeUnmount: function () { + beforeUnmount: function() { if (this.observer) { this.observer.disconnect(); } @@ -151,13 +151,13 @@ export default { * * @param {Object | null} observation The new selected observation, or `null` if no type is selected. */ - updateSelected: function (observation) { + updateSelected: function(observation) { this.selectedObservation = observation; }, /** * Randomly sets a selected observation. */ - selectRandomObservation: function () { + selectRandomObservation: function() { const randomNumber = Math.floor(Math.random() * 100000); this.selectedObservation = this.observationsArray[randomNumber % this.observationsArray.length]; } diff --git a/openlibrary/components/ObservationForm/components/CardBody.vue b/openlibrary/components/ObservationForm/components/CardBody.vue index 3ba378e914b..5af5981134a 100644 --- a/openlibrary/components/ObservationForm/components/CardBody.vue +++ b/openlibrary/components/ObservationForm/components/CardBody.vue @@ -81,7 +81,7 @@ export default { /** * Returns an array of all of this book tag type's currently selected values. */ - selectedValues: function () { + selectedValues: function() { return this.allSelectedValues[this.type]?.length ? this.allSelectedValues[this.type] : []; } }, @@ -94,7 +94,7 @@ export default { * @param {boolean} isSelected `true` if a chip is selected, `false` otherwise. * @param {String} text The text that the updated chip is displaying. */ - updateSelected: function (isSelected, text) { + updateSelected: function(isSelected, text) { let updatedValues = this.allSelectedValues[this.type] ? this.allSelectedValues[this.type] : []; if (isSelected) { diff --git a/openlibrary/components/ObservationForm/components/CategorySelector.vue b/openlibrary/components/ObservationForm/components/CategorySelector.vue index b89204a3cc3..b0345a3f70e 100644 --- a/openlibrary/components/ObservationForm/components/CategorySelector.vue +++ b/openlibrary/components/ObservationForm/components/CategorySelector.vue @@ -74,7 +74,7 @@ export default { default: 0 } }, - data: function () { + data: function() { return { /** * The ID of the selected book tag type. @@ -91,7 +91,7 @@ export default { * @param {boolean} isSelected Whether or not a chip is currently selected. * @param {String} text The text displayed by a chip. */ - updateSelected: function (isSelected, text) { + updateSelected: function(isSelected, text) { if (isSelected) { // TODO: This for loop shouldn't be necessary for (let i = 0; i < this.observationsArray.length; ++i) { @@ -112,7 +112,7 @@ export default { * * @param {number} id A chip's id. */ - isSelected: function (id) { + isSelected: function(id) { return this.selectedId === id; }, /** @@ -123,7 +123,7 @@ export default { * * @returns {String} An HTML code representing selections of a type. */ - displaySymbol: function (type) { + displaySymbol: function(type) { if (this.allSelectedValues[type] && this.allSelectedValues[type].length) { // ✔ - Heavy checkmark return '✔'; diff --git a/openlibrary/components/ObservationForm/components/OLChip.vue b/openlibrary/components/ObservationForm/components/OLChip.vue index 45cbfc180b7..fc9e8a963d2 100644 --- a/openlibrary/components/ObservationForm/components/OLChip.vue +++ b/openlibrary/components/ObservationForm/components/OLChip.vue @@ -51,7 +51,7 @@ export default { default: '' } }, - data: function () { + data: function() { return { /** * Tracks whether this chip is currently selected. @@ -67,12 +67,12 @@ export default { * * @returns 'click' if this chip can be selected, otherwise `null` */ - canSelect: function () { + canSelect: function() { return this.selectable ? 'click' : null; } }, watch: { - selected (newValue) { + selected(newValue) { this.isSelected = newValue; } }, @@ -80,7 +80,7 @@ export default { /** * Toggles the value of `isSelected` and fires an `update-selected` event. */ - onClick: function () { + onClick: function() { this.toggleSelected(); /** * Update selected event. @@ -93,7 +93,7 @@ export default { /** * Toggles the state of `isSelected` */ - toggleSelected: function () { + toggleSelected: function() { this.isSelected = !this.isSelected; } } diff --git a/openlibrary/components/ObservationForm/components/SavedTags.vue b/openlibrary/components/ObservationForm/components/SavedTags.vue index ecf79dba6dd..04c90bb5d62 100644 --- a/openlibrary/components/ObservationForm/components/SavedTags.vue +++ b/openlibrary/components/ObservationForm/components/SavedTags.vue @@ -80,7 +80,7 @@ export default { required: true } }, - data: function () { + data: function() { return { /** * Contains class strings for each selected book tag @@ -100,7 +100,7 @@ export default { /** * An array of a patron's book tags. */ - selectedValues: function () { + selectedValues: function() { const results = []; for (const type in this.allSelectedValues) { @@ -118,7 +118,7 @@ export default { * * @param {String} chipText The text of the selected tag chip, in the form "<type>: <value>" */ - removeItem: function (chipText) { + removeItem: function(chipText) { const [type, value] = chipText.split(': '); const valueIndex = this.allSelectedValues[type].indexOf(value); @@ -143,7 +143,7 @@ export default { * * @param {String} value The chip's key. */ - addHoverClass: function (value) { + addHoverClass: function(value) { this.classLists[value] = 'hover'; }, /** @@ -151,7 +151,7 @@ export default { * * @param {String} value The chip's key. */ - removeHoverClass: function (value) { + removeHoverClass: function(value) { this.classLists[value] = ''; }, /** @@ -160,7 +160,7 @@ export default { * @param {String} value The chip's key * @returns The chip's class list string. */ - getClassList: function (value) { + getClassList: function(value) { return this.classLists[value] ? this.classLists[value] : ''; } } diff --git a/openlibrary/components/ObservationForm/components/ValueCard.vue b/openlibrary/components/ObservationForm/components/ValueCard.vue index 5fdded40018..c569458edcb 100644 --- a/openlibrary/components/ObservationForm/components/ValueCard.vue +++ b/openlibrary/components/ObservationForm/components/ValueCard.vue @@ -54,7 +54,7 @@ export default { values: { type: Array, required: true, - validator: function (arr) { + validator: function(arr) { for (const item of arr) { if (typeof(item) !== 'string') { return false; diff --git a/openlibrary/plugins/openlibrary/js/book-page-lists.js b/openlibrary/plugins/openlibrary/js/book-page-lists.js index af8c44bb966..48ea0fd5ff3 100644 --- a/openlibrary/plugins/openlibrary/js/book-page-lists.js +++ b/openlibrary/plugins/openlibrary/js/book-page-lists.js @@ -6,7 +6,7 @@ import { initAsyncFollowing } from './following'; * * @param elem {HTMLElement} Container for book page lists section */ -export function initListsSection (elem) { +export function initListsSection(elem) { // Show loading indicator const loadingIndicator = elem.querySelector('.loadingIndicator'); loadingIndicator.classList.remove('hidden'); @@ -64,7 +64,7 @@ export function initListsSection (elem) { * Initialize private buttons after the lists section has been loaded * @param {HTMLElement} container - The container that now has the loaded content */ -function initPrivateButtonsAfterLoad (container) { +function initPrivateButtonsAfterLoad(container) { const privateButtons = container.querySelectorAll('.list-follow-card__private-button'); if (privateButtons.length > 0) { import(/* webpackChunkName: "private-buttons" */ './private-button') @@ -74,7 +74,7 @@ function initPrivateButtonsAfterLoad (container) { } } -async function fetchPartials (workId, editionId) { +async function fetchPartials(workId, editionId) { const params = {}; if (workId) { params.workId = workId; diff --git a/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js b/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js index 2126881bf4a..35333ed4378 100644 --- a/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js +++ b/openlibrary/plugins/openlibrary/js/breadcrumb_select/index.js @@ -3,12 +3,12 @@ * * @param {NodeList<HTMLElement>} crumbs - NodeList of breadcrumb select elements. */ -export function initBreadcrumbSelect (crumbs) { +export function initBreadcrumbSelect(crumbs) { const allowedKeys = new Set(['Tab', 'Enter', ' ']); const preventedKeys = new Set(['ArrowUp', 'ArrowDown']); // watch crumbs for changes, // ensures it's a full value change, not a user exploring options via keyboard - function handleNavEvents (nav) { + function handleNavEvents(nav) { let ignoreChange = false; nav.addEventListener('change', () => { diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js index 8f8a7e17bfd..d0af1c9a38b 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/MenuOption.js @@ -38,7 +38,7 @@ export class MenuOption { * @param {OptionState} optionState * @param {Number} taggedWorksCount Number of selected works which have the given tag */ - constructor (tag, optionState, taggedWorksCount) { + constructor(tag, optionState, taggedWorksCount) { /** * Reference to the root element of this MenuOption. * @@ -80,7 +80,7 @@ export class MenuOption { * Must be called before an event handler can be attached to * this menu option */ - initialize () { + initialize() { this.createMenuOption(); } @@ -90,7 +90,7 @@ export class MenuOption { * Stores newly created element as `rootElement`. The new element is not * attached to the DOM, and does not yet have any attached event handlers. */ - createMenuOption () { + createMenuOption() { const parentElem = document.createElement('div'); parentElem.classList.add('selected-tag'); @@ -120,7 +120,7 @@ export class MenuOption { /** * Removes this MenuOption from the DOM. */ - remove () { + remove() { this.rootElement.remove(); } @@ -134,7 +134,7 @@ export class MenuOption { * @see {@link MenuOptionState} * @see {initialize} */ - updateMenuOptionState (menuOptionState) { + updateMenuOptionState(menuOptionState) { if (this.rootElement) { // `rootElement` not set until `initialize` is called this.optionState = menuOptionState; const statusIndicator = this.rootElement.querySelector('.selected-tag__status'); @@ -165,7 +165,7 @@ export class MenuOption { * * Fires an `option-hidden` event when this is called. */ - hide () { + hide() { this.rootElement.classList.add('hidden'); this.rootElement.dispatchEvent(new CustomEvent('option-hidden')); } @@ -173,14 +173,14 @@ export class MenuOption { /** * Shows this menu option. */ - show () { + show() { this.rootElement.classList.remove('hidden'); } /** * Stages the selected menu option. */ - stage () { + stage() { this.rootElement.classList.add('selected-tag--staged'); } } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js index 889b545626d..5e94e437d5d 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/BulkTagger/SortedMenuOptionContainer.js @@ -18,7 +18,7 @@ export class SortedMenuOptionContainer { * * @param {HTMLElement} element The container */ - constructor (element) { + constructor(element) { this.rootElement = element; this.sortedMenuOptions = []; } @@ -28,7 +28,7 @@ export class SortedMenuOptionContainer { * * @param {...MenuOption} menuOptions Menu options to be added to the container. */ - add (...menuOptions) { + add(...menuOptions) { for (const option of menuOptions) { const index = this.findIndex(option); this.sortedMenuOptions.splice(index, 0, option); @@ -42,7 +42,7 @@ export class SortedMenuOptionContainer { * @param {MenuOption} menuOption The option being attached to the DOM. * @param {Number} index The index where the given option will be inserted. */ - updateViewOnAdd (menuOption, index) { + updateViewOnAdd(menuOption, index) { if (index === 0) { this.rootElement.prepend(menuOption.rootElement); } else { @@ -56,7 +56,7 @@ export class SortedMenuOptionContainer { * * @param {...MenuOption} menuOptions Options that are to be removed from this container */ - remove (...menuOptions) { + remove(...menuOptions) { for (const option of menuOptions) { const index = this.findIndex(option); const removed = this.sortedMenuOptions.splice(index, 1); @@ -71,7 +71,7 @@ export class SortedMenuOptionContainer { * @param {MenuOption} menuOption * @returns {Number} Index where the given menu option should be inserted. */ - findIndex (menuOption) { + findIndex(menuOption) { let index = 0; // XXX : Binary search? @@ -99,7 +99,7 @@ export class SortedMenuOptionContainer { * @param {MenuOption} menuOption The object that we are searching for * @returns {boolean} `true` if a matching menu option exists in this container */ - contains (menuOption) { + contains(menuOption) { return this.sortedMenuOptions.some((option) => menuOption.tag.equals(option.tag)); } @@ -109,7 +109,7 @@ export class SortedMenuOptionContainer { * @param {Tag} tag * @returns {boolean} `true` if a menu option which represents the given tag is in this container. */ - containsOptionWithTag (tag) { + containsOptionWithTag(tag) { return this.sortedMenuOptions.some((option) => tag.equals(option.tag)); } @@ -119,14 +119,14 @@ export class SortedMenuOptionContainer { * @param {Tag} tag * @returns {MenuOption|undefined} The first matching menu option, or `undefined` if none were found. */ - findByTag (tag) { + findByTag(tag) { return this.sortedMenuOptions.find((option) => tag.equals(option.tag)); } /** * Removes all menu options from this container. */ - clear () { + clear() { while (this.sortedMenuOptions.length > 0) { this.sortedMenuOptions.pop(); } diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js index bdffeaf38f8..4c7ef444a73 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/index.js @@ -3,7 +3,7 @@ * * @returns HTML for the bulk tagging form */ -export function renderBulkTagger () { +export function renderBulkTagger() { return `<form action="/tags/bulk_tag_works" method="post" class="bulk-tagging-form hidden"> <div class="form-header"> <p>Manage Subjects</p> diff --git a/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js b/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js index ae35c571df3..363db838d8e 100644 --- a/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js +++ b/openlibrary/plugins/openlibrary/js/bulk-tagger/models/Tag.js @@ -33,7 +33,7 @@ export const subjectTypeMapping = { * @returns {Number} * @see {Array.sort} */ -export function compare (tagA, tagB) { +export function compare(tagA, tagB) { const lowerA = createComparableTag(tagA); const lowerB = createComparableTag(tagB); @@ -64,7 +64,7 @@ export function compare (tagA, tagB) { * @returns {Object} Tag-like object that is suitable to use for sorting comparisons. * @see {compare} */ -function createComparableTag (tag) { +function createComparableTag(tag) { return { tagName: tag.tagName.toLowerCase(), tagType: tag.tagType.toLowerCase() @@ -90,7 +90,7 @@ export class Tag { * * @throws Will throw an error if both `tagType` and `displayType` are falsey */ - constructor (tagName, tagType = null, displayType = null) { + constructor(tagName, tagType = null, displayType = null) { if (!(tagType || displayType)) { throw new Error('Tag must have at least one type'); } @@ -107,7 +107,7 @@ export class Tag { * @returns {String} The corresponding technical tag type * @throws Will throw an error if the given type is unrecognized. */ - convertToType (displayType) { + convertToType(displayType) { const result = subjectTypeMapping[displayType]; if (!result) { throw new Error('Unrecognized `displayType` value'); @@ -123,7 +123,7 @@ export class Tag { * @returns {String} A type string that can be displayed in the UI * @throws Will throw an error if the given type is unrecognized */ - convertToDisplayType (tagType) { + convertToDisplayType(tagType) { const result = displayTypeMapping[tagType]; if (!result) { throw new Error('Unrecognized `tagType` value'); @@ -140,7 +140,7 @@ export class Tag { * @param {Tag} tag * @returns `true` if the given tag is considered equivalent to this tag. */ - equals (tag) { + equals(tag) { const lowerSelf = createComparableTag(this); const lowerTag = createComparableTag(tag); diff --git a/openlibrary/plugins/openlibrary/js/clampers.js b/openlibrary/plugins/openlibrary/js/clampers.js index 673b4f5c48f..b170551f1be 100644 --- a/openlibrary/plugins/openlibrary/js/clampers.js +++ b/openlibrary/plugins/openlibrary/js/clampers.js @@ -2,7 +2,7 @@ * @param {NodeListOf<Element>} clampers * */ -export function initClampers (clampers) { +export function initClampers(clampers) { for (const clamper of clampers) { if (clamper.clientHeight === clamper.scrollHeight) { clamper.classList.remove('clamp'); diff --git a/openlibrary/plugins/openlibrary/js/compact-title/index.js b/openlibrary/plugins/openlibrary/js/compact-title/index.js index 91039cb0ef5..1ee3c7f14c6 100644 --- a/openlibrary/plugins/openlibrary/js/compact-title/index.js +++ b/openlibrary/plugins/openlibrary/js/compact-title/index.js @@ -25,12 +25,12 @@ let mainTitleElem; * @param {HTMLElement} navbar The book page navbar component * @param {HTMLElement} title The compact title component */ -export function initCompactTitle (navbar, title) { +export function initCompactTitle(navbar, title) { mainTitleElem = document.querySelector('.work-title-and-author.desktop .work-title'); // Show compact title on page reload: onScroll(navbar, title); // And update on scroll - window.addEventListener('scroll', function () { + window.addEventListener('scroll', function() { onScroll(navbar, title); }); } @@ -44,7 +44,7 @@ export function initCompactTitle (navbar, title) { * @param {HTMLElement} navbar The book page navbar component * @param {HTMLElement} title The compact title component */ -function onScroll (navbar, title) { +function onScroll(navbar, title) { const compactTitleBounds = title.getBoundingClientRect(); const navbarBounds = navbar.getBoundingClientRect(); const mainTitleBounds = mainTitleElem.getBoundingClientRect(); diff --git a/openlibrary/plugins/openlibrary/js/covers.js b/openlibrary/plugins/openlibrary/js/covers.js index 7224649c1ce..6a6b339ce21 100644 --- a/openlibrary/plugins/openlibrary/js/covers.js +++ b/openlibrary/plugins/openlibrary/js/covers.js @@ -8,7 +8,7 @@ import 'jquery-ui-touch-punch'; // this makes drag-to-reorder work on touch devi import { closePopup } from './utils'; //cover/change.html -export function initCoversChange () { +export function initCoversChange() { // Pull data from data-config of class "manageCovers" in covers/manage.html const data_config_json = $('.manageCovers').data('config'); const doc_type_key = data_config_json['key']; @@ -18,34 +18,34 @@ export function initCoversChange () { // Add iframes lazily when the popup is loaded. // This avoids fetching the iframes along with main page. $('.coverPop') - .on('click', function () { + .on('click', function() { // clear the content of #imagesAdd and #imagesManage before adding new $('.imagesAdd').html(''); $('.imagesManage').html(''); if (doc_type_key === '/type/work') { $('.imagesAdd').prepend('<div class="throbber"><h3>$_("Searching for covers")</h3></div>'); } - setTimeout(function () { + setTimeout(function() { // add iframe to add images add_iframe('.imagesAdd', add_url); // add iframe to manage images add_iframe('.imagesManage', manage_url); }, 0); }) - .on('cbox_cleanup', function () { + .on('cbox_cleanup', function() { $('.imagesAdd').html(''); $('.imagesManage').html(''); }); } -function add_iframe (selector, src) { +function add_iframe(selector, src) { $(selector) .append('<iframe frameborder="0" height="580" width="580" marginheight="0" marginwidth="0" scrolling="auto"></iframe>') .find('iframe') .attr('src', src); } -function showLoadingIndicator () { +function showLoadingIndicator() { const loadingIndicator = document.querySelector('.loadingIndicator'); const formDivs = document.querySelectorAll('.ol-cover-form, .imageIntro'); @@ -56,8 +56,8 @@ function showLoadingIndicator () { } // covers/manage.html and covers/add.html -export function initCoversAddManage () { - $('.ol-cover-form').on('submit', function () { +export function initCoversAddManage() { + $('.ol-cover-form').on('submit', function() { showLoadingIndicator(); }); @@ -73,7 +73,7 @@ export function initCoversAddManage () { // covers/saved.html // Uses parent.$ in place of $ where elements lie outside of the "saved" window -export function initCoversSaved () { +export function initCoversSaved() { // Save the new image // Pull data from data-config of class "imageSaved" in covers/saved.html const data_config_json = parent.$('.manageCovers').data('config'); @@ -117,7 +117,7 @@ export function initCoversSaved () { } // This function will be triggered when the user clicks the "Paste" button -async function pasteImage () { +async function pasteImage() { let formData = null; try { const clipboardItems = await navigator.clipboard.read(); @@ -158,11 +158,11 @@ async function pasteImage () { } } -export function initPasteForm (coverForm) { +export function initPasteForm(coverForm) { const pasteButton = coverForm.querySelector('#pasteButton'); let formData = null; - pasteButton.addEventListener('click', async () => { + pasteButton.addEventListener('click', async() => { formData = await pasteImage(coverForm); pasteButton.textContent = 'Change Image'; }); diff --git a/openlibrary/plugins/openlibrary/js/dropper/Dropper.js b/openlibrary/plugins/openlibrary/js/dropper/Dropper.js index d841c0ecefa..d9eaccf44fa 100644 --- a/openlibrary/plugins/openlibrary/js/dropper/Dropper.js +++ b/openlibrary/plugins/openlibrary/js/dropper/Dropper.js @@ -29,7 +29,7 @@ export class Dropper { * * @param {HTMLElement} dropper Reference to the dropper's root element */ - constructor (dropper) { + constructor(dropper) { /** * References the root element of the dropper. * @@ -76,7 +76,7 @@ export class Dropper { /** * Adds click listener to dropper's toggle arrow. */ - initialize () { + initialize() { this.dropClick.addEventListener('click', () => { this.toggleDropper(); }); @@ -88,7 +88,7 @@ export class Dropper { * Subclasses of `Dropper` may override this to add * functionality that should occur on dropper open. */ - onOpen () {} + onOpen() {} /** * Function that is called after a dropper has closed. @@ -96,7 +96,7 @@ export class Dropper { * Subclasses of `Dropper` may override this to add * functionality that should occur on dropper close. */ - onClose () {} + onClose() {} /** * Function that is called when the drop-click affordance of @@ -104,7 +104,7 @@ export class Dropper { * * Subclasses of `Dropper` may override this as needed. */ - onDisabledClick () {} + onDisabledClick() {} /** * Closes dropper if opened; opens dropper if closed. @@ -115,7 +115,7 @@ export class Dropper { * Calls either `onOpen()` or `onClose()` after the dropper * has been toggled. */ - toggleDropper () { + toggleDropper() { if (this.isDropperDisabled) { this.onDisabledClick(); } else { @@ -140,7 +140,7 @@ export class Dropper { * Calls `onDisabledClick()` if this dropper is disabled. * Otherwise, closes dropper and calls `onClose()`. */ - closeDropper () { + closeDropper() { if (this.isDropperDisabled) { this.onDisabledClick(); } else { diff --git a/openlibrary/plugins/openlibrary/js/dropper/index.js b/openlibrary/plugins/openlibrary/js/dropper/index.js index eac18e6db8c..a6070d178f9 100644 --- a/openlibrary/plugins/openlibrary/js/dropper/index.js +++ b/openlibrary/plugins/openlibrary/js/dropper/index.js @@ -11,23 +11,23 @@ const droppers = []; * * @param {HTMLCollection<HTMLElement>} dropperElements */ -export function initDroppers (dropperElements) { +export function initDroppers(dropperElements) { for (const dropper of dropperElements) { droppers.push(dropper); - $(dropper).on('click', '.dropclick', debounce(function () { + $(dropper).on('click', '.dropclick', debounce(function() { $(this).next('.dropdown').slideToggle(25); $(this).parent().next('.dropdown').slideToggle(25); $(this).parent().find('.arrow').toggleClass('up'); }, 300, false)); - $(dropper).on('click', '.dropper__close', debounce(function () { + $(dropper).on('click', '.dropper__close', debounce(function() { closeDropper($(dropper)); }, 300, false)); } // Close any open dropdown list if the user clicks outside of component: - $(document).on('click', function (event) { + $(document).on('click', function(event) { for (const dropper of droppers) { if (!dropper.contains(event.target)) { closeDropper($(dropper)); @@ -40,7 +40,7 @@ export function initDroppers (dropperElements) { * close an open dropdown in a given container * @param {jQuery.Object} $container */ -function closeDropper ($container) { +function closeDropper($container) { $container.find('.dropdown').slideUp(25); // Legacy droppers $container.find('.generic-dropper__dropdown').slideUp(25); // New generic droppers $container.find('.arrow').removeClass('up'); @@ -56,11 +56,11 @@ function closeDropper ($container) { * * @param {NodeList<HTMLElement>} dropperElements */ -export function initGenericDroppers (dropperElements) { +export function initGenericDroppers(dropperElements) { const genericDroppers = Array.from(dropperElements); // Close any open dropdown if the user clicks outside of component: - $(document).on('click', function (event) { + $(document).on('click', function(event) { for (const dropper of genericDroppers) { if (!dropper.contains(event.target)) { closeDropper($(dropper)); diff --git a/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js b/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js index 89f135e3772..ccd143fdbc4 100644 --- a/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js +++ b/openlibrary/plugins/openlibrary/js/edition-nav-bar/EditionNavBar.js @@ -8,7 +8,7 @@ export default class EdtionNavBar { * * @param {HTMLElement} navbarWrapper */ - constructor (navbarWrapper) { + constructor(navbarWrapper) { /** * Reference to the parent element of the navbar. * @type {HTMLElement} @@ -51,7 +51,7 @@ export default class EdtionNavBar { /** * Adds the necessary event handlers to the navbar. */ - initialize () { + initialize() { // Add click listeners to navbar items: for (let i = 0; i < this.navItems.length; ++i) { this.navItems[i].addEventListener('click', () => { @@ -98,7 +98,7 @@ export default class EdtionNavBar { * Determines this navbar's position on the page and updates the selected * nav item. */ - updateSelected () { + updateSelected() { const navbarHeight = this.navbarWrapper.getBoundingClientRect().height; if (navbarHeight > 0) { let i = this.navItems.length; @@ -118,7 +118,7 @@ export default class EdtionNavBar { * * @param {HTMLElement} selectedItem Newly selected nav item */ - scrollNavbar (selectedItem) { + scrollNavbar(selectedItem) { // Note: We don't use the browser native scrollIntoView method because // that method scrolls _recursively_, so it also tries to scroll the // body to center the element on the screen, causing weird jitters. @@ -137,7 +137,7 @@ export default class EdtionNavBar { * * @param {HTMLLIElement} selectedElem Element corresponding to the 'selected' navbar item. */ - selectElement (selectedElem) { + selectElement(selectedElem) { for (const li of this.navItems) { li.classList.remove('selected'); } diff --git a/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js b/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js index 8b36a5766c1..1342027e6d2 100644 --- a/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js +++ b/openlibrary/plugins/openlibrary/js/edition-nav-bar/index.js @@ -11,7 +11,7 @@ const navbars = []; * * @param {HTMLCollection<HTMLElement>} navbarWrappers Each navbar found on the book page */ -export function initNavbars (navbarWrappers) { +export function initNavbars(navbarWrappers) { for (const wrapper of navbarWrappers) { const navbar = new EdtionNavBar(wrapper); navbars.push(navbar); @@ -26,7 +26,7 @@ export function initNavbars (navbarWrappers) { * something other then a scroll event (e.g. when * stickied to a new position). */ -export function updateSelectedNavItem () { +export function updateSelectedNavItem() { for (const navbar of navbars) { navbar.updateSelected(); } diff --git a/openlibrary/plugins/openlibrary/js/editions-table/index.js b/openlibrary/plugins/openlibrary/js/editions-table/index.js index 1ff7a0b7999..ad19d4f6dcb 100755 --- a/openlibrary/plugins/openlibrary/js/editions-table/index.js +++ b/openlibrary/plugins/openlibrary/js/editions-table/index.js @@ -4,14 +4,14 @@ import '../../../../../static/css/legacy-datatables.css'; const DEFAULT_LENGTH = 3; const LS_RESULTS_LENGTH_KEY = 'editions-table.resultsLength'; -export function initEditionsTable () { +export function initEditionsTable() { var rowCount; let currentLength; // Prevent reinitialization of the editions datatable if ($.fn.DataTable.isDataTable($('#editions'))) { return; } - $('#editions th.title').on('mouseover', function (){ + $('#editions th.title').on('mouseover', function(){ if ($(this).hasClass('sorting_asc')) { $(this).attr('title', 'Sort latest to earliest'); } else if ($(this).hasClass('sorting_desc')) { @@ -20,7 +20,7 @@ export function initEditionsTable () { $(this).attr('title', 'Sort by publish date'); } }); - $('#editions th.read').on('mouseover', function (){ + $('#editions th.read').on('mouseover', function(){ if ($(this).hasClass('sorting_asc')) { $(this).attr('title', 'Push readable versions to the bottom'); } else if ($(this).hasClass('sorting_desc')) { @@ -30,7 +30,7 @@ export function initEditionsTable () { } }); - function toggleSorting (e) { + function toggleSorting(e) { $('#editions th span').html(''); $(e).find('span').html(' ↑'); if ($(e).hasClass('sorting_asc')) { @@ -41,15 +41,15 @@ export function initEditionsTable () { } $('#editions th.read span').html(' ↑'); - $('#editions th').on('mouseup', function () { + $('#editions th').on('mouseup', function() { toggleSorting(this); }); - $('#editions').on('length.dt', function (e, settings, length) { + $('#editions').on('length.dt', function(e, settings, length) { localStorage.setItem(LS_RESULTS_LENGTH_KEY, length); }); - $('#editions th').on('keydown', function (e) { + $('#editions th').on('keydown', function(e) { if (e.key === 'Enter') { toggleSorting(this); } @@ -79,7 +79,7 @@ export function initEditionsTable () { bStateSave: false, bAutoWidth: false, pageLength: currentLength ? currentLength : DEFAULT_LENGTH, - drawCallback: function () { + drawCallback: function() { if ($('#ile-toolbar')) { const editionStorage = JSON.parse(sessionStorage.getItem('ile-items'))['edition']; const matchEdition = (string) => { diff --git a/openlibrary/plugins/openlibrary/js/following.js b/openlibrary/plugins/openlibrary/js/following.js index b5d618ba1a1..fdeae520f22 100644 --- a/openlibrary/plugins/openlibrary/js/following.js +++ b/openlibrary/plugins/openlibrary/js/following.js @@ -1,8 +1,8 @@ import { PersistentToast } from './Toast'; -export async function initAsyncFollowing (followForms) { +export async function initAsyncFollowing(followForms) { followForms.forEach(form => { - form.addEventListener('submit', async (e) => { + form.addEventListener('submit', async(e) => { e.preventDefault(); const url = form.action; const formData = new FormData(form); diff --git a/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js b/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js index 9ac3934dbbf..5bcf7c8236a 100644 --- a/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js +++ b/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js @@ -1,6 +1,6 @@ import { buildPartialsUrl } from './utils'; -export function initFulltextSearchSuggestion (fulltextSearchSuggestion) { +export function initFulltextSearchSuggestion(fulltextSearchSuggestion) { const isLoading = showLoadingIndicators(fulltextSearchSuggestion); if (isLoading) { const query = fulltextSearchSuggestion.dataset.query; @@ -8,7 +8,7 @@ export function initFulltextSearchSuggestion (fulltextSearchSuggestion) { } } -function showLoadingIndicators (fulltextSearchSuggestion) { +function showLoadingIndicators(fulltextSearchSuggestion) { let isLoading = false; const loadingIndicator = fulltextSearchSuggestion.querySelector('.loadingIndicator'); if (loadingIndicator) { @@ -17,7 +17,7 @@ function showLoadingIndicators (fulltextSearchSuggestion) { } return isLoading; } -async function getPartials (fulltextSearchSuggestion, query) { +async function getPartials(fulltextSearchSuggestion, query) { return fetch(buildPartialsUrl('FulltextSearchSuggestion', {data: query})) .then((resp) => { if (resp.status !== 200) { @@ -57,6 +57,6 @@ async function getPartials (fulltextSearchSuggestion, query) { * * @returns {string} HTML for a retry link. */ -function renderRetryLink () { +function renderRetryLink() { return '<span class="fulltext-suggestions__retry">Failed to fetch fulltext search suggestions. <a href="javascript:;">Retry?</a></span>'; } diff --git a/openlibrary/plugins/openlibrary/js/go-back-links.js b/openlibrary/plugins/openlibrary/js/go-back-links.js index 92f99285853..831f781b7f1 100644 --- a/openlibrary/plugins/openlibrary/js/go-back-links.js +++ b/openlibrary/plugins/openlibrary/js/go-back-links.js @@ -4,7 +4,7 @@ * * @param {NodeList<HTMLElement>} goBackLinks */ -export function initGoBackLinks (goBackLinks) { +export function initGoBackLinks(goBackLinks) { for (const link of goBackLinks) { link.addEventListener('click', () => { if (history.length > 2) { diff --git a/openlibrary/plugins/openlibrary/js/graphs/index.js b/openlibrary/plugins/openlibrary/js/graphs/index.js index e6d698cf8a6..192926e9842 100644 --- a/openlibrary/plugins/openlibrary/js/graphs/index.js +++ b/openlibrary/plugins/openlibrary/js/graphs/index.js @@ -1,7 +1,7 @@ import { loadGraphIfExists, loadEditionsGraph } from './plot'; import options from './options.js'; -export function plotAdminGraphs () { +export function plotAdminGraphs() { loadGraphIfExists('editgraph', {}, 'edit(s) on'); loadGraphIfExists('membergraph', {}, 'new members(s) on'); loadGraphIfExists('works_minigraph', {}, ' works on '); @@ -13,7 +13,7 @@ export function plotAdminGraphs () { loadGraphIfExists('books-added-per-day', options.booksAdded); } -export function initHomepageGraphs () { +export function initHomepageGraphs() { loadGraphIfExists('visitors-graph', {}, 'unique visitors on', '#e44028'); loadGraphIfExists('members-graph', {}, 'new members on', '#748d36'); loadGraphIfExists('edits-graph', {}, 'catalog edits on', '#00636a'); @@ -21,13 +21,13 @@ export function initHomepageGraphs () { loadGraphIfExists('ebooks-graph', {}, 'ebooks borrowed on', '#35672e'); } -export function initPublishersGraph () { +export function initPublishersGraph() { if (document.getElementById('chartPubHistory')) { loadEditionsGraph('chartPubHistory', {}, 'editions in'); } } -export function init () { +export function init() { plotAdminGraphs(); initHomepageGraphs(); initPublishersGraph(); diff --git a/openlibrary/plugins/openlibrary/js/graphs/plot.js b/openlibrary/plugins/openlibrary/js/graphs/plot.js index fd163244fc1..ba3df306062 100644 --- a/openlibrary/plugins/openlibrary/js/graphs/plot.js +++ b/openlibrary/plugins/openlibrary/js/graphs/plot.js @@ -15,7 +15,7 @@ import 'flot/jquery.flot.time.js'; * - http://localhost:8080/subjects/fantasy#sort=date_published&ebooks=true * - http://localhost:8080/publishers/Barnes_&_Noble */ -export function loadEditionsGraph () { +export function loadEditionsGraph() { var data, options, placeholder, plot, dateFrom, dateTo, previousPoint; data = [{data: JSON.parse(document.getElementById('graph-json-chartPubHistory').textContent)}]; @@ -52,7 +52,7 @@ export function loadEditionsGraph () { }; placeholder = $('#chartPubHistory'); - function showTooltip (x, y, contents) { + function showTooltip(x, y, contents) { $(`<div id="chartLabel">${contents}</div>`).css({ position: 'absolute', display: 'none', @@ -68,7 +68,7 @@ export function loadEditionsGraph () { }).appendTo('body').fadeIn(200); } previousPoint = null; - placeholder.bind('plothover', function (event, pos, item) { + placeholder.bind('plothover', function(event, pos, item) { var x, y; $('#x').text(pos.x.toFixed(0)); $('#y').text(pos.y.toFixed(0)); @@ -93,7 +93,7 @@ export function loadEditionsGraph () { } }); - placeholder.bind('plotclick', function (event, pos, item) { + placeholder.bind('plotclick', function(event, pos, item) { if (item) { plot.unhighlight(); @@ -107,7 +107,7 @@ export function loadEditionsGraph () { } }); - placeholder.bind('plotselected', function (event, ranges) { + placeholder.bind('plotselected', function(event, ranges) { plot = $.plot(placeholder, data, $.extend(true, {}, options, { xaxis: { min: ranges.xaxis.from, max: ranges.xaxis.to }, @@ -120,7 +120,7 @@ export function loadEditionsGraph () { applyDateFilter(yearFrom, yearTo); }); - function applyDateFilter (yearFrom, yearTo, hideSelector='.chartUnzoom', showSelector='.chartZoom') { + function applyDateFilter(yearFrom, yearTo, hideSelector='.chartUnzoom', showSelector='.chartZoom') { document.dispatchEvent(new CustomEvent('filter', { detail: { yearFrom: yearFrom, yearTo: yearTo } })); $(hideSelector).hide(); $(showSelector).removeClass('hidden').show(); @@ -130,7 +130,7 @@ export function loadEditionsGraph () { dateFrom = plot.getAxes().xaxis.min.toFixed(0); dateTo = plot.getAxes().xaxis.max.toFixed(0); - $('.resetSelection').on('click', function () { + $('.resetSelection').on('click', function() { plot = $.plot(placeholder, data, options); const yearFrom = plot.getAxes().xaxis.min.toFixed(0); @@ -147,7 +147,7 @@ export function loadEditionsGraph () { } } -export function plot_minigraph (node, data) { +export function plot_minigraph(node, data) { var options = { series: { lines: { @@ -168,7 +168,7 @@ export function plot_minigraph (node, data) { $.plot(node, [data], options); } -export function plot_tooltip_graph (node, data, tooltip_message, color='#748d36') { +export function plot_tooltip_graph(node, data, tooltip_message, color='#748d36') { var i, options, graph; // empty set of rows. Escape early. if (!data.length) { @@ -204,7 +204,7 @@ export function plot_tooltip_graph (node, data, tooltip_message, color='#748d36' graph = $.plot(node, [data], options); - function showTooltip (x, y, contents) { + function showTooltip(x, y, contents) { $(`<div id="chartLabelA">${contents}</div>`).css({ position: 'absolute', display: 'none', @@ -220,7 +220,7 @@ export function plot_tooltip_graph (node, data, tooltip_message, color='#748d36' boxShadow: '1px 1px 1px #000' }).appendTo('body').fadeIn(200); } - node.bind('plothover', function (event, pos, item) { + node.bind('plothover', function(event, pos, item) { var date, milli, x, y; $('#x').text(pos.x); $('#y').text(pos.y.toFixed(0)); @@ -246,7 +246,7 @@ export function plot_tooltip_graph (node, data, tooltip_message, color='#748d36' * @param {string} [color] in hexidecimal to apply to the bars of a tooltip graph. * Ignored if options and no tooltip_message is passed. */ -export function loadGraph (id, options = {}, tooltip_message = '', color = null) { +export function loadGraph(id, options = {}, tooltip_message = '', color = null) { let data; const node = document.getElementById(id); const graphSelector = `graph-json-${id}`; @@ -282,7 +282,7 @@ export function loadGraph (id, options = {}, tooltip_message = '', color = null) * @param {string} [color] in hexidecimal to apply to the bars of a tooltip graph. * Ignored if options and no tooltip_message is passed. */ -export function loadGraphIfExists (id, options, tooltip_message, color) { +export function loadGraphIfExists(id, options, tooltip_message, color) { if ($(`#${id}`).length) { loadGraph(id, options, tooltip_message, color); } diff --git a/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js b/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js index 2e8b4c0f18c..e9eab127859 100644 --- a/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js +++ b/openlibrary/plugins/openlibrary/js/ia_thirdparty_logins.js @@ -3,13 +3,13 @@ * * @param {*} element - The element to be modified by the handleMessageEvent function. */ -export function initMessageEventListener (element) { +export function initMessageEventListener(element) { /** * Handles messages from archive.org and performs actions based on the message type. * * @param {MessageEvent} e - The message event. */ - function handleMessageEvent (e) { + function handleMessageEvent(e) { if (!/[./]archive\.org$$/.test(e.origin)) return; if (e.data.type === 'resize') { diff --git a/openlibrary/plugins/openlibrary/js/idValidation.js b/openlibrary/plugins/openlibrary/js/idValidation.js index 35977660381..c7b7343a29d 100644 --- a/openlibrary/plugins/openlibrary/js/idValidation.js +++ b/openlibrary/plugins/openlibrary/js/idValidation.js @@ -3,7 +3,7 @@ * @param {String} isbn ISBN string for parsing * @returns {String} parsed isbn string */ -export function parseIsbn (isbn) { +export function parseIsbn(isbn) { return isbn.replace(/[ -]/g, ''); } @@ -13,7 +13,7 @@ export function parseIsbn (isbn) { * @param {String} isbn ISBN string to check * @returns {boolean} true if the isbn has a valid format */ -export function isFormatValidIsbn10 (isbn) { +export function isFormatValidIsbn10(isbn) { const regex = /^[0-9]{9}[0-9X]$/; return regex.test(isbn); } @@ -24,7 +24,7 @@ export function isFormatValidIsbn10 (isbn) { * @param {String} isbn ISBN string for validating * @returns {boolean} true if ISBN string is a valid ISBN 10 */ -export function isChecksumValidIsbn10 (isbn) { +export function isChecksumValidIsbn10(isbn) { const chars = isbn.replace('X', 'A').split(''); chars.reverse(); @@ -42,7 +42,7 @@ export function isChecksumValidIsbn10 (isbn) { * @param {String} isbn ISBN string to check * @returns {boolean} true if the isbn has a valid format */ -export function isFormatValidIsbn13 (isbn) { +export function isFormatValidIsbn13(isbn) { const regex = /^[0-9]{13}$/; return regex.test(isbn); } @@ -53,7 +53,7 @@ export function isFormatValidIsbn13 (isbn) { * @param {String} isbn ISBN string for validating * @returns {Boolean} true if ISBN string is a valid ISBN 13 */ -export function isChecksumValidIsbn13 (isbn) { +export function isChecksumValidIsbn13(isbn) { const chars = isbn.split(''); const sum = chars .map((char, idx) => ((idx % 2 * 2 + 1) * parseInt(char, 10))) @@ -69,7 +69,7 @@ export function isChecksumValidIsbn13 (isbn) { * @param {String} lccn LCCN string for parsing * @returns {String} parsed LCCN string */ -export function parseLccn (lccn) { +export function parseLccn(lccn) { // cleaning initial lccn entry const parsed = lccn // any alpha characters need to be lowercase @@ -97,7 +97,7 @@ export function parseLccn (lccn) { * @param {String} lccn LCCN string to test for valid syntax * @returns {boolean} true if given LCCN is valid syntax, false otherwise */ -export function isValidLccn (lccn) { +export function isValidLccn(lccn) { // matching parsed entry to regex representing valid lccn // regex taken from /openlibrary/utils/lccn.py const regex = /^([a-z]|[a-z]?([a-z]{2}|[0-9]{2})|[a-z]{2}[0-9]{2})?[0-9]{8}$/; @@ -109,7 +109,7 @@ export function isValidLccn (lccn) { * @param {String} oclc OCLC string for parsing * @returns {String} parsed OCLC string */ -export function parseOclc (oclc) { +export function parseOclc(oclc) { // cleaning initial oclc entry return oclc // remove any whitespace @@ -127,7 +127,7 @@ export function parseOclc (oclc) { * @param {String} oclc OCLC string to test for valid syntax * @returns {boolean} true if given OCLC is valid syntax, false otherwise */ -export function isValidOclc (oclc) { +export function isValidOclc(oclc) { // matching parsed entry to regex representing valid oclc const regex = /^[1-9][0-9]*$/; return regex.test(oclc); @@ -142,7 +142,7 @@ export function isValidOclc (oclc) { * @param {String} newId New identifier entry to be checked * @returns {boolean} true if the new identifier has already been entered */ -export function isIdDupe (idEntries, newId) { +export function isIdDupe(idEntries, newId) { // check each current entry value against new identifier return Array.from(idEntries).some( entry => entry['value'] === newId diff --git a/openlibrary/plugins/openlibrary/js/ile/utils/ol.js b/openlibrary/plugins/openlibrary/js/ile/utils/ol.js index a38775a1b98..44333f2bd20 100644 --- a/openlibrary/plugins/openlibrary/js/ile/utils/ol.js +++ b/openlibrary/plugins/openlibrary/js/ile/utils/ol.js @@ -11,7 +11,7 @@ import uniqBy from 'lodash/uniqBy'; * @param {WorkOLID} old_work * @param {WorkOLID} new_work */ -export async function move_to_work (edition_ids, old_work, new_work) { +export async function move_to_work(edition_ids, old_work, new_work) { for (const olid of edition_ids) { const url = `/books/${olid}.json`; const record = await fetch(url).then(r => r.json()); @@ -30,7 +30,7 @@ export async function move_to_work (edition_ids, old_work, new_work) { * @param {AuthorOLID} old_author * @param {AuthorOLID} new_author */ -export async function move_to_author (work_ids, old_author, new_author) { +export async function move_to_author(work_ids, old_author, new_author) { for (const olid of work_ids) { const url = `/works/${olid}.json`; const record = await fetch(url).then(r => r.json()); diff --git a/openlibrary/plugins/openlibrary/js/index.js b/openlibrary/plugins/openlibrary/js/index.js index 0ffd7ef4b82..cf222b78fb8 100644 --- a/openlibrary/plugins/openlibrary/js/index.js +++ b/openlibrary/plugins/openlibrary/js/index.js @@ -24,7 +24,7 @@ initServiceWorker(); initAnalytics(); // Initialise some things -jQuery(function () { +jQuery(function() { // conditionally load polyfill for <details> tags (IE11) // See http://diveintohtml5.info/everything.html#details if (!('open' in document.createElement('details'))) { @@ -40,7 +40,7 @@ jQuery(function () { // Polyfill for .closest() if (!Element.prototype.closest) { - Element.prototype.closest = function (s) { + Element.prototype.closest = function(s) { let el = this; do { if (Element.prototype.matches.call(el, s)) return el; @@ -66,7 +66,7 @@ jQuery(function () { $('.no-img img').hide(); // disable save button after click - $('button[name=\'_save\']').on('submit', function () { + $('button[name=\'_save\']').on('submit', function() { $(this).attr('disabled', true); }); @@ -418,30 +418,30 @@ jQuery(function () { }); } - $(document).on('click', '.slide-toggle', function () { + $(document).on('click', '.slide-toggle', function() { $(`#${$(this).attr('aria-controls')}`).slideToggle(); }); - $('#wikiselect').on('focus', function (){$(this).trigger('select');}); + $('#wikiselect').on('focus', function(){$(this).trigger('select');}); - $('.hamburger-component .mask-menu').on('click', function () { + $('.hamburger-component .mask-menu').on('click', function() { $('details[open]').not(this).removeAttr('open'); }); - $('.header-dropdown').on('keydown', function (event) { + $('.header-dropdown').on('keydown', function(event) { if (event.key === 'Escape') { $('.header-dropdown > details[open]').removeAttr('open'); } }); - $('.dropdown-menu').each(function () { - $(this).find('a').last().on('focusout', function () { + $('.dropdown-menu').each(function() { + $(this).find('a').last().on('focusout', function() { $('.header-dropdown > details[open]').removeAttr('open'); }); }); // Open one dropdown at a time. - $(document).on('click', function (event) { + $(document).on('click', function(event) { const $openMenus = $('.header-dropdown details[open]').parents('.header-dropdown'); $openMenus .filter((_, menu) => !$(event.target).closest(menu).length) diff --git a/openlibrary/plugins/openlibrary/js/interstitial.js b/openlibrary/plugins/openlibrary/js/interstitial.js index 4d0dcfd03fb..a7fbc577c9c 100644 --- a/openlibrary/plugins/openlibrary/js/interstitial.js +++ b/openlibrary/plugins/openlibrary/js/interstitial.js @@ -1,4 +1,4 @@ -export function initInterstitial (elem) { +export function initInterstitial(elem) { let seconds = elem.dataset.wait; const url = elem.dataset.url; const timerElement = elem.querySelector('#timer'); diff --git a/openlibrary/plugins/openlibrary/js/isbnOverride.js b/openlibrary/plugins/openlibrary/js/isbnOverride.js index c50e875ced9..9a92f6da138 100644 --- a/openlibrary/plugins/openlibrary/js/isbnOverride.js +++ b/openlibrary/plugins/openlibrary/js/isbnOverride.js @@ -9,7 +9,7 @@ */ export const isbnOverride = { data: null, - set (isbnData) { this.data = isbnData; }, - get () { return this.data; }, - clear () { this.data = null; }, + set(isbnData) { this.data = isbnData; }, + get() { return this.data; }, + clear() { this.data = null; }, }; diff --git a/openlibrary/plugins/openlibrary/js/jquery.repeat.js b/openlibrary/plugins/openlibrary/js/jquery.repeat.js index 82b2f8416df..824ffd0f637 100644 --- a/openlibrary/plugins/openlibrary/js/jquery.repeat.js +++ b/openlibrary/plugins/openlibrary/js/jquery.repeat.js @@ -6,9 +6,9 @@ import { isbnOverride } from '../../openlibrary/js/isbnOverride'; * * Used in addbook process. */ -export function init () { +export function init() { // used in books/edit/exercpt, books/edit/web and books/edit/edition - $.fn.repeat = function (options) { + $.fn.repeat = function(options) { var addSelector, removeSelector, id, elems, t, code, nextRowId; options = options || {}; @@ -22,7 +22,7 @@ export function init () { template: $(`${id}-template`) }; - function createTemplate (selector) { + function createTemplate(selector) { code = $(selector).html() .replace(/%7B%7B/gi, '<%=') .replace(/%7D%7D/gi, '%>') @@ -38,9 +38,9 @@ export function init () { * object representing. * @return {object} data mapping names to values */ - function formdata () { + function formdata() { var data = {}; - $(':input', elems.form).each(function () { + $(':input', elems.form).each(function() { var $e = $(this), name = $e.attr('name'), type = $e.attr('type'), @@ -60,7 +60,7 @@ export function init () { * Creates a removable `repeat-item`. * @param {jQuery.Event} event */ - function onAdd (event) { + function onAdd(event) { var data, newid; const isbnOverrideData = isbnOverride.get(); event.preventDefault(); @@ -100,7 +100,7 @@ export function init () { elems._this.trigger('repeat-add'); } - function onRemove (event) { + function onRemove(event) { event.preventDefault(); $(this).parents('.repeat-item').eq(0).remove(); elems._this.trigger('repeat-remove'); diff --git a/openlibrary/plugins/openlibrary/js/jsdef.js b/openlibrary/plugins/openlibrary/js/jsdef.js index 18cb47c4f31..ed238867945 100644 --- a/openlibrary/plugins/openlibrary/js/jsdef.js +++ b/openlibrary/plugins/openlibrary/js/jsdef.js @@ -21,7 +21,7 @@ import { truncate, cond } from './utils'; */ //used in templates/lib/pagination.html -export function range (begin, end, step) { +export function range(begin, end, step) { var r, i; step = step || 1; if (end === undefined) { @@ -42,7 +42,7 @@ export function range (begin, end, step) { * > " - ".join(["a", "b", "c"]) * a - b - c */ -export function join (items) { +export function join(items) { return items.join(this); } @@ -51,12 +51,12 @@ export function join (items) { */ // used in templates/admin/loans.html -export function len (array) { +export function len(array) { return array.length; } // used in templates/type/permission/edit.html -export function enumerate (a) { +export function enumerate(a) { var b = new Array(a.length); var i; for (i in a) { @@ -65,7 +65,7 @@ export function enumerate (a) { return b; } -export function ForLoop (parent, seq) { +export function ForLoop(parent, seq) { this.parent = parent; this.seq = seq; @@ -73,7 +73,7 @@ export function ForLoop (parent, seq) { this.index0 = -1; } -ForLoop.prototype.next = function () { +ForLoop.prototype.next = function() { var i = this.index0+1; this.index0 = i; @@ -91,7 +91,7 @@ ForLoop.prototype.next = function () { }; // used in plugins/upstream/jsdef.py -export function foreach (seq, parent_loop, callback) { +export function foreach(seq, parent_loop, callback) { var loop = new ForLoop(parent_loop, seq); var i, args, j; @@ -114,7 +114,7 @@ export function foreach (seq, parent_loop, callback) { } // used in templates/lists/widget.html -export function websafe (value) { +export function websafe(value) { // Safari 6 is failing with weird javascript error in this function. // Added try-catch to avoid it. try { @@ -135,7 +135,7 @@ export function websafe (value) { * Quote a string * @param {string|number} text to quote */ -export function htmlquote (text) { +export function htmlquote(text) { // This code exists for compatibility with template.js text = String(text); text = text.replace(/&/g, '&'); // Must be done first! @@ -146,7 +146,7 @@ export function htmlquote (text) { return text; } -export function is_jsdef () { +export function is_jsdef() { return true; } @@ -160,11 +160,11 @@ export function is_jsdef () { * @param {string} key - the key to get from the object * @param {any} def - the default value to return if the key isn't found */ -export function jsdef_get (obj, key, def=null) { +export function jsdef_get(obj, key, def=null) { return (key in obj) ? obj[key] : def; } -export function exposeGlobally () { +export function exposeGlobally() { // Extend existing prototypes String.prototype.join = join; diff --git a/openlibrary/plugins/openlibrary/js/lazy-carousel.js b/openlibrary/plugins/openlibrary/js/lazy-carousel.js index 1aca14dcc44..5eed57e3bdd 100644 --- a/openlibrary/plugins/openlibrary/js/lazy-carousel.js +++ b/openlibrary/plugins/openlibrary/js/lazy-carousel.js @@ -7,7 +7,7 @@ import { buildPartialsUrl } from './utils'; * * @param elems {NodeList<HTMLElement>} Collection of placeholder carousel elements */ -export function initLazyCarousel (elems) { +export function initLazyCarousel(elems) { // Create intersection observer const intersectionObserver = new IntersectionObserver(intersectionCallback, { root: null, @@ -34,7 +34,7 @@ export function initLazyCarousel (elems) { * @param data {object} * @returns {Promise<Response>} */ -async function fetchPartials (data) { +async function fetchPartials(data) { return fetch(buildPartialsUrl('LazyCarousel', {...data})); } @@ -49,7 +49,7 @@ async function fetchPartials (data) { * * @param target {HTMLElement} A placeholder element for a carousel */ -function doFetchAndUpdate (target) { +function doFetchAndUpdate(target) { const config = JSON.parse(target.dataset.config); const loadingIndicator = target.querySelector('.loadingIndicator'); @@ -95,7 +95,7 @@ function doFetchAndUpdate (target) { * * @param target {Element} */ -function handleRetry (target) { +function handleRetry(target) { target.querySelector('.loadingIndicator').classList.remove('hidden'); target.querySelector('.lazy-carousel-retry').classList.add('hidden'); const carouselFallbackElem = target.querySelector('.lazy-carousel-fallback'); @@ -113,7 +113,7 @@ function handleRetry (target) { * @param entries {Array<IntersectionObserverEntry>} * @param observer {IntersectionObserver} */ -function intersectionCallback (entries, observer) { +function intersectionCallback(entries, observer) { entries.forEach(entry => { if (entry.isIntersecting) { const target = entry.target; diff --git a/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js b/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js index d00cc8da961..aca22bf8a28 100644 --- a/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js +++ b/openlibrary/plugins/openlibrary/js/lazy-thing-preview.js @@ -18,7 +18,7 @@ import chunk from 'lodash/chunk'; * Currently only works with works, editions, and authors. */ export class LazyThingPreview { - constructor () { + constructor() { /** @type {Array<{key: string, render_fn: Function}>} */ this.queue = []; /** @type {Object<string, object>} */ @@ -27,7 +27,7 @@ export class LazyThingPreview { this.renderDebounced = debounce(this.render.bind(this), 100); } - init () { + init() { $('.lazy-thing-preview').each((i, el) => { this.push({ key: el.dataset.key, @@ -39,7 +39,7 @@ export class LazyThingPreview { /** * @param {{key: string, render_fn_name: string}} arg0 */ - push ({key, render_fn_name}) { + push({key, render_fn_name}) { const render_fn = window[render_fn_name]; if (this.cache[key]) { this.renderKey(key, render_fn, this.cache[key]); @@ -54,7 +54,7 @@ export class LazyThingPreview { * @param {Function} render_fn * @param {object} book */ - renderKey (key, render_fn, book) { + renderKey(key, render_fn, book) { const $el = $(`.lazy-thing-preview[data-key="${key}"]`); $el.html(render_fn(book)); } @@ -63,7 +63,7 @@ export class LazyThingPreview { * @param {string[]} keys * @returns {AsyncGenerator<object[]>} */ - async* getThings (keys) { + async* getThings(keys) { const workKeys = keys.filter(key => key.startsWith('/works/')); const editionKeys = keys.filter(key => key.startsWith('/books/')); const authorKeys = keys.filter(key => key.startsWith('/authors/')); @@ -100,7 +100,7 @@ export class LazyThingPreview { } } - async render () { + async render() { const keys = this.queue.map(({key}) => key); const render_fn_map = Object.fromEntries( this.queue.map(({key, render_fn}) => [key, render_fn]) diff --git a/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js b/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js index 8c76b293ae7..2d62daf4021 100644 --- a/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js +++ b/openlibrary/plugins/openlibrary/js/librarian-dashboard/index.js @@ -8,7 +8,7 @@ let i18nStrings; * * @param {HTMLDetailsElement} rootElement */ -export function initLibrarianDashboard (rootElement) { +export function initLibrarianDashboard(rootElement) { i18nStrings = JSON.parse(rootElement.dataset.i18n); const table = rootElement.querySelector('.dq-table'); rootElement.addEventListener('click', () => { @@ -22,7 +22,7 @@ export function initLibrarianDashboard (rootElement) { * @param {HTMLTableElement} table * @returns {Promise<void>} */ -async function populateTable (table) { +async function populateTable(table) { const bookCount = Number(table.dataset.totalBooks); const rows = table.querySelectorAll('.dq-table__row'); @@ -36,7 +36,7 @@ async function populateTable (table) { * @param {number} totalCount Total number of search results * @returns {Promise<void>} */ -async function updateRow (row, totalCount) { +async function updateRow(row, totalCount) { const queryFragment = row.dataset.queryFragment; const apiUrl = buildUrl(queryFragment, false); const searchPageUrl = buildUrl(queryFragment); @@ -83,7 +83,7 @@ async function updateRow (row, totalCount) { * @param {string} queryFragment * @param {boolean} forUi */ -function buildUrl (queryFragment, forUi = true) { +function buildUrl(queryFragment, forUi = true) { const match = window.location.pathname.match(/authors\/(OL\d+A)/); const queryParamString = match ? `?q=author_key:${match[1]}` : window.location.search; @@ -98,7 +98,7 @@ function buildUrl (queryFragment, forUi = true) { * @param {HTMLTableRowElement} row * @param {string} newCellMarkup Markup for the new status cells */ -function replaceStatusCells (row, newCellMarkup) { +function replaceStatusCells(row, newCellMarkup) { const statusCells = row.querySelectorAll('td:not(.dq-table__criterion-cell)'); for (const cell of statusCells) { cell.remove(); @@ -118,7 +118,7 @@ function replaceStatusCells (row, newCellMarkup) { * * @returns {string} HTML string */ -function renderResultsCells (results, totalCount, failingHref) { +function renderResultsCells(results, totalCount, failingHref) { const numFound = results.numFound; const percentage = Math.floor(((totalCount - numFound) / totalCount) * 100); @@ -136,7 +136,7 @@ function renderResultsCells (results, totalCount, failingHref) { * * @returns {string} HTML string */ -function renderRetryCell () { +function renderRetryCell() { return `<td> <button class="dqs-run-again"> ${i18nStrings['reload']} @@ -150,7 +150,7 @@ function renderRetryCell () { * @param {string} href Link to search page for failing query * @returns {string} */ -function renderErrorCell (href) { +function renderErrorCell(href) { return `<td colspan="2"> <a href="${href}">${i18nStrings['error']}</a> </td>`; @@ -161,6 +161,6 @@ function renderErrorCell (href) { * * @returns {string} */ -function renderPendingCell () { +function renderPendingCell() { return `<td colspan="3">${i18nStrings['loading']}</td>`; } diff --git a/openlibrary/plugins/openlibrary/js/list_books.js b/openlibrary/plugins/openlibrary/js/list_books.js index 9e0c77afcd1..ab4c6b6ef4f 100644 --- a/openlibrary/plugins/openlibrary/js/list_books.js +++ b/openlibrary/plugins/openlibrary/js/list_books.js @@ -3,21 +3,21 @@ export class ListBooks { * @param {HTMLElement} listBooks * @param {HTMLElement} layoutToolbar **/ - constructor (listBooks, layoutToolbar) { + constructor(listBooks, layoutToolbar) { this.listBooks = listBooks; this.layoutToolbar = layoutToolbar; this.activeLayout = this.layoutToolbar.querySelector('a.active'); } - attach () { + attach() { $(this.layoutToolbar).on('click', 'a', this.updateLayout.bind(this)); } /** * @param {MouseEvent} event */ - updateLayout (event) { + updateLayout(event) { event.preventDefault(); const layoutAnchor = event.target; this.layoutToolbar.querySelector('a.active').classList.remove('active'); @@ -27,7 +27,7 @@ export class ListBooks { document.cookie = `LBL=${layout}; path=/; max-age=31536000`; } - static init () { + static init() { // Assume only one list-books/layout per page new ListBooks( document.querySelector('.list-books'), diff --git a/openlibrary/plugins/openlibrary/js/lists/ListService.js b/openlibrary/plugins/openlibrary/js/lists/ListService.js index 7311c6e24f1..7f501321c2c 100644 --- a/openlibrary/plugins/openlibrary/js/lists/ListService.js +++ b/openlibrary/plugins/openlibrary/js/lists/ListService.js @@ -12,7 +12,7 @@ import { buildPartialsUrl } from '../utils'; * @param {object} data Object containing the new list's name, description, and seeds. * @returns {Promise<Response>} The results of the POST request */ -export async function createList (userKey, data) { +export async function createList(userKey, data) { return await fetch(`${userKey}/lists.json`, { method: 'post', headers: { @@ -30,7 +30,7 @@ export async function createList (userKey, data) { * @param {object} seed Object containing the new list's name, description, and seeds. * @returns {Promise<Response>} The result of the POST request */ -export async function addItem (listKey, seed) { +export async function addItem(listKey, seed) { const body = { add: [seed] }; return await fetch(`${listKey}/seeds.json`, { method: 'post', @@ -49,7 +49,7 @@ export async function addItem (listKey, seed) { * @param {string|{ key: string }} seed The item being removed from the list. * @returns {Promise<Response>} The POST response */ -export async function removeItem (listKey, seed) { +export async function removeItem(listKey, seed) { const body = { remove: [seed] }; return await fetch(`${listKey}/seeds.json`, { method: 'post', @@ -62,7 +62,7 @@ export async function removeItem (listKey, seed) { } // XXX : jsdoc -export async function getListPartials () { +export async function getListPartials() { return await fetch(buildPartialsUrl('MyBooksDropperLists'), { headers: { 'Content-Type': 'application/json', diff --git a/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js b/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js index 845eef9a51d..d626b6ca0cc 100644 --- a/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js +++ b/openlibrary/plugins/openlibrary/js/lists/ListViewBody.js @@ -10,7 +10,7 @@ import 'jquery-ui/ui/widgets/dialog'; const itemsWithDeleteList = $('.deleteList .resultTitle'); if (itemsWithDeleteList.length) { const deleteListLink = $('.listDelete--myLists'); - itemsWithDeleteList.each(function () { + itemsWithDeleteList.each(function() { $(deleteListLink).clone().prependTo(this).removeClass('hidden'); }); @@ -24,7 +24,7 @@ if (itemsWithDeleteList.length) { const itemsWithDeleteSeed = $('.deleteSeed .resultTitle'); if (itemsWithDeleteSeed.length) { const deleteSeedLink = $('.seedDelete--myLists'); - itemsWithDeleteSeed.each(function () { + itemsWithDeleteSeed.each(function() { $(deleteSeedLink).clone().prependTo(this).removeClass('hidden'); }); @@ -38,7 +38,7 @@ if (itemsWithDeleteSeed.length) { * @param {string} seed - path to seed book being removed, ex: /books/OL23269118M * @param {function} success - click function */ -function remove_seed (list_key, seed, success) { +function remove_seed(list_key, seed, success) { if (seed[0] === '/') { seed = {key: seed}; } @@ -52,7 +52,7 @@ function remove_seed (list_key, seed, success) { }), dataType: 'json', - beforeSend: function (xhr) { + beforeSend: function(xhr) { xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Accept', 'application/json'); }, @@ -63,7 +63,7 @@ function remove_seed (list_key, seed, success) { /** * @returns {number} count of number of seed books in a list */ -function get_seed_count () { +function get_seed_count() { return $('ul#listResults').children().length; } @@ -85,7 +85,7 @@ const getConfirmButtonLabelText = () => { // Add listeners to each .listDelete link element // Sometimes .listDelete is dynamically added to the DOM, so we'll add the listener to a parent element -$('#listResults').on('click', '.listDelete a', function () { +$('#listResults').on('click', '.listDelete a', function() { if (get_seed_count() > 1 && !$(this).parent().hasClass('listDelete--myLists')) { $('#remove-seed-dialog') .data('seed-key', $(this).closest('[data-seed-key]').data('seed-key')) @@ -111,13 +111,13 @@ $('#remove-seed-dialog').dialog({ ConfirmRemoveSeed: { text: getConfirmButtonLabelText(), id: 'remove-seed-dialog--confirm', - click: function () { + click: function() { var list_key = $(this).data('list-key'); var seed_key = $(this).data('seed-key'); var _this = this; - remove_seed(list_key, seed_key, function () { + remove_seed(list_key, seed_key, function() { $(`[data-seed-key='${seed_key}']`).remove(); // update seed count $('#list-items-count').load(`${location.href} #list-items-count`); @@ -132,7 +132,7 @@ $('#remove-seed-dialog').dialog({ CancelRemoveSeed: { text: getCancelButtonLabelText(), id: 'remove-seed-dialog--cancel', - click: function () { + click: function() { $(this).dialog('close'); $('#remove-seed-dialog').addClass('hidden'); } @@ -150,11 +150,11 @@ $('#delete-list-dialog').dialog({ ConfirmDeleteList: { text: getConfirmButtonLabelText(), id: 'delete-list-dialog--confirm', - click: function () { + click: function() { var list_key = $(this).data('list-key'); var _this = this; - $.post(`${list_key}/delete.json`, function () { + $.post(`${list_key}/delete.json`, function() { $(_this).dialog('close'); window.location.reload(); }); @@ -163,7 +163,7 @@ $('#delete-list-dialog').dialog({ CancelDeleteList: { text: getCancelButtonLabelText(), id: 'delete-list-dialog--cancel', - click: function () { + click: function() { $(this).dialog('close'); } } diff --git a/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js b/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js index 716e9fdf235..2c522e21077 100644 --- a/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js +++ b/openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js @@ -32,7 +32,7 @@ export class ShowcaseItem { * * @param {HTMLElement} showcaseElem */ - constructor (showcaseElem) { + constructor(showcaseElem) { /** * Reference to the root element of this component. * @member {HTMLElement} @@ -97,7 +97,7 @@ export class ShowcaseItem { * Attaches click listeners to the showcase item's "Remove from list" * affordance. */ - initialize () { + initialize() { this.removeFromListAffordance.addEventListener('click', (event) => { event.preventDefault(); this.removeShowcaseItem(); @@ -110,7 +110,7 @@ export class ShowcaseItem { * Removes any affiliated showcase items from the DOM, and updates all * dropper list affordances. */ - async removeShowcaseItem () { + async removeShowcaseItem() { await removeItem(this.listKey, this.seed) .then(response => response.json()) .then(() => { @@ -140,7 +140,7 @@ export class ShowcaseItem { * Removes self from the myBooksStore's showcase array * upon success. */ - removeSelf () { + removeSelf() { const showcases = myBooksStore.getShowcases(); const thisIndex = showcases.indexOf(this); if (thisIndex >= 0) { @@ -160,7 +160,7 @@ export class ShowcaseItem { * * @param {boolean} showWorks `true` if only active showcase items related to works should be displayed */ - toggleVisibility (showWorks) { + toggleVisibility(showWorks) { if (this.isActiveShowcase) { if (showWorks) { if (this.isWork) { @@ -185,7 +185,7 @@ export class ShowcaseItem { * @param {string} seedKey * @return {boolean} `true` if the given keys match this item's keys */ - isShowcaseForListAndSeed (listKey, seedKey) { + isShowcaseForListAndSeed(listKey, seedKey) { return (this.listKey === listKey) && (this.seedKey === seedKey); } } @@ -205,7 +205,7 @@ const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; * @param {string} seed * @returns {string} Type of the given seed key. */ -function getSeedType (seed) { +function getSeedType(seed) { // XXX : validate input? if (seed[0] !== '/') { return 'subject'; @@ -233,7 +233,7 @@ function getSeedType (seed) { * @param {string} [coverUrl] * @returns {HTMLLIElement} */ -export function createActiveShowcaseItem (listKey, seedKey, listTitle, coverUrl = DEFAULT_COVER_URL) { +export function createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl = DEFAULT_COVER_URL) { if (!i18nStrings) { const i18nInput = document.querySelector('input[name=list-i18n-strings]'); i18nStrings = JSON.parse(i18nInput.value); @@ -275,7 +275,7 @@ export function createActiveShowcaseItem (listKey, seedKey, listTitle, coverUrl * * @param {boolean} showWorksOnly */ -export function toggleActiveShowcaseItems (showWorksOnly) { +export function toggleActiveShowcaseItems(showWorksOnly) { for (const item of myBooksStore.getShowcases()) { item.toggleVisibility(showWorksOnly); } @@ -296,7 +296,7 @@ export function toggleActiveShowcaseItems (showWorksOnly) { * @param {string} listTitle * @param {string} [coverUrl] */ -export function attachNewActiveShowcaseItem (listKey, seedKey, listTitle, coverUrl = DEFAULT_COVER_URL) { +export function attachNewActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl = DEFAULT_COVER_URL) { const activeListsShowcase = document.querySelector('.already-lists'); if (activeListsShowcase) { diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js index 8ea03ca21f0..198101c4ac4 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable.js @@ -20,7 +20,7 @@ export default class MergeRequestTable { * * @param {HTMLElement} mergeRequestTable */ - constructor (mergeRequestTable) { + constructor(mergeRequestTable) { /** * The `username` of the authenticated patron, or '' if logged out. * @@ -53,7 +53,7 @@ export default class MergeRequestTable { /** * Hydrates the librarian request table. */ - initialize () { + initialize() { this.tableHeader.initialize(); document.addEventListener('click', (event) => this.tableHeader.closeMenusIfClickOutside(event)); this.tableRows.forEach(elem => elem.initialize()); diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js index 48d79d10e2d..fc44a161bec 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableHeader.js @@ -19,7 +19,7 @@ export default class TableHeader { * * @param {HTMLElement} tableHeader */ - constructor (tableHeader) { + constructor(tableHeader) { /** * References to each select menu. These are always visible * in the header bar, and, when clicked, display a drop-down @@ -52,7 +52,7 @@ export default class TableHeader { /** * Hydrates the table header affordances. */ - initialize () { + initialize() { this.initFilters(); } @@ -62,7 +62,7 @@ export default class TableHeader { * @param {Event} event * @param {string} menuButtonId */ - toggleAMenuWhileClosingOthers (event, menuButtonId) { + toggleAMenuWhileClosingOthers(event, menuButtonId) { // prevent closing of menu on bubbling unless click menuButton itself if (event.target.id === menuButtonId) { // close other open menus, then toggle selected menu @@ -76,7 +76,7 @@ export default class TableHeader { * * @param {string} menuButtonId */ - closeOtherMenus (menuButtonId) { + closeOtherMenus(menuButtonId) { this.dropMenuButtons.forEach((menuButton) => { if (menuButton.id !== menuButtonId) { menuButton.firstElementChild.classList.add('hidden'); @@ -89,7 +89,7 @@ export default class TableHeader { * * @param {Event} event */ - filterMenuItems (event) { + filterMenuItems(event) { const input = document.getElementById(event.target.id); const filter = input.value.toUpperCase(); const menu = input.closest('.mr-dropdown-menu'); @@ -107,7 +107,7 @@ export default class TableHeader { * * @param {Event} event */ - closeMenusIfClickOutside (event) { + closeMenusIfClickOutside(event) { const menusClicked = Array.from(this.dropMenuButtons).filter((menuButton) => { return menuButton.contains(event.target); }); @@ -121,7 +121,7 @@ export default class TableHeader { * Initialize events for merge queue filter dropdown menu functionality. * */ - initFilters () { + initFilters() { this.dropMenuButtons.forEach((menuButton) => { menuButton.addEventListener('click', (event) => { this.toggleAMenuWhileClosingOthers(event, menuButton.id); diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js index 76cb134d942..77a90956c81 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/MergeRequestTable/TableRow.js @@ -9,7 +9,7 @@ import { FadingToast } from '../../Toast'; let i18nStrings; -export function setI18nStrings (localizedStrings) { +export function setI18nStrings(localizedStrings) { i18nStrings = localizedStrings; } @@ -34,7 +34,7 @@ export class TableRow { * @param {HTMLElement} row Root element of a table row * @param {string} username `username` of logged-in patron. Empty if unauthenticated. */ - constructor (row, username) { + constructor(row, username) { /** * Reference to this row. * @@ -132,7 +132,7 @@ export class TableRow { /** * Hydrates interactive elements in this row. */ - initialize () { + initialize() { this.toggleCommentButton.addEventListener('click', () => this.toggleComments()); if (this.closeRequestButton) { this.closeRequestButton.addEventListener('click', () => this.closeRequest()); @@ -153,7 +153,7 @@ export class TableRow { * the full comments panel is hidden. This function toggles * each element's visibility. */ - toggleComments () { + toggleComments() { this.commentPreview.classList.toggle('hidden'); this.fullCommentsPanel.classList.toggle('hidden'); @@ -165,7 +165,7 @@ export class TableRow { * Closes the request linked to this row, and removes this * row from the DOM. */ - async closeRequest () { + async closeRequest() { const comment = prompt(i18nStrings['close_request_comment_prompt']); if (comment !== null) { // Comment will be `null` if "Cancel" button pressed await declineRequest(this.mrid, comment) @@ -187,7 +187,7 @@ export class TableRow { * * Updates the view on success. */ - async addComment () { + async addComment() { const comment = this.commentReplyInput.value.trim(); if (comment) { await commentOnRequest(this.mrid, comment) @@ -215,7 +215,7 @@ export class TableRow { * * @param {string} comment The newly added comment. */ - updateCommentViews (comment) { + updateCommentViews(comment) { const escapedComment = document.createTextNode(comment); // Update preview: @@ -240,7 +240,7 @@ export class TableRow { * * Hides the review button, and shows the assignee display. */ - async claimRequest () { + async claimRequest() { await claimRequest(this.mrid) .then(result => result.json()) .then(data => { @@ -257,7 +257,7 @@ export class TableRow { * * Hides the assignee display and shows the review button on success. */ - async unassignReviewer () { + async unassignReviewer() { await unassignRequest(this.mrid) .then(result => result.json()) .then(data => { diff --git a/openlibrary/plugins/openlibrary/js/merge-request-table/index.js b/openlibrary/plugins/openlibrary/js/merge-request-table/index.js index 113dfdf5daf..0b710fb9fc4 100644 --- a/openlibrary/plugins/openlibrary/js/merge-request-table/index.js +++ b/openlibrary/plugins/openlibrary/js/merge-request-table/index.js @@ -5,7 +5,7 @@ import MergeRequestTable from './MergeRequestTable'; * * @param {HTMLElement} elem Reference to the queue's root element. */ -export function initLibrarianQueue (elem) { +export function initLibrarianQueue(elem) { const librarianQueue = new MergeRequestTable(elem); librarianQueue.initialize(); } diff --git a/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js b/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js index b92a747fdc3..76947bfe26f 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js +++ b/openlibrary/plugins/openlibrary/js/my-books/CreateListForm.js @@ -28,7 +28,7 @@ export class CreateListForm { * * @param {HTMLElement} form */ - constructor (form) { + constructor(form) { /** * References this form's "Create List" button. * @@ -57,7 +57,7 @@ export class CreateListForm { /** * Attaches click listener to the "Create List" button. */ - initialize () { + initialize() { this.createListButton.addEventListener('click', (event) =>{ event.preventDefault(); this.createNewList(); @@ -79,7 +79,7 @@ export class CreateListForm { * * @async */ - async createNewList () { + async createNewList() { // Construct seed object for first list item: const listTitle = websafe(this.listTitleInput.value); const listDescription = websafe(this.listDescriptionInput.value); @@ -118,7 +118,7 @@ export class CreateListForm { * @param {string} listKey Key of the newly created list * @param {string} listTitle Title of the new list */ - updateDroppersOnListCreation (listKey, listTitle, coverUrl) { + updateDroppersOnListCreation(listKey, listTitle, coverUrl) { const droppers = myBooksStore.getDroppers(); const openDropper = myBooksStore.getOpenDropper(); @@ -131,7 +131,7 @@ export class CreateListForm { /** * Clears the list title and desciption fields in the form. */ - resetForm () { + resetForm() { this.listTitleInput.value = ''; this.listDescriptionInput.value = ''; } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js index b705a92f539..7fd568b7c0c 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper.js @@ -32,7 +32,7 @@ export class MyBooksDropper extends Dropper { * * @param {HTMLElement} dropper */ - constructor (dropper) { + constructor(dropper) { super(dropper); const dropperActionCallbacks = { @@ -85,7 +85,7 @@ export class MyBooksDropper extends Dropper { /** * Hydrates dropper contents and loads patron's lists. */ - initialize () { + initialize() { super.initialize(); this.readingLogForms.initialize(); @@ -103,9 +103,9 @@ export class MyBooksDropper extends Dropper { * @param {HTMLElement} loadingIndicator * @returns {NodeJS.Timer} */ - initLoadingAnimation (loadingIndicator) { + initLoadingAnimation(loadingIndicator) { let count = 0; - const intervalId = setInterval(function () { + const intervalId = setInterval(function() { let ellipsis = ''; for (let i = 0; i < count % 4; ++i) { ellipsis += '.'; @@ -123,7 +123,7 @@ export class MyBooksDropper extends Dropper { * * @param {string} partialHtml */ - updateReadingLists (partialHtml) { + updateReadingLists(partialHtml) { clearInterval(this.loadingAnimationId); this.replaceLoadingIndicators(this.loadingIndicator, partialHtml); } @@ -137,7 +137,7 @@ export class MyBooksDropper extends Dropper { * * @returns {Array<string>} */ - getSeedKeys () { + getSeedKeys() { const results = [this.readingLists.seedKey]; if (this.readingLists.workKey) { results.push(this.readingLists.workKey); @@ -158,7 +158,7 @@ export class MyBooksDropper extends Dropper { * @param {HTMLElement} dropperListsPlaceholder Loading indicator found inside of the dropdown content * @param {ListPartials} partials */ - replaceLoadingIndicators (dropperListsPlaceholder, partialHTML) { + replaceLoadingIndicators(dropperListsPlaceholder, partialHTML) { const dropperParent = dropperListsPlaceholder ? dropperListsPlaceholder.parentElement : null; if (dropperParent) { @@ -179,7 +179,7 @@ export class MyBooksDropper extends Dropper { * * @param shelf {ReadingLogShelf} */ - updateShelfDisplay (shelf) { + updateShelfDisplay(shelf) { this.readingLogForms.updateActivatedStatus(true); this.readingLogForms.updatePrimaryBookshelfId(Number(shelf)); this.readingLogForms.updatePrimaryButtonText(this.readingLogForms.getDisplayString(shelf)); @@ -199,7 +199,7 @@ export class MyBooksDropper extends Dropper { * * @override */ - onOpen () { + onOpen() { myBooksStore.setOpenDropper(this); } @@ -210,7 +210,7 @@ export class MyBooksDropper extends Dropper { * * @override */ - onDisabledClick () { + onDisabledClick() { window.location = `/account/login?redirect=${location.pathname}`; } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js index b2aed540a3f..55956db9868 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/CheckInComponents.js @@ -20,7 +20,7 @@ const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; * @param {Number} year * @returns `true` if the given year is a leap year. */ -function isLeapYear (year) { +function isLeapYear(year) { return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); } @@ -40,7 +40,7 @@ export class CheckInComponents { /** * @param checkInContainer */ - constructor (checkInContainer) { + constructor(checkInContainer) { // HTML for the check-in components is not rendered if // the patron is unauthenticated, or if the dropper // is for an orphaned edition. @@ -86,7 +86,7 @@ export class CheckInComponents { this.checkInForm = undefined; } - initialize () { + initialize() { this.checkInPrompt.initialize(); this.checkInPrompt.getRootElement().addEventListener('submit-check-in', (event) => { const year = event.detail.year; @@ -172,7 +172,7 @@ export class CheckInComponents { * * @returns {HTMLElement} */ - createModalContentFromTemplate () { + createModalContentFromTemplate() { const templateElem = document.createElement('template'); const modalContentTemplate = document.querySelector('#check-in-form-modal'); templateElem.innerHTML = modalContentTemplate.outerHTML; @@ -189,7 +189,7 @@ export class CheckInComponents { * @param {number|null} month * @param {number|null} day */ - updateDateAndShowDisplay (year, month = null, day = null) { + updateDateAndShowDisplay(year, month = null, day = null) { // Update last read date display let dateString = String(year); if (month) { @@ -226,7 +226,7 @@ export class CheckInComponents { * @param {string} url * @returns {Promise<Response>} */ - postCheckIn (eventData, url) { + postCheckIn(eventData, url) { return fetch(url, { method: 'POST', headers: { @@ -243,7 +243,7 @@ export class CheckInComponents { * @param {string} eventId * @returns {Promise<Response>} */ - async deleteCheckIn (eventId) { + async deleteCheckIn(eventId) { return fetch(`/check-ins/${eventId}`, { method: 'DELETE' }); @@ -257,7 +257,7 @@ export class CheckInComponents { * @param {number|null} day * @returns {CheckInEventPostRequestData} */ - prepareEventRequest (year, month = null, day = null) { + prepareEventRequest(year, month = null, day = null) { // Get event id const eventId = this.checkInForm.getEventId(); @@ -285,49 +285,49 @@ export class CheckInComponents { * * @returns {boolean} */ - hasReadDate () { + hasReadDate() { return !this.checkInDisplay.getRootElement().classList.contains('hidden'); } /** * Resets the check-in form. */ - resetForm () { + resetForm() { this.checkInForm.resetForm(); } /** * Show the check-in display. */ - showCheckInDisplay () { + showCheckInDisplay() { this.checkInDisplay.show(); } /** * Hide the check-in display. */ - hideCheckInDisplay () { + hideCheckInDisplay() { this.checkInDisplay.hide(); } /** * Show the check-in prompt. */ - showCheckInPrompt () { + showCheckInPrompt() { this.checkInPrompt.show(); } /** * Hide the check-in prompt. */ - hideCheckInPrompt () { + hideCheckInPrompt() { this.checkInPrompt.hide(); } /** * Closes the opened `colorbox` modal. */ - closeModal () { + closeModal() { $.colorbox.close(); } } @@ -342,11 +342,11 @@ class CheckInPrompt { /** * @param {HTMLElement} checkInPrompt */ - constructor (checkInPrompt) { + constructor(checkInPrompt) { this.rootElem = checkInPrompt; } - initialize () { + initialize() { const yearLink = this.rootElem.querySelector('.prompt-current-year'); yearLink.addEventListener('click', () => { // Get the current year @@ -374,7 +374,7 @@ class CheckInPrompt { * @param {number|null} month * @param {number|null} day */ - dispatchCheckInSubmission (year, month = null, day = null) { + dispatchCheckInSubmission(year, month = null, day = null) { const submitEvent = new CustomEvent('submit-check-in', { detail: { year: year, @@ -388,14 +388,14 @@ class CheckInPrompt { /** * Hides this check-in prompt. */ - hide () { + hide() { this.rootElem.classList.add('hidden'); } /** * Shows this check-in prompt. */ - show () { + show() { this.rootElem.classList.remove('hidden'); } @@ -403,7 +403,7 @@ class CheckInPrompt { * Returns reference to the root element of this check-in prompt. * @returns {HTMLElement} */ - getRootElement () { + getRootElement() { return this.rootElem; } } @@ -417,7 +417,7 @@ class CheckInDisplay { /** * @param {HTMLElement} checkInDisplay */ - constructor (checkInDisplay) { + constructor(checkInDisplay) { this.rootElem = checkInDisplay; this.dateDisplayElem = this.rootElem.querySelector('.check-in-date'); } @@ -427,28 +427,28 @@ class CheckInDisplay { * * @param {string} date */ - updateDateDisplay (date) { + updateDateDisplay(date) { this.dateDisplayElem.textContent = date; } /** * Hides this date display. */ - hide () { + hide() { this.rootElem.classList.add('hidden'); } /** * Shows this date display. */ - show () { + show() { this.rootElem.classList.remove('hidden'); } /** * @returns {HTMLElement} */ - getRootElement () { + getRootElement() { return this.rootElem; } } @@ -471,7 +471,7 @@ export class CheckInForm { * @param {string|null} lastReadDate * @param {number|null} eventId */ - constructor (formElem, workOlid, editionKey = null, lastReadDate = null, eventId = null) { + constructor(formElem, workOlid, editionKey = null, lastReadDate = null, eventId = null) { this.rootElem = formElem; this.workOlid = workOlid; this.editionKey = editionKey; @@ -534,7 +534,7 @@ export class CheckInForm { this.deleteButton = this.rootElem.querySelector('.check-in__delete-btn'); } - initialize () { + initialize() { // Set form's action this.rootElem.action = `/works/${this.workOlid}/check-ins.json`; // Set form's event ID @@ -614,7 +614,7 @@ export class CheckInForm { /** * Gets currently selected date, then updates the form. */ - onDateSelectionChange () { + onDateSelectionChange() { const year = this.yearSelect.selectedIndex ? Number(this.yearSelect.value) : null; this.updateSelectedDate(year, this.monthSelect.selectedIndex, this.daySelect.selectedIndex); } @@ -626,7 +626,7 @@ export class CheckInForm { * @param {number|null} month * @param {number|null} day */ - updateSelectedDate (year = null, month = null, day = null) { + updateSelectedDate(year = null, month = null, day = null) { if (!month) { day = null; } @@ -671,7 +671,7 @@ export class CheckInForm { * * @param {number} daysInMonth */ - updateDayOptions (daysInMonth) { + updateDayOptions(daysInMonth) { for (let i = 0; i < this.daySelect.options.length; ++i) { if (i <= daysInMonth) { this.daySelect.options[i].classList.remove('hidden'); @@ -687,7 +687,7 @@ export class CheckInForm { * Unsets the `event_id` input value, hides the delete button, and * resets the date select elements to their default values. */ - resetForm () { + resetForm() { this.setEventId(''); this.updateSelectedDate(); this.hideDeleteButton(); @@ -696,14 +696,14 @@ export class CheckInForm { /** * Shows this form's delete button. */ - showDeleteButton () { + showDeleteButton() { this.deleteButton.classList.remove('invisible'); } /** * Hides this form's delete button. */ - hideDeleteButton () { + hideDeleteButton() { this.deleteButton.classList.add('invisible'); } @@ -712,7 +712,7 @@ export class CheckInForm { * * @returns {number|null} The selected year, or `null` if none selected */ - getSelectedYear () { + getSelectedYear() { return this.yearSelect.selectedIndex ? Number(this.yearSelect.value) : null; } @@ -721,7 +721,7 @@ export class CheckInForm { * * @returns {number|null} The selected month, or `null` if none selected */ - getSelectedMonth () { + getSelectedMonth() { return this.monthSelect.selectedIndex || null; } @@ -730,7 +730,7 @@ export class CheckInForm { * * @returns {number|null} The selected day, or `null` if none selected */ - getSelectedDay () { + getSelectedDay() { return this.daySelect.selectedIndex || null; } @@ -739,7 +739,7 @@ export class CheckInForm { * * @returns {string} */ - getEventId () { + getEventId() { return this.eventIdInput.value; } @@ -748,7 +748,7 @@ export class CheckInForm { * * @param value */ - setEventId (value) { + setEventId(value) { this.eventIdInput.value = value; } @@ -757,7 +757,7 @@ export class CheckInForm { * * @returns {string} */ - getEventType () { + getEventType() { return this.eventTypeInput.value; } @@ -766,7 +766,7 @@ export class CheckInForm { * * @returns {string} */ - getEditionKey () { + getEditionKey() { return this.editionKeyInput.value; } @@ -775,7 +775,7 @@ export class CheckInForm { * * @returns {string} */ - getFormAction () { + getFormAction() { return this.rootElem.action; } @@ -784,7 +784,7 @@ export class CheckInForm { * * @returns {HTMLFormElement} */ - getRootElement () { + getRootElement() { return this.rootElem; } } diff --git a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js index b44bc6c9958..aa89a84a6d6 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js +++ b/openlibrary/plugins/openlibrary/js/my-books/MyBooksDropper/ReadingLists.js @@ -22,7 +22,7 @@ export class ReadingLists { * Adds functionality to the given dropper's list affordances. * @param {HTMLElement} dropper */ - constructor (dropper) { + constructor(dropper) { /** * References the given My Books Dropper root element. * @@ -92,7 +92,7 @@ export class ReadingLists { /** * Adds functionality to all of the dropper's list affordances. */ - initialize () { + initialize() { this.initModifyListAffordances(this.dropper.querySelectorAll('.modify-list')); const openListModalButton = this.dropper.querySelector('.create-new-list'); @@ -112,7 +112,7 @@ export class ReadingLists { /** * Updates dropdown list affordances when an update occurs. */ - updateListDisplays () { + updateListDisplays() { const isWorkSelected = this.workCheckBox && this.workCheckBox.checked; for (const key of Object.keys(this.patronLists)) { const listData = this.patronLists[key]; @@ -135,7 +135,7 @@ export class ReadingLists { * @param {boolean} isListMember True if the item is on the list * @param {string} listKey Unique identifier for a list */ - toggleDisplayedType (isListMember, listKey) { + toggleDisplayedType(isListMember, listKey) { const listData = this.patronLists[listKey]; if (isListMember) { @@ -153,7 +153,7 @@ export class ReadingLists { * * @param {NodeList<HTMLElement>} modifyListElements */ - initModifyListAffordances (modifyListElements) { + initModifyListAffordances(modifyListElements) { for (const elem of modifyListElements) { const listItemKeys = elem.dataset.listItems; const listKey = elem.dataset.listKey; @@ -204,7 +204,7 @@ export class ReadingLists { * @param {string} listKey Unique key for list * @param {boolean} isAddingItem `true` if an item is being added to a list */ - async modifyList (listKey, isAddingItem) { + async modifyList(listKey, isAddingItem) { let seed; const isWork = this.workCheckBox && this.workCheckBox.checked; @@ -265,7 +265,7 @@ export class ReadingLists { * @param {boolean} isWork `true` if a work was added or removed * @param {boolean} wasItemAdded `true` if item was added to list */ - updateViewAfterModifyingList (listKey, isWork, wasItemAdded) { + updateViewAfterModifyingList(listKey, isWork, wasItemAdded) { if (isWork) { this.patronLists[listKey].workOnList = wasItemAdded; } else { @@ -283,7 +283,7 @@ export class ReadingLists { * * @param {HTMLElement} openListModalButton */ - addOpenListModalClickListener (openListModalButton) { + addOpenListModalClickListener(openListModalButton) { openListModalButton.addEventListener('click', (event) => { event.preventDefault(); @@ -305,7 +305,7 @@ export class ReadingLists { * @param {boolean} isActive `True` if this dropper's seed is on the list * @param {string} coverUrl URL for the list's cover image */ - onListCreationSuccess (listKey, listTitle, isActive, coverUrl) { + onListCreationSuccess(listKey, listTitle, isActive, coverUrl) { const dropperListAffordance = this.createDropdownListAffordance(listKey, listTitle, isActive); this.patronLists[listKey] = { @@ -333,7 +333,7 @@ export class ReadingLists { * @param {boolean} isActive `true` if the seed is on this list * @returns {HTMLElement} Reference to the newly created element */ - createDropdownListAffordance (listKey, listTitle, isActive) { + createDropdownListAffordance(listKey, listTitle, isActive) { const itemMarkUp = `<span class="list__status-indicator"></span> <a href="${listKey}" class="modify-list dropper__close" data-list-cover-url="${listKey}" data-list-key="${listKey}">${listTitle}</a> `; @@ -360,7 +360,7 @@ export class ReadingLists { * * @returns {string} The seed key */ - getSeed () { + getSeed() { if (this.workCheckBox && this.workCheckBox.checked) { // seed is the work key: return this.workKey; diff --git a/openlibrary/plugins/openlibrary/js/my-books/index.js b/openlibrary/plugins/openlibrary/js/my-books/index.js index c1e50d9314d..1534af56ddb 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/index.js +++ b/openlibrary/plugins/openlibrary/js/my-books/index.js @@ -7,7 +7,7 @@ import { removeChildren } from '../utils'; // XXX : jsdoc // XXX : decompose -export function initMyBooksAffordances (dropperElements, showcaseElements) { +export function initMyBooksAffordances(dropperElements, showcaseElements) { const showcases = []; for (const elem of showcaseElements) { const showcase = new ShowcaseItem(elem); @@ -91,7 +91,7 @@ export function initMyBooksAffordances (dropperElements, showcaseElements) { * @param workKey {string} * @returns {MyBooksDropper|undefined} */ -export function findDropperForWork (workKey) { +export function findDropperForWork(workKey) { return myBooksStore.getDroppers().find(dropper => { return workKey === dropper.workKey; }); diff --git a/openlibrary/plugins/openlibrary/js/my-books/store/index.js b/openlibrary/plugins/openlibrary/js/my-books/store/index.js index 59756cdf267..9a520d0de25 100644 --- a/openlibrary/plugins/openlibrary/js/my-books/store/index.js +++ b/openlibrary/plugins/openlibrary/js/my-books/store/index.js @@ -11,7 +11,7 @@ class MyBooksStore { /** * Initializes the store. */ - constructor () { + constructor() { this._store = { droppers: [], showcases: [], @@ -23,56 +23,56 @@ class MyBooksStore { /** * @returns {Array<MyBooksDropper>} */ - getDroppers () { + getDroppers() { return this._store.droppers; } /** * @param {Array<MyBooksDropper>} droppers */ - setDroppers (droppers) { + setDroppers(droppers) { this._store.droppers = droppers; } /** * @returns {Array<ShowcaseItem>} */ - getShowcases () { + getShowcases() { return this._store.showcases; } /** * @param {Array<ShowcaseItem>} showcases */ - setShowcases (showcases) { + setShowcases(showcases) { this._store.showcases = showcases; } /** * @returns {string} */ - getUserKey () { + getUserKey() { return this._store.userKey; } /** * @param {string} userKey */ - setUserKey (userKey) { + setUserKey(userKey) { this._store.userKey = userKey; } /** * @returns {MyBooksDropper} */ - getOpenDropper () { + getOpenDropper() { return this._store.openDropper; } /** * @param {MyBooksDropper} dropper */ - setOpenDropper (dropper) { + setOpenDropper(dropper) { this._store.openDropper = dropper; } } diff --git a/openlibrary/plugins/openlibrary/js/native-dialog/index.js b/openlibrary/plugins/openlibrary/js/native-dialog/index.js index d39d96130cc..a6c7ed9c45a 100644 --- a/openlibrary/plugins/openlibrary/js/native-dialog/index.js +++ b/openlibrary/plugins/openlibrary/js/native-dialog/index.js @@ -6,9 +6,9 @@ * 2. The dialog receives a `close-dialog` event. * @param {HTMLCollection<HTMLDialogElement>} elems */ -export function initDialogs (elems) { +export function initDialogs(elems) { for (const elem of elems) { - elem.addEventListener('click', function (event) { + elem.addEventListener('click', function(event) { // Event target exclusions needed for FireFox, which sets mouse positions to zero on // <select> and <option> clicks @@ -16,11 +16,11 @@ export function initDialogs (elems) { elem.close(); } }); - elem.addEventListener('close-dialog', function () { + elem.addEventListener('close-dialog', function() { elem.close(); }); const closeIcon = elem.querySelector('.native-dialog--close'); - closeIcon.addEventListener('click', function () { + closeIcon.addEventListener('click', function() { elem.close(); }); } @@ -33,7 +33,7 @@ export function initDialogs (elems) { * @param {HTMLDialogElement} dialog * @returns `true` if the click was out of bounds. */ -function isOutOfBounds (event, dialog) { +function isOutOfBounds(event, dialog) { const rect = dialog.getBoundingClientRect(); return ( event.clientX < rect.left || diff --git a/openlibrary/plugins/openlibrary/js/nonjquery_utils.js b/openlibrary/plugins/openlibrary/js/nonjquery_utils.js index 3c93e07cedc..15d080a9ac6 100644 --- a/openlibrary/plugins/openlibrary/js/nonjquery_utils.js +++ b/openlibrary/plugins/openlibrary/js/nonjquery_utils.js @@ -12,11 +12,11 @@ * @param {Boolean} [execAsap] * @returns {Function} */ -export function debounce (func, threshold=100, execAsap=false) { +export function debounce(func, threshold=100, execAsap=false) { let timeout; - return function debounced () { + return function debounced() { const obj = this, args = arguments; - function delayed () { + function delayed() { if (!execAsap) func.apply(obj, args); timeout = null; diff --git a/openlibrary/plugins/openlibrary/js/offline-banner.js b/openlibrary/plugins/openlibrary/js/offline-banner.js index 87bef872a97..917579fe51a 100644 --- a/openlibrary/plugins/openlibrary/js/offline-banner.js +++ b/openlibrary/plugins/openlibrary/js/offline-banner.js @@ -1,4 +1,4 @@ -export function initOfflineBanner () { +export function initOfflineBanner() { window.addEventListener('offline', () => { $('#offline-info').slideDown(); diff --git a/openlibrary/plugins/openlibrary/js/ol.analytics.js b/openlibrary/plugins/openlibrary/js/ol.analytics.js index 78c76651912..19490624f17 100644 --- a/openlibrary/plugins/openlibrary/js/ol.analytics.js +++ b/openlibrary/plugins/openlibrary/js/ol.analytics.js @@ -5,14 +5,14 @@ * */ -export default function initAnalytics () { +export default function initAnalytics() { var vs, i; var startTime = new Date(); if (window.archive_analytics) { // Setup analytics, depends on script loaded from CDN window.archive_analytics.set_up_event_tracking(); - window.archive_analytics.ol_send_event_ping = function (values) { + window.archive_analytics.ol_send_event_ping = function(values) { var endTime = new Date(); window.archive_analytics.send_ping({ service: 'ol', @@ -36,7 +36,7 @@ export default function initAnalytics () { if (window.flights){ window.flights.init(); } - $(document).on('click', '[data-ol-link-track]', function () { + $(document).on('click', '[data-ol-link-track]', function() { var category_action = $(this).attr('data-ol-link-track').split('|'); // for testing, // console.log(category_action[0], category_action[1]); @@ -50,7 +50,7 @@ export default function initAnalytics () { window.vs = vs; // NOTE: This might cause issues if this script is made async #4474 - window.addEventListener('DOMContentLoaded', function send_analytics_pageview () { + window.addEventListener('DOMContentLoaded', function send_analytics_pageview() { window.archive_analytics.send_pageview({}); }); } diff --git a/openlibrary/plugins/openlibrary/js/ol.js b/openlibrary/plugins/openlibrary/js/ol.js index e9be0e3ee45..3341ef10ad9 100644 --- a/openlibrary/plugins/openlibrary/js/ol.js +++ b/openlibrary/plugins/openlibrary/js/ol.js @@ -6,11 +6,11 @@ import { SearchModeSelector, mode as searchMode } from './SearchUtils'; /* Sets the key in the website cookie to the specified value */ -function setValueInCookie (key, value) { +function setValueInCookie(key, value) { document.cookie = `${key}=${value};path=/`; } -export default function init () { +export default function init() { const urlParams = getJsonFromUrl(location.search); if (urlParams.mode) { searchMode.write(urlParams.mode); @@ -26,17 +26,17 @@ export default function init () { initWebsiteTranslationOptions(); } -export function initBorrowAndReadLinks () { +export function initBorrowAndReadLinks() { // LOADING ONCLICK FUNCTIONS FOR BORROW AND READ LINKS // used in openlibrary/macros/AvailabilityButton.html and openlibrary/macros/LoanStatus.html - $(function (){ - $('.cta-btn--ia.cta-btn--borrow,.cta-btn--ia.cta-btn--read').on('click', function (){ + $(function(){ + $('.cta-btn--ia.cta-btn--borrow,.cta-btn--ia.cta-btn--read').on('click', function(){ $(this).removeClass('cta-btn cta-btn--available').addClass('cta-btn cta-btn--available--load'); }); }); - $(function (){ - $('#waitlist_ebook').on('click', function (){ + $(function(){ + $('#waitlist_ebook').on('click', function(){ $(this).removeClass('cta-btn cta-btn--unavailable').addClass('cta-btn cta-btn--unavailable--load'); }); }); @@ -44,8 +44,8 @@ export function initBorrowAndReadLinks () { } -export function initWebsiteTranslationOptions () { - $('.locale-options li a').on('click', function (event) { +export function initWebsiteTranslationOptions() { + $('.locale-options li a').on('click', function(event) { event.preventDefault(); const locale = $(this).data('lang-id'); setValueInCookie('HTTP_LANG', locale); diff --git a/openlibrary/plugins/openlibrary/js/partner_ol_lib.js b/openlibrary/plugins/openlibrary/js/partner_ol_lib.js index 9642d851f0b..13504c8c311 100644 --- a/openlibrary/plugins/openlibrary/js/partner_ol_lib.js +++ b/openlibrary/plugins/openlibrary/js/partner_ol_lib.js @@ -1,7 +1,7 @@ /** * @param {string} container */ -function getIsbnToElementMap (container) { +function getIsbnToElementMap(container) { const reISBN = /(978)?[0-9]{9}[0-9X]/i; const elements = Array.from(document.querySelectorAll(container)); const isbnElementMap = {}; @@ -18,7 +18,7 @@ function getIsbnToElementMap (container) { * @param {string[]} isbnList * @returns {Promise<Array>} */ -async function getAvailabilityDataFromOpenLibrary (isbnList) { +async function getAvailabilityDataFromOpenLibrary(isbnList) { const apiBaseUrl = 'https://openlibrary.org/search.json'; const apiUrl = `${apiBaseUrl}?fields=*,availability&q=isbn:${isbnList.join('+OR+')}`; const response = await fetch(apiUrl); @@ -47,7 +47,7 @@ async function getAvailabilityDataFromOpenLibrary (isbnList) { * textOnBtn: "Open Library!" * }); */ -async function addOpenLibraryButtons (options) { +async function addOpenLibraryButtons(options) { const {bookContainer, selectorToPlaceBtnIn, textOnBtn} = options; if (bookContainer === undefined) { throw Error( diff --git a/openlibrary/plugins/openlibrary/js/patron_exports.js b/openlibrary/plugins/openlibrary/js/patron_exports.js index a4c41c20f8d..7d9638ff88f 100644 --- a/openlibrary/plugins/openlibrary/js/patron_exports.js +++ b/openlibrary/plugins/openlibrary/js/patron_exports.js @@ -3,7 +3,7 @@ * * @param {HTMLElement} buttonElement */ -function disableButton (buttonElement) { +function disableButton(buttonElement) { buttonElement.setAttribute('disabled', 'true'); buttonElement.setAttribute('aria-disabled', 'true'); } @@ -16,7 +16,7 @@ function disableButton (buttonElement) { * * @param {NodeList<HTMLFormElement>} elems */ -export function initPatronExportForms (elems) { +export function initPatronExportForms(elems) { elems.forEach((form) => { const submitButton = form.querySelector('input[type=submit]'); form.addEventListener('submit', () => { diff --git a/openlibrary/plugins/openlibrary/js/private-button.js b/openlibrary/plugins/openlibrary/js/private-button.js index a2053f0691f..bae7b6af1b0 100644 --- a/openlibrary/plugins/openlibrary/js/private-button.js +++ b/openlibrary/plugins/openlibrary/js/private-button.js @@ -1,6 +1,6 @@ import { FadingToast } from './Toast'; -export function initPrivateButtons (buttons) { +export function initPrivateButtons(buttons) { buttons.forEach(button => { button.addEventListener('click', (event) => { event.preventDefault(); diff --git a/openlibrary/plugins/openlibrary/js/python.js b/openlibrary/plugins/openlibrary/js/python.js index 13a1b0c81d5..393e6b7fe88 100644 --- a/openlibrary/plugins/openlibrary/js/python.js +++ b/openlibrary/plugins/openlibrary/js/python.js @@ -7,7 +7,7 @@ * @param {mixed} n * @return {string} */ -export function commify (n) { +export function commify(n) { var text = n.toString(); var re = /(\d+)(\d{3})/; @@ -19,7 +19,7 @@ export function commify (n) { } // Implementation of Python urllib.urlencode in Javascript. -export function urlencode (query) { +export function urlencode(query) { var parts = []; var k; for (k in query) { @@ -28,7 +28,7 @@ export function urlencode (query) { return parts.join('&'); } -export function slice (array, begin, end) { +export function slice(array, begin, end) { var a = []; var i; for (i=begin; i < Math.min(array.length, end); i++) { diff --git a/openlibrary/plugins/openlibrary/js/reading-goals/index.js b/openlibrary/plugins/openlibrary/js/reading-goals/index.js index fbc291a67bc..e243d6913a2 100644 --- a/openlibrary/plugins/openlibrary/js/reading-goals/index.js +++ b/openlibrary/plugins/openlibrary/js/reading-goals/index.js @@ -6,7 +6,7 @@ import { buildPartialsUrl } from '../utils'; * * @param {HTMLCollection<HTMLElement>} links Prompts for adding a reading goal */ -export function initYearlyGoalPrompt (links) { +export function initYearlyGoalPrompt(links) { for (const link of links) { if (!link.classList.contains('goal-set')) { link.addEventListener('click', onYearlyGoalClick); @@ -17,7 +17,7 @@ export function initYearlyGoalPrompt (links) { /** * Finds and shows the yearly goal modal. */ -function onYearlyGoalClick () { +function onYearlyGoalClick() { const yearlyGoalModal = document.querySelector('#yearly-goal-modal'); yearlyGoalModal.showModal(); } @@ -33,7 +33,7 @@ function onYearlyGoalClick () { * * @param {HTMLCollection<HTMLElement>} elems ELements which display only the current year */ -export function displayLocalYear (elems) { +export function displayLocalYear(elems) { const localYear = new Date().getFullYear(); for (const elem of elems) { const serverYear = Number(elem.dataset.serverYear); @@ -48,7 +48,7 @@ export function displayLocalYear (elems) { * * @param {HTMLCollection<HTMLElement>} editLinks Edit goal links */ -export function initGoalEditLinks (editLinks) { +export function initGoalEditLinks(editLinks) { for (const link of editLinks) { const parent = link.closest('.reading-goal-progress'); const modal = parent.querySelector('dialog'); @@ -64,8 +64,8 @@ export function initGoalEditLinks (editLinks) { * @param {HTMLElement} editLink An edit goal link * @param {HTMLDialogElement} modal The modal that will be shown */ -function addGoalEditClickListener (editLink, modal) { - editLink.addEventListener('click', function () { +function addGoalEditClickListener(editLink, modal) { + editLink.addEventListener('click', function() { modal.showModal(); }); } @@ -76,7 +76,7 @@ function addGoalEditClickListener (editLink, modal) { * * @param {HTMLCollection<HTMLElement>} submitButtons Submit goal buttons */ -export function initGoalSubmitButtons (submitButtons) { +export function initGoalSubmitButtons(submitButtons) { for (const button of submitButtons) { addGoalSubmissionListener(button); } @@ -89,8 +89,8 @@ export function initGoalSubmitButtons (submitButtons) { * the action set a new goal, or updated an existing goal. * @param {HTMLELement} submitButton Reading goal form submit button */ -function addGoalSubmissionListener (submitButton) { - submitButton.addEventListener('click', function (event) { +function addGoalSubmissionListener(submitButton) { + submitButton.addEventListener('click', function(event) { event.preventDefault(); const form = submitButton.closest('form'); @@ -161,7 +161,7 @@ function addGoalSubmissionListener (submitButton) { * @param {HTMLElement} elem A reading goal progress component * @param {Number} goal The new reading goal */ -function updateProgressComponent (elem, goal) { +function updateProgressComponent(elem, goal) { // Calculate new percentage: const booksReadSpan = elem.querySelector('.reading-goal-progress__books-read'); const booksRead = Number(booksReadSpan.textContent); @@ -183,7 +183,7 @@ function updateProgressComponent (elem, goal) { * @param {NodeList} yearlyGoalElems Containers for progress components and reading goal links. * @param {string} goalYear Year that the goal is set for. */ -function fetchProgressAndUpdateViews (yearlyGoalElems, goalYear) { +function fetchProgressAndUpdateViews(yearlyGoalElems, goalYear) { fetch(buildPartialsUrl('ReadingGoalProgress', {year: goalYear})) .then((response) => { if (!response.ok) { @@ -191,7 +191,7 @@ function fetchProgressAndUpdateViews (yearlyGoalElems, goalYear) { } return response.json(); }) - .then(function (data) { + .then(function(data) { const html = data['partials']; yearlyGoalElems.forEach((yearlyGoalElem) => { const progress = document.createElement('SPAN'); diff --git a/openlibrary/plugins/openlibrary/js/readinglog_stats.js b/openlibrary/plugins/openlibrary/js/readinglog_stats.js index 293c06bef9c..8ba61b38c85 100644 --- a/openlibrary/plugins/openlibrary/js/readinglog_stats.js +++ b/openlibrary/plugins/openlibrary/js/readinglog_stats.js @@ -39,7 +39,7 @@ import 'chartjs-plugin-datalabels'; /** * @param {Config} config */ -export function init (config) { +export function init(config) { Chart.scaleService.updateScaleDefaults('linear', { ticks: { beginAtZero: true, stepSize: 1 } }); const authors_by_id = fromPairs(config.authors.map(a => [a.key, a])); @@ -50,7 +50,7 @@ export function init (config) { * @param {Element} container * @param {HTMLCanvasElement} canvas */ - function createWorkChart (config, chartConfig, container, canvas) { + function createWorkChart(config, chartConfig, container, canvas) { /** @type {{[key: string]: Work[]}} */ const grouped = {}; /** @type {Work[]} */ @@ -137,7 +137,7 @@ export function init (config) { }, ]; - function buildSparql (authors) { + function buildSparql(authors) { return ` SELECT DISTINCT ?x ?xLabel ?olid ${ @@ -220,13 +220,13 @@ export function init (config) { * @param {string} key * @return {any} */ -function getPath (obj, key) { +function getPath(obj, key) { /** * @param {object} obj * @param {string[]} param1 * @return {any} */ - function main (obj, [head, ...rest]) { + function main(obj, [head, ...rest]) { if (typeof(obj) === 'undefined') return undefined; if (!head) return obj; if (head.endsWith('[]')) return obj[head.slice(0, -2)].flatMap(x => main(x, rest)); diff --git a/openlibrary/plugins/openlibrary/js/return-form/index.js b/openlibrary/plugins/openlibrary/js/return-form/index.js index c6b08b6d00f..ade5d8a1a45 100644 --- a/openlibrary/plugins/openlibrary/js/return-form/index.js +++ b/openlibrary/plugins/openlibrary/js/return-form/index.js @@ -4,7 +4,7 @@ * * @param {NodeList<HTMLElement>} returnForms */ -export function initReturnForms (returnForms) { +export function initReturnForms(returnForms) { for (const form of returnForms) { const i18nStrings = JSON.parse(form.dataset.i18n); form.addEventListener('submit', (event) => { diff --git a/openlibrary/plugins/openlibrary/js/search.js b/openlibrary/plugins/openlibrary/js/search.js index e0b36ecf20c..23f92ed9dc6 100644 --- a/openlibrary/plugins/openlibrary/js/search.js +++ b/openlibrary/plugins/openlibrary/js/search.js @@ -11,7 +11,7 @@ import { buildPartialsUrl } from './utils'; * @param {Number} start_facet_count initial number of displayed facets * @param {Number} facet_inc number of hidden facets to be displayed */ -export function more (header, start_facet_count, facet_inc) { +export function more(header, start_facet_count, facet_inc) { const facetEntry = `div.${header} div.facetEntry`; const shown = $(`${facetEntry}:not(:hidden)`).length; const total = $(facetEntry).length; @@ -33,7 +33,7 @@ export function more (header, start_facet_count, facet_inc) { * @param {Number} start_facet_count initial number of displayed facets * @param {Number} facet_inc number of displayed facets to be hidden */ -export function less (header, start_facet_count, facet_inc) { +export function less(header, start_facet_count, facet_inc) { const facetEntry = `div.${header} div.facetEntry`; const shown = $(`${facetEntry}:not(:hidden)`).length; const total = $(facetEntry).length; @@ -64,7 +64,7 @@ export function less (header, start_facet_count, facet_inc) { * * @param {HTMLElement} facetsElem Root element of the search facets sidebar component */ -export async function initSearchFacets (facetsElem) { +export async function initSearchFacets(facetsElem) { const asyncLoad = facetsElem.dataset.asyncLoad; if (asyncLoad) { @@ -96,16 +96,16 @@ export async function initSearchFacets (facetsElem) { /** * Adds click listeners to the "show more" and "show less" facet affordances. */ -function hydrateFacets () { +function hydrateFacets() { const data_config_json = $('#searchFacets').data('config'); const start_facet_count = data_config_json['start_facet_count']; const facet_inc = data_config_json['facet_inc']; $('.header_bull').hide(); - $('.header_more').on('click', function (){ + $('.header_more').on('click', function(){ more($(this).data('header'), start_facet_count, facet_inc); }); - $('.header_less').on('click', function (){ + $('.header_less').on('click', function(){ less($(this).data('header'), start_facet_count, facet_inc); }); } @@ -127,7 +127,7 @@ function hydrateFacets () { * * @throws Error when `/partials` response is not in 200-299 range. */ -function fetchPartials (param) { +function fetchPartials(param) { const data = { param: param, path: location.pathname, @@ -152,7 +152,7 @@ function fetchPartials (param) { * @param {string} markup HTML markup for a single element * @returns {HTMLElement} */ -function createElementFromMarkup (markup) { +function createElementFromMarkup(markup) { const template = document.createElement('template'); template.innerHTML = markup; return template.content.children[0]; @@ -166,7 +166,7 @@ function createElementFromMarkup (markup) { * @param {IntersectionObserverInit} options * @returns {Promise<void>} */ -async function whenVisible (elem, options = {}) { +async function whenVisible(elem, options = {}) { return new Promise((resolve) => { const intersectionObserver = new IntersectionObserver( (entries, observer) => { diff --git a/openlibrary/plugins/openlibrary/js/service-worker-matchers.js b/openlibrary/plugins/openlibrary/js/service-worker-matchers.js index 4385198e5b3..09205f144f2 100644 --- a/openlibrary/plugins/openlibrary/js/service-worker-matchers.js +++ b/openlibrary/plugins/openlibrary/js/service-worker-matchers.js @@ -7,34 +7,34 @@ It is in a is a separate file to avoid this error when writing tests: */ -export function matchMiscFiles ({ url }) { +export function matchMiscFiles({ url }) { const miscFiles = ['/favicon.ico', '/static/manifest.json', '/cdn/archive.org/athena.js', '/cdn/archive.org/donate.js']; return miscFiles.includes(url.pathname); } -export function matchSmallMediumCovers ({ url }) { +export function matchSmallMediumCovers({ url }) { const regex = /-[SM].jpg$/; return regex.test(url.pathname); } -export function matchLargeCovers ({ url }) { +export function matchLargeCovers({ url }) { const regex = /-L.jpg$/; return regex.test(url.pathname); } -export function matchStaticImages ({ url }) { +export function matchStaticImages({ url }) { const regex = /^\/images\/|^\/static\/images\//; return regex.test(url.pathname); } -export function matchStaticBuild ({ url }) { +export function matchStaticBuild({ url }) { const regex = /^\/static\/build\/.*(\.js|\.css)/; const localhost = url.origin.includes('localhost'); return !localhost && regex.test(url.pathname); } -export function matchArchiveOrgImage ({ url }) { +export function matchArchiveOrgImage({ url }) { // most importantly, to cache your profile picture from loading every time // also caches some covers return url.href.startsWith('https://archive.org/services/img/'); diff --git a/openlibrary/plugins/openlibrary/js/signup.js b/openlibrary/plugins/openlibrary/js/signup.js index 81ab015815d..5bbb3947450 100644 --- a/openlibrary/plugins/openlibrary/js/signup.js +++ b/openlibrary/plugins/openlibrary/js/signup.js @@ -1,6 +1,6 @@ import { debounce } from './nonjquery_utils.js'; -export function initSignupForm () { +export function initSignupForm() { const signupForm = document.querySelector('form[name=signup]'); const submitBtn = document.querySelector('button[name=signup]'); const rpdCheckbox = document.querySelector('#pd-request'); @@ -21,14 +21,14 @@ export function initSignupForm () { const USERNAME_MAXLENGTH = 20; // Callback that is called when grecaptcha.execute() is successful - function submitCreateAccountForm () { + function submitCreateAccountForm() { signupForm.submit(); } window.submitCreateAccountForm = submitCreateAccountForm; // Checks whether reportValidity exists for cross-browser compatibility // Includes invalid input count to account for checks not covered by reportValidity - $(signupForm).on('submit', function (e) { + $(signupForm).on('submit', function(e) { e.preventDefault(); validatePDSelection(); const numInvalidInputs = signupForm.querySelectorAll('.invalid').length; @@ -39,7 +39,7 @@ export function initSignupForm () { } }); - $('#username').on('keyup', function (){ + $('#username').on('keyup', function(){ const value = $(this).val(); $('#userUrl').addClass('darkgreen').text(value).css('font-weight', '700'); }); @@ -51,7 +51,7 @@ export function initSignupForm () { * @param {string} errorDiv The ID (incl #) of the div where the error msg will be rendered * @param {string} errorMsg The error message text */ - function renderError (inputId, errorDiv, errorMsg) { + function renderError(inputId, errorDiv, errorMsg) { $(inputId).addClass('invalid'); $(`label[for=${inputId.slice(1)}]`).addClass('invalid'); $(errorDiv).text(errorMsg); @@ -63,13 +63,13 @@ export function initSignupForm () { * @param {string} inputId The ID (incl #) of the input the error relates to * @param {string} errorDiv The ID (incl #) of the div where the error msg is currently rendered */ - function clearError (inputId, errorDiv) { + function clearError(inputId, errorDiv) { $(inputId).removeClass('invalid'); $(`label[for=${inputId.slice(1)}]`).removeClass('invalid'); $(errorDiv).text(''); } - function validateUsername () { + function validateUsername() { const value_username = $('#username').val(); usernameSuccessIcon.hide(); @@ -95,7 +95,7 @@ export function initSignupForm () { url: '/account/validate', data: { username: value_username }, type: 'GET', - success: function (errors) { + success: function(errors) { usernameLoadingIcon.hide(); if (errors.username) { @@ -108,7 +108,7 @@ export function initSignupForm () { }); } - function validateEmail () { + function validateEmail() { const value_email = $('#emailAddr').val(); emailSuccessIcon.hide(); @@ -129,7 +129,7 @@ export function initSignupForm () { url: '/account/validate', data: { email: value_email }, type: 'GET', - success: function (errors) { + success: function(errors) { emailLoadingIcon.hide(); if (errors.email) { @@ -142,7 +142,7 @@ export function initSignupForm () { }); } - function validatePassword () { + function validatePassword() { const value_password = $('#password').val(); if (value_password === '') { @@ -158,7 +158,7 @@ export function initSignupForm () { clearError('#password', '#passwordMessage'); } - function validatePDSelection () { + function validatePDSelection() { if (!rpdCheckbox.checked) { clearError('#pd_program', '#pd_programMessage'); pdaSelector.setAttribute('aria-invalid', 'false'); @@ -175,7 +175,7 @@ export function initSignupForm () { } // Maps input ID attribute to corresponding validation function - function validateInput (input) { + function validateInput(input) { const id = $(input).attr('id'); if (id === 'emailAddr') { validateEmail(); @@ -191,25 +191,25 @@ export function initSignupForm () { const $nonCheckboxInputs = $('form[name=signup] input:not([type="checkbox"])'); // Validates input fields already marked as invalid on value change - $nonCheckboxInputs.on('input', debounce(function (){ + $nonCheckboxInputs.on('input', debounce(function(){ if ($(this).hasClass('invalid')) { validateInput(this); } }, 50)); // Validates all other input fields (i.e. not already marked as invalid) on blur - $nonCheckboxInputs.on('blur', function () { + $nonCheckboxInputs.on('blur', function() { if (!$(this).hasClass('invalid')) { validateInput(this); } }); // Validates the print-disability authority selection when the selection changes - $('form[name=signup] select').on('change', function () { + $('form[name=signup] select').on('change', function() { validatePDSelection(); }); - function updateSelectorVisibility () { + function updateSelectorVisibility() { if (rpdCheckbox.checked) { pdaSelectorContainer.classList.remove('hidden'); rpdCheckbox.setAttribute('aria-expanded', 'true'); @@ -230,7 +230,7 @@ export function initSignupForm () { validatePDSelection(); } -export function initLoginForm () { +export function initLoginForm() { const loginForm = $('form[name=login]'); const loadingText = loginForm.data('i18n')['loading_text']; diff --git a/openlibrary/plugins/openlibrary/js/star-ratings/index.js b/openlibrary/plugins/openlibrary/js/star-ratings/index.js index a782b722838..6ae9c46db00 100644 --- a/openlibrary/plugins/openlibrary/js/star-ratings/index.js +++ b/openlibrary/plugins/openlibrary/js/star-ratings/index.js @@ -2,15 +2,15 @@ import { FadingToast } from '../Toast.js'; import { findDropperForWork } from '../my-books'; import { ReadingLogShelves } from '../my-books/MyBooksDropper/ReadingLogForms'; -export function initRatingHandlers (ratingForms) { +export function initRatingHandlers(ratingForms) { for (const form of ratingForms) { - form.addEventListener('submit', function (e) { + form.addEventListener('submit', function(e) { handleRatingSubmission(e, form); }); } } -function handleRatingSubmission (event, form) { +function handleRatingSubmission(event, form) { event.preventDefault(); // Continue only if selected star is different from previous rating if (!event.submitter.classList.contains('star-selected')) { diff --git a/openlibrary/plugins/openlibrary/js/stats/index.js b/openlibrary/plugins/openlibrary/js/stats/index.js index f07055317d2..6e9ac877a7a 100644 --- a/openlibrary/plugins/openlibrary/js/stats/index.js +++ b/openlibrary/plugins/openlibrary/js/stats/index.js @@ -5,7 +5,7 @@ * @returns {Promise<void>} * @see /openlibrary/templates/admin/index.html */ -export async function initUniqueLoginCounts (containerElem) { +export async function initUniqueLoginCounts(containerElem) { const loadingIndicator = containerElem.querySelector('.loadingIndicator'); const i18nStrings = JSON.parse(containerElem.dataset.i18n); @@ -30,6 +30,6 @@ export async function initUniqueLoginCounts (containerElem) { * @returns {Promise<Response>} * @see `monthly_logins` class in /openlibrary/plugins/openlibrary/api.py */ -async function fetchCounts () { +async function fetchCounts() { return fetch('/api/monthly_logins.json'); } diff --git a/openlibrary/plugins/openlibrary/js/tabs.js b/openlibrary/plugins/openlibrary/js/tabs.js index 06a53fbd04d..83c7335af28 100644 --- a/openlibrary/plugins/openlibrary/js/tabs.js +++ b/openlibrary/plugins/openlibrary/js/tabs.js @@ -1,9 +1,9 @@ const TABS_OPTIONS = { fx: { opacity: 'toggle' } }; import 'jquery-ui/ui/widgets/tabs'; -export function initTabs ($node) { +export function initTabs($node) { $node.tabs(TABS_OPTIONS); - $node.filter('.autohash').on('tabsselect', function (event, ui) { + $node.filter('.autohash').on('tabsselect', function(event, ui) { document.location.hash = ui.panel.id; }); } diff --git a/openlibrary/plugins/openlibrary/js/team.js b/openlibrary/plugins/openlibrary/js/team.js index 674bf34d5f9..ba1084a76df 100644 --- a/openlibrary/plugins/openlibrary/js/team.js +++ b/openlibrary/plugins/openlibrary/js/team.js @@ -1,6 +1,6 @@ import team from '../../../templates/about/team.json'; import { updateURLParameters } from './utils'; -export function initTeamFilter () { +export function initTeamFilter() { const currentYear = new Date().getFullYear().toString(); // Photos const default_profile_image = diff --git a/openlibrary/plugins/openlibrary/js/template.js b/openlibrary/plugins/openlibrary/js/template.js index 5e2eded2786..ff1a151d308 100644 --- a/openlibrary/plugins/openlibrary/js/template.js +++ b/openlibrary/plugins/openlibrary/js/template.js @@ -2,18 +2,18 @@ // // Inspired by http://ejohn.org/blog/javascript-micro-templating/ -export default function Template (tmpl_text) { +export default function Template(tmpl_text) { var s = []; var js = ['var _p=[];', 'with(env) {']; var tokens, i, t, f, g; - function addCode (text) { + function addCode(text) { js.push(text); } - function addExpr (text) { + function addExpr(text) { js.push(`_p.push(htmlquote(${text}));`); } - function addText (text) { + function addText(text) { js.push(`_p.push(__s[${s.length}]);`); s.push(text); } @@ -35,10 +35,10 @@ export default function Template (tmpl_text) { js.push('}', 'return _p.join(\'\');'); f = new Function(['__s', 'env'], js.join('\n')); - g = function (env) { + g = function(env) { return f(s, env); }; - g.toString = function () { return tmpl_text; }; - g.toCode = function () { return f.toString(); }; + g.toString = function() { return tmpl_text; }; + g.toCode = function() { return f.toString(); }; return g; } diff --git a/openlibrary/plugins/openlibrary/js/type_changer.js b/openlibrary/plugins/openlibrary/js/type_changer.js index 542082ef876..650cc6ae546 100644 --- a/openlibrary/plugins/openlibrary/js/type_changer.js +++ b/openlibrary/plugins/openlibrary/js/type_changer.js @@ -2,10 +2,10 @@ * Functionality for TypeChanger.html */ -export function initTypeChanger (elem) { +export function initTypeChanger(elem) { // /about?m=edit - where this code is run - function changeTemplate () { + function changeTemplate() { // Change the template of the page based on the selected value const searchParams = new URLSearchParams(window.location.search); const t = elem.value; diff --git a/openlibrary/plugins/openlibrary/js/utils.js b/openlibrary/plugins/openlibrary/js/utils.js index 8f76f3a5c10..e7704006126 100644 --- a/openlibrary/plugins/openlibrary/js/utils.js +++ b/openlibrary/plugins/openlibrary/js/utils.js @@ -5,13 +5,13 @@ See: https://github.com/internetarchive/openlibrary/pull/9180#issuecomment-21079 */ // closes active popup -export function closePopup () { +export function closePopup() { // Note we don't import colorbox here, since it's on the parent parent.jQuery.fn.colorbox.close(); } // used in templates/admin/imports.html -export function truncate (text, limit) { +export function truncate(text, limit) { if (text.length > limit) { return `${text.substr(0, limit)}...`; } else { @@ -20,7 +20,7 @@ export function truncate (text, limit) { } // used in openlibrary/templates/books/edit/excerpts.html -export function cond (predicate, true_value, false_value) { +export function cond(predicate, true_value, false_value) { if (predicate) { return true_value; } @@ -34,7 +34,7 @@ export function cond (predicate, true_value, false_value) { * * @param {...HTMLElement} elements */ -export function removeChildren (...elements) { +export function removeChildren(...elements) { for (const elem of elements) { if (elem) { while (elem.firstChild) { @@ -45,7 +45,7 @@ export function removeChildren (...elements) { } // Function to add or update multiple query parameters -export function updateURLParameters (params) { +export function updateURLParameters(params) { // Get the current URL const url = new URL(window.location.href); @@ -64,16 +64,16 @@ export function updateURLParameters (params) { * Remove leading/trailing empty space on field deselect. * @param string a value for document.querySelectorAll() */ -export function trimInputValues (param) { +export function trimInputValues(param) { const inputs = document.querySelectorAll(param); inputs.forEach(input => { - input.addEventListener('blur', function () { + input.addEventListener('blur', function() { this.value = this.value.trim(); }); }); } -export function buildPartialsUrl (component, params = {}) { +export function buildPartialsUrl(component, params = {}) { const curUrl = new URL(window.location.href); const url = new URL(`${location.origin}/partials/${component}.json`); diff --git a/openlibrary/plugins/openlibrary/js/waitlist.js b/openlibrary/plugins/openlibrary/js/waitlist.js index bf992e30c10..a48a15e940b 100644 --- a/openlibrary/plugins/openlibrary/js/waitlist.js +++ b/openlibrary/plugins/openlibrary/js/waitlist.js @@ -5,7 +5,7 @@ import 'jquery-ui/ui/widgets/dialog'; * * @param {NodeList<HTMLElement>} leaveWaitlistLinks - NodeList of leave waitlist links */ -export function initLeaveWaitlist (leaveWaitlistLinks) { +export function initLeaveWaitlist(leaveWaitlistLinks) { for (const link of leaveWaitlistLinks) { link.addEventListener('click', () => { const $link = $(link); diff --git a/static/bookmarklets/import_webbook.js b/static/bookmarklets/import_webbook.js index a698c978e07..e1103664991 100644 --- a/static/bookmarklets/import_webbook.js +++ b/static/bookmarklets/import_webbook.js @@ -1,5 +1,5 @@ /* eslint-disable no-unused-labels */ -javascript:(async ()=> { +javascript:(async()=> { const url = prompt('Enter the book URL you want to import:'); if (!url) return; const promptText = `You are an expert book metadata librarian and research assistant. Your role is to search the web and collect accurate data to produce the most useful, factual, complete, and patron-oriented book page possible for the URL: diff --git a/stories/.storybook/main.js b/stories/.storybook/main.js index 87d9d43833a..f60a24987a5 100644 --- a/stories/.storybook/main.js +++ b/stories/.storybook/main.js @@ -1,7 +1,7 @@ const webpackConfig = require('../../webpack.config'); module.exports = { - webpackFinal: async (config) => { + webpackFinal: async(config) => { config.module.rules = config.module.rules.concat( webpackConfig.module.rules ); diff --git a/tests/unit/js/SelectionManager.test.js b/tests/unit/js/SelectionManager.test.js index 88b81e73b09..a0eec67b6d6 100644 --- a/tests/unit/js/SelectionManager.test.js +++ b/tests/unit/js/SelectionManager.test.js @@ -1,6 +1,6 @@ import SelectionManager from '../../../openlibrary/plugins/openlibrary/js/ile/utils/SelectionManager/SelectionManager.js'; -function createTestElementsForProcessClick () { +function createTestElementsForProcessClick() { const listItem = document.createElement('li'); listItem.classList.add('searchResultItem', 'ile-selectable'); @@ -18,7 +18,7 @@ function createTestElementsForProcessClick () { return {listItem, link}; } -function setupSelectionManager () { +function setupSelectionManager() { const sm = new SelectionManager(null, '/search'); sm.ile = { $statusImages: { append: jest.fn() } }; sm.selectedItems = { work: [] }; diff --git a/tests/unit/js/editionsEditPage.test.js b/tests/unit/js/editionsEditPage.test.js index b9ccb9b0e26..1dda06e0541 100644 --- a/tests/unit/js/editionsEditPage.test.js +++ b/tests/unit/js/editionsEditPage.test.js @@ -36,7 +36,7 @@ beforeEach(() => { $(document.body).html(testData.editionIdentifiersSample); $('#identifiers').repeat({ vars: {prefix: 'edition--'}, - validate: function (data) {return validateIdentifiers(data);}, + validate: function(data) {return validateIdentifiers(data);}, }); }); diff --git a/tests/unit/js/jsdef.test.js b/tests/unit/js/jsdef.test.js index 54fd843d9f1..87cd8f64962 100644 --- a/tests/unit/js/jsdef.test.js +++ b/tests/unit/js/jsdef.test.js @@ -21,7 +21,7 @@ test('jsdef: foreach', () => { const listToLoop = [1, 2, 3]; expect.assertions(1); return new Promise((resolve) => { - foreach(listToLoop, loop, function () { + foreach(listToLoop, loop, function() { called += 1; if (called === 3) { expect(called).toBe(3); diff --git a/tests/unit/js/sample-html/dropper-test-data.js b/tests/unit/js/sample-html/dropper-test-data.js index 080dc8dbefa..23ded486df3 100644 --- a/tests/unit/js/sample-html/dropper-test-data.js +++ b/tests/unit/js/sample-html/dropper-test-data.js @@ -13,7 +13,7 @@ export const closedDropperMarkup = generateDropperMarkup(false); export const disabledDropperMarkup = generateDropperMarkup(false, true); -function generateDropperMarkup (isDropperOpen, isDropperDisabled = false) { +function generateDropperMarkup(isDropperOpen, isDropperDisabled = false) { let wrapperClasses = 'generic-dropper-wrapper'; let arrowClasses = 'arrow'; diff --git a/tests/unit/js/sample-html/lists-test-data.js b/tests/unit/js/sample-html/lists-test-data.js index 43df855010b..4cdf36b2983 100644 --- a/tests/unit/js/sample-html/lists-test-data.js +++ b/tests/unit/js/sample-html/lists-test-data.js @@ -1,4 +1,4 @@ -function createListFormMarkup (isFilled) { +function createListFormMarkup(isFilled) { const listName = isFilled ? 'My New List' : ''; const listDescription = isFilled ? 'A list for all of my books' : ''; @@ -51,7 +51,7 @@ const DEFAULT_COVER_URL = '/images/icons/avatar_book-sm.png'; * @param {boolean} isActiveShowcase * @param {Array<ShowcaseDetails>} showcaseData */ -function createShowcaseMarkup (isActiveShowcase, showcaseData) { +function createShowcaseMarkup(isActiveShowcase, showcaseData) { const listId = isActiveShowcase ? 'already-lists' : 'list-lists'; const listClasses = 'listLists'.concat(isActiveShowcase ? ' already-lists' : ''); diff --git a/tests/unit/js/search.test.js b/tests/unit/js/search.test.js index 2f05cbb99e5..58285498249 100644 --- a/tests/unit/js/search.test.js +++ b/tests/unit/js/search.test.js @@ -8,7 +8,7 @@ import { more, less } from '../../../openlibrary/plugins/openlibrary/js/search.j * @param {Number} minVisibleFacet minimum number of visible facet * @return {String} HTML search facets section */ -function createSearchFacets (totalFacet = 2, visibleFacet = 2, minVisibleFacet = 2) { +function createSearchFacets(totalFacet = 2, visibleFacet = 2, minVisibleFacet = 2) { const divSearchFacets = document.createElement('DIV'); divSearchFacets.setAttribute('id', 'searchFacets'); divSearchFacets.innerHTML = ` @@ -59,7 +59,7 @@ function createSearchFacets (totalFacet = 2, visibleFacet = 2, minVisibleFacet = * @param {Number} totalFacet total number of facet * @param {Number} expectedVisibleFacet expected number of visible facet */ -function checkFacetVisibility (totalFacet, expectedVisibleFacet) { +function checkFacetVisibility(totalFacet, expectedVisibleFacet) { const facetEntryList = document.getElementsByClassName('facetEntry'); test('facetEntry element number', () => { @@ -85,7 +85,7 @@ function checkFacetVisibility (totalFacet, expectedVisibleFacet) { * @param {Number} minVisibleFacet minimum visible facet number * @param {Number} expectedVisibleFacet expected number of visible facet */ -function checkFacetMoreLessVisibility (totalFacet, minVisibleFacet, expectedVisibleFacet) { +function checkFacetMoreLessVisibility(totalFacet, minVisibleFacet, expectedVisibleFacet) { if (expectedVisibleFacet <= minVisibleFacet) { test('element "test_more"', () => { expect(document.getElementById('test_more').style.display).not.toBe('none'); @@ -122,7 +122,7 @@ function checkFacetMoreLessVisibility (totalFacet, minVisibleFacet, expectedVisi const _originalGetClientRects = window.Element.prototype.getClientRects; // Stubbed getClientRects to enable jQuery ':hidden' selector used by 'more' and 'less' functions -const _stubbedGetClientRects = function () { +const _stubbedGetClientRects = function() { let node = this; while (node) { if (node === document) { diff --git a/tests/unit/js/service-worker-matchers.test.js b/tests/unit/js/service-worker-matchers.test.js index f1da4ddb08c..a21ae924d50 100644 --- a/tests/unit/js/service-worker-matchers.test.js +++ b/tests/unit/js/service-worker-matchers.test.js @@ -2,7 +2,7 @@ import { matchMiscFiles, matchSmallMediumCovers, matchLargeCovers, matchStaticIm // Helper function to create a URL object -function _u (url) { +function _u(url) { return { url: new URL(url) }; } // Group related tests together