diff --git a/bundlesize.config.json b/bundlesize.config.json index 7097775d2fd..6e06baa217a 100644 --- a/bundlesize.config.json +++ b/bundlesize.config.json @@ -102,7 +102,7 @@ }, { "path": "static/build/page-user.css", - "maxSize": "27KB" + "maxSize": "27.05KB" } ] } diff --git a/openlibrary/i18n/messages.pot b/openlibrary/i18n/messages.pot index c463ba8c654..1b02f77d596 100644 --- a/openlibrary/i18n/messages.pot +++ b/openlibrary/i18n/messages.pot @@ -1030,17 +1030,13 @@ msgstr "" msgid "Solr Editions" msgstr "" -#: admin/inspect/store.html search/lists.html work_search.html -msgid "No results found." -msgstr "" - -#: search/lists.html work_search.html +#: work_search.html #, python-format -msgid "Search for books containing the phrase \"%s\"?" +msgid "No %(path_id)s directly matched your search" msgstr "" -#: work_search.html -msgid "Add a new book to Open Library?" +#: search/authors.html search/lists.html search/subjects.html work_search.html +msgid "Checking Search Inside matches" msgstr "" #: account/reading_log.html search/authors.html search/subjects.html @@ -1490,8 +1486,8 @@ msgstr "" msgid "Thanks!" msgstr "" -#: BookByline.html SearchResultsWork.html account/notes.html -#: account/observations.html +#: BookByline.html FulltextSearchSuggestionItem.html SearchResultsWork.html +#: account/notes.html account/observations.html msgid "Unknown author" msgstr "" @@ -2668,6 +2664,10 @@ msgstr "" msgid "Json" msgstr "" +#: admin/inspect/store.html +msgid "No results found." +msgstr "" + #: admin/ip/index.html msgid "[Admin Center] IP Addresses" msgstr "" @@ -3688,10 +3688,10 @@ msgstr "" msgid "Links" msgstr "" -#: IABook.html SearchResultsWork.html books/edition-sort.html -#: jsdef/LazyWorkPreview.html lists/list_overview.html lists/preview.html -#: lists/snippet.html lists/widget.html my_books/dropdown_content.html -#: type/work/editions.html +#: FulltextSearchSuggestionItem.html IABook.html SearchResultsWork.html +#: books/edition-sort.html jsdef/LazyWorkPreview.html lists/list_overview.html +#: lists/preview.html lists/snippet.html lists/widget.html +#: my_books/dropdown_content.html type/work/editions.html #, python-format msgid "Cover of: %(title)s" msgstr "" @@ -6050,7 +6050,7 @@ msgid "Merge authors" msgstr "" #: search/authors.html -msgid "No hits" +msgid "No authors directly matched your search" msgstr "" #: search/inside.html @@ -6058,14 +6058,13 @@ msgstr "" msgid "Search Open Library for %s" msgstr "" -#: BookSearchInside.html SearchNavigation.html search/inside.html -#: search/snippets.html +#: BookSearchInside.html FulltextSearchSuggestion.html SearchNavigation.html +#: search/inside.html search/snippets.html msgid "Search Inside" msgstr "" #: search/inside.html -#, python-format -msgid "No hits for: %(query)s" +msgid "No Search Inside text matched your search" msgstr "" #: search/inside.html @@ -6090,6 +6089,10 @@ msgstr "" msgid "Search Lists" msgstr "" +#: search/lists.html +msgid "No lists directly matched your search" +msgstr "" + #: search/publishers.html msgid "Publishers Search" msgstr "" @@ -6155,6 +6158,10 @@ msgstr "" msgid "Search Subjects" msgstr "" +#: search/subjects.html +msgid "No subjects directly matched your search" +msgstr "" + #: search/subjects.html msgid "time" msgstr "" @@ -7325,10 +7332,49 @@ msgstr "" msgid "Expires" msgstr "" +#: FulltextSearchSuggestion.html +msgid "Checking for Search Inside matches" +msgstr "" + +#: FulltextSearchSuggestion.html +msgid "Search Inside Icon" +msgstr "" + +#: FulltextSearchSuggestion.html +msgid "search inside icon" +msgstr "" + +#: FulltextSearchSuggestion.html +#, python-format +msgid "%(book_count)s books found with matching passages" +msgstr "" + +#: FulltextSearchSuggestion.html +#, python-format +msgid "See all %(results_count)s Search Inside Matches" +msgstr "" + +#: FulltextSearchSuggestion.html +msgid "right chevron" +msgstr "" + #: FulltextSnippet.html msgid "See All Results" msgstr "" +#: FulltextSuggestionSnippet.html +msgid "❝" +msgstr "" + +#: FulltextSuggestionSnippet.html +msgid "❞" +msgstr "" + +#: FulltextSuggestionSnippet.html +#, python-format +msgid "Page: %(page)s" +msgstr "" + #: IABook.html #, python-format msgid "Borrowed from Internet Archive: %(title)s" diff --git a/openlibrary/macros/FulltextSearchSuggestion.html b/openlibrary/macros/FulltextSearchSuggestion.html new file mode 100644 index 00000000000..30fd62fe4e2 --- /dev/null +++ b/openlibrary/macros/FulltextSearchSuggestion.html @@ -0,0 +1,35 @@ +$def with(query, results, page=1) + +$def render_snippet(query, doc): + $:macros.FulltextSuggestionSnippet(query, doc=doc) + +$if results and results.get('hits'): + $ hits = results['hits'].get('hits', [])[:4] + $ num_found = commify(results['hits'].get('total', 0)) + $ ia_base_url = "https://archive.org" + + $:macros.LoadingIndicator(_("Checking for Search Inside matches")) +
+
+ + $_('search inside icon') + +

