Skip to content

Commit

Permalink
Remove usage of jquery (#115)
Browse files Browse the repository at this point in the history
There are still more code that depends on jquery and underscorejs,
but this is a good start.

- Use the fetch API instead of jquery's ajax.
- Use the URL and URLSearchParams objects instead of our custom code
  to parse search parameters.
- Had to update the tests, because the new version of selenium remove the `find_element_by*` methods (pytest-selenium installs the latest version by default).
- Also updated pytest-selenium, which restricts the version of selenium to 4.x.
  • Loading branch information
stsewd committed Aug 3, 2022
1 parent ec07503 commit 5880a64
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 219 deletions.
3 changes: 0 additions & 3 deletions docs/js-api-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@ JavaScript API Reference
Following are the functions that are defined in `rtd_sphinx_search.js`_,

.. js:autofunction:: debounce
.. js:autofunction:: convertObjToUrlParams
.. js:autofunction:: updateUrl
.. js:autofunction:: createDomNode
.. js:autofunction:: _is_string
.. js:autofunction:: _is_array
.. js:autofunction:: get_section_html
.. js:autofunction:: get_domain_html
.. js:autofunction:: generateSingleResult
Expand Down
180 changes: 55 additions & 125 deletions sphinx_search/static/js/rtd_sphinx_search.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const MAX_SUBSTRING_LIMIT = 100;
const ANIMATION_TIME = 200;
const FETCH_RESULTS_DELAY = 250;
const CLEAR_RESULTS_DELAY = 300;
const RTD_SEARCH_PARAMETER = "rtd_search";

/**
* Debounce the function.
Expand Down Expand Up @@ -42,7 +43,7 @@ const debounce = (func, wait) => {

/**
* Wrapper around underscorejs's template function.
*
*
* This is to make it work with new and old versions.
*/
const render_template = (template, data) => {
Expand All @@ -55,69 +56,21 @@ const render_template = (template, data) => {
return result;
};

/**
* Take an object as parameter and convert it to
* url params string.
*
* Eg. if ``obj = { 'a': 1, 'b': 2, 'c': ['hello', 'world'] }``, then it will return
* the string ``a=1&b=2&c=hello,world``
*
* @param {Object} obj the object to be converted
* @return {String|Array} object in url params form
*/
const convertObjToUrlParams = obj => {
let params = Object.keys(obj).map(function(key) {
if (_is_string(key)) {
const s = key + "=" + encodeURI(obj[key]);
return s;
}
});

// removing empty strings from the 'params' array
let final_params = [];
for (let i = 0; i < params.length; ++i) {
if (_is_string(params[i])) {
final_params.push(params[i]);
}
}
if (final_params.length === 1) {
return final_params[0];
} else {
let final_url_params = final_params.join("&");
return final_url_params;
}
};


/**
* Adds/removes "rtd_search" url parameter to the url.
*/
const updateUrl = () => {
let origin = window.location.origin;
let path = window.location.pathname;
let url_params = $.getQueryParameters();
let hash = window.location.hash;
let parsed_url = new URL(window.location.href);
let search_query = getSearchTerm();
// search_query should not be an empty string
// search_query should not be an empty string.
if (search_query.length > 0) {
url_params.rtd_search = search_query;
parsed_url.searchParams.set(RTD_SEARCH_PARAMETER, search_query);
} else {
delete url_params.rtd_search;
parsed_url.searchParams.delete(RTD_SEARCH_PARAMETER);
}

let window_location_search = convertObjToUrlParams(url_params) + hash;

// this happens during the tests,
// when window.location.origin is "null" in Firefox
// then correct URL is contained by window.location.pathname
// which starts with "file://"
let url = path + "?" + window_location_search;
if (origin.substring(0, 4) === "http") {
url = origin + url;
}

// update url
window.history.pushState({}, null, url);
// Update url.
window.history.pushState({}, null, parsed_url.toString());
};


Expand Down Expand Up @@ -172,18 +125,6 @@ const _is_string = str => {
}
};

/**
* Checks if data type is a non-empty array
* @param {*} data data whose type is to be checked
* @return {Boolean} returns true if data is non-empty array, else returns false
*/
const _is_array = arr => {
if (Array.isArray(arr) && arr.length > 0) {
return true;
} else {
return false;
}
};

/**
* Generate and return html structure
Expand Down Expand Up @@ -538,11 +479,12 @@ const getErrorDiv = err_msg => {
* and appends the results to <div class="search__outer"> node,
* which is already created when the page was loaded.
*
* @param {String} search_url url on which request will be sent
* @param {String} projectName name (slug) of the project
* @param {String} api_endpoint: API endpoint
* @param {Object} parameters: search parameters
* @param {String} projectName: name (slug) of the project
* @return {Function} debounced function with debounce time of 500ms
*/
const fetchAndGenerateResults = (search_url, projectName) => {
const fetchAndGenerateResults = (api_endpoint, parameters, projectName) => {
let search_outer = document.querySelector(".search__outer");

// Removes all results (if there is any),
Expand All @@ -553,52 +495,48 @@ const fetchAndGenerateResults = (search_url, projectName) => {
search_loding.innerHTML = "<strong>Searching ....</strong>";
search_outer.appendChild(search_loding);

let ajaxFunc = () => {
let fetchFunc = () => {
// Update URL just before fetching the results
updateUrl();
updateSearchBar();

$.ajax({
url: search_url,
crossDomain: true,
xhrFields: {
withCredentials: true
},
complete: (resp, status_code) => {
if (
status_code === "success" ||
typeof resp.responseJSON !== "undefined"
) {
if (resp.responseJSON.results.length > 0) {
let search_result_box = generateSuggestionsList(
resp.responseJSON,
projectName
);
removeResults();
search_outer.appendChild(search_result_box);

// remove active classes from all suggestions
// if the mouse hovers, otherwise styles from
// :hover and .active will clash.
search_outer.addEventListener("mouseenter", e => {
removeAllActive();
});
} else {
removeResults();
let err_div = getErrorDiv("No results found");
search_outer.appendChild(err_div);
}
}
},
error: (resp, status_code, error) => {
const url = api_endpoint + "?" + new URLSearchParams(parameters).toString();

fetch(url, {method: "GET"})
.then(response => {
if (!response.ok) {
throw new Error();
}
return response.json();
})
.then(data => {
if (data.results.length > 0) {
let search_result_box = generateSuggestionsList(
data,
projectName
);
removeResults();
search_outer.appendChild(search_result_box);

// remove active classes from all suggestions
// if the mouse hovers, otherwise styles from
// :hover and .active will clash.
search_outer.addEventListener("mouseenter", e => {
removeAllActive();
});
} else {
removeResults();
let err_div = getErrorDiv("There was an error. Please try again.");
let err_div = getErrorDiv("No results found");
search_outer.appendChild(err_div);
}
})
.catch(error => {
removeResults();
let err_div = getErrorDiv("There was an error. Please try again.");
search_outer.appendChild(err_div);
});
};
ajaxFunc = debounce(ajaxFunc, FETCH_RESULTS_DELAY);
return ajaxFunc;
return debounce(fetchFunc, FETCH_RESULTS_DELAY);
};

/**
Expand Down Expand Up @@ -696,7 +634,6 @@ window.addEventListener("DOMContentLoaded", () => {
if (window.hasOwnProperty("READTHEDOCS_DATA")) {
const project = READTHEDOCS_DATA.project;
const version = READTHEDOCS_DATA.version;
const language = READTHEDOCS_DATA.language || "en";
const api_host = READTHEDOCS_DATA.proxied_api_host || '/_';

let initialHtml = generateAndReturnInitialHtml();
Expand All @@ -720,25 +657,18 @@ window.addEventListener("DOMContentLoaded", () => {

search_outer_input.addEventListener("input", e => {
let search_query = getSearchTerm();

let search_params = {
q: search_query,
project: project,
version: version,
language: language,
};

const search_url =
api_host +
"/api/v2/search/?" +
convertObjToUrlParams(search_params);

if (search_query.length > 0) {
if (current_request !== null) {
// cancel previous ajax request.
current_request.cancel();
}
current_request = fetchAndGenerateResults(search_url, project);
const search_endpoint = api_host + "/api/v2/search/";
const search_params = {
q: search_query,
project: project,
version: version,
};
current_request = fetchAndGenerateResults(search_endpoint, search_params, project);
current_request();
} else {
// if the last request returns the results,
Expand Down Expand Up @@ -825,9 +755,9 @@ window.addEventListener("DOMContentLoaded", () => {
// if "rtd_search" is present in URL parameters,
// then open the search modal and show the results
// for the value of "rtd_search"
let url_params = $.getQueryParameters();
if (_is_array(url_params.rtd_search)) {
let query = decodeURIComponent(url_params.rtd_search);
const url_params = new URLSearchParams(document.location.search);
const query = url_params.get(RTD_SEARCH_PARAMETER);
if (query !== null) {
showSearchModal(query);
search_outer_input.value = query;

Expand Down
Loading

0 comments on commit 5880a64

Please sign in to comment.