Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-docs.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSuzanne Selhorn <sselhorn@gitlab.com>2023-08-15 00:38:11 +0300
committerSuzanne Selhorn <sselhorn@gitlab.com>2023-08-15 00:38:11 +0300
commitfb97a56db704f205050a150e05176e2af4990b4b (patch)
treec52017f9575f68780167e71f02d2d69d2c5a8664
parent87b10d25f454e35af4bcf2f6c9d1d6281209354a (diff)
parentcbffa2a3409ddf27f88ef489bb81f5d9b907a201 (diff)
Merge branch 'gps-search-result-scroll' into 'main'
Scroll to search query on result pages See merge request https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/4140 Merged-by: Suzanne Selhorn <sselhorn@gitlab.com> Co-authored-by: Sarah German <sgerman@gitlab.com>
-rw-r--r--content/assets/stylesheets/stylesheet.scss5
-rw-r--r--content/frontend/search/components/lunr_results.vue7
-rw-r--r--content/frontend/search/google.js3
-rw-r--r--content/frontend/search/lunrsearch.js42
-rw-r--r--content/frontend/search/search_helpers.js55
-rw-r--r--content/frontend/services/google_search_api.js7
-rw-r--r--spec/frontend/search/search_helper_spec.js32
7 files changed, 128 insertions, 23 deletions
diff --git a/content/assets/stylesheets/stylesheet.scss b/content/assets/stylesheets/stylesheet.scss
index 751c5b18..ebaa60bd 100644
--- a/content/assets/stylesheets/stylesheet.scss
+++ b/content/assets/stylesheets/stylesheet.scss
@@ -20,6 +20,11 @@ body {
}
}
+h2,
+h3,
+h4,
+h5,
+h6,
:target {
scroll-margin-top: $header-height + 1rem;
}
diff --git a/content/frontend/search/components/lunr_results.vue b/content/frontend/search/components/lunr_results.vue
index 7d147127..f579938d 100644
--- a/content/frontend/search/components/lunr_results.vue
+++ b/content/frontend/search/components/lunr_results.vue
@@ -1,7 +1,7 @@
<script>
/* global lunr */
import { GlSearchBoxByClick, GlLink } from '@gitlab/ui';
-import { getSearchParamsFromURL, updateURLParams } from '../search_helpers';
+import { getSearchParamsFromURL, updateURLParams, searchResultQueryParam } from '../search_helpers';
import { isArchivesSite } from '../../default/environment';
export default {
@@ -70,7 +70,10 @@ export default {
Object.keys(this.results).forEach((key) => {
const contentItem = this.contentMap.find(({ id }) => id === this.results[key].ref);
this.results[key].title = contentItem.h1;
- this.results[key].link = `/${this.results[key].ref}`;
+ this.results[key].link = `/${this.results[key].ref}${searchResultQueryParam(
+ this.query,
+ this.results[key].ref,
+ )}`;
});
// Rewrite links to include the version prefix if this is the archives site.
diff --git a/content/frontend/search/google.js b/content/frontend/search/google.js
index 153ef650..b13e6b49 100644
--- a/content/frontend/search/google.js
+++ b/content/frontend/search/google.js
@@ -1,8 +1,9 @@
/* global Vue */
import GoogleSearchForm from './components/google_search_form.vue';
-import { activateKeyboardShortcut } from './search_helpers';
+import { activateKeyboardShortcut, scrollToQuery } from './search_helpers';
document.addEventListener('DOMContentLoaded', () => {
+ scrollToQuery();
activateKeyboardShortcut();
const { isHomepage } = document.querySelector('body').dataset;
diff --git a/content/frontend/search/lunrsearch.js b/content/frontend/search/lunrsearch.js
index 5ca9229a..15c215e9 100644
--- a/content/frontend/search/lunrsearch.js
+++ b/content/frontend/search/lunrsearch.js
@@ -1,26 +1,30 @@
/* global Vue */
import LunrResults from './components/lunr_results.vue';
import SearchForm from './components/lunr_search_form.vue';
+import { activateKeyboardShortcut, scrollToQuery } from './search_helpers';
-// Search results page (/search)
document.addEventListener('DOMContentLoaded', () => {
- return new Vue({
- el: '.js-lunrsearch',
- render(createElement) {
- return createElement(LunrResults);
- },
- });
-});
+ scrollToQuery();
+ activateKeyboardShortcut();
-// Homepage and interior navbar search forms
-document.addEventListener('DOMContentLoaded', () => {
- return new Vue({
- el: '.js-search-form',
- components: {
- SearchForm,
- },
- render(createElement) {
- return createElement(SearchForm);
- },
- });
+ // Search results page (/search)
+ (() =>
+ new Vue({
+ el: '.js-lunrsearch',
+ render(createElement) {
+ return createElement(LunrResults);
+ },
+ }))();
+
+ // Homepage and interior navbar search forms
+ (() =>
+ new Vue({
+ el: '.js-search-form',
+ components: {
+ SearchForm,
+ },
+ render(createElement) {
+ return createElement(SearchForm);
+ },
+ }))();
});
diff --git a/content/frontend/search/search_helpers.js b/content/frontend/search/search_helpers.js
index 744d7302..c4c22b7d 100644
--- a/content/frontend/search/search_helpers.js
+++ b/content/frontend/search/search_helpers.js
@@ -118,3 +118,58 @@ export const activateKeyboardShortcut = () => {
document.querySelector('input[type="search"]').focus();
});
};
+
+/**
+ * Find the highest-level scrollable header that contains a given string.
+ *
+ * We need a regex here to match only if there are word boundaries or slashes.
+ * For example:
+ * - "to" should match in "login to gitlab" but not "repository"
+ * - "AI" should match in "AI/ML" but not "FAIL"
+ */
+export const findHighestHeader = (query) => {
+ const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
+
+ const regex = new RegExp(`(?<=^|\\s|\\/)${query}s?(?=$|\\s|\\/)`, 'gi');
+ const matches = Array.from(headings).filter((heading) => heading.textContent.match(regex));
+
+ if (matches.length) {
+ return matches.sort((a, b) => a.tagName.localeCompare(b.tagName))[0];
+ }
+
+ return null;
+};
+
+/**
+ * If a search query is in the URL parameters and a heading, scroll to it.
+ */
+export const scrollToQuery = () => {
+ const { qParam } = getSearchParamsFromURL();
+ if (!qParam) return;
+
+ const header = findHighestHeader(qParam);
+ if (header && header.tagName !== 'H1') {
+ header.scrollIntoView({ behavior: 'smooth' });
+ }
+};
+
+/**
+ * Generate a query string to be appended to search result links.
+ *
+ * This is used to limit which pages we run scrollToQuery() on.
+ */
+export const searchResultQueryParam = (query, link) => {
+ const pages = new Set(['/ee/ci/yaml/']);
+
+ // Check if the search result is included in the pages set.
+ let linkPath = '';
+ try {
+ const url = new URL(link); // Google results are full URLs
+ linkPath = url.pathname;
+ } catch {
+ linkPath = `/${link.replace('/index.html', '/')}`; // Lunr results are just paths
+ }
+
+ if (pages.has(linkPath)) return `?query=${query}`;
+ return '';
+};
diff --git a/content/frontend/services/google_search_api.js b/content/frontend/services/google_search_api.js
index c0b81b0e..e33fac77 100644
--- a/content/frontend/services/google_search_api.js
+++ b/content/frontend/services/google_search_api.js
@@ -1,5 +1,7 @@
/* global GOOGLE_SEARCH_KEY */
+import { searchResultQueryParam } from '../search/search_helpers';
+
export const GPS_ENDPOINT = 'https://www.googleapis.com/customsearch/v1/siterestrict?';
export const GPS_ID = '97494f9fe316a426d';
export const MAX_RESULTS_PER_PAGE = 10;
@@ -37,7 +39,10 @@ export const fetchResults = async (query, filters, pageNumber, numResults) => {
.replace(' ...', '')
.replaceAll('`', '')
.trim(),
- relativeLink: item.link.replace('https://docs.gitlab.com/', '/'),
+ relativeLink: `${item.link.replace('https://docs.gitlab.com/', '/')}${searchResultQueryParam(
+ query,
+ item.link,
+ )}`,
breadcrumbs: item.pagemap.metatags[0]['gitlab-docs-breadcrumbs'] ?? '',
}));
}
diff --git a/spec/frontend/search/search_helper_spec.js b/spec/frontend/search/search_helper_spec.js
new file mode 100644
index 00000000..9fe01063
--- /dev/null
+++ b/spec/frontend/search/search_helper_spec.js
@@ -0,0 +1,32 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import { findHighestHeader } from '../../../content/frontend/search/search_helpers';
+
+describe('frontend/search/search_helpers', () => {
+ beforeEach(() => {
+ // Create a mock DOM environment
+ document.body.innerHTML =
+ '<!DOCTYPE html><html><body><h2>login to gitlab</h2><h3>AI/ML</h3><h4>repository</h4></body></html>';
+ });
+
+ it('should find the correct scrollable header for query "to"', () => {
+ const result = findHighestHeader('to');
+ expect(result).toBeDefined();
+ expect(result.textContent).toBe('login to gitlab');
+ expect(result.tagName.toLowerCase()).toBe('h2');
+ });
+
+ it('should find the correct scrollable header for query "AI"', () => {
+ const result = findHighestHeader('AI');
+ expect(result).toBeDefined();
+ expect(result.textContent).toBe('AI/ML');
+ expect(result.tagName.toLowerCase()).toBe('h3');
+ });
+
+ it('should not find any scrollable header for query "FAIL"', () => {
+ const result = findHighestHeader('FAIL');
+ expect(result).toBeNull();
+ });
+});