+ $_("Search Inside") — $_('%(book_count)s books found with matching passages', book_count=num_found) +

+
+ $for doc in hits: + $if doc.get('edition'): + $:macros.FulltextSearchSuggestionItem(doc['edition'], snippet=render_snippet(query, doc)) + +
\ No newline at end of file diff --git a/openlibrary/macros/FulltextSearchSuggestionItem.html b/openlibrary/macros/FulltextSearchSuggestionItem.html new file mode 100644 index 00000000000..220c0baa071 --- /dev/null +++ b/openlibrary/macros/FulltextSearchSuggestionItem.html @@ -0,0 +1,91 @@ +$def with (doc, snippet=None, attrs=None, blur=False) + +$code: + max_rendered_authors = 9 + doc_type = ( + 'infogami_work' if doc.get('type', {}).get('key') == '/type/work' else + 'infogami_edition' if doc.get('type', {}).get('key') == '/type/edition' else + 'solr_work' if not doc.get('editions') else + 'solr_edition' + ) + + selected_ed = doc + if doc_type == 'solr_edition': + selected_ed = doc.get('editions')[0] + + book_url = doc.url() if doc_type.startswith('infogami_') else doc.key + if doc_type == 'solr_edition': + work_edition_url = book_url + '?edition=' + urlquote('key:' + selected_ed.key) + else: + book_provider = get_book_provider(doc) + if book_provider and doc_type.endswith('_work'): + work_edition_url = book_url + '?edition=' + urlquote(book_provider.get_best_identifier_slug(doc)) + else: + work_edition_url = book_url + + work_edition_all_url = work_edition_url + if '?' in work_edition_url: + work_edition_all_url += '&mode=all' + else: + work_edition_all_url += '?mode=all' + + edition_work = None + if doc_type == 'infogami_edition' and 'works' in doc: + edition_work = doc['works'][0] + + full_title = selected_ed.get('title', '') + (': ' + selected_ed.subtitle if selected_ed.get('subtitle') else '') + if doc_type == 'infogami_edition' and edition_work: + full_work_title = edition_work.get('title', '') + (': ' + edition_work.subtitle if edition_work.get('subtitle') else '') + else: + full_work_title = doc.get('title', '') + (': ' + doc.subtitle if doc.get('subtitle') else '') + +$ blur_cover = "fsi__bookcover-img-blur" if blur else "" + +
+
+ + $ cover = get_cover_url(selected_ed) or "/images/icons/avatar_book-sm.png" + + $_('Cover of: %(title)s', title=full_title) + + +
+
+

+ +

