diff --git a/gulp.d/tasks/build-preview-pages.js b/gulp.d/tasks/build-preview-pages.js index 8641661a..8cbae9a1 100644 --- a/gulp.d/tasks/build-preview-pages.js +++ b/gulp.d/tasks/build-preview-pages.js @@ -142,8 +142,8 @@ function copyImages (src, dest) { function relativize (to, { data: { root } }) { if (!to) return '#' - const from = root.page.url if (to.charAt() !== '/') return to + const from = root.page.url if (!from) return (root.site.path || '') + to let hash = '' const hashIdx = to.indexOf('#') diff --git a/src/css/header.css b/src/css/header.css index 702d96a4..1f6e1adb 100644 --- a/src/css/header.css +++ b/src/css/header.css @@ -291,11 +291,47 @@ body { } .search { - height: 40px; + align-items: center; border-left: 1px solid var(--toolbar-border-color); + display: flex; + height: 100%; + padding: 8px 10px; + position: relative; width: 192px; } +.search .field { + align-items: center; + border: 1px solid var(--kbd-border-color); + border-radius: 0.1em; + display: flex; + flex: auto; + height: inherit; +} + +.search .field * { + color: var(--body-font-dark-color); +} + +.search .query { + background: transparent; + border: none; + flex: auto; + font-size: 0.95rem; + height: inherit; + padding: 2px 0.25em; + width: 7ex; +} + +.search .filter, +.search .filter label { + margin-right: 0.5em; +} + +.search .query:focus { + outline: none; +} + .edit-this-page { height: 40px; line-height: 40px; diff --git a/src/css/vendor/docsearch.css b/src/css/vendor/docsearch.css index 62c086d8..ca6fecd3 100644 --- a/src/css/vendor/docsearch.css +++ b/src/css/vendor/docsearch.css @@ -1,50 +1,110 @@ /*! docsearch 2.6.x | © Algolia | github.com/algolia/docsearch */ @import "docsearch.js/dist/cdn/docsearch.css"; -.algolia-autocomplete .ds-dropdown-menu [class^="ds-dataset-"] { - max-height: calc(100vh - 3.25rem); +.algolia-autocomplete-results { + position: relative; + top: 14px; + right: -1px; } -@media screen and (max-width: 768px) { - .algolia-autocomplete .ds-dropdown-menu { - min-width: calc(100vw - 2.75rem) !important; - } +.algolia-autocomplete { + left: auto !important; + top: auto !important; } -body .algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-] { +.algolia-autocomplete .ds-dropdown-menu { background: var(--panel-background); + border: 1px solid var(--codetools-border-color); + border-radius: 4px; + box-shadow: none; + display: flex !important; + flex-direction: column; + max-height: calc(var(--nav-height) - 8px); + max-width: none; + min-width: auto; + position: absolute; + top: 0; + right: 0; + width: 35vw; } -body .algolia-autocomplete .ds-dropdown-menu::before { - background: var(--panel-background); +.algolia-autocomplete .ds-dropdown-menu::before { + content: none; } -body .algolia-autocomplete .algolia-docsearch-suggestion { - background: var(--panel-background); +.algolia-autocomplete .ds-dropdown-menu .ds-dataset-1 { + background: none; + border: none; + border-radius: inherit; + overscroll-behavior: none; + padding: 0 8px; } -body .algolia-autocomplete .algolia-docsearch-suggestion--category-header { - color: var(--body-font-color); +.algolia-autocomplete .ds-dropdown-menu .ds-suggestions { + margin: 8px 0; } -body .algolia-autocomplete .algolia-docsearch-suggestion--title { - color: var(--body-font-color); +.algolia-autocomplete .algolia-docsearch-suggestion { + background: none; } -body .algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column { - color: var(--color-gray-50, #ccc); +.algolia-autocomplete .algolia-docsearch-suggestion--category-header, +.algolia-autocomplete .algolia-docsearch-suggestion--title { + color: inherit; } -body .algolia-autocomplete .algolia-docsearch-suggestion--highlight { +.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column { + color: var(--color-gray-50); +} + +.algolia-autocomplete .algolia-docsearch-suggestion--highlight { color: var(--link-font-color); } -body .algolia-autocomplete .algolia-docsearch-footer { - width: 140px; +.algolia-autocomplete .ds-footer { + align-items: center; + display: flex; + justify-content: space-between; + background-color: var(--body-background-color); + border-top: 1px solid var(--codetools-border-color); + padding: 10px 8px 8px; + border-radius: 0 0 4px 4px; +} + +.algolia-autocomplete .ds-pagination { + display: flex; + align-items: center; + font-size: 0.9em; + line-height: 1; +} + +.algolia-autocomplete .ds-pagination > :not(:first-child) { + margin-left: 0.5em; +} + +.algolia-autocomplete .ds-pagination a { + color: var(--link-font-color); +} + +.algolia-autocomplete .ds-pagination--prev::before, +.algolia-autocomplete .ds-pagination--next::after { + display: inline-block; + width: 0.5em; + font-size: 1.2em; + line-height: 0; +} + +.algolia-autocomplete .ds-pagination--prev::before { + content: "\2039"; +} + +.algolia-autocomplete .ds-pagination--next::after { + content: "\203a"; + text-align: right; } -body .algolia-autocomplete .algolia-docsearch-footer--logo { - background: url('../img/search-by-algolia-light-background.svg') no-repeat; - background-size: 130px 20px; - width: 140px; +.algolia-autocomplete .algolia-docsearch-footer { + margin-top: 0; + width: 112px; + height: 16px; } diff --git a/src/js/vendor/docsearch.bundle.js b/src/js/vendor/docsearch.bundle.js index 6a1dcb7b..5c161aee 100644 --- a/src/js/vendor/docsearch.bundle.js +++ b/src/js/vendor/docsearch.bundle.js @@ -1,23 +1,306 @@ -/*! docsearch 2.6.x | © Algolia | github.com/algolia/docsearch **/ -;(function () { +;(function () { /*! docsearch 2.6.x | © Algolia | github.com/algolia/docsearch */ 'use strict' + var FORWARD_BACK_TYPE = 2 + var SEARCH_FILTER_ACTIVE_KEY = 'docs:search-filter-active' + var SAVED_SEARCH_STATE_KEY = 'docs:saved-search-state' + var SAVED_SEARCH_STATE_VERSION = '1' + activateSearch(require('docsearch.js/dist/cdn/docsearch.js'), document.getElementById('search-script').dataset) function activateSearch (docsearch, config) { - var input = docsearch({ + appendStylesheet(config.stylesheet) + var baseAlgoliaOptions = { + hitsPerPage: parseInt(config.pageSize, 10) || 20, // cannot exceed the hitsPerPage value defined on the index + } + var searchField = document.getElementById(config.searchFieldId || 'search') + searchField.appendChild(Object.assign(document.createElement('div'), { className: 'algolia-autocomplete-results' })) + var controller = docsearch({ appId: config.appId, apiKey: config.apiKey, indexName: config.indexName, - inputSelector: config.inputSelector || '#search', - autocompleteOptions: { hint: false, keyboardShortcuts: ['s'] }, - baseAlgoliaOptions: { hitsPerPage: parseInt(config.hitsPerPage, 10) || 20 }, - }).input - var typeahead = input.data('aaAutocomplete') - input.on('autocomplete:closed', function () { - typeahead.setVal() + inputSelector: '#' + searchField.id + ' .query', + autocompleteOptions: { + autoselect: false, + debug: true, + hint: false, + minLength: 2, + appendTo: '#' + searchField.id + ' .algolia-autocomplete-results', + autoWidth: false, + templates: { + footer: + '
', + }, + }, + baseAlgoliaOptions: baseAlgoliaOptions, }) - typeahead.setVal() + var input = controller.input + var typeahead = input.data('aaAutocomplete') + var dropdown = typeahead.dropdown + var menu = dropdown.$menu + var dataset = dropdown.datasets[0] + dataset.cache = false + dataset.source = controller.getAutocompleteSource(undefined, processQuery.bind(typeahead, controller)) + delete dataset.templates.footer + controller.queryDataCallback = processQueryData.bind(typeahead) + typeahead.setVal() // clear value on page reload + input.on('autocomplete:closed', clearSearch.bind(typeahead)) + input.on('autocomplete:cursorchanged autocomplete:cursorremoved', saveSearchState.bind(typeahead)) + input.on('autocomplete:selected', onSuggestionSelected.bind(typeahead)) + input.on('autocomplete:updated', onResultsUpdated.bind(typeahead)) + dropdown._ensureVisible = ensureVisible + menu.off('mousedown.aa') + menu.off('mouseenter.aa') + menu.off('mouseleave.aa') + var suggestionSelector = '.' + dropdown.cssClasses.prefix + dropdown.cssClasses.suggestion + menu.on('mousedown.aa', suggestionSelector, onSuggestionMouseDown.bind(dropdown)) + typeahead.$facetFilterInput = input + .closest('#' + searchField.id) + .find('.filter input') + .on('change', toggleFilter.bind(typeahead)) + .prop('checked', window.localStorage.getItem(SEARCH_FILTER_ACTIVE_KEY) === 'true') + menu.find('.ds-pagination--prev').on('click', paginate.bind(typeahead, -1)).css('visibility', 'hidden') + menu.find('.ds-pagination--next').on('click', paginate.bind(typeahead, 1)).css('visibility', 'hidden') + monitorCtrlKey.call(typeahead) + searchField.addEventListener('click', confineEvent) + document.documentElement.addEventListener('click', clearSearch.bind(typeahead)) + document.addEventListener('keydown', handleShortcuts.bind(typeahead)) if (input.attr('autofocus') != null) input.focus() + window.addEventListener('pageshow', reactivateSearch.bind(typeahead)) + } + + function reactivateSearch (e) { + var navigation = window.performance.navigation || {} + if ('type' in navigation) { + if (navigation.type !== FORWARD_BACK_TYPE) { + return + } else if (e.persisted && !isClosed(this)) { + this.$input.focus() + this.$input.val(this.getVal()) + this.dropdown.datasets[0].page = this.dropdown.$menu.find('.ds-pagination--curr').data('page') + } else if (window.sessionStorage.getItem('docs:restore-search-on-back') === 'true') { + if (!window.matchMedia('(min-width: 1024px)').matches) document.querySelector('.navbar-burger').click() + restoreSearch.call(this) + } + } + window.sessionStorage.removeItem('docs:restore-search-on-back') + } + + function appendStylesheet (href) { + document.head.appendChild(Object.assign(document.createElement('link'), { rel: 'stylesheet', href: href })) + } + + function onResultsUpdated () { + var dropdown = this.dropdown + var restoring = dropdown.restoring + delete dropdown.restoring + if (isClosed(this)) return + updatePagination.call(dropdown) + if (restoring && restoring.query === this.getVal() && restoring.filter === this.$facetFilterInput.prop('checked')) { + var cursor = restoring.cursor + if (cursor) dropdown._moveCursor(cursor) + } else { + saveSearchState.call(this) + } + } + + function toggleFilter (e) { + if ('restoring' in this.dropdown) return + window.localStorage.setItem(SEARCH_FILTER_ACTIVE_KEY, e.target.checked) + isClosed(this) ? this.$input.focus() : (this.dropdown.datasets[0].page = 0) || requery.call(this) + } + + function confineEvent (e) { + e.stopPropagation() + } + + function ensureVisible (el) { + var container = getScrollableResultsContainer(this)[0] + if (container.scrollHeight === container.offsetHeight) return + var delta + var item = el[0] + if ((delta = 15 + item.offsetTop + item.offsetHeight - (container.offsetHeight + container.scrollTop)) > 0) { + container.scrollTop += delta + } + if ((delta = item.offsetTop - container.scrollTop) < 0) { + container.scrollTop += delta + } + } + + function getScrollableResultsContainer (dropdown) { + return dropdown.datasets[0].$el + } + + function handleShortcuts (e) { + var target = e.target || {} + if (e.altKey || target.isContentEditable || 'disabled' in target) return + if (e.ctrlKey && e.key === '<') return restoreSearch.call(this) + if (e.ctrlKey ? e.key === '/' : e.key === 's') { + this.$input.focus() + e.preventDefault() + e.stopPropagation() + } + } + + function isClosed (typeahead) { + var query = typeahead.getVal() + return !query || query !== typeahead.dropdown.datasets[0].query + } + + function monitorCtrlKey () { + this.$input.on('keydown', onCtrlKeyDown.bind(this)) + this.dropdown.$menu.on('keyup', onCtrlKeyUp.bind(this)) + } + + function onCtrlKeyDown (e) { + if (e.key !== 'Control') return + this.ctrlKeyDown = true + var container = getScrollableResultsContainer(this.dropdown) + var prevScrollTop = container.scrollTop() + this.dropdown.getCurrentCursor().find('a').focus() + container.scrollTop(prevScrollTop) // calling focus can cause the container to scroll, so restore it + } + + function onCtrlKeyUp (e) { + if (e.key !== 'Control') return + delete this.ctrlKeyDown + this.$input.focus() + } + + function onSuggestionMouseDown (e) { + var dropdown = this + var suggestion = dropdown._getSuggestions().filter('#' + e.currentTarget.id) + if (suggestion[0] === dropdown._getCursor()[0]) return + dropdown._removeCursor() + dropdown._setCursor(suggestion, false) + } + + function onSuggestionSelected (e, suggestion, datasetNum, context) { + if (!this.ctrlKeyDown) { + if (context.selectionMethod === 'click') saveSearchState.call(this) + window.sessionStorage.setItem('docs:restore-search-on-back', 'true') + } + e.isDefaultPrevented = function () { + return true + } + } + + function paginate (delta, e) { + e.preventDefault() + var dataset = this.dropdown.datasets[0] + dataset.page = (dataset.page || 0) + delta + requery.call(this) + } + + function updatePagination () { + var result = this.datasets[0].result + var page = result.page + var menu = this.$menu + menu + .find('.ds-pagination--curr') + .html(result.pages ? 'Page ' + (page + 1) + ' of ' + result.pages : 'No results') + .data('page', page) + menu.find('.ds-pagination--prev').css('visibility', page > 0 ? '' : 'hidden') + menu.find('.ds-pagination--next').css('visibility', result.pages > page + 1 ? '' : 'hidden') + getScrollableResultsContainer(this).scrollTop(0) + } + + function requery (query) { + this.$input.focus() + query === undefined ? (query = this.input.getInputValue()) : this.input.setInputValue(query, true) + this.input.setQuery(query) + this.dropdown.update(query) + this.dropdown.open() + } + + function clearSearch () { + this.isActivated = true // we can't rely on this state being correct + this.setVal() + delete this.ctrlKeyDown + delete this.dropdown.datasets[0].result + } + + function processQuery (controller, query) { + var algoliaOptions = {} + if (this.$facetFilterInput.prop('checked')) { + algoliaOptions.facetFilters = [this.$facetFilterInput.data('facetFilter')] + } + var dataset = this.dropdown.datasets[0] + var activeResult = dataset.result + algoliaOptions.page = activeResult && activeResult.query !== query ? (dataset.page = 0) : dataset.page || 0 + controller.algoliaOptions = Object.keys(algoliaOptions).length + ? Object.assign({}, controller.baseAlgoliaOptions, algoliaOptions) + : controller.baseAlgoliaOptions + } + + function processQueryData (data) { + var result = data.results[0] + this.dropdown.datasets[0].result = { page: result.page, pages: result.nbPages, query: result.query } + result.hits = preserveHitOrder(result.hits) + } + + // preserves the original order of results by qualifying unique occurrences of the same lvl0 and lvl1 values + function preserveHitOrder (hits) { + var prevLvl0 + var lvl0Qualifiers = {} + var lvl1Qualifiers = {} + return hits.map(function (hit) { + var lvl0 = hit.hierarchy.lvl0 + var lvl1 = hit.hierarchy.lvl1 + var lvl0Qualifier = lvl0Qualifiers[lvl0] + if (lvl0 !== prevLvl0) { + lvl0Qualifiers[lvl0] = lvl0Qualifier == null ? (lvl0Qualifier = '') : (lvl0Qualifier += ' ') + lvl1Qualifiers = {} + } + if (lvl0Qualifier) hit.hierarchy.lvl0 = lvl0 + lvl0Qualifier + if (lvl1 in lvl1Qualifiers) { + hit.hierarchy.lvl1 = lvl1 + (lvl1Qualifiers[lvl1] += ' ') + } else { + lvl1Qualifiers[lvl1] = '' + } + prevLvl0 = lvl0 + return hit + }) + } + + function readSavedSearchState () { + try { + var state = window.localStorage.getItem(SAVED_SEARCH_STATE_KEY) + if (state && (state = JSON.parse(state))._version.toString() === SAVED_SEARCH_STATE_VERSION) return state + } catch (e) { + window.localStorage.removeItem(SAVED_SEARCH_STATE_KEY) + } + } + + function restoreSearch () { + var searchState = readSavedSearchState() + if (!searchState) return + this.dropdown.restoring = searchState + this.$facetFilterInput.prop('checked', searchState.filter) // change event will be ignored + var dataset = this.dropdown.datasets[0] + dataset.page = searchState.page + delete dataset.result + requery.call(this, searchState.query) // cursor is restored by onResultsUpdated + } + + function saveSearchState () { + if (isClosed(this)) return + window.localStorage.setItem( + SAVED_SEARCH_STATE_KEY, + JSON.stringify({ + _version: SAVED_SEARCH_STATE_VERSION, + cursor: this.dropdown.getCurrentCursor().index() + 1, + filter: this.$facetFilterInput.prop('checked'), + page: this.dropdown.datasets[0].page, + query: this.getVal(), + }) + ) } })() diff --git a/src/partials/footer-scripts.hbs b/src/partials/footer-scripts.hbs index 4403bdaf..5a2bdae5 100644 --- a/src/partials/footer-scripts.hbs +++ b/src/partials/footer-scripts.hbs @@ -2,5 +2,5 @@ {{#if env.ALGOLIA_API_KEY}} - + {{/if}} diff --git a/src/partials/head-styles.hbs b/src/partials/head-styles.hbs index 5dabc0bd..8e72e6a3 100644 --- a/src/partials/head-styles.hbs +++ b/src/partials/head-styles.hbs @@ -5,4 +5,3 @@ - diff --git a/src/partials/toolbar.hbs b/src/partials/toolbar.hbs index e42f499c..3f68dea4 100644 --- a/src/partials/toolbar.hbs +++ b/src/partials/toolbar.hbs @@ -5,7 +5,12 @@ {{/with}} {{> breadcrumbs}}