+ +
+ $if snippet: + $:snippet +
+
+
+ diff --git a/openlibrary/macros/FulltextSuggestionSnippet.html b/openlibrary/macros/FulltextSuggestionSnippet.html new file mode 100644 index 00000000000..3cad6607185 --- /dev/null +++ b/openlibrary/macros/FulltextSuggestionSnippet.html @@ -0,0 +1,26 @@ +$def with (q, doc=None) + +$ ia = doc.get('fields', {}).get('identifier', [''])[0] +$ ia_base_url = "https://archive.org" +$ availability = doc.get('availability', {}) +$ snippet = doc.get('highlight', {}).get('text', [''])[0] +$ page_nums = doc.get('fields', {}).get('page_num', []) +$if len(page_nums) == 1 and isinstance(page_nums[0], list): + $ page_nums = page_nums[0] +$ page = ', '.join(str(num) for num in page_nums) + +$if snippet: +
+ $if snippet: +
+
$_('❝')
+ + …$:(snippet.replace("<", "«").replace(">", "»").replace("{{{", "").replace("}}}", ""))… +   +
$_('❞')
+ $if page: + +
+
diff --git a/openlibrary/plugins/openlibrary/code.py b/openlibrary/plugins/openlibrary/code.py index 2ef6cfdb6c1..8ece430c8cd 100644 --- a/openlibrary/plugins/openlibrary/code.py +++ b/openlibrary/plugins/openlibrary/code.py @@ -41,6 +41,7 @@ from openlibrary.utils.isbn import isbn_13_to_isbn_10, isbn_10_to_isbn_13, canonical from openlibrary.core.models import Edition from openlibrary.core.lending import get_availability +from openlibrary.core.fulltext import fulltext_search import openlibrary.core.stats from openlibrary.plugins.openlibrary.home import format_work_data from openlibrary.plugins.openlibrary.stats import increment_error_count @@ -1114,6 +1115,7 @@ def GET(self): args[0], args[1] ) partial = {"partials": str(macro)} + elif component == 'SearchFacets': data = json.loads(i.data) path = data.get('path') @@ -1150,6 +1152,18 @@ def GET(self): "activeFacets": str(active_facets).strip(), } + elif component == "FulltextSearchSuggestion": + query = i.get('data', '') + data = fulltext_search(query) + hits = data.get('hits', []) + if not hits['hits']: + macro = '
' + else: + macro = web.template.Template.globals[ + 'macros' + ].FulltextSearchSuggestion(query, data) + partial = {"partials": str(macro)} + return delegate.RawText(json.dumps(partial)) diff --git a/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js b/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js new file mode 100644 index 00000000000..96e7980d91c --- /dev/null +++ b/openlibrary/plugins/openlibrary/js/fulltext-search-suggestion.js @@ -0,0 +1,61 @@ +export function initFulltextSearchSuggestion(fulltextSearchSuggestion) { + 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 +} +async function getPartials(fulltextSearchSuggestion, query) { + const queryParam = encodeURIComponent(query) + return fetch(`/partials.json?_component=FulltextSearchSuggestion&data=${queryParam}`) + .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) + }) + } + + }) +} + +/** + * Returns HTML string with error message and retry link. + * + * @returns {string} HTML for a retry link. + */ +function renderRetryLink() { + return 'Failed to fetch fulltext search suggestions. Retry?' +} diff --git a/openlibrary/plugins/openlibrary/js/index.js b/openlibrary/plugins/openlibrary/js/index.js index d3412d36181..a0aed4020e8 100644 --- a/openlibrary/plugins/openlibrary/js/index.js +++ b/openlibrary/plugins/openlibrary/js/index.js @@ -555,4 +555,12 @@ jQuery(function () { 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)) + } }); + diff --git a/openlibrary/templates/search/authors.html b/openlibrary/templates/search/authors.html index 82871bcf79e..448efdbe4b3 100644 --- a/openlibrary/templates/search/authors.html +++ b/openlibrary/templates/search/authors.html @@ -48,7 +48,13 @@

$_("Search Authors")

$_('Is the same author listed twice?') $_('Merge authors')
$else: -

$_('No hits')

+
+
$:_('No authors directly matched your search')
+
+
+
+ $:macros.LoadingIndicator(_("Checking Search Inside matches")) +
$elif q: -
- $_("No results found.") - $_('Search for books containing the phrase "%s"?', q) +
+
$:_('No lists directly matched your search')
+
+
+
+ $:macros.LoadingIndicator(_("Checking Search Inside matches"))
-
diff --git a/openlibrary/templates/search/subjects.html b/openlibrary/templates/search/subjects.html index 2928552c0c0..220c3fab657 100644 --- a/openlibrary/templates/search/subjects.html +++ b/openlibrary/templates/search/subjects.html @@ -27,7 +27,16 @@

$if q: $ response = get_results(q, offset=offset, limit=results_per_page) $if not response.error: -

$ungettext('%(count)s hit', '%(count)s hits', response.num_found, count=commify(response.num_found))

+ $if response.num_found: +

$ungettext('%(count)s hit', '%(count)s hits', response.num_found, count=commify(response.num_found))

+ $else: +
+
$:_('No subjects directly matched your search')
+
+
+
+ $:macros.LoadingIndicator(_("Checking Search Inside matches")) +
$if q and response.error: diff --git a/openlibrary/templates/work_search.html b/openlibrary/templates/work_search.html index c3d09b8ad6f..076f0c2a5e6 100644 --- a/openlibrary/templates/work_search.html +++ b/openlibrary/templates/work_search.html @@ -48,21 +48,21 @@

$_("Search Books")

$if param and not search_response.docs: + $if ctx.path == '/search': + $ path_id = 'books' + $else: + $ path_id = ctx.path.split('/') $ query = query_param('q') $# Temporarily (2020-03-26) disable automatically showing full-text $# results because of performance issues due to increased load. See $# this commit to revert.
-
$_("No results found.")
+
$:_('No %(path_id)s directly matched your search', path_id=path_id)

-
- $_('Search for books containing the phrase "%s"?' % query) -
-
-
- $_("Add a new book to Open Library?") -
+
+ $:macros.LoadingIndicator(_("Checking Search Inside matches")) +
$elif param and not search_response.error and len(search_response.docs):
diff --git a/static/css/components/fulltext-search-suggestion-item.less b/static/css/components/fulltext-search-suggestion-item.less new file mode 100644 index 00000000000..112a3e68f1f --- /dev/null +++ b/static/css/components/fulltext-search-suggestion-item.less @@ -0,0 +1,96 @@ +.fsi { + &__main { + .display-flex(); + flex-direction: row; + background-color: @white; + padding: 5px; + margin-bottom: 5px; + border-bottom: 1px solid @light-beige; + border-radius: 3px; + overflow: hidden; + @media (min-width: @width-breakpoint-tablet) { + flex-direction: row; + } + } + + &__book-cover { + overflow: hidden; + height: 100%; + width: auto; + min-width: 90px; + border-radius: 4px; + align-self: center; + margin-top: 5px; + @media (min-width: @width-breakpoint-tablet) { + height: 130px; + margin-bottom: 5px; + } + } + + &__book-cover-img { + height: 100%; + width: 100%; + object-fit: cover; + } + + &__book-cover-img-blur { + filter: blur(4px); + } + + &__book-details { + display: flex; + flex-direction: column; + gap: 10px; + margin: 5px 10px 0; + @media (min-width: @width-breakpoint-tablet) { + align-items: flex-start; + } + } + + &__title-author { + display: flex; + flex-direction: column; + padding: 5px 3px 0 10px; + width: 50%; + @media (min-width: @width-breakpoint-mobile-s) { + width: 70% + } + @media (min-width: @width-breakpoint-mobile-m) { + width: 80% + } + } + + &__book-title, + &__book-author { + width: 80%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-family: @lucida_sans_serif-6; + } + + &__book-title { + font-size: large; + margin: 0; + display: block; + padding: 0; + font-weight: 700; + line-height: 1.2em; + } + + &__book-title a:link, + &__book-title a:visited { + text-decoration: none; + color: @black; + } + + &__book-title a:hover { + color: @header-nav-hover-color; + } + + &__book-author { + line-height: 1.2em; + margin: 3px 0 0; + font-size: medium; + } +} diff --git a/static/css/components/fulltext-search-suggestion.less b/static/css/components/fulltext-search-suggestion.less new file mode 100644 index 00000000000..168b5e06d7e --- /dev/null +++ b/static/css/components/fulltext-search-suggestion.less @@ -0,0 +1,83 @@ +/** + * Search Result Item + * https://github.com/internetarchive/openlibrary/wiki/Design-Pattern-Library#searchresultitem + */ + +.fulltext-suggestions{ + list-style-type: none; + line-height: 1.5em; + background-color: @grey-fafafa; + border-radius: 5px; + padding: 5px; + margin-bottom: 3px; + border-bottom: 1px solid @light-beige; + .display-flex(); + flex-direction: column; + + &__header { + display: flex; + flex-direction: row; + align-items: start; + padding: 5px; + margin-bottom: 3px; + } + + &__icon { + margin-top: 5px; + min-height: 30px; + min-width: 30px; + @media (min-width: @width-breakpoint-tablet) { + align-items: center; + } + } + + &__title { + display: block; + margin: 0; + padding: 5px; + font-size: medium; + font-weight: 700; + font-family: @lucida_sans_serif-6; + @media (min-width: @width-breakpoint-tablet) { + font-size: large; + } + a:link, + a:visited { + color: @black; + text-decoration: none; + } + a:hover { + color: @link-blue; + } + } + + &__footer { + display: flex; + justify-content: center; + margin: 0; + padding: 5px; + font-size: small; + font-weight: 700; + font-family: @lucida_sans_serif-6; + @media (min-width: @width-breakpoint-mobile-m) { + font-size: medium; + } + @media (min-width: @width-breakpoint-tablet) { + font-size: large; + justify-content: end; + } + } + + &__right-chevron { + max-height: 20px; + max-width: 20px; + display: none; + @media (min-width: @width-breakpoint-mobile-m) { + display: inline; + } + } + + &__retry { + text-align: center; + } +} diff --git a/static/css/components/fulltext-suggestion-snippet.less b/static/css/components/fulltext-suggestion-snippet.less new file mode 100644 index 00000000000..7e8d12be771 --- /dev/null +++ b/static/css/components/fulltext-suggestion-snippet.less @@ -0,0 +1,47 @@ +.fsi-snippet { + display: inline; + width: 60%; + padding: 5px; + margin: 0; + font-size: small; + @media (min-width: @width-breakpoint-mobile-m) { + width: 90%; + } + @media (min-width: @width-breakpoint-tablet) { + width: auto; + font-size: medium; + } + + &__main { + padding: 5px; + margin: 0 5px; + line-height: 1.6em; + + &__page-num a { + color: @dark-grey; + display: inline; + text-decoration: none; + font-family: @lucida_sans_serif-1; + } + + a:link, + a:hover, + a:visited { + text-decoration: none; + } + + a:link { + font-family: @georgia_serif-1; + color: @black; + } + + a:hover { + color: @link-blue; + } + } + + &__quotation-mark { + font-size: larger; + display: inline; + } +} diff --git a/static/css/less/breakpoints.less b/static/css/less/breakpoints.less index 7601e3292f7..8dea0230664 100644 --- a/static/css/less/breakpoints.less +++ b/static/css/less/breakpoints.less @@ -1,3 +1,5 @@ +@width-breakpoint-mobile-s: 375px; +@width-breakpoint-mobile-m: 425px; @width-breakpoint-mobile: 450px; @width-breakpoint-tablet: 768px; // 48em @width-breakpoint-desktop: 960px; // 60em diff --git a/static/css/page-user.less b/static/css/page-user.less index 8ff38472ce5..10d6154968e 100644 --- a/static/css/page-user.less +++ b/static/css/page-user.less @@ -224,6 +224,12 @@ tr.table-row.selected{ transform: translateY(-50%); } +// Import styles for fulltext-search-suggestion card +@import (less) "components/fulltext-search-suggestion.less"; +// Import styles for fulltext-search-suggestion card item +@import (less) "components/fulltext-search-suggestion-item.less"; +// Import styles for fulltext-search-suggestion card item snippet +@import (less) "components/fulltext-suggestion-snippet.less"; // Import styles for author infobox @import (less) "components/author-infobox.less"; // Import all common components diff --git a/static/images/icons/icon_search-inside.svg b/static/images/icons/icon_search-inside.svg new file mode 100644 index 00000000000..7982c49cd98 --- /dev/null +++ b/static/images/icons/icon_search-inside.svg @@ -0,0 +1 @@ + \ No newline at end of file