mirror of
https://github.com/searxng/searxng.git
synced 2026-07-26 18:01:33 +00:00
Compare commits
13 Commits
b606103352
...
9bb04e288d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bb04e288d | ||
|
|
5f364ad999 | ||
|
|
547d9dcee7 | ||
|
|
4304150d7f | ||
|
|
9b4ea64429 | ||
|
|
4e586a7eca | ||
|
|
48cec1fff6 | ||
|
|
34e993a854 | ||
|
|
60bd8b90f0 | ||
|
|
adc4361eb9 | ||
|
|
7e1c7096ce | ||
|
|
0b913053a7 | ||
|
|
4fb6105d69 |
@@ -36,12 +36,24 @@ indent_size = 2
|
||||
[*.js]
|
||||
indent_size = 2
|
||||
|
||||
[*.ts]
|
||||
indent_size = 2
|
||||
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
insert_final_newline = ignore
|
||||
|
||||
[*.map]
|
||||
indent_size = ignore
|
||||
insert_final_newline = ignore
|
||||
|
||||
# Minified JavaScript files shouldn't be changed
|
||||
[**.min.js]
|
||||
[*.min.js]
|
||||
indent_style = ignore
|
||||
insert_final_newline = ignore
|
||||
|
||||
# Minified CSS files shouldn't be changed
|
||||
[*.min.css]
|
||||
indent_style = ignore
|
||||
insert_final_newline = ignore
|
||||
|
||||
|
||||
3
.github/workflows/integration.yml
vendored
3
.github/workflows/integration.yml
vendored
@@ -92,5 +92,8 @@ jobs:
|
||||
- name: Setup venv
|
||||
run: make V=1 install
|
||||
|
||||
- name: Lint
|
||||
run: make themes.lint
|
||||
|
||||
- name: Build
|
||||
run: make themes.all
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
nodejs 23.5.0
|
||||
nodejs 24.3.0
|
||||
python 3.13.1
|
||||
shellcheck 0.10.0
|
||||
sqlite 3.47.2
|
||||
sqlite 3.47.2
|
||||
|
||||
2
Makefile
2
Makefile
@@ -81,7 +81,7 @@ MANAGE += py.build py.clean
|
||||
MANAGE += pyenv pyenv.install pyenv.uninstall
|
||||
MANAGE += format.python format.shell
|
||||
MANAGE += test.yamllint test.pylint test.black test.pybabel test.unit test.coverage test.robot test.rst test.clean test.themes test.pyright test.shfmt
|
||||
MANAGE += themes.all themes.simple themes.fix themes.lint themes.test
|
||||
MANAGE += themes.all themes.simple themes.simple.analyze themes.fix themes.lint themes.test
|
||||
MANAGE += static.build.commit static.build.drop static.build.restore
|
||||
MANAGE += nvm.install nvm.clean nvm.status nvm.nodejs
|
||||
MANAGE += go.env.dev go.clean
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
|
||||
"files": {
|
||||
"includes": ["**", "!dist/**", "!node_modules/**"],
|
||||
"includes": ["**", "!dist", "!node_modules"],
|
||||
"ignoreUnknown": true
|
||||
},
|
||||
"vcs": {
|
||||
@@ -28,7 +28,111 @@
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
"recommended": true,
|
||||
"complexity": {
|
||||
"noForEach": "error",
|
||||
"noImplicitCoercions": "error",
|
||||
"useSimplifiedLogicExpression": "error"
|
||||
},
|
||||
"correctness": {
|
||||
"noGlobalDirnameFilename": "error",
|
||||
"noUndeclaredVariables": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"checkTypes": true
|
||||
}
|
||||
},
|
||||
"useImportExtensions": "error",
|
||||
"useJsonImportAttributes": "error",
|
||||
"useSingleJsDocAsterisk": "error"
|
||||
},
|
||||
"nursery": {
|
||||
"noMisusedPromises": "warn",
|
||||
"noUselessUndefined": "warn",
|
||||
"useExhaustiveSwitchCases": "warn",
|
||||
"useExplicitType": "warn"
|
||||
},
|
||||
"performance": {
|
||||
"noAwaitInLoops": "error",
|
||||
"noBarrelFile": "error",
|
||||
"noDelete": "error",
|
||||
"noNamespaceImport": "error",
|
||||
"noReExportAll": "error",
|
||||
"useTopLevelRegex": "error"
|
||||
},
|
||||
"style": {
|
||||
"noCommonJs": "error",
|
||||
"noEnum": "error",
|
||||
"noInferrableTypes": "error",
|
||||
"noNamespace": "error",
|
||||
"noNegationElse": "error",
|
||||
"noNestedTernary": "error",
|
||||
"noParameterAssign": "error",
|
||||
"noParameterProperties": "error",
|
||||
"noRestrictedTypes": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"types": {
|
||||
"Element": {
|
||||
"message": "Element is too generic",
|
||||
"use": "HTMLElement"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"noSubstr": "error",
|
||||
"noUnusedTemplateLiteral": "error",
|
||||
"noUselessElse": "error",
|
||||
"noYodaExpression": "error",
|
||||
"useAsConstAssertion": "error",
|
||||
"useAtIndex": "error",
|
||||
"useCollapsedElseIf": "error",
|
||||
"useCollapsedIf": "error",
|
||||
"useConsistentArrayType": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"syntax": "shorthand"
|
||||
}
|
||||
},
|
||||
"useConsistentBuiltinInstantiation": "error",
|
||||
"useConsistentMemberAccessibility": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"accessibility": "explicit"
|
||||
}
|
||||
},
|
||||
"useConsistentObjectDefinitions": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"syntax": "explicit"
|
||||
}
|
||||
},
|
||||
"useDefaultSwitchClause": "error",
|
||||
"useExplicitLengthCheck": "error",
|
||||
"useForOf": "error",
|
||||
"useGroupedAccessorPairs": "error",
|
||||
"useNumberNamespace": "error",
|
||||
"useNumericSeparators": "error",
|
||||
"useObjectSpread": "error",
|
||||
"useReadonlyClassProperties": "error",
|
||||
"useShorthandAssign": "error",
|
||||
"useSingleVarDeclarator": "error",
|
||||
"useThrowNewError": "error",
|
||||
"useThrowOnlyError": "error",
|
||||
"useTrimStartEnd": "error",
|
||||
"useUnifiedTypeSignatures": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noAlert": "error",
|
||||
"noBitwiseOperators": "error",
|
||||
"noConstantBinaryExpressions": "error",
|
||||
"noEmptyBlockStatements": "error",
|
||||
"noEvolvingTypes": "error",
|
||||
"noUnassignedVariables": "error",
|
||||
"noVar": "error",
|
||||
"useNumberToFixedDigitsArgument": "error",
|
||||
"useStaticResponseMethods": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
this file is generated automatically by searxng_extra/update/update_pygments.py
|
||||
using pygments version 2.19.2
|
||||
using pygments version 2.19.2:
|
||||
|
||||
./manage templates.simple.pygments
|
||||
*/
|
||||
|
||||
|
||||
|
||||
2273
client/simple/package-lock.json
generated
2273
client/simple/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,36 +1,51 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "simple",
|
||||
"name": "@searxng/theme-simple",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "npm run build:icons && npm run build:vite",
|
||||
"build:icons": "node theme_icons.js",
|
||||
"build:icons": "node theme_icons.ts",
|
||||
"build:vite": "vite build",
|
||||
"clean": "rm -Rf node_modules",
|
||||
"fix": "npm run fix:stylelint && npm run fix:biome && npm run fix:package",
|
||||
"fix:biome": "biome check --write",
|
||||
"fix:package": "sort-package-json --quiet",
|
||||
"fix:stylelint": "stylelint --fix strict 'src/**/*.{scss,sass,less,styl}'",
|
||||
"lint": "npm run lint:biome",
|
||||
"lint:biome": "biome lint"
|
||||
"lint": "npm run lint:biome && npm run lint:tsc",
|
||||
"lint:biome": "biome lint",
|
||||
"lint:tsc": "tsc --noEmit"
|
||||
},
|
||||
"browserslist": [
|
||||
"Chrome >= 93",
|
||||
"Firefox >= 92",
|
||||
"Safari >= 15.4",
|
||||
"not dead"
|
||||
],
|
||||
"dependencies": {
|
||||
"ionicons": "~8.0.0",
|
||||
"normalize.css": "8.0.1",
|
||||
"ol": "~10.6.0",
|
||||
"swiped-events": "1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "~2.0.6",
|
||||
"edge.js": "^6.2.1",
|
||||
"ionicons": "^8.0.10",
|
||||
"leaflet": "^1.9.4",
|
||||
"less": "^4.3.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"sharp": "^0.34.2",
|
||||
"sort-package-json": "^3.4.0",
|
||||
"stylelint": "^16.21.1",
|
||||
"stylelint-config-standard-less": "^3.0.1",
|
||||
"stylelint-prettier": "^5.0.3",
|
||||
"svgo": "^4.0.0",
|
||||
"swiped-events": "^1.2.0",
|
||||
"vite": "^7.0.2",
|
||||
"vite-plugin-static-copy": "^3.1.0"
|
||||
"@biomejs/biome": "2.2.0",
|
||||
"@types/node": "~24.3.0",
|
||||
"browserslist": "~4.25.2",
|
||||
"browserslist-to-esbuild": "~2.1.0",
|
||||
"edge.js": "~6.3.0",
|
||||
"less": "~4.4.0",
|
||||
"lightningcss": "~1.30.0",
|
||||
"sharp": "~0.34.0",
|
||||
"sort-package-json": "~3.4.0",
|
||||
"stylelint": "~16.23.1",
|
||||
"stylelint-config-standard-less": "~3.0.0",
|
||||
"stylelint-prettier": "~5.0.0",
|
||||
"svgo": "~4.0.0",
|
||||
"typescript": "~5.9.0",
|
||||
"vite": "npm:rolldown-vite@7.1.3",
|
||||
"vite-bundle-analyzer": "~1.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
5
client/simple/src/js/core/index.ts
Normal file
5
client/simple/src/js/core/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import "./router.ts";
|
||||
import "./toolkit.ts";
|
||||
import "./listener.ts";
|
||||
7
client/simple/src/js/core/listener.ts
Normal file
7
client/simple/src/js/core/listener.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { listen } from "./toolkit.ts";
|
||||
|
||||
listen("click", ".close", function (this: HTMLElement) {
|
||||
(this.parentNode as HTMLElement)?.classList.add("invisible");
|
||||
});
|
||||
40
client/simple/src/js/core/router.ts
Normal file
40
client/simple/src/js/core/router.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { Endpoints, endpoint, ready, settings } from "./toolkit.ts";
|
||||
|
||||
ready(
|
||||
() => {
|
||||
import("../main/keyboard.ts");
|
||||
import("../main/search.ts");
|
||||
|
||||
if (settings.autocomplete) {
|
||||
import("../main/autocomplete.ts");
|
||||
}
|
||||
},
|
||||
{ on: [endpoint === Endpoints.index] }
|
||||
);
|
||||
|
||||
ready(
|
||||
() => {
|
||||
import("../main/keyboard.ts");
|
||||
import("../main/mapresult.ts");
|
||||
import("../main/results.ts");
|
||||
import("../main/search.ts");
|
||||
|
||||
if (settings.infinite_scroll) {
|
||||
import("../main/infinite_scroll.ts");
|
||||
}
|
||||
|
||||
if (settings.autocomplete) {
|
||||
import("../main/autocomplete.ts");
|
||||
}
|
||||
},
|
||||
{ on: [endpoint === Endpoints.results] }
|
||||
);
|
||||
|
||||
ready(
|
||||
() => {
|
||||
import("../main/preferences.ts");
|
||||
},
|
||||
{ on: [endpoint === Endpoints.preferences] }
|
||||
);
|
||||
142
client/simple/src/js/core/toolkit.ts
Normal file
142
client/simple/src/js/core/toolkit.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type { KeyBindingLayout } from "../main/keyboard.ts";
|
||||
|
||||
// synced with searx/webapp.py get_client_settings
|
||||
type Settings = {
|
||||
advanced_search?: boolean;
|
||||
autocomplete?: string;
|
||||
autocomplete_min?: number;
|
||||
doi_resolver?: string;
|
||||
favicon_resolver?: string;
|
||||
hotkeys?: KeyBindingLayout;
|
||||
infinite_scroll?: boolean;
|
||||
method?: "GET" | "POST";
|
||||
query_in_title?: boolean;
|
||||
results_on_new_tab?: boolean;
|
||||
safesearch?: 0 | 1 | 2;
|
||||
search_on_category_select?: boolean;
|
||||
theme?: string;
|
||||
theme_static_path?: string;
|
||||
translations?: Record<string, string>;
|
||||
url_formatting?: "pretty" | "full" | "host";
|
||||
};
|
||||
|
||||
type HTTPOptions = {
|
||||
body?: BodyInit;
|
||||
timeout?: number;
|
||||
};
|
||||
|
||||
type ReadyOptions = {
|
||||
// all values must be truthy for the callback to be executed
|
||||
on?: (boolean | undefined)[];
|
||||
};
|
||||
|
||||
type AssertElement = (element?: HTMLElement | null) => asserts element is HTMLElement;
|
||||
|
||||
export type EndpointsKeys = keyof typeof Endpoints;
|
||||
|
||||
export const Endpoints = {
|
||||
index: "index",
|
||||
results: "results",
|
||||
preferences: "preferences",
|
||||
unknown: "unknown"
|
||||
} as const;
|
||||
|
||||
export const mutable = {
|
||||
closeDetail: undefined as (() => void) | undefined,
|
||||
scrollPageToSelected: undefined as (() => void) | undefined,
|
||||
selectImage: undefined as ((resultElement: HTMLElement) => void) | undefined,
|
||||
selectNext: undefined as ((openDetailView?: boolean) => void) | undefined,
|
||||
selectPrevious: undefined as ((openDetailView?: boolean) => void) | undefined
|
||||
};
|
||||
|
||||
const getEndpoint = (): EndpointsKeys => {
|
||||
const metaEndpoint = document.querySelector('meta[name="endpoint"]')?.getAttribute("content");
|
||||
|
||||
if (metaEndpoint && metaEndpoint in Endpoints) {
|
||||
return metaEndpoint as EndpointsKeys;
|
||||
}
|
||||
|
||||
return Endpoints.unknown;
|
||||
};
|
||||
|
||||
const getSettings = (): Settings => {
|
||||
const settings = document.querySelector("script[client_settings]")?.getAttribute("client_settings");
|
||||
if (!settings) return {};
|
||||
|
||||
try {
|
||||
return JSON.parse(atob(settings));
|
||||
} catch (error) {
|
||||
console.error("Failed to load client_settings:", error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const assertElement: AssertElement = (element?: HTMLElement | null): asserts element is HTMLElement => {
|
||||
if (!element) {
|
||||
throw new Error("Bad assertion: DOM element not found");
|
||||
}
|
||||
};
|
||||
|
||||
export const http = async (method: string, url: string | URL, options?: HTTPOptions): Promise<Response> => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), options?.timeout ?? 30_000);
|
||||
|
||||
const res = await fetch(url, {
|
||||
body: options?.body,
|
||||
method: method,
|
||||
signal: controller.signal
|
||||
}).finally(() => clearTimeout(timeoutId));
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const listen = <K extends keyof DocumentEventMap, E extends HTMLElement>(
|
||||
type: string | K,
|
||||
target: string | Document | E,
|
||||
listener: (this: E, event: DocumentEventMap[K]) => void | Promise<void>,
|
||||
options?: AddEventListenerOptions
|
||||
): void => {
|
||||
if (typeof target !== "string") {
|
||||
target.addEventListener(type, listener as EventListener, options);
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener(
|
||||
type,
|
||||
(event: Event) => {
|
||||
for (const node of event.composedPath()) {
|
||||
if (node instanceof HTMLElement && node.matches(target)) {
|
||||
try {
|
||||
listener.call(node as E, event as DocumentEventMap[K]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
options
|
||||
);
|
||||
};
|
||||
|
||||
export const ready = (callback: () => void, options?: ReadyOptions): void => {
|
||||
for (const condition of options?.on ?? []) {
|
||||
if (!condition) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState !== "loading") {
|
||||
callback();
|
||||
} else {
|
||||
listen("DOMContentLoaded", document, callback, { once: true });
|
||||
}
|
||||
};
|
||||
|
||||
export const endpoint: EndpointsKeys = getEndpoint();
|
||||
export const settings: Settings = getSettings();
|
||||
@@ -1,19 +0,0 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
((w, d) => {
|
||||
// add data- properties
|
||||
const getLastScriptElement = () => {
|
||||
const scripts = d.getElementsByTagName("script");
|
||||
return scripts[scripts.length - 1];
|
||||
};
|
||||
|
||||
const script = d.currentScript || getLastScriptElement();
|
||||
|
||||
w.searxng = {
|
||||
settings: JSON.parse(atob(script.getAttribute("client_settings")))
|
||||
};
|
||||
|
||||
// update the css
|
||||
const htmlElement = d.getElementsByTagName("html")[0];
|
||||
htmlElement.classList.remove("no-js");
|
||||
htmlElement.classList.add("js");
|
||||
})(window, document);
|
||||
@@ -1,178 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* (C) Copyright Contributors to the SearXNG project.
|
||||
* (C) Copyright Contributors to the searx project (2014 - 2021).
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
window.searxng = ((w, d) => {
|
||||
// not invented here toolkit with bugs fixed elsewhere
|
||||
// purposes : be just good enough and as small as possible
|
||||
|
||||
// from https://plainjs.com/javascript/events/live-binding-event-handlers-14/
|
||||
if (w.Element) {
|
||||
((ElementPrototype) => {
|
||||
ElementPrototype.matches =
|
||||
ElementPrototype.matches ||
|
||||
ElementPrototype.matchesSelector ||
|
||||
ElementPrototype.webkitMatchesSelector ||
|
||||
ElementPrototype.msMatchesSelector ||
|
||||
function (selector) {
|
||||
const nodes = (this.parentNode || this.document).querySelectorAll(selector);
|
||||
let i = -1;
|
||||
while (nodes[++i] && nodes[i] !== this);
|
||||
return !!nodes[i];
|
||||
};
|
||||
})(Element.prototype);
|
||||
}
|
||||
|
||||
function callbackSafe(callback, el, e) {
|
||||
try {
|
||||
callback.call(el, e);
|
||||
} catch (exception) {
|
||||
console.log(exception);
|
||||
}
|
||||
}
|
||||
|
||||
const searxng = window.searxng || {};
|
||||
|
||||
searxng.on = (obj, eventType, callback, useCapture) => {
|
||||
useCapture = useCapture || false;
|
||||
if (typeof obj !== "string") {
|
||||
// obj HTMLElement, HTMLDocument
|
||||
obj.addEventListener(eventType, callback, useCapture);
|
||||
} else {
|
||||
// obj is a selector
|
||||
d.addEventListener(
|
||||
eventType,
|
||||
(e) => {
|
||||
let el = e.target || e.srcElement;
|
||||
let found = false;
|
||||
|
||||
while (el?.matches && el !== d) {
|
||||
found = el.matches(obj);
|
||||
|
||||
if (found) break;
|
||||
|
||||
el = el.parentElement;
|
||||
}
|
||||
|
||||
if (found) {
|
||||
callbackSafe(callback, el, e);
|
||||
}
|
||||
},
|
||||
useCapture
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
searxng.ready = (callback) => {
|
||||
if (document.readyState !== "loading") {
|
||||
callback.call(w);
|
||||
} else {
|
||||
w.addEventListener("DOMContentLoaded", callback.bind(w));
|
||||
}
|
||||
};
|
||||
|
||||
searxng.http = (method, url, data = null) =>
|
||||
new Promise((resolve, reject) => {
|
||||
try {
|
||||
const req = new XMLHttpRequest();
|
||||
req.open(method, url, true);
|
||||
req.timeout = 20000;
|
||||
|
||||
// On load
|
||||
req.onload = () => {
|
||||
if (req.status === 200) {
|
||||
resolve(req.response, req.responseType);
|
||||
} else {
|
||||
reject(Error(req.statusText));
|
||||
}
|
||||
};
|
||||
|
||||
// Handle network errors
|
||||
req.onerror = () => {
|
||||
reject(Error("Network Error"));
|
||||
};
|
||||
|
||||
req.onabort = () => {
|
||||
reject(Error("Transaction is aborted"));
|
||||
};
|
||||
|
||||
req.ontimeout = () => {
|
||||
reject(Error("Timeout"));
|
||||
};
|
||||
|
||||
// Make the request
|
||||
if (data) {
|
||||
req.send(data);
|
||||
} else {
|
||||
req.send();
|
||||
}
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
});
|
||||
|
||||
searxng.loadStyle = (src) => {
|
||||
const path = `${searxng.settings.theme_static_path}/${src}`;
|
||||
const id = `style_${src.replace(".", "_")}`;
|
||||
let s = d.getElementById(id);
|
||||
if (s === null) {
|
||||
s = d.createElement("link");
|
||||
s.setAttribute("id", id);
|
||||
s.setAttribute("rel", "stylesheet");
|
||||
s.setAttribute("type", "text/css");
|
||||
s.setAttribute("href", path);
|
||||
d.body.appendChild(s);
|
||||
}
|
||||
};
|
||||
|
||||
searxng.loadScript = (src, callback) => {
|
||||
const path = `${searxng.settings.theme_static_path}/${src}`;
|
||||
const id = `script_${src.replace(".", "_")}`;
|
||||
let s = d.getElementById(id);
|
||||
if (s === null) {
|
||||
s = d.createElement("script");
|
||||
s.setAttribute("id", id);
|
||||
s.setAttribute("src", path);
|
||||
s.onload = callback;
|
||||
s.onerror = () => {
|
||||
s.setAttribute("error", "1");
|
||||
};
|
||||
d.body.appendChild(s);
|
||||
} else if (!s.hasAttribute("error")) {
|
||||
try {
|
||||
callback.apply(s, []);
|
||||
} catch (exception) {
|
||||
console.log(exception);
|
||||
}
|
||||
} else {
|
||||
console.log(`callback not executed : script '${path}' not loaded.`);
|
||||
}
|
||||
};
|
||||
|
||||
searxng.insertBefore = (newNode, referenceNode) => {
|
||||
referenceNode.parentNode.insertBefore(newNode, referenceNode);
|
||||
};
|
||||
|
||||
searxng.insertAfter = (newNode, referenceNode) => {
|
||||
referenceNode.parentNode.insertAfter(newNode, referenceNode.nextSibling);
|
||||
};
|
||||
|
||||
searxng.on(".close", "click", function () {
|
||||
this.parentNode.classList.add("invisible");
|
||||
});
|
||||
|
||||
function getEndpoint() {
|
||||
for (const className of d.getElementsByTagName("body")[0].classList.values()) {
|
||||
if (className.endsWith("_endpoint")) {
|
||||
return className.split("_")[0];
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
searxng.endpoint = getEndpoint();
|
||||
|
||||
return searxng;
|
||||
})(window, document);
|
||||
131
client/simple/src/js/main/autocomplete.ts
Normal file
131
client/simple/src/js/main/autocomplete.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { assertElement, http, listen, settings } from "../core/toolkit.ts";
|
||||
|
||||
const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => {
|
||||
try {
|
||||
let res: Response;
|
||||
|
||||
if (settings.method === "GET") {
|
||||
res = await http("GET", `./autocompleter?q=${query}`);
|
||||
} else {
|
||||
res = await http("POST", "./autocompleter", { body: new URLSearchParams({ q: query }) });
|
||||
}
|
||||
|
||||
const results = await res.json();
|
||||
|
||||
const autocomplete = document.querySelector<HTMLElement>(".autocomplete");
|
||||
assertElement(autocomplete);
|
||||
|
||||
const autocompleteList = document.querySelector<HTMLUListElement>(".autocomplete ul");
|
||||
assertElement(autocompleteList);
|
||||
|
||||
autocomplete.classList.add("open");
|
||||
autocompleteList.replaceChildren();
|
||||
|
||||
// show an error message that no result was found
|
||||
if (results?.[1]?.length === 0) {
|
||||
const noItemFoundMessage = Object.assign(document.createElement("li"), {
|
||||
className: "no-item-found",
|
||||
textContent: settings.translations?.no_item_found ?? "No results found"
|
||||
});
|
||||
autocompleteList.append(noItemFoundMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const fragment = new DocumentFragment();
|
||||
|
||||
for (const result of results[1]) {
|
||||
const li = Object.assign(document.createElement("li"), { textContent: result });
|
||||
|
||||
listen("mousedown", li, () => {
|
||||
qInput.value = result;
|
||||
|
||||
const form = document.querySelector<HTMLFormElement>("#search");
|
||||
form?.submit();
|
||||
|
||||
autocomplete.classList.remove("open");
|
||||
});
|
||||
|
||||
fragment.append(li);
|
||||
}
|
||||
|
||||
autocompleteList.append(fragment);
|
||||
} catch (error) {
|
||||
console.error("Error fetching autocomplete results:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const qInput = document.getElementById("q") as HTMLInputElement | null;
|
||||
assertElement(qInput);
|
||||
|
||||
let timeoutId: number;
|
||||
|
||||
listen("input", qInput, () => {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
const query = qInput.value;
|
||||
const minLength = settings.autocomplete_min ?? 2;
|
||||
|
||||
if (query.length < minLength) return;
|
||||
|
||||
timeoutId = window.setTimeout(async () => {
|
||||
if (query === qInput.value) {
|
||||
await fetchResults(qInput, query);
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
|
||||
const autocomplete: HTMLElement | null = document.querySelector<HTMLElement>(".autocomplete");
|
||||
const autocompleteList: HTMLUListElement | null = document.querySelector<HTMLUListElement>(".autocomplete ul");
|
||||
if (autocompleteList) {
|
||||
listen("keyup", qInput, (event: KeyboardEvent) => {
|
||||
const listItems = [...autocompleteList.children] as HTMLElement[];
|
||||
|
||||
const currentIndex = listItems.findIndex((item) => item.classList.contains("active"));
|
||||
let newCurrentIndex = -1;
|
||||
|
||||
switch (event.key) {
|
||||
case "ArrowUp": {
|
||||
const currentItem = listItems[currentIndex];
|
||||
if (currentItem && currentIndex >= 0) {
|
||||
currentItem.classList.remove("active");
|
||||
}
|
||||
// we need to add listItems.length to the index calculation here because the JavaScript modulos
|
||||
// operator doesn't work with negative numbers
|
||||
newCurrentIndex = (currentIndex - 1 + listItems.length) % listItems.length;
|
||||
break;
|
||||
}
|
||||
case "ArrowDown": {
|
||||
const currentItem = listItems[currentIndex];
|
||||
if (currentItem && currentIndex >= 0) {
|
||||
currentItem.classList.remove("active");
|
||||
}
|
||||
newCurrentIndex = (currentIndex + 1) % listItems.length;
|
||||
break;
|
||||
}
|
||||
case "Tab":
|
||||
case "Enter":
|
||||
if (autocomplete) {
|
||||
autocomplete.classList.remove("open");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (newCurrentIndex !== -1) {
|
||||
const selectedItem = listItems[newCurrentIndex];
|
||||
if (selectedItem) {
|
||||
selectedItem.classList.add("active");
|
||||
|
||||
if (!selectedItem.classList.contains("no-item-found")) {
|
||||
const qInput = document.getElementById("q") as HTMLInputElement | null;
|
||||
if (qInput) {
|
||||
qInput.value = selectedItem.textContent ?? "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/* global searxng */
|
||||
|
||||
searxng.ready(() => {
|
||||
searxng.infinite_scroll_supported =
|
||||
"IntersectionObserver" in window &&
|
||||
"IntersectionObserverEntry" in window &&
|
||||
"intersectionRatio" in window.IntersectionObserverEntry.prototype;
|
||||
|
||||
if (searxng.endpoint !== "results") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!searxng.infinite_scroll_supported) {
|
||||
console.log("IntersectionObserver not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
const d = document;
|
||||
const onlyImages = d.getElementById("results").classList.contains("only_template_images");
|
||||
|
||||
function newLoadSpinner() {
|
||||
const loader = d.createElement("div");
|
||||
loader.classList.add("loader");
|
||||
return loader;
|
||||
}
|
||||
|
||||
function replaceChildrenWith(element, children) {
|
||||
element.textContent = "";
|
||||
children.forEach((child) => element.appendChild(child));
|
||||
}
|
||||
|
||||
function loadNextPage(callback) {
|
||||
const form = d.querySelector("#pagination form.next_page");
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
replaceChildrenWith(d.querySelector("#pagination"), [newLoadSpinner()]);
|
||||
const formData = new FormData(form);
|
||||
searxng
|
||||
.http("POST", d.querySelector("#search").getAttribute("action"), formData)
|
||||
.then((response) => {
|
||||
const nextPageDoc = new DOMParser().parseFromString(response, "text/html");
|
||||
const articleList = nextPageDoc.querySelectorAll("#urls article");
|
||||
const paginationElement = nextPageDoc.querySelector("#pagination");
|
||||
d.querySelector("#pagination").remove();
|
||||
if (articleList.length > 0 && !onlyImages) {
|
||||
// do not add <hr> element when there are only images
|
||||
d.querySelector("#urls").appendChild(d.createElement("hr"));
|
||||
}
|
||||
articleList.forEach((articleElement) => {
|
||||
d.querySelector("#urls").appendChild(articleElement);
|
||||
});
|
||||
if (paginationElement) {
|
||||
d.querySelector("#results").appendChild(paginationElement);
|
||||
callback();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
const e = d.createElement("div");
|
||||
e.textContent = searxng.settings.translations.error_loading_next_page;
|
||||
e.classList.add("dialog-error");
|
||||
e.setAttribute("role", "alert");
|
||||
replaceChildrenWith(d.querySelector("#pagination"), [e]);
|
||||
});
|
||||
}
|
||||
|
||||
if (searxng.settings.infinite_scroll && searxng.infinite_scroll_supported) {
|
||||
const intersectionObserveOptions = {
|
||||
rootMargin: "20rem"
|
||||
};
|
||||
const observedSelector = "article.result:last-child";
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
const paginationEntry = entries[0];
|
||||
if (paginationEntry.isIntersecting) {
|
||||
observer.unobserve(paginationEntry.target);
|
||||
loadNextPage(() => observer.observe(d.querySelector(observedSelector), intersectionObserveOptions));
|
||||
}
|
||||
});
|
||||
observer.observe(d.querySelector(observedSelector), intersectionObserveOptions);
|
||||
}
|
||||
});
|
||||
100
client/simple/src/js/main/infinite_scroll.ts
Normal file
100
client/simple/src/js/main/infinite_scroll.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { assertElement, http, settings } from "../core/toolkit.ts";
|
||||
|
||||
const newLoadSpinner = (): HTMLDivElement => {
|
||||
return Object.assign(document.createElement("div"), {
|
||||
className: "loader"
|
||||
});
|
||||
};
|
||||
|
||||
const loadNextPage = async (onlyImages: boolean, callback: () => void): Promise<void> => {
|
||||
const searchForm = document.querySelector<HTMLFormElement>("#search");
|
||||
assertElement(searchForm);
|
||||
|
||||
const form = document.querySelector<HTMLFormElement>("#pagination form.next_page");
|
||||
assertElement(form);
|
||||
|
||||
const action = searchForm.getAttribute("action");
|
||||
if (!action) {
|
||||
throw new Error("Form action not defined");
|
||||
}
|
||||
|
||||
const paginationElement = document.querySelector<HTMLElement>("#pagination");
|
||||
assertElement(paginationElement);
|
||||
|
||||
paginationElement.replaceChildren(newLoadSpinner());
|
||||
|
||||
try {
|
||||
const res = await http("POST", action, { body: new FormData(form) });
|
||||
const nextPage = await res.text();
|
||||
if (!nextPage) return;
|
||||
|
||||
const nextPageDoc = new DOMParser().parseFromString(nextPage, "text/html");
|
||||
const articleList = nextPageDoc.querySelectorAll<HTMLElement>("#urls article");
|
||||
const nextPaginationElement = nextPageDoc.querySelector<HTMLElement>("#pagination");
|
||||
|
||||
document.querySelector("#pagination")?.remove();
|
||||
|
||||
const urlsElement = document.querySelector<HTMLElement>("#urls");
|
||||
if (!urlsElement) {
|
||||
throw new Error("URLs element not found");
|
||||
}
|
||||
|
||||
if (articleList.length > 0 && !onlyImages) {
|
||||
// do not add <hr> element when there are only images
|
||||
urlsElement.appendChild(document.createElement("hr"));
|
||||
}
|
||||
|
||||
urlsElement.append(...Array.from(articleList));
|
||||
|
||||
if (nextPaginationElement) {
|
||||
const results = document.querySelector<HTMLElement>("#results");
|
||||
results?.appendChild(nextPaginationElement);
|
||||
callback();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading next page:", error);
|
||||
|
||||
const errorElement = Object.assign(document.createElement("div"), {
|
||||
textContent: settings.translations?.error_loading_next_page ?? "Error loading next page",
|
||||
className: "dialog-error"
|
||||
});
|
||||
errorElement.setAttribute("role", "alert");
|
||||
document.querySelector("#pagination")?.replaceChildren(errorElement);
|
||||
}
|
||||
};
|
||||
|
||||
const resultsElement: HTMLElement | null = document.getElementById("results");
|
||||
if (!resultsElement) {
|
||||
throw new Error("Results element not found");
|
||||
}
|
||||
|
||||
const onlyImages: boolean = resultsElement.classList.contains("only_template_images");
|
||||
const observedSelector = "article.result:last-child";
|
||||
|
||||
const intersectionObserveOptions: IntersectionObserverInit = {
|
||||
rootMargin: "320px"
|
||||
};
|
||||
|
||||
const observer: IntersectionObserver = new IntersectionObserver((entries: IntersectionObserverEntry[]) => {
|
||||
const [paginationEntry] = entries;
|
||||
|
||||
if (paginationEntry?.isIntersecting) {
|
||||
observer.unobserve(paginationEntry.target);
|
||||
|
||||
loadNextPage(onlyImages, () => {
|
||||
const nextObservedElement = document.querySelector<HTMLElement>(observedSelector);
|
||||
if (nextObservedElement) {
|
||||
observer.observe(nextObservedElement);
|
||||
}
|
||||
}).then(() => {
|
||||
// wait until promise is resolved
|
||||
});
|
||||
}
|
||||
}, intersectionObserveOptions);
|
||||
|
||||
const initialObservedElement: HTMLElement | null = document.querySelector<HTMLElement>(observedSelector);
|
||||
if (initialObservedElement) {
|
||||
observer.observe(initialObservedElement);
|
||||
}
|
||||
@@ -1,473 +0,0 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
/* global searxng */
|
||||
|
||||
searxng.ready(() => {
|
||||
function isElementInDetail(el) {
|
||||
while (el !== undefined) {
|
||||
if (el.classList.contains("detail")) {
|
||||
return true;
|
||||
}
|
||||
if (el.classList.contains("result")) {
|
||||
// we found a result, no need to go to the root of the document:
|
||||
// el is not inside a <div class="detail"> element
|
||||
return false;
|
||||
}
|
||||
el = el.parentNode;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getResultElement(el) {
|
||||
while (el !== undefined) {
|
||||
if (el.classList.contains("result")) {
|
||||
return el;
|
||||
}
|
||||
el = el.parentNode;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isImageResult(resultElement) {
|
||||
return resultElement?.classList.contains("result-images");
|
||||
}
|
||||
|
||||
searxng.on(".result", "click", function (e) {
|
||||
if (!isElementInDetail(e.target)) {
|
||||
highlightResult(this)(true, true);
|
||||
const resultElement = getResultElement(e.target);
|
||||
if (isImageResult(resultElement)) {
|
||||
e.preventDefault();
|
||||
searxng.selectImage(resultElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
searxng.on(
|
||||
".result a",
|
||||
"focus",
|
||||
(e) => {
|
||||
if (!isElementInDetail(e.target)) {
|
||||
const resultElement = getResultElement(e.target);
|
||||
if (resultElement && resultElement.getAttribute("data-vim-selected") === null) {
|
||||
highlightResult(resultElement)(true);
|
||||
}
|
||||
if (isImageResult(resultElement)) {
|
||||
searxng.selectImage(resultElement);
|
||||
}
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
/* common base for layouts */
|
||||
const baseKeyBinding = {
|
||||
Escape: {
|
||||
key: "ESC",
|
||||
fun: removeFocus,
|
||||
des: "remove focus from the focused input",
|
||||
cat: "Control"
|
||||
},
|
||||
c: {
|
||||
key: "c",
|
||||
fun: copyURLToClipboard,
|
||||
des: "copy url of the selected result to the clipboard",
|
||||
cat: "Results"
|
||||
},
|
||||
h: {
|
||||
key: "h",
|
||||
fun: toggleHelp,
|
||||
des: "toggle help window",
|
||||
cat: "Other"
|
||||
},
|
||||
i: {
|
||||
key: "i",
|
||||
fun: searchInputFocus,
|
||||
des: "focus on the search input",
|
||||
cat: "Control"
|
||||
},
|
||||
n: {
|
||||
key: "n",
|
||||
fun: GoToNextPage(),
|
||||
des: "go to next page",
|
||||
cat: "Results"
|
||||
},
|
||||
o: {
|
||||
key: "o",
|
||||
fun: openResult(false),
|
||||
des: "open search result",
|
||||
cat: "Results"
|
||||
},
|
||||
p: {
|
||||
key: "p",
|
||||
fun: GoToPreviousPage(),
|
||||
des: "go to previous page",
|
||||
cat: "Results"
|
||||
},
|
||||
r: {
|
||||
key: "r",
|
||||
fun: reloadPage,
|
||||
des: "reload page from the server",
|
||||
cat: "Control"
|
||||
},
|
||||
t: {
|
||||
key: "t",
|
||||
fun: openResult(true),
|
||||
des: "open the result in a new tab",
|
||||
cat: "Results"
|
||||
}
|
||||
};
|
||||
const keyBindingLayouts = {
|
||||
default: Object.assign(
|
||||
{
|
||||
/* SearXNG layout */
|
||||
ArrowLeft: {
|
||||
key: "←",
|
||||
fun: highlightResult("up"),
|
||||
des: "select previous search result",
|
||||
cat: "Results"
|
||||
},
|
||||
ArrowRight: {
|
||||
key: "→",
|
||||
fun: highlightResult("down"),
|
||||
des: "select next search result",
|
||||
cat: "Results"
|
||||
}
|
||||
},
|
||||
baseKeyBinding
|
||||
),
|
||||
|
||||
vim: Object.assign(
|
||||
{
|
||||
/* Vim-like Key Layout. */
|
||||
b: {
|
||||
key: "b",
|
||||
fun: scrollPage(-window.innerHeight),
|
||||
des: "scroll one page up",
|
||||
cat: "Navigation"
|
||||
},
|
||||
f: {
|
||||
key: "f",
|
||||
fun: scrollPage(window.innerHeight),
|
||||
des: "scroll one page down",
|
||||
cat: "Navigation"
|
||||
},
|
||||
u: {
|
||||
key: "u",
|
||||
fun: scrollPage(-window.innerHeight / 2),
|
||||
des: "scroll half a page up",
|
||||
cat: "Navigation"
|
||||
},
|
||||
d: {
|
||||
key: "d",
|
||||
fun: scrollPage(window.innerHeight / 2),
|
||||
des: "scroll half a page down",
|
||||
cat: "Navigation"
|
||||
},
|
||||
g: {
|
||||
key: "g",
|
||||
fun: scrollPageTo(-document.body.scrollHeight, "top"),
|
||||
des: "scroll to the top of the page",
|
||||
cat: "Navigation"
|
||||
},
|
||||
v: {
|
||||
key: "v",
|
||||
fun: scrollPageTo(document.body.scrollHeight, "bottom"),
|
||||
des: "scroll to the bottom of the page",
|
||||
cat: "Navigation"
|
||||
},
|
||||
k: {
|
||||
key: "k",
|
||||
fun: highlightResult("up"),
|
||||
des: "select previous search result",
|
||||
cat: "Results"
|
||||
},
|
||||
j: {
|
||||
key: "j",
|
||||
fun: highlightResult("down"),
|
||||
des: "select next search result",
|
||||
cat: "Results"
|
||||
},
|
||||
y: {
|
||||
key: "y",
|
||||
fun: copyURLToClipboard,
|
||||
des: "copy url of the selected result to the clipboard",
|
||||
cat: "Results"
|
||||
}
|
||||
},
|
||||
baseKeyBinding
|
||||
)
|
||||
};
|
||||
|
||||
const keyBindings = keyBindingLayouts[searxng.settings.hotkeys] || keyBindingLayouts.default;
|
||||
|
||||
searxng.on(document, "keydown", (e) => {
|
||||
// check for modifiers so we don't break browser's hotkeys
|
||||
if (
|
||||
// biome-ignore lint/suspicious/noPrototypeBuiltins: FIXME: support for Chromium 93-87, Firefox 92-78, Safari 15.4-14
|
||||
Object.prototype.hasOwnProperty.call(keyBindings, e.key) &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey &&
|
||||
!e.shiftKey &&
|
||||
!e.metaKey
|
||||
) {
|
||||
const tagName = e.target.tagName.toLowerCase();
|
||||
if (e.key === "Escape") {
|
||||
keyBindings[e.key].fun(e);
|
||||
} else {
|
||||
if (e.target === document.body || tagName === "a" || tagName === "button") {
|
||||
e.preventDefault();
|
||||
keyBindings[e.key].fun();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function highlightResult(which) {
|
||||
return (noScroll, keepFocus) => {
|
||||
let current = document.querySelector(".result[data-vim-selected]"),
|
||||
effectiveWhich = which;
|
||||
if (current === null) {
|
||||
// no selection : choose the first one
|
||||
current = document.querySelector(".result");
|
||||
if (current === null) {
|
||||
// no first one : there are no results
|
||||
return;
|
||||
}
|
||||
// replace up/down actions by selecting first one
|
||||
if (which === "down" || which === "up") {
|
||||
effectiveWhich = current;
|
||||
}
|
||||
}
|
||||
|
||||
let next,
|
||||
results = document.querySelectorAll(".result");
|
||||
results = Array.from(results); // convert NodeList to Array for further use
|
||||
|
||||
if (typeof effectiveWhich !== "string") {
|
||||
next = effectiveWhich;
|
||||
} else {
|
||||
switch (effectiveWhich) {
|
||||
case "visible": {
|
||||
const top = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
const bot = top + document.documentElement.clientHeight;
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
next = results[i];
|
||||
const etop = next.offsetTop;
|
||||
const ebot = etop + next.clientHeight;
|
||||
|
||||
if (ebot <= bot && etop > top) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "down":
|
||||
next = results[results.indexOf(current) + 1] || current;
|
||||
break;
|
||||
case "up":
|
||||
next = results[results.indexOf(current) - 1] || current;
|
||||
break;
|
||||
case "bottom":
|
||||
next = results[results.length - 1];
|
||||
break;
|
||||
// biome-ignore lint/complexity/noUselessSwitchCase: fallthrough is intended
|
||||
case "top":
|
||||
/* falls through */
|
||||
default:
|
||||
next = results[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (next) {
|
||||
current.removeAttribute("data-vim-selected");
|
||||
next.setAttribute("data-vim-selected", "true");
|
||||
if (!keepFocus) {
|
||||
const link = next.querySelector("h3 a") || next.querySelector("a");
|
||||
if (link !== null) {
|
||||
link.focus();
|
||||
}
|
||||
}
|
||||
if (!noScroll) {
|
||||
scrollPageToSelected();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function reloadPage() {
|
||||
document.location.reload(true);
|
||||
}
|
||||
|
||||
function removeFocus(e) {
|
||||
const tagName = e.target.tagName.toLowerCase();
|
||||
if (document.activeElement && (tagName === "input" || tagName === "select" || tagName === "textarea")) {
|
||||
document.activeElement.blur();
|
||||
} else {
|
||||
searxng.closeDetail();
|
||||
}
|
||||
}
|
||||
|
||||
function pageButtonClick(css_selector) {
|
||||
return () => {
|
||||
const button = document.querySelector(css_selector);
|
||||
if (button) {
|
||||
button.click();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function GoToNextPage() {
|
||||
return pageButtonClick('nav#pagination .next_page button[type="submit"]');
|
||||
}
|
||||
|
||||
function GoToPreviousPage() {
|
||||
return pageButtonClick('nav#pagination .previous_page button[type="submit"]');
|
||||
}
|
||||
|
||||
function scrollPageToSelected() {
|
||||
const sel = document.querySelector(".result[data-vim-selected]");
|
||||
if (sel === null) {
|
||||
return;
|
||||
}
|
||||
const wtop = document.documentElement.scrollTop || document.body.scrollTop,
|
||||
wheight = document.documentElement.clientHeight,
|
||||
etop = sel.offsetTop,
|
||||
ebot = etop + sel.clientHeight,
|
||||
offset = 120;
|
||||
// first element ?
|
||||
if (sel.previousElementSibling === null && ebot < wheight) {
|
||||
// set to the top of page if the first element
|
||||
// is fully included in the viewport
|
||||
window.scroll(window.scrollX, 0);
|
||||
return;
|
||||
}
|
||||
if (wtop > etop - offset) {
|
||||
window.scroll(window.scrollX, etop - offset);
|
||||
} else {
|
||||
const wbot = wtop + wheight;
|
||||
if (wbot < ebot + offset) {
|
||||
window.scroll(window.scrollX, ebot - wheight + offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scrollPage(amount) {
|
||||
return () => {
|
||||
window.scrollBy(0, amount);
|
||||
highlightResult("visible")();
|
||||
};
|
||||
}
|
||||
|
||||
function scrollPageTo(position, nav) {
|
||||
return () => {
|
||||
window.scrollTo(0, position);
|
||||
highlightResult(nav)();
|
||||
};
|
||||
}
|
||||
|
||||
function searchInputFocus() {
|
||||
window.scrollTo(0, 0);
|
||||
const q = document.querySelector("#q");
|
||||
q.focus();
|
||||
if (q.setSelectionRange) {
|
||||
const len = q.value.length;
|
||||
q.setSelectionRange(len, len);
|
||||
}
|
||||
}
|
||||
|
||||
function openResult(newTab) {
|
||||
return () => {
|
||||
let link = document.querySelector(".result[data-vim-selected] h3 a");
|
||||
if (link === null) {
|
||||
link = document.querySelector(".result[data-vim-selected] > a");
|
||||
}
|
||||
if (link !== null) {
|
||||
const url = link.getAttribute("href");
|
||||
if (newTab) {
|
||||
window.open(url);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function initHelpContent(divElement) {
|
||||
const categories = {};
|
||||
|
||||
for (const k in keyBindings) {
|
||||
const key = keyBindings[k];
|
||||
categories[key.cat] = categories[key.cat] || [];
|
||||
categories[key.cat].push(key);
|
||||
}
|
||||
|
||||
const sorted = Object.keys(categories).sort((a, b) => categories[b].length - categories[a].length);
|
||||
|
||||
if (sorted.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<a href="#" class="close" aria-label="close" title="close">×</a>';
|
||||
html += "<h3>How to navigate SearXNG with hotkeys</h3>";
|
||||
html += "<table>";
|
||||
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const cat = categories[sorted[i]];
|
||||
|
||||
const lastCategory = i === sorted.length - 1;
|
||||
const first = i % 2 === 0;
|
||||
|
||||
if (first) {
|
||||
html += "<tr>";
|
||||
}
|
||||
html += "<td>";
|
||||
|
||||
html += `<h4>${cat[0].cat}</h4>`;
|
||||
html += '<ul class="list-unstyled">';
|
||||
|
||||
for (const cj in cat) {
|
||||
html += `<li><kbd>${cat[cj].key}</kbd> ${cat[cj].des}</li>`;
|
||||
}
|
||||
|
||||
html += "</ul>";
|
||||
html += "</td>"; // col-sm-*
|
||||
|
||||
if (!first || lastCategory) {
|
||||
html += "</tr>"; // row
|
||||
}
|
||||
}
|
||||
|
||||
html += "</table>";
|
||||
|
||||
divElement.innerHTML = html;
|
||||
}
|
||||
|
||||
function toggleHelp() {
|
||||
let helpPanel = document.querySelector("#vim-hotkeys-help");
|
||||
if (helpPanel === undefined || helpPanel === null) {
|
||||
// first call
|
||||
helpPanel = document.createElement("div");
|
||||
helpPanel.id = "vim-hotkeys-help";
|
||||
helpPanel.className = "dialog-modal";
|
||||
initHelpContent(helpPanel);
|
||||
const body = document.getElementsByTagName("body")[0];
|
||||
body.appendChild(helpPanel);
|
||||
} else {
|
||||
// toggle hidden
|
||||
helpPanel.classList.toggle("invisible");
|
||||
}
|
||||
}
|
||||
|
||||
function copyURLToClipboard() {
|
||||
const currentUrlElement = document.querySelector(".result[data-vim-selected] h3 a");
|
||||
if (currentUrlElement === null) return;
|
||||
|
||||
const url = currentUrlElement.getAttribute("href");
|
||||
navigator.clipboard.writeText(url);
|
||||
}
|
||||
|
||||
searxng.scrollPageToSelected = scrollPageToSelected;
|
||||
searxng.selectNext = highlightResult("down");
|
||||
searxng.selectPrevious = highlightResult("up");
|
||||
});
|
||||
468
client/simple/src/js/main/keyboard.ts
Normal file
468
client/simple/src/js/main/keyboard.ts
Normal file
@@ -0,0 +1,468 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { assertElement, listen, mutable, settings } from "../core/toolkit.ts";
|
||||
|
||||
export type KeyBindingLayout = "default" | "vim";
|
||||
|
||||
type KeyBinding = {
|
||||
key: string;
|
||||
fun: (event: KeyboardEvent) => void;
|
||||
des: string;
|
||||
cat: string;
|
||||
};
|
||||
|
||||
type HighlightResultElement = "down" | "up" | "visible" | "bottom" | "top";
|
||||
|
||||
/* common base for layouts */
|
||||
const baseKeyBinding: Record<string, KeyBinding> = {
|
||||
Escape: {
|
||||
key: "ESC",
|
||||
fun: (event: KeyboardEvent) => removeFocus(event),
|
||||
des: "remove focus from the focused input",
|
||||
cat: "Control"
|
||||
},
|
||||
c: {
|
||||
key: "c",
|
||||
fun: () => copyURLToClipboard(),
|
||||
des: "copy url of the selected result to the clipboard",
|
||||
cat: "Results"
|
||||
},
|
||||
h: {
|
||||
key: "h",
|
||||
fun: () => toggleHelp(keyBindings),
|
||||
des: "toggle help window",
|
||||
cat: "Other"
|
||||
},
|
||||
i: {
|
||||
key: "i",
|
||||
fun: () => searchInputFocus(),
|
||||
des: "focus on the search input",
|
||||
cat: "Control"
|
||||
},
|
||||
n: {
|
||||
key: "n",
|
||||
fun: () => GoToNextPage(),
|
||||
des: "go to next page",
|
||||
cat: "Results"
|
||||
},
|
||||
o: {
|
||||
key: "o",
|
||||
fun: () => openResult(false),
|
||||
des: "open search result",
|
||||
cat: "Results"
|
||||
},
|
||||
p: {
|
||||
key: "p",
|
||||
fun: () => GoToPreviousPage(),
|
||||
des: "go to previous page",
|
||||
cat: "Results"
|
||||
},
|
||||
r: {
|
||||
key: "r",
|
||||
fun: () => reloadPage(),
|
||||
des: "reload page from the server",
|
||||
cat: "Control"
|
||||
},
|
||||
t: {
|
||||
key: "t",
|
||||
fun: () => openResult(true),
|
||||
des: "open the result in a new tab",
|
||||
cat: "Results"
|
||||
}
|
||||
};
|
||||
|
||||
const keyBindingLayouts: Record<KeyBindingLayout, Record<string, KeyBinding>> = {
|
||||
// SearXNG layout
|
||||
default: {
|
||||
ArrowLeft: {
|
||||
key: "←",
|
||||
fun: () => highlightResult("up")(),
|
||||
des: "select previous search result",
|
||||
cat: "Results"
|
||||
},
|
||||
ArrowRight: {
|
||||
key: "→",
|
||||
fun: () => highlightResult("down")(),
|
||||
des: "select next search result",
|
||||
cat: "Results"
|
||||
},
|
||||
...baseKeyBinding
|
||||
},
|
||||
|
||||
// Vim-like keyboard layout
|
||||
vim: {
|
||||
b: {
|
||||
key: "b",
|
||||
fun: () => scrollPage(-window.innerHeight),
|
||||
des: "scroll one page up",
|
||||
cat: "Navigation"
|
||||
},
|
||||
d: {
|
||||
key: "d",
|
||||
fun: () => scrollPage(window.innerHeight / 2),
|
||||
des: "scroll half a page down",
|
||||
cat: "Navigation"
|
||||
},
|
||||
f: {
|
||||
key: "f",
|
||||
fun: () => scrollPage(window.innerHeight),
|
||||
des: "scroll one page down",
|
||||
cat: "Navigation"
|
||||
},
|
||||
g: {
|
||||
key: "g",
|
||||
fun: () => scrollPageTo(-document.body.scrollHeight, "top"),
|
||||
des: "scroll to the top of the page",
|
||||
cat: "Navigation"
|
||||
},
|
||||
j: {
|
||||
key: "j",
|
||||
fun: () => highlightResult("down")(),
|
||||
des: "select next search result",
|
||||
cat: "Results"
|
||||
},
|
||||
k: {
|
||||
key: "k",
|
||||
fun: () => highlightResult("up")(),
|
||||
des: "select previous search result",
|
||||
cat: "Results"
|
||||
},
|
||||
u: {
|
||||
key: "u",
|
||||
fun: () => scrollPage(-window.innerHeight / 2),
|
||||
des: "scroll half a page up",
|
||||
cat: "Navigation"
|
||||
},
|
||||
v: {
|
||||
key: "v",
|
||||
fun: () => scrollPageTo(document.body.scrollHeight, "bottom"),
|
||||
des: "scroll to the bottom of the page",
|
||||
cat: "Navigation"
|
||||
},
|
||||
y: {
|
||||
key: "y",
|
||||
fun: () => copyURLToClipboard(),
|
||||
des: "copy url of the selected result to the clipboard",
|
||||
cat: "Results"
|
||||
},
|
||||
...baseKeyBinding
|
||||
}
|
||||
};
|
||||
|
||||
const keyBindings: Record<string, KeyBinding> =
|
||||
settings.hotkeys && settings.hotkeys in keyBindingLayouts
|
||||
? keyBindingLayouts[settings.hotkeys]
|
||||
: keyBindingLayouts.default;
|
||||
|
||||
const isElementInDetail = (element?: HTMLElement): boolean => {
|
||||
const ancestor = element?.closest(".detail, .result");
|
||||
return ancestor?.classList.contains("detail") ?? false;
|
||||
};
|
||||
|
||||
const getResultElement = (element?: HTMLElement): HTMLElement | undefined => {
|
||||
return element?.closest(".result") ?? undefined;
|
||||
};
|
||||
|
||||
const isImageResult = (resultElement?: HTMLElement): boolean => {
|
||||
return resultElement?.classList.contains("result-images") ?? false;
|
||||
};
|
||||
|
||||
const highlightResult =
|
||||
(which: HighlightResultElement | HTMLElement) =>
|
||||
(noScroll?: boolean, keepFocus?: boolean): void => {
|
||||
let effectiveWhich = which;
|
||||
let current = document.querySelector<HTMLElement>(".result[data-vim-selected]");
|
||||
if (!current) {
|
||||
// no selection : choose the first one
|
||||
current = document.querySelector<HTMLElement>(".result");
|
||||
if (!current) {
|
||||
// no first one : there are no results
|
||||
return;
|
||||
}
|
||||
// replace up/down actions by selecting first one
|
||||
if (which === "down" || which === "up") {
|
||||
effectiveWhich = current;
|
||||
}
|
||||
}
|
||||
|
||||
const results = Array.from(document.querySelectorAll<HTMLElement>(".result"));
|
||||
|
||||
let next: HTMLElement | undefined;
|
||||
|
||||
if (typeof effectiveWhich !== "string") {
|
||||
next = effectiveWhich;
|
||||
} else {
|
||||
switch (effectiveWhich) {
|
||||
case "visible": {
|
||||
const top = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
const bot = top + document.documentElement.clientHeight;
|
||||
|
||||
for (const element of results) {
|
||||
const etop = element.offsetTop;
|
||||
const ebot = etop + element.clientHeight;
|
||||
if (ebot <= bot && etop > top) {
|
||||
next = element;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "down":
|
||||
next = results[results.indexOf(current) + 1] || current;
|
||||
break;
|
||||
case "up":
|
||||
next = results[results.indexOf(current) - 1] || current;
|
||||
break;
|
||||
case "bottom":
|
||||
next = results.at(-1);
|
||||
break;
|
||||
// biome-ignore lint/complexity/noUselessSwitchCase: fallthrough is intended
|
||||
case "top":
|
||||
default:
|
||||
next = results[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (next) {
|
||||
current.removeAttribute("data-vim-selected");
|
||||
next.setAttribute("data-vim-selected", "true");
|
||||
|
||||
if (!keepFocus) {
|
||||
const link = next.querySelector<HTMLAnchorElement>("h3 a") || next.querySelector<HTMLAnchorElement>("a");
|
||||
link?.focus();
|
||||
}
|
||||
|
||||
if (!noScroll) {
|
||||
mutable.scrollPageToSelected?.();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const reloadPage = (): void => {
|
||||
document.location.reload();
|
||||
};
|
||||
|
||||
const removeFocus = (event: KeyboardEvent): void => {
|
||||
const target = event.target as HTMLElement;
|
||||
const tagName = target?.tagName?.toLowerCase();
|
||||
|
||||
if (document.activeElement && (tagName === "input" || tagName === "select" || tagName === "textarea")) {
|
||||
(document.activeElement as HTMLElement).blur();
|
||||
} else {
|
||||
mutable.closeDetail?.();
|
||||
}
|
||||
};
|
||||
|
||||
const pageButtonClick = (css_selector: string): void => {
|
||||
const button = document.querySelector<HTMLButtonElement>(css_selector);
|
||||
if (button) {
|
||||
button.click();
|
||||
}
|
||||
};
|
||||
|
||||
const GoToNextPage = (): void => {
|
||||
pageButtonClick('nav#pagination .next_page button[type="submit"]');
|
||||
};
|
||||
|
||||
const GoToPreviousPage = (): void => {
|
||||
pageButtonClick('nav#pagination .previous_page button[type="submit"]');
|
||||
};
|
||||
|
||||
mutable.scrollPageToSelected = (): void => {
|
||||
const sel = document.querySelector<HTMLElement>(".result[data-vim-selected]");
|
||||
if (!sel) return;
|
||||
|
||||
const wtop = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
const height = document.documentElement.clientHeight;
|
||||
const etop = sel.offsetTop;
|
||||
const ebot = etop + sel.clientHeight;
|
||||
const offset = 120;
|
||||
|
||||
// first element ?
|
||||
if (!sel.previousElementSibling && ebot < height) {
|
||||
// set to the top of page if the first element
|
||||
// is fully included in the viewport
|
||||
window.scroll(window.scrollX, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (wtop > etop - offset) {
|
||||
window.scroll(window.scrollX, etop - offset);
|
||||
} else {
|
||||
const wbot = wtop + height;
|
||||
if (wbot < ebot + offset) {
|
||||
window.scroll(window.scrollX, ebot - height + offset);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const scrollPage = (amount: number): void => {
|
||||
window.scrollBy(0, amount);
|
||||
highlightResult("visible")();
|
||||
};
|
||||
|
||||
const scrollPageTo = (position: number, nav: HighlightResultElement): void => {
|
||||
window.scrollTo(0, position);
|
||||
highlightResult(nav)();
|
||||
};
|
||||
|
||||
const searchInputFocus = (): void => {
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
const q = document.querySelector<HTMLInputElement>("#q");
|
||||
if (q) {
|
||||
q.focus();
|
||||
|
||||
if (q.setSelectionRange) {
|
||||
const len = q.value.length;
|
||||
|
||||
q.setSelectionRange(len, len);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const openResult = (newTab: boolean): void => {
|
||||
let link = document.querySelector<HTMLAnchorElement>(".result[data-vim-selected] h3 a");
|
||||
if (!link) {
|
||||
link = document.querySelector<HTMLAnchorElement>(".result[data-vim-selected] > a");
|
||||
}
|
||||
if (!link) return;
|
||||
|
||||
const url = link.getAttribute("href");
|
||||
if (url) {
|
||||
if (newTab) {
|
||||
window.open(url);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const initHelpContent = (divElement: HTMLElement, keyBindings: typeof baseKeyBinding): void => {
|
||||
const categories: Record<string, KeyBinding[]> = {};
|
||||
|
||||
for (const binding of Object.values(keyBindings)) {
|
||||
const cat = binding.cat;
|
||||
categories[cat] ??= [];
|
||||
categories[cat].push(binding);
|
||||
}
|
||||
|
||||
const sortedCategoryKeys = Object.keys(categories).sort(
|
||||
(a, b) => (categories[b]?.length ?? 0) - (categories[a]?.length ?? 0)
|
||||
);
|
||||
|
||||
let html = '<a href="#" class="close" aria-label="close" title="close">×</a>';
|
||||
html += "<h3>How to navigate SearXNG with hotkeys</h3>";
|
||||
html += "<table>";
|
||||
|
||||
for (const [i, categoryKey] of sortedCategoryKeys.entries()) {
|
||||
const bindings = categories[categoryKey];
|
||||
if (!bindings || bindings.length === 0) continue;
|
||||
|
||||
const isFirst = i % 2 === 0;
|
||||
const isLast = i === sortedCategoryKeys.length - 1;
|
||||
|
||||
if (isFirst) {
|
||||
html += "<tr>";
|
||||
}
|
||||
|
||||
html += "<td>";
|
||||
html += `<h4>${categoryKey}</h4>`;
|
||||
html += '<ul class="list-unstyled">';
|
||||
|
||||
for (const binding of bindings) {
|
||||
html += `<li><kbd>${binding.key}</kbd> ${binding.des}</li>`;
|
||||
}
|
||||
|
||||
html += "</ul>";
|
||||
html += "</td>";
|
||||
|
||||
if (!isFirst || isLast) {
|
||||
html += "</tr>";
|
||||
}
|
||||
}
|
||||
|
||||
html += "</table>";
|
||||
|
||||
divElement.innerHTML = html;
|
||||
};
|
||||
|
||||
const toggleHelp = (keyBindings: typeof baseKeyBinding): void => {
|
||||
let helpPanel = document.querySelector<HTMLElement>("#vim-hotkeys-help");
|
||||
if (helpPanel) {
|
||||
// toggle hidden
|
||||
helpPanel.classList.toggle("invisible");
|
||||
} else {
|
||||
// first call
|
||||
helpPanel = Object.assign(document.createElement("div"), {
|
||||
id: "vim-hotkeys-help",
|
||||
className: "dialog-modal"
|
||||
});
|
||||
initHelpContent(helpPanel, keyBindings);
|
||||
const body = document.getElementsByTagName("body")[0];
|
||||
if (body) {
|
||||
body.appendChild(helpPanel);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const copyURLToClipboard = async (): Promise<void> => {
|
||||
const currentUrlElement = document.querySelector<HTMLAnchorElement>(".result[data-vim-selected] h3 a");
|
||||
assertElement(currentUrlElement);
|
||||
|
||||
const url = currentUrlElement.getAttribute("href");
|
||||
if (url) {
|
||||
await navigator.clipboard.writeText(url);
|
||||
}
|
||||
};
|
||||
|
||||
listen("click", ".result", function (this: HTMLElement, event: PointerEvent) {
|
||||
if (!isElementInDetail(event.target as HTMLElement)) {
|
||||
highlightResult(this)(true, true);
|
||||
|
||||
const resultElement = getResultElement(event.target as HTMLElement);
|
||||
|
||||
if (resultElement && isImageResult(resultElement)) {
|
||||
event.preventDefault();
|
||||
mutable.selectImage?.(resultElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// FIXME: Focus might also trigger Pointer event ^^^
|
||||
listen(
|
||||
"focus",
|
||||
".result a",
|
||||
(event: FocusEvent) => {
|
||||
if (!isElementInDetail(event.target as HTMLElement)) {
|
||||
const resultElement = getResultElement(event.target as HTMLElement);
|
||||
|
||||
if (resultElement && !resultElement.hasAttribute("data-vim-selected")) {
|
||||
highlightResult(resultElement)(true);
|
||||
}
|
||||
|
||||
if (resultElement && isImageResult(resultElement)) {
|
||||
event.preventDefault();
|
||||
mutable.selectImage?.(resultElement);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
|
||||
listen("keydown", document, (event: KeyboardEvent) => {
|
||||
// check for modifiers so we don't break browser's hotkeys
|
||||
if (Object.hasOwn(keyBindings, event.key) && !event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey) {
|
||||
const tagName = (event.target as HTMLElement)?.tagName?.toLowerCase();
|
||||
|
||||
if (event.key === "Escape") {
|
||||
keyBindings[event.key]?.fun(event);
|
||||
} else if (event.target === document.body || tagName === "a" || tagName === "button") {
|
||||
event.preventDefault();
|
||||
keyBindings[event.key]?.fun(event);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mutable.selectNext = highlightResult("down");
|
||||
mutable.selectPrevious = highlightResult("up");
|
||||
@@ -1,77 +0,0 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
/* global L */
|
||||
((_w, _d, searxng) => {
|
||||
searxng.ready(() => {
|
||||
searxng.on(".searxng_init_map", "click", function (event) {
|
||||
// no more request
|
||||
this.classList.remove("searxng_init_map");
|
||||
|
||||
//
|
||||
const leaflet_target = this.dataset.leafletTarget;
|
||||
const map_lon = parseFloat(this.dataset.mapLon);
|
||||
const map_lat = parseFloat(this.dataset.mapLat);
|
||||
const map_zoom = parseFloat(this.dataset.mapZoom);
|
||||
const map_boundingbox = JSON.parse(this.dataset.mapBoundingbox);
|
||||
const map_geojson = JSON.parse(this.dataset.mapGeojson);
|
||||
|
||||
searxng.loadStyle("css/leaflet.css");
|
||||
searxng.loadScript("js/leaflet.js", () => {
|
||||
let map_bounds = null;
|
||||
if (map_boundingbox) {
|
||||
const southWest = L.latLng(map_boundingbox[0], map_boundingbox[2]);
|
||||
const northEast = L.latLng(map_boundingbox[1], map_boundingbox[3]);
|
||||
map_bounds = L.latLngBounds(southWest, northEast);
|
||||
}
|
||||
|
||||
// init map
|
||||
const map = L.map(leaflet_target);
|
||||
// create the tile layer with correct attribution
|
||||
const osmMapnikUrl = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
|
||||
const osmMapnikAttrib = 'Map data © <a href="https://openstreetmap.org">OpenStreetMap</a> contributors';
|
||||
const osmMapnik = new L.TileLayer(osmMapnikUrl, { minZoom: 1, maxZoom: 19, attribution: osmMapnikAttrib });
|
||||
const osmWikimediaUrl = "https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png";
|
||||
const osmWikimediaAttrib =
|
||||
'Wikimedia maps | Maps data © <a href="https://openstreetmap.org">OpenStreetMap contributors</a>';
|
||||
const osmWikimedia = new L.TileLayer(osmWikimediaUrl, {
|
||||
minZoom: 1,
|
||||
maxZoom: 19,
|
||||
attribution: osmWikimediaAttrib
|
||||
});
|
||||
// init map view
|
||||
if (map_bounds) {
|
||||
// TODO hack: https://github.com/Leaflet/Leaflet/issues/2021
|
||||
// Still useful ?
|
||||
setTimeout(() => {
|
||||
map.fitBounds(map_bounds, {
|
||||
maxZoom: 17
|
||||
});
|
||||
}, 0);
|
||||
} else if (map_lon && map_lat) {
|
||||
if (map_zoom) {
|
||||
map.setView(new L.LatLng(map_lat, map_lon), map_zoom);
|
||||
} else {
|
||||
map.setView(new L.LatLng(map_lat, map_lon), 8);
|
||||
}
|
||||
}
|
||||
|
||||
map.addLayer(osmMapnik);
|
||||
|
||||
const baseLayers = {
|
||||
"OSM Mapnik": osmMapnik,
|
||||
"OSM Wikimedia": osmWikimedia
|
||||
};
|
||||
|
||||
L.control.layers(baseLayers).addTo(map);
|
||||
|
||||
if (map_geojson) {
|
||||
L.geoJson(map_geojson).addTo(map);
|
||||
} /* else if(map_bounds) {
|
||||
L.rectangle(map_bounds, {color: "#ff7800", weight: 3, fill:false}).addTo(map);
|
||||
} */
|
||||
});
|
||||
|
||||
// this event occur only once per element
|
||||
event.preventDefault();
|
||||
});
|
||||
});
|
||||
})(window, document, window.searxng);
|
||||
86
client/simple/src/js/main/mapresult.ts
Normal file
86
client/simple/src/js/main/mapresult.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { listen } from "../core/toolkit.ts";
|
||||
|
||||
listen("click", ".searxng_init_map", async function (this: HTMLElement, event: Event) {
|
||||
event.preventDefault();
|
||||
this.classList.remove("searxng_init_map");
|
||||
|
||||
const {
|
||||
View,
|
||||
OlMap,
|
||||
TileLayer,
|
||||
VectorLayer,
|
||||
OSM,
|
||||
VectorSource,
|
||||
Style,
|
||||
Stroke,
|
||||
Fill,
|
||||
Circle,
|
||||
fromLonLat,
|
||||
GeoJSON,
|
||||
Feature,
|
||||
Point
|
||||
} = await import("../pkg/ol.ts");
|
||||
import("ol/ol.css");
|
||||
|
||||
const { leafletTarget: target, mapLon, mapLat, mapGeojson } = this.dataset;
|
||||
|
||||
const lon = Number.parseFloat(mapLon || "0");
|
||||
const lat = Number.parseFloat(mapLat || "0");
|
||||
const view = new View({ maxZoom: 16, enableRotation: false });
|
||||
const map = new OlMap({
|
||||
target: target,
|
||||
layers: [new TileLayer({ source: new OSM({ maxZoom: 16 }) })],
|
||||
view: view
|
||||
});
|
||||
|
||||
try {
|
||||
const markerSource = new VectorSource({
|
||||
features: [
|
||||
new Feature({
|
||||
geometry: new Point(fromLonLat([lon, lat]))
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
const markerLayer = new VectorLayer({
|
||||
source: markerSource,
|
||||
style: new Style({
|
||||
image: new Circle({
|
||||
radius: 6,
|
||||
fill: new Fill({ color: "#3050ff" })
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
map.addLayer(markerLayer);
|
||||
} catch (error) {
|
||||
console.error("Failed to create marker layer:", error);
|
||||
}
|
||||
|
||||
if (mapGeojson) {
|
||||
try {
|
||||
const geoSource = new VectorSource({
|
||||
features: new GeoJSON().readFeatures(JSON.parse(mapGeojson), {
|
||||
dataProjection: "EPSG:4326",
|
||||
featureProjection: "EPSG:3857"
|
||||
})
|
||||
});
|
||||
|
||||
const geoLayer = new VectorLayer({
|
||||
source: geoSource,
|
||||
style: new Style({
|
||||
stroke: new Stroke({ color: "#3050ff", width: 2 }),
|
||||
fill: new Fill({ color: "#3050ff33" })
|
||||
})
|
||||
});
|
||||
|
||||
map.addLayer(geoLayer);
|
||||
|
||||
view.fit(geoSource.getExtent(), { padding: [20, 20, 20, 20] });
|
||||
} catch (error) {
|
||||
console.error("Failed to create GeoJSON layer:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,52 +0,0 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
((_w, d, searxng) => {
|
||||
if (searxng.endpoint !== "preferences") {
|
||||
return;
|
||||
}
|
||||
|
||||
searxng.ready(() => {
|
||||
let engine_descriptions = null;
|
||||
|
||||
function load_engine_descriptions() {
|
||||
if (engine_descriptions == null) {
|
||||
searxng.http("GET", "engine_descriptions.json").then((content) => {
|
||||
engine_descriptions = JSON.parse(content);
|
||||
for (const [engine_name, description] of Object.entries(engine_descriptions)) {
|
||||
const elements = d.querySelectorAll(`[data-engine-name="${engine_name}"] .engine-description`);
|
||||
for (const element of elements) {
|
||||
const source = ` (<i>${searxng.settings.translations.Source}: ${description[1]}</i>)`;
|
||||
element.innerHTML = description[0] + source;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const el of d.querySelectorAll("[data-engine-name]")) {
|
||||
searxng.on(el, "mouseenter", load_engine_descriptions);
|
||||
}
|
||||
|
||||
const enableAllEngines = d.querySelectorAll(".enable-all-engines");
|
||||
const disableAllEngines = d.querySelectorAll(".disable-all-engines");
|
||||
const engineToggles = d.querySelectorAll("tbody input[type=checkbox][class~=checkbox-onoff]");
|
||||
const toggleEngines = (enable) => {
|
||||
for (const el of engineToggles) {
|
||||
// check if element visible, so that only engines of the current category are modified
|
||||
if (el.offsetParent !== null) el.checked = !enable;
|
||||
}
|
||||
};
|
||||
for (const el of enableAllEngines) {
|
||||
searxng.on(el, "click", () => toggleEngines(true));
|
||||
}
|
||||
for (const el of disableAllEngines) {
|
||||
searxng.on(el, "click", () => toggleEngines(false));
|
||||
}
|
||||
|
||||
const copyHashButton = d.querySelector("#copy-hash");
|
||||
searxng.on(copyHashButton, "click", (e) => {
|
||||
e.preventDefault();
|
||||
navigator.clipboard.writeText(copyHashButton.dataset.hash);
|
||||
copyHashButton.innerText = copyHashButton.dataset.copiedText;
|
||||
});
|
||||
});
|
||||
})(window, document, window.searxng);
|
||||
70
client/simple/src/js/main/preferences.ts
Normal file
70
client/simple/src/js/main/preferences.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { http, listen, settings } from "../core/toolkit.ts";
|
||||
|
||||
let engineDescriptions: Record<string, [string, string]> | undefined;
|
||||
|
||||
const loadEngineDescriptions = async (): Promise<void> => {
|
||||
if (engineDescriptions) return;
|
||||
try {
|
||||
const res = await http("GET", "engine_descriptions.json");
|
||||
engineDescriptions = await res.json();
|
||||
} catch (error) {
|
||||
console.error("Error fetching engineDescriptions:", error);
|
||||
}
|
||||
if (!engineDescriptions) return;
|
||||
|
||||
for (const [engine_name, [description, source]] of Object.entries(engineDescriptions)) {
|
||||
const elements = document.querySelectorAll<HTMLElement>(`[data-engine-name="${engine_name}"] .engine-description`);
|
||||
const sourceText = ` (<i>${settings.translations?.Source}: ${source}</i>)`;
|
||||
|
||||
for (const element of elements) {
|
||||
element.innerHTML = description + sourceText;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleEngines = (enable: boolean, engineToggles: NodeListOf<HTMLInputElement>): void => {
|
||||
for (const engineToggle of engineToggles) {
|
||||
// check if element visible, so that only engines of the current category are modified
|
||||
if (engineToggle.offsetParent) {
|
||||
engineToggle.checked = !enable;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const engineElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>("[data-engine-name]");
|
||||
for (const engineElement of engineElements) {
|
||||
listen("mouseenter", engineElement, loadEngineDescriptions);
|
||||
}
|
||||
|
||||
const engineToggles: NodeListOf<HTMLInputElement> = document.querySelectorAll<HTMLInputElement>(
|
||||
"tbody input[type=checkbox][class~=checkbox-onoff]"
|
||||
);
|
||||
|
||||
const enableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(".enable-all-engines");
|
||||
for (const engine of enableAllEngines) {
|
||||
listen("click", engine, () => toggleEngines(true, engineToggles));
|
||||
}
|
||||
|
||||
const disableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(".disable-all-engines");
|
||||
for (const engine of disableAllEngines) {
|
||||
listen("click", engine, () => toggleEngines(false, engineToggles));
|
||||
}
|
||||
|
||||
const copyHashButton: HTMLElement | null = document.querySelector<HTMLElement>("#copy-hash");
|
||||
if (copyHashButton) {
|
||||
listen("click", copyHashButton, async (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const { copiedText, hash } = copyHashButton.dataset;
|
||||
if (!(copiedText && hash)) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(hash);
|
||||
copyHashButton.innerText = copiedText;
|
||||
} catch (error) {
|
||||
console.error("Failed to copy hash:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
|
||||
import "../../../node_modules/swiped-events/src/swiped-events.js";
|
||||
|
||||
((w, d, searxng) => {
|
||||
if (searxng.endpoint !== "results") {
|
||||
return;
|
||||
}
|
||||
|
||||
searxng.ready(() => {
|
||||
d.querySelectorAll("#urls img").forEach((img) =>
|
||||
img.addEventListener(
|
||||
"error",
|
||||
() => {
|
||||
// console.log("ERROR can't load: " + img.src);
|
||||
img.src = `${window.searxng.settings.theme_static_path}/img/img_load_error.svg`;
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
);
|
||||
|
||||
if (d.querySelector("#search_url button#copy_url")) {
|
||||
d.querySelector("#search_url button#copy_url").style.display = "block";
|
||||
}
|
||||
|
||||
searxng.on(".btn-collapse", "click", function () {
|
||||
const btnLabelCollapsed = this.getAttribute("data-btn-text-collapsed");
|
||||
const btnLabelNotCollapsed = this.getAttribute("data-btn-text-not-collapsed");
|
||||
const target = this.getAttribute("data-target");
|
||||
const targetElement = d.querySelector(target);
|
||||
let html = this.innerHTML;
|
||||
if (this.classList.contains("collapsed")) {
|
||||
html = html.replace(btnLabelCollapsed, btnLabelNotCollapsed);
|
||||
} else {
|
||||
html = html.replace(btnLabelNotCollapsed, btnLabelCollapsed);
|
||||
}
|
||||
this.innerHTML = html;
|
||||
this.classList.toggle("collapsed");
|
||||
targetElement.classList.toggle("invisible");
|
||||
});
|
||||
|
||||
searxng.on(".media-loader", "click", function () {
|
||||
const target = this.getAttribute("data-target");
|
||||
const iframe_load = d.querySelector(`${target} > iframe`);
|
||||
const srctest = iframe_load.getAttribute("src");
|
||||
if (srctest === null || srctest === undefined || srctest === false) {
|
||||
iframe_load.setAttribute("src", iframe_load.getAttribute("data-src"));
|
||||
}
|
||||
});
|
||||
|
||||
searxng.on("#copy_url", "click", function () {
|
||||
const target = this.parentElement.querySelector("pre");
|
||||
navigator.clipboard.writeText(target.innerText);
|
||||
this.innerText = this.dataset.copiedText;
|
||||
});
|
||||
|
||||
// searxng.selectImage (gallery)
|
||||
// -----------------------------
|
||||
|
||||
// setTimeout() ID, needed to cancel *last* loadImage
|
||||
let imgTimeoutID;
|
||||
|
||||
// progress spinner, while an image is loading
|
||||
const imgLoaderSpinner = d.createElement("div");
|
||||
imgLoaderSpinner.classList.add("loader");
|
||||
|
||||
// singleton image object, which is used for all loading processes of a
|
||||
// detailed image
|
||||
const imgLoader = new Image();
|
||||
|
||||
const loadImage = (imgSrc, onSuccess) => {
|
||||
// if defered image load exists, stop defered task.
|
||||
if (imgTimeoutID) clearTimeout(imgTimeoutID);
|
||||
|
||||
// defer load of the detail image for 1 sec
|
||||
imgTimeoutID = setTimeout(() => {
|
||||
imgLoader.src = imgSrc;
|
||||
}, 1000);
|
||||
|
||||
// set handlers in the on-properties
|
||||
imgLoader.onload = () => {
|
||||
onSuccess();
|
||||
imgLoaderSpinner.remove();
|
||||
};
|
||||
imgLoader.onerror = () => {
|
||||
imgLoaderSpinner.remove();
|
||||
};
|
||||
};
|
||||
|
||||
searxng.selectImage = (resultElement) => {
|
||||
// add a class that can be evaluated in the CSS and indicates that the
|
||||
// detail view is open
|
||||
d.getElementById("results").classList.add("image-detail-open");
|
||||
|
||||
// add a hash to the browser history so that pressing back doesn't return
|
||||
// to the previous page this allows us to dismiss the image details on
|
||||
// pressing the back button on mobile devices
|
||||
window.location.hash = "#image-viewer";
|
||||
|
||||
searxng.scrollPageToSelected();
|
||||
|
||||
// if there is none element given by the caller, stop here
|
||||
if (!resultElement) return;
|
||||
|
||||
// find <img> object in the element, if there is none, stop here.
|
||||
const img = resultElement.querySelector(".result-images-source img");
|
||||
if (!img) return;
|
||||
|
||||
// <img src="" data-src="http://example.org/image.jpg">
|
||||
const src = img.getAttribute("data-src");
|
||||
|
||||
// already loaded high-res image or no high-res image available
|
||||
if (!src) return;
|
||||
|
||||
// use the image thumbnail until the image is fully loaded
|
||||
const thumbnail = resultElement.querySelector(".image_thumbnail");
|
||||
img.src = thumbnail.src;
|
||||
|
||||
// show a progress spinner
|
||||
const detailElement = resultElement.querySelector(".detail");
|
||||
detailElement.appendChild(imgLoaderSpinner);
|
||||
|
||||
// load full size image in background
|
||||
loadImage(src, () => {
|
||||
// after the singelton loadImage has loaded the detail image into the
|
||||
// cache, it can be used in the origin <img> as src property.
|
||||
img.src = src;
|
||||
img.removeAttribute("data-src");
|
||||
});
|
||||
};
|
||||
|
||||
searxng.closeDetail = () => {
|
||||
d.getElementById("results").classList.remove("image-detail-open");
|
||||
// remove #image-viewer hash from url by navigating back
|
||||
if (window.location.hash === "#image-viewer") window.history.back();
|
||||
searxng.scrollPageToSelected();
|
||||
};
|
||||
searxng.on(".result-detail-close", "click", (e) => {
|
||||
e.preventDefault();
|
||||
searxng.closeDetail();
|
||||
});
|
||||
searxng.on(".result-detail-previous", "click", (e) => {
|
||||
e.preventDefault();
|
||||
searxng.selectPrevious(false);
|
||||
});
|
||||
searxng.on(".result-detail-next", "click", (e) => {
|
||||
e.preventDefault();
|
||||
searxng.selectNext(false);
|
||||
});
|
||||
|
||||
// listen for the back button to be pressed and dismiss the image details when called
|
||||
window.addEventListener("hashchange", () => {
|
||||
if (window.location.hash !== "#image-viewer") searxng.closeDetail();
|
||||
});
|
||||
|
||||
d.querySelectorAll(".swipe-horizontal").forEach((obj) => {
|
||||
obj.addEventListener("swiped-left", () => {
|
||||
searxng.selectNext(false);
|
||||
});
|
||||
obj.addEventListener("swiped-right", () => {
|
||||
searxng.selectPrevious(false);
|
||||
});
|
||||
});
|
||||
|
||||
w.addEventListener(
|
||||
"scroll",
|
||||
() => {
|
||||
const e = d.getElementById("backToTop"),
|
||||
scrollTop = document.documentElement.scrollTop || document.body.scrollTop,
|
||||
results = d.getElementById("results");
|
||||
if (e !== null) {
|
||||
if (scrollTop >= 100) {
|
||||
results.classList.add("scrolling");
|
||||
} else {
|
||||
results.classList.remove("scrolling");
|
||||
}
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
})(window, document, window.searxng);
|
||||
177
client/simple/src/js/main/results.ts
Normal file
177
client/simple/src/js/main/results.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import "../../../node_modules/swiped-events/src/swiped-events.js";
|
||||
import { assertElement, listen, mutable, settings } from "../core/toolkit.ts";
|
||||
|
||||
let imgTimeoutID: number;
|
||||
|
||||
const imageLoader = (resultElement: HTMLElement): void => {
|
||||
if (imgTimeoutID) clearTimeout(imgTimeoutID);
|
||||
|
||||
const imgElement = resultElement.querySelector<HTMLImageElement>(".result-images-source img");
|
||||
if (!imgElement) return;
|
||||
|
||||
// use thumbnail until full image loads
|
||||
const thumbnail = resultElement.querySelector<HTMLImageElement>(".image_thumbnail");
|
||||
if (thumbnail) {
|
||||
if (thumbnail.src === `${settings.theme_static_path}/img/img_load_error.svg`) return;
|
||||
|
||||
imgElement.onerror = (): void => {
|
||||
imgElement.src = thumbnail.src;
|
||||
};
|
||||
|
||||
imgElement.src = thumbnail.src;
|
||||
}
|
||||
|
||||
const imgSource = imgElement.getAttribute("data-src");
|
||||
if (!imgSource) return;
|
||||
|
||||
// unsafe nodejs specific, cast to https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#return_value
|
||||
// https://github.com/searxng/searxng/pull/5073#discussion_r2265767231
|
||||
imgTimeoutID = setTimeout(() => {
|
||||
imgElement.src = imgSource;
|
||||
imgElement.removeAttribute("data-src");
|
||||
}, 1000) as unknown as number;
|
||||
};
|
||||
|
||||
const imageThumbnails: NodeListOf<HTMLImageElement> =
|
||||
document.querySelectorAll<HTMLImageElement>("#urls img.image_thumbnail");
|
||||
for (const thumbnail of imageThumbnails) {
|
||||
if (thumbnail.complete && thumbnail.naturalWidth === 0) {
|
||||
thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`;
|
||||
}
|
||||
|
||||
thumbnail.onerror = (): void => {
|
||||
thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`;
|
||||
};
|
||||
}
|
||||
|
||||
const copyUrlButton: HTMLButtonElement | null =
|
||||
document.querySelector<HTMLButtonElement>("#search_url button#copy_url");
|
||||
copyUrlButton?.style.setProperty("display", "block");
|
||||
|
||||
mutable.selectImage = (resultElement: HTMLElement): void => {
|
||||
// add a class that can be evaluated in the CSS and indicates that the
|
||||
// detail view is open
|
||||
const resultsElement = document.getElementById("results");
|
||||
resultsElement?.classList.add("image-detail-open");
|
||||
|
||||
// add a hash to the browser history so that pressing back doesn't return
|
||||
// to the previous page this allows us to dismiss the image details on
|
||||
// pressing the back button on mobile devices
|
||||
window.location.hash = "#image-viewer";
|
||||
|
||||
mutable.scrollPageToSelected?.();
|
||||
|
||||
// if there is no element given by the caller, stop here
|
||||
if (!resultElement) return;
|
||||
|
||||
imageLoader(resultElement);
|
||||
};
|
||||
|
||||
mutable.closeDetail = (): void => {
|
||||
const resultsElement = document.getElementById("results");
|
||||
resultsElement?.classList.remove("image-detail-open");
|
||||
|
||||
// remove #image-viewer hash from url by navigating back
|
||||
if (window.location.hash === "#image-viewer") {
|
||||
window.history.back();
|
||||
}
|
||||
|
||||
mutable.scrollPageToSelected?.();
|
||||
};
|
||||
|
||||
listen("click", ".btn-collapse", function (this: HTMLElement) {
|
||||
const btnLabelCollapsed = this.getAttribute("data-btn-text-collapsed");
|
||||
const btnLabelNotCollapsed = this.getAttribute("data-btn-text-not-collapsed");
|
||||
const target = this.getAttribute("data-target");
|
||||
|
||||
if (!(target && btnLabelCollapsed && btnLabelNotCollapsed)) return;
|
||||
|
||||
const targetElement = document.querySelector<HTMLElement>(target);
|
||||
assertElement(targetElement);
|
||||
|
||||
const isCollapsed = this.classList.contains("collapsed");
|
||||
const newLabel = isCollapsed ? btnLabelNotCollapsed : btnLabelCollapsed;
|
||||
const oldLabel = isCollapsed ? btnLabelCollapsed : btnLabelNotCollapsed;
|
||||
|
||||
this.innerHTML = this.innerHTML.replace(oldLabel, newLabel);
|
||||
this.classList.toggle("collapsed");
|
||||
|
||||
targetElement.classList.toggle("invisible");
|
||||
});
|
||||
|
||||
listen("click", ".media-loader", function (this: HTMLElement) {
|
||||
const target = this.getAttribute("data-target");
|
||||
if (!target) return;
|
||||
|
||||
const iframeLoad = document.querySelector<HTMLIFrameElement>(`${target} > iframe`);
|
||||
assertElement(iframeLoad);
|
||||
|
||||
const srctest = iframeLoad.getAttribute("src");
|
||||
if (!srctest) {
|
||||
const dataSrc = iframeLoad.getAttribute("data-src");
|
||||
if (dataSrc) {
|
||||
iframeLoad.setAttribute("src", dataSrc);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
listen("click", "#copy_url", async function (this: HTMLElement) {
|
||||
const target = this.parentElement?.querySelector<HTMLPreElement>("pre");
|
||||
assertElement(target);
|
||||
|
||||
await navigator.clipboard.writeText(target.innerText);
|
||||
const copiedText = this.dataset.copiedText;
|
||||
if (copiedText) {
|
||||
this.innerText = copiedText;
|
||||
}
|
||||
});
|
||||
|
||||
listen("click", ".result-detail-close", (event: Event) => {
|
||||
event.preventDefault();
|
||||
mutable.closeDetail?.();
|
||||
});
|
||||
|
||||
listen("click", ".result-detail-previous", (event: Event) => {
|
||||
event.preventDefault();
|
||||
mutable.selectPrevious?.(false);
|
||||
});
|
||||
|
||||
listen("click", ".result-detail-next", (event: Event) => {
|
||||
event.preventDefault();
|
||||
mutable.selectNext?.(false);
|
||||
});
|
||||
|
||||
// listen for the back button to be pressed and dismiss the image details when called
|
||||
window.addEventListener("hashchange", () => {
|
||||
if (window.location.hash !== "#image-viewer") {
|
||||
mutable.closeDetail?.();
|
||||
}
|
||||
});
|
||||
|
||||
const swipeHorizontal: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(".swipe-horizontal");
|
||||
for (const element of swipeHorizontal) {
|
||||
listen("swiped-left", element, () => {
|
||||
mutable.selectNext?.(false);
|
||||
});
|
||||
|
||||
listen("swiped-right", element, () => {
|
||||
mutable.selectPrevious?.(false);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener(
|
||||
"scroll",
|
||||
() => {
|
||||
const backToTopElement = document.getElementById("backToTop");
|
||||
const resultsElement = document.getElementById("results");
|
||||
|
||||
if (backToTopElement && resultsElement) {
|
||||
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
const isScrolling = scrollTop >= 100;
|
||||
resultsElement.classList.toggle("scrolling", isScrolling);
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
@@ -1,197 +0,0 @@
|
||||
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||
/* exported AutoComplete */
|
||||
|
||||
((_w, d, searxng) => {
|
||||
const qinput_id = "q";
|
||||
let qinput;
|
||||
|
||||
const isMobile = window.matchMedia("only screen and (max-width: 50em)").matches;
|
||||
const isResultsPage = document.querySelector("main").id === "main_results";
|
||||
|
||||
function submitIfQuery() {
|
||||
if (qinput.value.length > 0) {
|
||||
const search = document.getElementById("search");
|
||||
setTimeout(search.submit.bind(search), 0);
|
||||
}
|
||||
}
|
||||
|
||||
function createClearButton(qinput) {
|
||||
const cs = document.getElementById("clear_search");
|
||||
const updateClearButton = () => {
|
||||
if (qinput.value.length === 0) {
|
||||
cs.classList.add("empty");
|
||||
} else {
|
||||
cs.classList.remove("empty");
|
||||
}
|
||||
};
|
||||
|
||||
// update status, event listener
|
||||
updateClearButton();
|
||||
cs.addEventListener("click", (ev) => {
|
||||
qinput.value = "";
|
||||
qinput.focus();
|
||||
updateClearButton();
|
||||
ev.preventDefault();
|
||||
});
|
||||
qinput.addEventListener("input", updateClearButton, false);
|
||||
}
|
||||
|
||||
const fetchResults = async (query) => {
|
||||
let request;
|
||||
if (searxng.settings.method === "GET") {
|
||||
const reqParams = new URLSearchParams();
|
||||
reqParams.append("q", query);
|
||||
request = fetch(`./autocompleter?${reqParams.toString()}`);
|
||||
} else {
|
||||
const formData = new FormData();
|
||||
formData.append("q", query);
|
||||
request = fetch("./autocompleter", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
}
|
||||
|
||||
request.then(async (response) => {
|
||||
const results = await response.json();
|
||||
|
||||
if (!results) return;
|
||||
|
||||
const autocomplete = d.querySelector(".autocomplete");
|
||||
const autocompleteList = d.querySelector(".autocomplete ul");
|
||||
autocomplete.classList.add("open");
|
||||
autocompleteList.innerHTML = "";
|
||||
|
||||
// show an error message that no result was found
|
||||
if (!results[1] || results[1].length === 0) {
|
||||
const noItemFoundMessage = document.createElement("li");
|
||||
noItemFoundMessage.classList.add("no-item-found");
|
||||
noItemFoundMessage.innerHTML = searxng.settings.translations.no_item_found;
|
||||
autocompleteList.appendChild(noItemFoundMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const result of results[1]) {
|
||||
const li = document.createElement("li");
|
||||
li.innerText = result;
|
||||
|
||||
searxng.on(li, "mousedown", () => {
|
||||
qinput.value = result;
|
||||
const form = d.querySelector("#search");
|
||||
form.submit();
|
||||
autocomplete.classList.remove("open");
|
||||
});
|
||||
autocompleteList.appendChild(li);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
searxng.ready(() => {
|
||||
// focus search input on large screens
|
||||
if (!isMobile && !isResultsPage) document.getElementById("q").focus();
|
||||
|
||||
qinput = d.getElementById(qinput_id);
|
||||
const autocomplete = d.querySelector(".autocomplete");
|
||||
const autocompleteList = d.querySelector(".autocomplete ul");
|
||||
|
||||
if (qinput !== null) {
|
||||
// clear button
|
||||
createClearButton(qinput);
|
||||
|
||||
// autocompleter
|
||||
if (searxng.settings.autocomplete) {
|
||||
searxng.on(qinput, "input", () => {
|
||||
const query = qinput.value;
|
||||
if (query.length < searxng.settings.autocomplete_min) return;
|
||||
|
||||
setTimeout(() => {
|
||||
if (query === qinput.value) fetchResults(query);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
searxng.on(qinput, "keyup", (e) => {
|
||||
let currentIndex = -1;
|
||||
const listItems = autocompleteList.children;
|
||||
for (let i = 0; i < listItems.length; i++) {
|
||||
if (listItems[i].classList.contains("active")) {
|
||||
currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let newCurrentIndex = -1;
|
||||
if (e.key === "ArrowUp") {
|
||||
if (currentIndex >= 0) listItems[currentIndex].classList.remove("active");
|
||||
// we need to add listItems.length to the index calculation here because the JavaScript modulos
|
||||
// operator doesn't work with negative numbers
|
||||
newCurrentIndex = (currentIndex - 1 + listItems.length) % listItems.length;
|
||||
} else if (e.key === "ArrowDown") {
|
||||
if (currentIndex >= 0) listItems[currentIndex].classList.remove("active");
|
||||
newCurrentIndex = (currentIndex + 1) % listItems.length;
|
||||
} else if (e.key === "Tab" || e.key === "Enter") {
|
||||
autocomplete.classList.remove("open");
|
||||
}
|
||||
|
||||
if (newCurrentIndex !== -1) {
|
||||
const selectedItem = listItems[newCurrentIndex];
|
||||
selectedItem.classList.add("active");
|
||||
|
||||
if (!selectedItem.classList.contains("no-item-found")) qinput.value = selectedItem.innerText;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Additionally to searching when selecting a new category, we also
|
||||
// automatically start a new search request when the user changes a search
|
||||
// filter (safesearch, time range or language) (this requires JavaScript
|
||||
// though)
|
||||
if (
|
||||
qinput !== null &&
|
||||
searxng.settings.search_on_category_select &&
|
||||
// If .search_filters is undefined (invisible) we are on the homepage and
|
||||
// hence don't have to set any listeners
|
||||
d.querySelector(".search_filters") != null
|
||||
) {
|
||||
searxng.on(d.getElementById("safesearch"), "change", submitIfQuery);
|
||||
searxng.on(d.getElementById("time_range"), "change", submitIfQuery);
|
||||
searxng.on(d.getElementById("language"), "change", submitIfQuery);
|
||||
}
|
||||
|
||||
const categoryButtons = d.querySelectorAll("button.category_button");
|
||||
for (const button of categoryButtons) {
|
||||
searxng.on(button, "click", (event) => {
|
||||
if (event.shiftKey) {
|
||||
event.preventDefault();
|
||||
button.classList.toggle("selected");
|
||||
return;
|
||||
}
|
||||
|
||||
// manually deselect the old selection when a new category is selected
|
||||
const selectedCategories = d.querySelectorAll("button.category_button.selected");
|
||||
for (const categoryButton of selectedCategories) {
|
||||
categoryButton.classList.remove("selected");
|
||||
}
|
||||
button.classList.add("selected");
|
||||
});
|
||||
}
|
||||
|
||||
// override form submit action to update the actually selected categories
|
||||
const form = d.querySelector("#search");
|
||||
if (form != null) {
|
||||
searxng.on(form, "submit", (event) => {
|
||||
event.preventDefault();
|
||||
const categoryValuesInput = d.querySelector("#selected-categories");
|
||||
if (categoryValuesInput) {
|
||||
const categoryValues = [];
|
||||
for (const categoryButton of categoryButtons) {
|
||||
if (categoryButton.classList.contains("selected")) {
|
||||
categoryValues.push(categoryButton.name.replace("category_", ""));
|
||||
}
|
||||
}
|
||||
categoryValuesInput.value = categoryValues.join(",");
|
||||
}
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
});
|
||||
})(window, document, window.searxng);
|
||||
106
client/simple/src/js/main/search.ts
Normal file
106
client/simple/src/js/main/search.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { assertElement, listen, settings } from "../core/toolkit.ts";
|
||||
|
||||
const submitIfQuery = (qInput: HTMLInputElement): void => {
|
||||
if (qInput.value.length > 0) {
|
||||
const search = document.getElementById("search") as HTMLFormElement | null;
|
||||
search?.submit();
|
||||
}
|
||||
};
|
||||
|
||||
const updateClearButton = (qInput: HTMLInputElement, cs: HTMLElement): void => {
|
||||
cs.classList.toggle("empty", qInput.value.length === 0);
|
||||
};
|
||||
|
||||
const createClearButton = (qInput: HTMLInputElement): void => {
|
||||
const cs = document.getElementById("clear_search");
|
||||
assertElement(cs);
|
||||
|
||||
updateClearButton(qInput, cs);
|
||||
|
||||
listen("click", cs, (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
qInput.value = "";
|
||||
qInput.focus();
|
||||
updateClearButton(qInput, cs);
|
||||
});
|
||||
|
||||
listen("input", qInput, () => updateClearButton(qInput, cs), { passive: true });
|
||||
};
|
||||
|
||||
const qInput = document.getElementById("q") as HTMLInputElement | null;
|
||||
assertElement(qInput);
|
||||
|
||||
const isMobile: boolean = window.matchMedia("(max-width: 50em)").matches;
|
||||
const isResultsPage: boolean = document.querySelector("main")?.id === "main_results";
|
||||
|
||||
// focus search input on large screens
|
||||
if (!(isMobile || isResultsPage)) {
|
||||
qInput.focus();
|
||||
}
|
||||
|
||||
createClearButton(qInput);
|
||||
|
||||
// Additionally to searching when selecting a new category, we also
|
||||
// automatically start a new search request when the user changes a search
|
||||
// filter (safesearch, time range or language) (this requires JavaScript
|
||||
// though)
|
||||
if (
|
||||
settings.search_on_category_select &&
|
||||
// If .search_filters is undefined (invisible) we are on the homepage and
|
||||
// hence don't have to set any listeners
|
||||
document.querySelector(".search_filters")
|
||||
) {
|
||||
const safesearchElement = document.getElementById("safesearch");
|
||||
if (safesearchElement) {
|
||||
listen("change", safesearchElement, () => submitIfQuery(qInput));
|
||||
}
|
||||
|
||||
const timeRangeElement = document.getElementById("time_range");
|
||||
if (timeRangeElement) {
|
||||
listen("change", timeRangeElement, () => submitIfQuery(qInput));
|
||||
}
|
||||
|
||||
const languageElement = document.getElementById("language");
|
||||
if (languageElement) {
|
||||
listen("change", languageElement, () => submitIfQuery(qInput));
|
||||
}
|
||||
}
|
||||
|
||||
const categoryButtons: HTMLButtonElement[] = [
|
||||
...document.querySelectorAll<HTMLButtonElement>("button.category_button")
|
||||
];
|
||||
for (const button of categoryButtons) {
|
||||
listen("click", button, (event: MouseEvent) => {
|
||||
if (event.shiftKey) {
|
||||
event.preventDefault();
|
||||
button.classList.toggle("selected");
|
||||
return;
|
||||
}
|
||||
|
||||
// deselect all other categories
|
||||
for (const categoryButton of categoryButtons) {
|
||||
categoryButton.classList.toggle("selected", categoryButton === button);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const form: HTMLFormElement | null = document.querySelector<HTMLFormElement>("#search");
|
||||
assertElement(form);
|
||||
|
||||
// override form submit action to update the actually selected categories
|
||||
listen("submit", form, (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const categoryValuesInput = document.querySelector<HTMLInputElement>("#selected-categories");
|
||||
if (categoryValuesInput) {
|
||||
const categoryValues = categoryButtons
|
||||
.filter((button) => button.classList.contains("selected"))
|
||||
.map((button) => button.name.replace("category_", ""));
|
||||
|
||||
categoryValuesInput.value = categoryValues.join(",");
|
||||
}
|
||||
|
||||
form.submit();
|
||||
});
|
||||
28
client/simple/src/js/pkg/ol.ts
Normal file
28
client/simple/src/js/pkg/ol.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { Feature, Map as OlMap, View } from "ol";
|
||||
import { createEmpty } from "ol/extent";
|
||||
import { GeoJSON } from "ol/format";
|
||||
import { Point } from "ol/geom";
|
||||
import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer";
|
||||
import { fromLonLat } from "ol/proj";
|
||||
import { OSM, Vector as VectorSource } from "ol/source";
|
||||
import { Circle, Fill, Stroke, Style } from "ol/style";
|
||||
|
||||
export {
|
||||
View,
|
||||
OlMap,
|
||||
TileLayer,
|
||||
VectorLayer,
|
||||
OSM,
|
||||
createEmpty,
|
||||
VectorSource,
|
||||
Style,
|
||||
Stroke,
|
||||
Fill,
|
||||
Circle,
|
||||
fromLonLat,
|
||||
GeoJSON,
|
||||
Feature,
|
||||
Point
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
import "./head/00_init.js";
|
||||
@@ -1,7 +0,0 @@
|
||||
import "./main/00_toolkit.js";
|
||||
import "./main/infinite_scroll.js";
|
||||
import "./main/keyboard.js";
|
||||
import "./main/mapresult.js";
|
||||
import "./main/preferences.js";
|
||||
import "./main/results.js";
|
||||
import "./main/search.js";
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.dialog-modal {
|
||||
animation-name: dialogmodal;
|
||||
animation-duration: 0.13s;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*! Autocomplete.js v2.6.3 | license MIT | (c) 2017, Baptiste Donaux | http://autocomplete-js.com */
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.autocomplete {
|
||||
position: absolute;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
@import "../../generated/pygments.less";
|
||||
|
||||
.codelines {
|
||||
@@ -18,12 +20,9 @@
|
||||
cursor: default;
|
||||
|
||||
&::selection {
|
||||
background: transparent; /* WebKit/Blink Browsers */
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-moz-selection {
|
||||
background: transparent; /* Gecko Browsers */
|
||||
}
|
||||
margin-right: 8px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
* SearXNG, A privacy-respecting, hackable metasearch engine
|
||||
*
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#main_results #results.image-detail-open.only_template_images {
|
||||
width: min(98%, 59.25rem) !important;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
iframe[src^="https://w.soundcloud.com"]
|
||||
{
|
||||
height: 120px;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#main_index {
|
||||
margin-top: 26vh;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.info-page {
|
||||
code {
|
||||
font-family: monospace;
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
|
||||
// Mixins
|
||||
.text-size-adjust (@property: 100%) {
|
||||
-webkit-text-size-adjust: @property;
|
||||
-ms-text-size-adjust: @property;
|
||||
-moz-text-size-adjust: @property;
|
||||
text-size-adjust: @property;
|
||||
}
|
||||
|
||||
@@ -22,7 +19,6 @@
|
||||
|
||||
// disable user selection
|
||||
.disable-user-select () {
|
||||
-webkit-touch-callout: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.stats_endpoint {
|
||||
.github-issue-button {
|
||||
display: block;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
Layout of the KeyValue result class
|
||||
*/
|
||||
|
||||
#main_results .result-keyvalue {
|
||||
caption {
|
||||
padding: 0.8rem 0.5rem;
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
@import (inline) "../../node_modules/normalize.css/normalize.css";
|
||||
@import "definitions.less";
|
||||
|
||||
.text-size-adjust (@property: 100%) {
|
||||
-webkit-text-size-adjust: @property;
|
||||
-ms-text-size-adjust: @property;
|
||||
-moz-text-size-adjust: @property;
|
||||
text-size-adjust: @property;
|
||||
}
|
||||
@import "mixins.less";
|
||||
|
||||
// Reset padding and margin
|
||||
html,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/*
|
||||
* SearXNG, A privacy-respecting, hackable metasearch engine
|
||||
*/
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#search {
|
||||
padding: 0;
|
||||
@@ -196,11 +194,6 @@ html.no-js #clear_search.hide_if_nojs {
|
||||
.ltr-rounded-left-corners(0.8rem);
|
||||
}
|
||||
|
||||
#q::-ms-clear,
|
||||
#q::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#send_search {
|
||||
.ltr-rounded-right-corners(0.8rem);
|
||||
|
||||
@@ -271,7 +264,6 @@ html.no-js #clear_search.hide_if_nojs {
|
||||
width: 100%;
|
||||
.ltr-text-align-left();
|
||||
overflow: scroll hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,11 +366,6 @@ html.no-js #clear_search.hide_if_nojs {
|
||||
|
||||
#categories {
|
||||
.disable-user-select;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#categories_container {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/*
|
||||
--center-page-width overrides the less variable @results-width when the results are centered
|
||||
see the CSS rules for #results in style.less ( grid-template-columns and gap).
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.ltr-left(@offset) {
|
||||
left: @offset;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.ltr-left(@offset) {
|
||||
right: @offset;
|
||||
}
|
||||
@@ -129,13 +131,7 @@
|
||||
}
|
||||
|
||||
// select HTML element
|
||||
@supports (
|
||||
(background-position-x: 100%) and
|
||||
(
|
||||
(appearance: none) or (-webkit-appearance: none) or
|
||||
(-moz-appearance: none)
|
||||
)
|
||||
) {
|
||||
@supports ((background-position-x: 100%) and ((appearance: none))) {
|
||||
select {
|
||||
border-width: 0 0 0 2rem;
|
||||
background-position-x: -2rem;
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
/*
|
||||
* SearXNG, A privacy-respecting, hackable metasearch engine
|
||||
*
|
||||
* To convert "style.less" to "style.css" run: $make styles
|
||||
*/
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
// stylelint-disable no-descending-specificity
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
// other solution : http://stackoverflow.com/questions/1577598/how-to-hide-parts-of-html-when-javascript-is-disabled/13857783#13857783
|
||||
|
||||
// stylelint-disable no-descending-specificity
|
||||
@@ -156,9 +158,7 @@ div.selectable_url {
|
||||
|
||||
td {
|
||||
padding: 0 1em 0 0;
|
||||
padding-top: 0;
|
||||
.ltr-padding-right(1rem);
|
||||
padding-bottom: 0;
|
||||
.ltr-padding-left(0);
|
||||
}
|
||||
|
||||
@@ -307,7 +307,7 @@ html body .tabs > input:checked {
|
||||
}
|
||||
|
||||
~ label {
|
||||
position: inherited;
|
||||
position: inherit;
|
||||
background: inherit;
|
||||
border-bottom: 2px solid transparent;
|
||||
font-weight: normal;
|
||||
@@ -347,17 +347,9 @@ select {
|
||||
}
|
||||
}
|
||||
|
||||
@supports (
|
||||
(background-position-x: 100%) and
|
||||
(
|
||||
(appearance: none) or (-webkit-appearance: none) or
|
||||
(-moz-appearance: none)
|
||||
)
|
||||
) {
|
||||
@supports ((background-position-x: 100%) and ((appearance: none))) {
|
||||
select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
border-width: 0 2rem 0 0;
|
||||
border-color: transparent;
|
||||
background: data-uri("image/svg+xml;charset=UTF-8", @select-light-svg-path)
|
||||
@@ -400,8 +392,6 @@ select {
|
||||
|
||||
/* -- checkbox-onoff -- */
|
||||
input.checkbox-onoff[type="checkbox"] {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
@@ -475,8 +465,6 @@ input.checkbox-onoff.reversed-checkbox[type="checkbox"] {
|
||||
/* -- checkbox -- */
|
||||
@supports (transform: rotate(-45deg)) {
|
||||
input[type="checkbox"]:not(.checkbox-onoff) {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
width: 20px;
|
||||
@@ -549,33 +537,16 @@ input.checkbox-onoff.reversed-checkbox[type="checkbox"] {
|
||||
border-right: 0.5em solid var(--color-toolkit-loader-border);
|
||||
border-bottom: 0.5em solid var(--color-toolkit-loader-border);
|
||||
border-left: 0.5em solid var(--color-toolkit-loader-borderleft);
|
||||
-webkit-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
-webkit-animation: load8 1.2s infinite linear;
|
||||
animation: load8 1.2s infinite linear;
|
||||
}
|
||||
|
||||
@-webkit-keyframes load8 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes load8 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -606,9 +577,6 @@ td:hover .engine-tooltip,
|
||||
margin: 0;
|
||||
padding: 0 0.125rem 0 4rem;
|
||||
width: 100%;
|
||||
width: -moz-available;
|
||||
width: -webkit-fill-available;
|
||||
width: fill;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
.loader,
|
||||
.loader::after {
|
||||
border-radius: 50%;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#answers .weather {
|
||||
summary {
|
||||
display: block;
|
||||
|
||||
2
client/simple/static/.gitattributes
vendored
2
client/simple/static/.gitattributes
vendored
@@ -1,2 +0,0 @@
|
||||
leaflet.css -diff
|
||||
leaflet.js -diff
|
||||
@@ -1,43 +1,38 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* Generate icons.html for the jinja templates of the simple theme.
|
||||
*/
|
||||
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { argv } from "node:process";
|
||||
import { jinja_svg_sets } from "./tools/jinja_svg_catalog.js";
|
||||
import type { Config as SvgoConfig } from "svgo";
|
||||
import { type IconSet, type JinjaMacro, jinja_svg_sets } from "./tools/jinja_svg_catalog.ts";
|
||||
|
||||
const HERE = `${dirname(argv[1])}/`;
|
||||
const dest = resolve(HERE, "../../searx/templates/simple/icons.html");
|
||||
const HERE = `${dirname(argv[1] || "")}/`;
|
||||
const dest: string = resolve(HERE, "../../searx/templates/simple/icons.html");
|
||||
|
||||
/** @type import("./tools/jinja_svg_catalog.js").JinjaMacro[] */
|
||||
const searxng_jinja_macros = [
|
||||
const searxng_jinja_macros: JinjaMacro[] = [
|
||||
{ name: "icon", class: "sxng-icon-set" },
|
||||
{ name: "icon_small", class: "sxng-icon-set-small" },
|
||||
{ name: "icon_big", class: "sxng-icon-set-big" }
|
||||
];
|
||||
|
||||
const sxng_icon_opts = {
|
||||
const sxng_icon_opts: SvgoConfig = {
|
||||
multipass: true,
|
||||
plugins: [
|
||||
{ name: "removeTitle" },
|
||||
{ name: "removeXMLNS" },
|
||||
"removeTitle",
|
||||
"removeXMLNS",
|
||||
{
|
||||
name: "addAttributesToSVGElement",
|
||||
params: {
|
||||
attributes: [
|
||||
{
|
||||
"aria-hidden": "true"
|
||||
}
|
||||
]
|
||||
attributes: [{ "aria-hidden": "true" }]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* @type import("./tools/jinja_svg_catalog.js").IconSet[]
|
||||
*/
|
||||
const simple_icons = [
|
||||
const simple_icons: IconSet[] = [
|
||||
{
|
||||
base: resolve(HERE, "node_modules/ionicons/dist/svg"),
|
||||
set: {
|
||||
@@ -1,64 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import sharp from "sharp";
|
||||
import { optimize as svgo } from "svgo";
|
||||
|
||||
/**
|
||||
* @typedef {object} Src2Dest - Mapping of src to dest
|
||||
* @property {string} src - Name of the source file.
|
||||
* @property {string} dest - Name of the destination file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert a list of SVG files to PNG.
|
||||
*
|
||||
* @param {Src2Dest[]} items - Array of SVG files (src: SVG, dest:PNG) to convert.
|
||||
*/
|
||||
async function svg2png(items) {
|
||||
for (const item of items) {
|
||||
try {
|
||||
fs.mkdir(path.dirname(item.dest), { recursive: true }, (err) => {
|
||||
if (err) throw err;
|
||||
});
|
||||
|
||||
const info = await sharp(item.src)
|
||||
.png({
|
||||
force: true,
|
||||
compressionLevel: 9,
|
||||
palette: true
|
||||
})
|
||||
.toFile(item.dest);
|
||||
|
||||
console.log(`[svg2png] created ${item.dest} -- bytes: ${info.size}, w:${info.width}px, h:${info.height}px`);
|
||||
} catch (err) {
|
||||
console.error(`ERROR: ${item.dest} -- ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize SVG images for WEB.
|
||||
*
|
||||
* @param {Src2Dest[]} items - Array of SVG files (src:SVG, dest:SVG) to optimize.
|
||||
* @param {import('svgo').Config} svgo_opts - Options passed to svgo.
|
||||
*/
|
||||
async function svg2svg(items, svgo_opts) {
|
||||
for (const item of items) {
|
||||
try {
|
||||
fs.mkdir(path.dirname(item.dest), { recursive: true }, (err) => {
|
||||
if (err) throw err;
|
||||
});
|
||||
|
||||
const raw = fs.readFileSync(item.src, "utf8");
|
||||
const opt = svgo(raw, svgo_opts);
|
||||
fs.writeFileSync(item.dest, opt.data);
|
||||
console.log(`[svg2svg] optimized: ${item.dest} -- src: ${item.src}`);
|
||||
} catch (err) {
|
||||
console.error(`ERROR: optimize src: ${item.src} -- ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { svg2png, svg2svg };
|
||||
64
client/simple/tools/img.ts
Normal file
64
client/simple/tools/img.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import sharp from "sharp";
|
||||
import type { Config } from "svgo";
|
||||
import { optimize as svgo } from "svgo";
|
||||
|
||||
// Mapping of src to dest
|
||||
export type Src2Dest = {
|
||||
// Name of the source file.
|
||||
src: string;
|
||||
// Name of the destination file.
|
||||
dest: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a list of SVG files to PNG.
|
||||
*
|
||||
* @param items - Array of SVG files (src: SVG, dest:PNG) to convert.
|
||||
*/
|
||||
export const svg2png = (items: Src2Dest[]): void => {
|
||||
for (const item of items) {
|
||||
fs.mkdirSync(path.dirname(item.dest), { recursive: true });
|
||||
|
||||
sharp(item.src)
|
||||
.png({
|
||||
force: true,
|
||||
compressionLevel: 9,
|
||||
palette: true
|
||||
})
|
||||
.toFile(item.dest)
|
||||
.then((info) => {
|
||||
console.log(`[svg2png] created ${item.dest} -- bytes: ${info.size}, w:${info.width}px, h:${info.height}px`);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`ERROR: ${item.dest} -- ${error}`);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Optimize SVG images for WEB.
|
||||
*
|
||||
* @param items - Array of SVG files (src:SVG, dest:SVG) to optimize.
|
||||
* @param svgo_opts - Options passed to svgo.
|
||||
*/
|
||||
export const svg2svg = (items: Src2Dest[], svgo_opts: Config): void => {
|
||||
for (const item of items) {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(item.dest), { recursive: true });
|
||||
|
||||
const raw = fs.readFileSync(item.src, "utf8");
|
||||
const opt = svgo(raw, svgo_opts);
|
||||
|
||||
fs.writeFileSync(item.dest, opt.data);
|
||||
console.log(`[svg2svg] optimized: ${item.dest} -- src: ${item.src}`);
|
||||
} catch (error) {
|
||||
console.error(`ERROR: optimize src: ${item.src} -- ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,6 @@
|
||||
{{--
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
This is a EDGE https://edgejs.dev/ template to generate a HTML Jinja template
|
||||
for the backend. Example output of this EDGE template:
|
||||
- https://github.com/searxng/searxng/blob/master/searx/templates/simple/icons.html
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Edge } from "edge.js";
|
||||
import { optimize as svgo } from "svgo";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const __jinja_class_placeholder__ = "__jinja_class_placeholder__";
|
||||
|
||||
// -- types
|
||||
|
||||
/**
|
||||
* @typedef {object} IconSet - A set of icons
|
||||
* @property {object} set - Object of SVG icons, where property name is the
|
||||
* name of the icon and value is the src of the SVG (relative to base).
|
||||
* @property {string} base - Folder in which the SVG src files are located.
|
||||
* @property {import("svgo").Config} svgo_opts - svgo options for this set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} IconSVG - Mapping of icon name to SVG source file.
|
||||
* @property {string} name - Name of the icon isource file.
|
||||
* @property {string} src - Name of the destination file.
|
||||
* @property {import("svgo").Config} svgo_opts - Options passed to svgo.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} JinjaMacro - Arguments to create a jinja macro
|
||||
* @property {string} name - Name of the jinja macro.
|
||||
* @property {string} class - SVG's class name (value of XML class attribute)
|
||||
*/
|
||||
|
||||
// -- functions
|
||||
|
||||
/**
|
||||
* Generate a jinja template with a catalog of SVG icons that can be
|
||||
* used in in other HTML jinja templates.
|
||||
*
|
||||
* @param {string} dest - filename of the generate jinja template.
|
||||
* @param {JinjaMacro} macros - Jinja macros to create.
|
||||
* @param {IconSVG[]} items - Array of SVG items.
|
||||
*/
|
||||
|
||||
function jinja_svg_catalog(dest, macros, items) {
|
||||
const svg_catalog = {};
|
||||
const edge_template = resolve(__dirname, "jinja_svg_catalog.html.edge");
|
||||
|
||||
items.forEach((item) => {
|
||||
/** @type {import("svgo").Config} */
|
||||
// JSON.stringify & JSON.parse are used to create a deep copy of the
|
||||
// item.svgo_opts object
|
||||
const svgo_opts = JSON.parse(JSON.stringify(item.svgo_opts));
|
||||
svgo_opts.plugins.push({
|
||||
name: "addClassesToSVGElement",
|
||||
params: {
|
||||
classNames: [__jinja_class_placeholder__]
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(item.src, "utf8");
|
||||
const opt = svgo(raw, svgo_opts);
|
||||
svg_catalog[item.name] = opt.data;
|
||||
} catch (err) {
|
||||
console.error(`ERROR: jinja_svg_catalog processing ${item.name} src: ${item.src} -- ${err}`);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
fs.mkdir(dirname(dest), { recursive: true }, (err) => {
|
||||
if (err) throw err;
|
||||
});
|
||||
|
||||
const ctx = {
|
||||
svg_catalog: svg_catalog,
|
||||
macros: macros,
|
||||
edge_template: edge_template,
|
||||
__jinja_class_placeholder__: __jinja_class_placeholder__,
|
||||
// see https://github.com/edge-js/edge/issues/162
|
||||
open_curly_brace: "{{",
|
||||
close_curly_brace: "}}"
|
||||
};
|
||||
|
||||
const jinjatmpl = Edge.create().renderRawSync(fs.readFileSync(edge_template, "utf-8"), ctx);
|
||||
|
||||
fs.writeFileSync(dest, jinjatmpl);
|
||||
console.log(`[jinja_svg_catalog] created: ${dest}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls jinja_svg_catalog for a collection of icon sets where each set has its
|
||||
* own parameters.
|
||||
*
|
||||
* @param {string} dest - filename of the generate jinja template.
|
||||
* @param {JinjaMacro} macros - Jinja macros to create.
|
||||
* @param {IconSet[]} sets - Array of SVG sets.
|
||||
*/
|
||||
function jinja_svg_sets(dest, macros, sets) {
|
||||
/** @type IconSVG[] */
|
||||
const items = [];
|
||||
const all = [];
|
||||
for (const obj of sets) {
|
||||
for (const [name, file] of Object.entries(obj.set)) {
|
||||
if (all.includes(name)) {
|
||||
throw new Error(`ERROR: ${name} has already been defined`);
|
||||
}
|
||||
items.push({
|
||||
name: name,
|
||||
src: resolve(obj.base, file),
|
||||
svgo_opts: obj.svgo_opts
|
||||
});
|
||||
}
|
||||
jinja_svg_catalog(dest, macros, items);
|
||||
}
|
||||
}
|
||||
|
||||
// -- exports
|
||||
|
||||
export { jinja_svg_sets, jinja_svg_catalog };
|
||||
118
client/simple/tools/jinja_svg_catalog.ts
Normal file
118
client/simple/tools/jinja_svg_catalog.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import fs from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { Edge } from "edge.js";
|
||||
import { type Config as SvgoConfig, optimize as svgo } from "svgo";
|
||||
|
||||
const __jinja_class_placeholder__ = "__jinja_class_placeholder__";
|
||||
|
||||
// A set of icons
|
||||
export type IconSet = {
|
||||
// Object of SVG icons, where property name is the name of the icon and value is the src of the SVG (relative to base)
|
||||
set: Record<string, string>;
|
||||
// Folder in which the SVG src files are located
|
||||
base: string;
|
||||
// svgo options for this set
|
||||
svgo_opts: SvgoConfig;
|
||||
};
|
||||
|
||||
// Mapping of icon name to SVG source file
|
||||
type IconSVG = {
|
||||
// Name of the icon isource file
|
||||
name: string;
|
||||
// Name of the destination file
|
||||
src: string;
|
||||
// Options passed to svgo
|
||||
svgo_opts: SvgoConfig;
|
||||
};
|
||||
|
||||
// Arguments to create a jinja macro
|
||||
export type JinjaMacro = {
|
||||
// Name of the jinja macro
|
||||
name: string;
|
||||
// SVG's class name (value of XML class attribute)
|
||||
class: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a jinja template with a catalog of SVG icons that can be
|
||||
* used in other HTML jinja templates.
|
||||
*
|
||||
* @param dest - filename of the generate jinja template.
|
||||
* @param macros - Jinja macros to create.
|
||||
* @param items - Array of SVG items.
|
||||
*/
|
||||
export const jinja_svg_catalog = (dest: string, macros: JinjaMacro[], items: IconSVG[]): void => {
|
||||
const svg_catalog: Record<string, string> = {};
|
||||
const edge_template = resolve(import.meta.dirname, "jinja_svg_catalog.html.edge");
|
||||
|
||||
for (const item of items) {
|
||||
// JSON.stringify & JSON.parse are used to create a deep copy of the item.svgo_opts object
|
||||
const svgo_opts: SvgoConfig = JSON.parse(JSON.stringify(item.svgo_opts));
|
||||
|
||||
svgo_opts.plugins?.push({
|
||||
name: "addClassesToSVGElement",
|
||||
params: {
|
||||
classNames: [__jinja_class_placeholder__]
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(item.src, "utf8");
|
||||
const opt = svgo(raw, svgo_opts);
|
||||
|
||||
svg_catalog[item.name] = opt.data;
|
||||
} catch (error) {
|
||||
console.error(`ERROR: jinja_svg_catalog processing ${item.name} src: ${item.src} -- ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
fs.mkdirSync(dirname(dest), { recursive: true });
|
||||
|
||||
const ctx = {
|
||||
svg_catalog: svg_catalog,
|
||||
macros: macros,
|
||||
edge_template: edge_template,
|
||||
__jinja_class_placeholder__: __jinja_class_placeholder__,
|
||||
// see https://github.com/edge-js/edge/issues/162
|
||||
open_curly_brace: "{{",
|
||||
close_curly_brace: "}}"
|
||||
};
|
||||
|
||||
const jinjatmpl = Edge.create().renderRawSync(fs.readFileSync(edge_template, "utf-8"), ctx);
|
||||
|
||||
fs.writeFileSync(dest, jinjatmpl);
|
||||
console.log(`[jinja_svg_catalog] created: ${dest}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calls jinja_svg_catalog for a collection of icon sets where each set has its
|
||||
* own parameters.
|
||||
*
|
||||
* @param dest - filename of the generate jinja template.
|
||||
* @param macros - Jinja macros to create.
|
||||
* @param sets - Array of SVG sets.
|
||||
*/
|
||||
export const jinja_svg_sets = (dest: string, macros: JinjaMacro[], sets: IconSet[]): void => {
|
||||
const items: IconSVG[] = [];
|
||||
const all: string[] = [];
|
||||
|
||||
for (const obj of sets) {
|
||||
for (const [name, file] of Object.entries(obj.set)) {
|
||||
if (all.includes(name)) {
|
||||
throw new Error(`ERROR: ${name} has already been defined`);
|
||||
}
|
||||
|
||||
all.push(name);
|
||||
items.push({
|
||||
name: name,
|
||||
src: resolve(obj.base, file),
|
||||
svgo_opts: obj.svgo_opts
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
jinja_svg_catalog(dest, macros, items);
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* Custom vite plugins to build the web-client components of the simple theme.
|
||||
*
|
||||
* HINT:
|
||||
*
|
||||
* This is an inital implementation for the migration of the build process
|
||||
* from grunt to vite. For fully support (vite: build & serve) more work is
|
||||
* needed.
|
||||
*/
|
||||
|
||||
import { svg2png, svg2svg } from "./img.js";
|
||||
|
||||
/**
|
||||
* Vite plugin to convert a list of SVG files to PNG.
|
||||
*
|
||||
* @param {import('./img.js').Src2Dest[]} items - Array of SVG files (src: SVG, dest:PNG) to convert.
|
||||
*/
|
||||
function plg_svg2png(items) {
|
||||
return {
|
||||
name: "searxng-simple-svg2png",
|
||||
apply: "build", // or 'serve'
|
||||
async writeBundle() {
|
||||
await svg2png(items);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vite plugin to optimize SVG images for WEB.
|
||||
*
|
||||
* @param {import('./img.js').Src2Dest[]} items - Array of SVG files (src:SVG, dest:SVG) to optimize.
|
||||
* @param {import('svgo').Config} svgo_opts - Options passed to svgo.
|
||||
*/
|
||||
function plg_svg2svg(items, svgo_opts) {
|
||||
return {
|
||||
name: "searxng-simple-svg2png",
|
||||
apply: "build", // or 'serve'
|
||||
async writeBundle() {
|
||||
await svg2svg(items, svgo_opts);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export { plg_svg2png, plg_svg2svg };
|
||||
45
client/simple/tools/plg.ts
Normal file
45
client/simple/tools/plg.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* Custom vite plugins to build the web-client components of the simple theme.
|
||||
*
|
||||
* HINT:
|
||||
* This is an inital implementation for the migration of the build process
|
||||
* from grunt to vite. For fully support (vite: build & serve) more work is
|
||||
* needed.
|
||||
*/
|
||||
|
||||
import type { Config } from "svgo";
|
||||
import type { Plugin } from "vite";
|
||||
import { type Src2Dest, svg2png, svg2svg } from "./img.ts";
|
||||
|
||||
/**
|
||||
* Vite plugin to convert a list of SVG files to PNG.
|
||||
*
|
||||
* @param items - Array of SVG files (src: SVG, dest:PNG) to convert.
|
||||
*/
|
||||
export const plg_svg2png = (items: Src2Dest[]): Plugin => {
|
||||
return {
|
||||
name: "searxng-simple-svg2png",
|
||||
apply: "build",
|
||||
writeBundle: () => {
|
||||
svg2png(items);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Vite plugin to optimize SVG images for WEB.
|
||||
*
|
||||
* @param items - Array of SVG files (src:SVG, dest:SVG) to optimize.
|
||||
* @param svgo_opts - Options passed to svgo.
|
||||
*/
|
||||
export const plg_svg2svg = (items: Src2Dest[], svgo_opts: Config): Plugin => {
|
||||
return {
|
||||
name: "searxng-simple-svg2svg",
|
||||
apply: "build",
|
||||
writeBundle: () => {
|
||||
svg2svg(items, svgo_opts);
|
||||
}
|
||||
};
|
||||
};
|
||||
40
client/simple/tsconfig.json
Normal file
40
client/simple/tsconfig.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"target": "ES2022",
|
||||
|
||||
"allowImportingTsExtensions": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"incremental": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
"strict": true,
|
||||
"allowUnreachableCode": false,
|
||||
"allowUnusedLabels": false,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
|
||||
"baseUrl": ".",
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["./"],
|
||||
"exclude": ["./node_modules/"]
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
/**
|
||||
* CONFIG: https://vite.dev/config/
|
||||
*/
|
||||
|
||||
import { resolve } from "node:path";
|
||||
import { defineConfig } from "vite";
|
||||
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||
import { plg_svg2png, plg_svg2svg } from "./tools/plg.js";
|
||||
|
||||
const ROOT = "../.."; // root of the git reposetory
|
||||
|
||||
const PATH = {
|
||||
dist: resolve(ROOT, "searx/static/themes/simple"),
|
||||
// dist: resolve(ROOT, "client/simple/dist"),
|
||||
|
||||
src: "src",
|
||||
modules: "node_modules",
|
||||
brand: "src/brand",
|
||||
static: resolve(ROOT, "client/simple/static"),
|
||||
leaflet: resolve(ROOT, "client/simple/node_modules/leaflet/dist"),
|
||||
templates: resolve(ROOT, "searx/templates/simple")
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import('svgo').Config}
|
||||
*/
|
||||
const svg2svg_opts = {
|
||||
plugins: [{ name: "preset-default" }, "sortAttrs", "convertStyleToAttrs"]
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import('svgo').Config}
|
||||
*/
|
||||
const svg2svg_favicon_opts = {
|
||||
plugins: [{ name: "preset-default" }, "sortAttrs"]
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
root: PATH.src,
|
||||
mode: "production",
|
||||
// mode: "development",
|
||||
|
||||
// FIXME: missing CCS sourcemaps!!
|
||||
// see: https://github.com/vitejs/vite/discussions/13845#discussioncomment-11992084
|
||||
//
|
||||
// what I have tried so far (see config below):
|
||||
//
|
||||
// - build.sourcemap
|
||||
// - esbuild.sourcemap
|
||||
// - css.preprocessorOptions.less.sourceMap
|
||||
css: {
|
||||
devSourcemap: true
|
||||
}, // end: css
|
||||
|
||||
build: {
|
||||
target: ["chrome87", "edge88", "firefox78", "safari14"],
|
||||
manifest: "manifest.json",
|
||||
emptyOutDir: true,
|
||||
assetsDir: "",
|
||||
outDir: PATH.dist,
|
||||
|
||||
sourcemap: true,
|
||||
minify: "esbuild",
|
||||
cssMinify: "esbuild",
|
||||
|
||||
rollupOptions: {
|
||||
input: {
|
||||
// build CSS files
|
||||
"css/searxng.min.css": `${PATH.src}/less/style-ltr.less`,
|
||||
"css/searxng-rtl.min.css": `${PATH.src}/less/style-rtl.less`,
|
||||
"css/rss.min.css": `${PATH.src}/less/rss.less`,
|
||||
|
||||
// build JS files
|
||||
"js/searxng.head.min": `${PATH.src}/js/searxng.head.js`,
|
||||
"js/searxng.min": `${PATH.src}/js/searxng.js`
|
||||
},
|
||||
|
||||
// file naming conventions / pathnames are relative to outDir (PATH.dist)
|
||||
output: {
|
||||
entryFileNames: "[name].js",
|
||||
chunkFileNames: "[name].js",
|
||||
assetFileNames: "[name].[ext]"
|
||||
// Vite does not support "rollupOptions.output.sourcemap".
|
||||
// Please use "build.sourcemap" instead.
|
||||
// sourcemap: true,
|
||||
}
|
||||
}
|
||||
}, // end: build
|
||||
|
||||
plugins: [
|
||||
// Leaflet
|
||||
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{ src: `${PATH.leaflet}/leaflet.{js,js.map}`, dest: `${PATH.dist}/js` },
|
||||
{ src: `${PATH.leaflet}/images/*.png`, dest: `${PATH.dist}/css/images/` },
|
||||
{ src: `${PATH.leaflet}/*.{css,css.map}`, dest: `${PATH.dist}/css` },
|
||||
{ src: `${PATH.static}/**/*`, dest: PATH.dist }
|
||||
]
|
||||
}),
|
||||
|
||||
// -- svg images
|
||||
|
||||
plg_svg2svg(
|
||||
[
|
||||
{ src: `${PATH.src}/svg/empty_favicon.svg`, dest: `${PATH.dist}/img/empty_favicon.svg` },
|
||||
{ src: `${PATH.src}/svg/select-dark.svg`, dest: `${PATH.dist}/img/select-dark.svg` },
|
||||
{ src: `${PATH.src}/svg/select-light.svg`, dest: `${PATH.dist}/img/select-light.svg` }
|
||||
],
|
||||
svg2svg_opts
|
||||
),
|
||||
|
||||
// SearXNG brand (static)
|
||||
|
||||
plg_svg2png([
|
||||
{ src: `${PATH.brand}/searxng-wordmark.svg`, dest: `${PATH.dist}/img/favicon.png` },
|
||||
{ src: `${PATH.brand}/searxng.svg`, dest: `${PATH.dist}/img/searxng.png` }
|
||||
]),
|
||||
|
||||
// -- svg
|
||||
plg_svg2svg(
|
||||
[
|
||||
{ src: `${PATH.brand}/searxng.svg`, dest: `${PATH.dist}/img/searxng.svg` },
|
||||
{ src: `${PATH.brand}/img_load_error.svg`, dest: `${PATH.dist}/img/img_load_error.svg` }
|
||||
],
|
||||
svg2svg_opts
|
||||
),
|
||||
|
||||
// -- favicon
|
||||
plg_svg2svg(
|
||||
[{ src: `${PATH.brand}/searxng-wordmark.svg`, dest: `${PATH.dist}/img/favicon.svg` }],
|
||||
svg2svg_favicon_opts
|
||||
),
|
||||
|
||||
// -- simple templates
|
||||
plg_svg2svg(
|
||||
[{ src: `${PATH.brand}/searxng-wordmark.svg`, dest: `${PATH.templates}/searxng-wordmark.min.svg` }],
|
||||
svg2svg_opts
|
||||
)
|
||||
] // end: plugins
|
||||
});
|
||||
168
client/simple/vite.config.ts
Normal file
168
client/simple/vite.config.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
/**
|
||||
* CONFIG: https://vite.dev/config/
|
||||
*/
|
||||
|
||||
import { resolve } from "node:path";
|
||||
import { constants as zlibConstants } from "node:zlib";
|
||||
import browserslistToEsbuild from "browserslist-to-esbuild";
|
||||
import { browserslistToTargets } from "lightningcss";
|
||||
import type { PreRenderedAsset } from "rolldown";
|
||||
import type { Config } from "svgo";
|
||||
import type { UserConfig } from "vite";
|
||||
import analyzer from "vite-bundle-analyzer";
|
||||
import manifest from "./package.json" with { type: "json" };
|
||||
import { plg_svg2png, plg_svg2svg } from "./tools/plg.ts";
|
||||
|
||||
const ROOT = "../../"; // root of the git repository
|
||||
|
||||
const PATH = {
|
||||
brand: "src/brand/",
|
||||
dist: resolve(ROOT, "searx/static/themes/simple/"),
|
||||
modules: "node_modules/",
|
||||
src: "src/",
|
||||
templates: resolve(ROOT, "searx/templates/simple/")
|
||||
} as const;
|
||||
|
||||
const svg2svg_opts: Config = {
|
||||
plugins: [{ name: "preset-default" }, "sortAttrs", "convertStyleToAttrs"]
|
||||
};
|
||||
|
||||
const svg2svg_favicon_opts: Config = {
|
||||
plugins: [{ name: "preset-default" }, "sortAttrs"]
|
||||
};
|
||||
|
||||
export default {
|
||||
base: "/static/themes/simple/",
|
||||
publicDir: "static/",
|
||||
mode: "production",
|
||||
// mode: "development",
|
||||
|
||||
build: {
|
||||
target: browserslistToEsbuild(manifest.browserslist),
|
||||
cssTarget: browserslistToEsbuild(manifest.browserslist),
|
||||
manifest: "manifest.json",
|
||||
emptyOutDir: true,
|
||||
assetsDir: "",
|
||||
outDir: PATH.dist,
|
||||
|
||||
sourcemap: true,
|
||||
|
||||
rollupOptions: {
|
||||
input: {
|
||||
// build CSS files
|
||||
"searxng-ltr.css": `${PATH.src}/less/style-ltr.less`,
|
||||
"searxng-rtl.css": `${PATH.src}/less/style-rtl.less`,
|
||||
"rss.css": `${PATH.src}/less/rss.less`,
|
||||
|
||||
// build script files
|
||||
"searxng.core": `${PATH.src}/js/core/index.ts`,
|
||||
|
||||
// ol
|
||||
ol: `${PATH.src}/js/pkg/ol.ts`,
|
||||
"ol.css": `${PATH.modules}/ol/ol.css`
|
||||
},
|
||||
|
||||
// file naming conventions / pathnames are relative to outDir (PATH.dist)
|
||||
output: {
|
||||
entryFileNames: "js/[name].min.js",
|
||||
chunkFileNames: "js/[name].min.js",
|
||||
assetFileNames: ({ names }: PreRenderedAsset): string => {
|
||||
const [name] = names;
|
||||
|
||||
const extension = name?.split(".").pop();
|
||||
switch (extension) {
|
||||
case "css":
|
||||
return "css/[name].min[extname]";
|
||||
case "js":
|
||||
return "js/[name].min[extname]";
|
||||
case "png":
|
||||
case "svg":
|
||||
return "img/[name][extname]";
|
||||
default:
|
||||
console.warn("Unknown asset:", name);
|
||||
return "[name][extname]";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, // end: build
|
||||
|
||||
plugins: [
|
||||
// -- bundle analyzer
|
||||
analyzer({
|
||||
enabled: process.env.VITE_BUNDLE_ANALYZE === "true",
|
||||
analyzerPort: "auto",
|
||||
summary: true,
|
||||
reportTitle: manifest.name,
|
||||
|
||||
// sidecars with max compression
|
||||
gzipOptions: {
|
||||
level: zlibConstants.Z_BEST_COMPRESSION
|
||||
},
|
||||
brotliOptions: {
|
||||
params: {
|
||||
[zlibConstants.BROTLI_PARAM_QUALITY]: zlibConstants.BROTLI_MAX_QUALITY
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
// -- svg images
|
||||
plg_svg2svg(
|
||||
[
|
||||
{ src: `${PATH.src}/svg/empty_favicon.svg`, dest: `${PATH.dist}/img/empty_favicon.svg` },
|
||||
{ src: `${PATH.src}/svg/select-dark.svg`, dest: `${PATH.dist}/img/select-dark.svg` },
|
||||
{ src: `${PATH.src}/svg/select-light.svg`, dest: `${PATH.dist}/img/select-light.svg` }
|
||||
],
|
||||
svg2svg_opts
|
||||
),
|
||||
|
||||
// SearXNG brand (static)
|
||||
plg_svg2png([
|
||||
{ src: `${PATH.brand}/searxng-wordmark.svg`, dest: `${PATH.dist}/img/favicon.png` },
|
||||
{ src: `${PATH.brand}/searxng.svg`, dest: `${PATH.dist}/img/searxng.png` }
|
||||
]),
|
||||
|
||||
// -- svg
|
||||
plg_svg2svg(
|
||||
[
|
||||
{ src: `${PATH.brand}/searxng.svg`, dest: `${PATH.dist}/img/searxng.svg` },
|
||||
{ src: `${PATH.brand}/img_load_error.svg`, dest: `${PATH.dist}/img/img_load_error.svg` }
|
||||
],
|
||||
svg2svg_opts
|
||||
),
|
||||
|
||||
// -- favicon
|
||||
plg_svg2svg(
|
||||
[{ src: `${PATH.brand}/searxng-wordmark.svg`, dest: `${PATH.dist}/img/favicon.svg` }],
|
||||
svg2svg_favicon_opts
|
||||
),
|
||||
|
||||
// -- simple templates
|
||||
plg_svg2svg(
|
||||
[{ src: `${PATH.brand}/searxng-wordmark.svg`, dest: `${PATH.templates}/searxng-wordmark.min.svg` }],
|
||||
svg2svg_opts
|
||||
)
|
||||
], // end: plugins
|
||||
|
||||
// FIXME: missing CCS sourcemaps!!
|
||||
// see: https://github.com/vitejs/vite/discussions/13845#discussioncomment-11992084
|
||||
//
|
||||
// what I have tried so far (see config below):
|
||||
//
|
||||
// - build.sourcemap
|
||||
// - esbuild.sourcemap
|
||||
// - css.preprocessorOptions.less.sourceMap
|
||||
css: {
|
||||
transformer: "lightningcss",
|
||||
lightningcss: {
|
||||
targets: browserslistToTargets(manifest.browserslist)
|
||||
},
|
||||
devSourcemap: true
|
||||
}, // end: css
|
||||
|
||||
experimental: {
|
||||
enableNativePlugin: true
|
||||
} // end: experimental
|
||||
} satisfies UserConfig;
|
||||
@@ -1,7 +1,7 @@
|
||||
contents:
|
||||
repositories:
|
||||
- https://mirrors.edge.kernel.org/alpine/edge/main
|
||||
- https://mirrors.edge.kernel.org/alpine/edge/community
|
||||
- https://dl-cdn.alpinelinux.org/alpine/edge/main
|
||||
- https://dl-cdn.alpinelinux.org/alpine/edge/community
|
||||
packages:
|
||||
- alpine-base
|
||||
- build-base
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
contents:
|
||||
repositories:
|
||||
- https://mirrors.edge.kernel.org/alpine/edge/main
|
||||
- https://dl-cdn.alpinelinux.org/alpine/edge/main
|
||||
packages:
|
||||
- alpine-baselayout
|
||||
- ca-certificates-bundle
|
||||
|
||||
@@ -103,8 +103,8 @@ Node.js environment (``make node.env``)
|
||||
|
||||
Node.js_ version {{version.node}} or higher is required to build the themes.
|
||||
If the requirement is not met, the build chain uses nvm_ (Node Version
|
||||
Manager) to install latest LTS of Node.js_ locally: there is no need to
|
||||
install nvm_ or npm_ on your system.
|
||||
Manager) to install Node.js_ locally: there is no need to install
|
||||
nvm_ or npm_ on your system.
|
||||
|
||||
To install NVM_ and Node.js_ in once you can use :ref:`make nvm.nodejs`.
|
||||
|
||||
@@ -150,7 +150,7 @@ setup.
|
||||
``make nvm.nodejs``
|
||||
-------------------
|
||||
|
||||
Install latest Node.js_ LTS locally (uses nvm_)::
|
||||
Install latest Node.js_ locally (uses nvm_)::
|
||||
|
||||
$ make nvm.nodejs
|
||||
INFO: install (update) NVM at /share/searxng/.nvm
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "searxng.org/devtools",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"pyright": "^1.1.403"
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ cov-core==1.15.0
|
||||
black==24.3.0
|
||||
pylint==3.3.7
|
||||
splinter==0.21.0
|
||||
selenium==4.34.2
|
||||
selenium==4.35.0
|
||||
Pallets-Sphinx-Themes==2.3.0
|
||||
Sphinx==7.4.7
|
||||
sphinx-issues==5.0.1
|
||||
|
||||
@@ -12,7 +12,7 @@ httpx-socks[asyncio]==0.10.0
|
||||
Brotli==1.1.0
|
||||
uvloop==0.21.0
|
||||
setproctitle==1.3.6
|
||||
valkey==6.1.0
|
||||
valkey==6.1.1
|
||||
markdown-it-py==3.0.0
|
||||
fasttext-predict==0.9.2.4
|
||||
tomli==2.2.1; python_version < '3.11'
|
||||
|
||||
2
searx/static/themes/simple/.gitattributes
vendored
2
searx/static/themes/simple/.gitattributes
vendored
@@ -1,2 +0,0 @@
|
||||
leaflet.css -diff
|
||||
leaflet.js -diff
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 696 B |
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 618 B |
@@ -1,661 +0,0 @@
|
||||
/* required styles */
|
||||
|
||||
.leaflet-pane,
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-tile-container,
|
||||
.leaflet-pane > svg,
|
||||
.leaflet-pane > canvas,
|
||||
.leaflet-zoom-box,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
/* Prevents IE11 from highlighting tiles in blue */
|
||||
.leaflet-tile::selection {
|
||||
background: transparent;
|
||||
}
|
||||
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||
.leaflet-safari .leaflet-tile {
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
}
|
||||
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||
.leaflet-safari .leaflet-tile-container {
|
||||
width: 1600px;
|
||||
height: 1600px;
|
||||
-webkit-transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
display: block;
|
||||
}
|
||||
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||
.leaflet-container .leaflet-overlay-pane svg {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
.leaflet-container .leaflet-marker-pane img,
|
||||
.leaflet-container .leaflet-shadow-pane img,
|
||||
.leaflet-container .leaflet-tile-pane img,
|
||||
.leaflet-container img.leaflet-image-layer,
|
||||
.leaflet-container .leaflet-tile {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.leaflet-container img.leaflet-tile {
|
||||
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
||||
mix-blend-mode: plus-lighter;
|
||||
}
|
||||
|
||||
.leaflet-container.leaflet-touch-zoom {
|
||||
-ms-touch-action: pan-x pan-y;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag {
|
||||
-ms-touch-action: pinch-zoom;
|
||||
/* Fallback for FF which doesn't support pinch-zoom */
|
||||
touch-action: none;
|
||||
touch-action: pinch-zoom;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.leaflet-container {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.leaflet-container a {
|
||||
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||
}
|
||||
.leaflet-tile {
|
||||
filter: inherit;
|
||||
visibility: hidden;
|
||||
}
|
||||
.leaflet-tile-loaded {
|
||||
visibility: inherit;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
width: 0;
|
||||
height: 0;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
z-index: 800;
|
||||
}
|
||||
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||
.leaflet-overlay-pane svg {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.leaflet-pane { z-index: 400; }
|
||||
|
||||
.leaflet-tile-pane { z-index: 200; }
|
||||
.leaflet-overlay-pane { z-index: 400; }
|
||||
.leaflet-shadow-pane { z-index: 500; }
|
||||
.leaflet-marker-pane { z-index: 600; }
|
||||
.leaflet-tooltip-pane { z-index: 650; }
|
||||
.leaflet-popup-pane { z-index: 700; }
|
||||
|
||||
.leaflet-map-pane canvas { z-index: 100; }
|
||||
.leaflet-map-pane svg { z-index: 200; }
|
||||
|
||||
.leaflet-vml-shape {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
.lvml {
|
||||
behavior: url(#default#VML);
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
||||
/* control positioning */
|
||||
|
||||
.leaflet-control {
|
||||
position: relative;
|
||||
z-index: 800;
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-top {
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-right {
|
||||
right: 0;
|
||||
}
|
||||
.leaflet-bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
.leaflet-left {
|
||||
left: 0;
|
||||
}
|
||||
.leaflet-control {
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
float: right;
|
||||
}
|
||||
.leaflet-top .leaflet-control {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.leaflet-left .leaflet-control {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* zoom and fade animations */
|
||||
|
||||
.leaflet-fade-anim .leaflet-popup {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
-moz-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||
opacity: 1;
|
||||
}
|
||||
.leaflet-zoom-animated {
|
||||
-webkit-transform-origin: 0 0;
|
||||
-ms-transform-origin: 0 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
svg.leaflet-zoom-animated {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-tile,
|
||||
.leaflet-pan-anim .leaflet-tile {
|
||||
-webkit-transition: none;
|
||||
-moz-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* cursors */
|
||||
|
||||
.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
.leaflet-grab {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
.leaflet-crosshair,
|
||||
.leaflet-crosshair .leaflet-interactive {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.leaflet-popup-pane,
|
||||
.leaflet-control {
|
||||
cursor: auto;
|
||||
}
|
||||
.leaflet-dragging .leaflet-grab,
|
||||
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||
.leaflet-dragging .leaflet-marker-draggable {
|
||||
cursor: move;
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* marker & overlays interactivity */
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-pane > svg path,
|
||||
.leaflet-tile-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.leaflet-marker-icon.leaflet-interactive,
|
||||
.leaflet-image-layer.leaflet-interactive,
|
||||
.leaflet-pane > svg path.leaflet-interactive,
|
||||
svg.leaflet-image-layer.leaflet-interactive path {
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* visual tweaks */
|
||||
|
||||
.leaflet-container {
|
||||
background: #ddd;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.leaflet-container a {
|
||||
color: #0078A8;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
border: 2px dotted #38f;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
|
||||
/* general typography */
|
||||
.leaflet-container {
|
||||
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
/* general toolbar styles */
|
||||
|
||||
.leaflet-bar {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-control-layers-toggle {
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
}
|
||||
.leaflet-bar a:hover,
|
||||
.leaflet-bar a:focus {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
.leaflet-bar a:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: none;
|
||||
}
|
||||
.leaflet-bar a.leaflet-disabled {
|
||||
cursor: default;
|
||||
background-color: #f4f4f4;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-bar a {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:first-child {
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
/* zoom control */
|
||||
|
||||
.leaflet-control-zoom-in,
|
||||
.leaflet-control-zoom-out {
|
||||
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||
text-indent: 1px;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
|
||||
/* layers control */
|
||||
|
||||
.leaflet-control-layers {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers.png);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.leaflet-retina .leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers-2x.png);
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers-toggle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.leaflet-control-layers .leaflet-control-layers-list,
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||
display: none;
|
||||
}
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.leaflet-control-layers-expanded {
|
||||
padding: 6px 10px 6px 6px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
.leaflet-control-layers-scrollbar {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.leaflet-control-layers-selector {
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-size: 1.08333em;
|
||||
}
|
||||
.leaflet-control-layers-separator {
|
||||
height: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
margin: 5px -10px 5px -6px;
|
||||
}
|
||||
|
||||
/* Default icon URLs */
|
||||
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||
background-image: url(images/marker-icon.png);
|
||||
}
|
||||
|
||||
|
||||
/* attribution and scale controls */
|
||||
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
margin: 0;
|
||||
}
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-scale-line {
|
||||
padding: 0 5px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.leaflet-control-attribution a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.leaflet-control-attribution a:hover,
|
||||
.leaflet-control-attribution a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.leaflet-attribution-flag {
|
||||
display: inline !important;
|
||||
vertical-align: baseline !important;
|
||||
width: 1em;
|
||||
height: 0.6669em;
|
||||
}
|
||||
.leaflet-left .leaflet-control-scale {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control-scale {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.leaflet-control-scale-line {
|
||||
border: 2px solid #777;
|
||||
border-top: none;
|
||||
line-height: 1.1;
|
||||
padding: 2px 5px 1px;
|
||||
white-space: nowrap;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
text-shadow: 1px 1px #fff;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child) {
|
||||
border-top: 2px solid #777;
|
||||
border-bottom: none;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||
border-bottom: 2px solid #777;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-attribution,
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
box-shadow: none;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
|
||||
/* popup */
|
||||
|
||||
.leaflet-popup {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.leaflet-popup-content-wrapper {
|
||||
padding: 1px;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.leaflet-popup-content {
|
||||
margin: 13px 24px 13px 20px;
|
||||
line-height: 1.3;
|
||||
font-size: 13px;
|
||||
font-size: 1.08333em;
|
||||
min-height: 1px;
|
||||
}
|
||||
.leaflet-popup-content p {
|
||||
margin: 17px 0;
|
||||
margin: 1.3em 0;
|
||||
}
|
||||
.leaflet-popup-tip-container {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-top: -1px;
|
||||
margin-left: -20px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-popup-tip {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
padding: 1px;
|
||||
|
||||
margin: -10px auto 0;
|
||||
pointer-events: auto;
|
||||
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
background: white;
|
||||
color: #333;
|
||||
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border: none;
|
||||
text-align: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font: 16px/24px Tahoma, Verdana, sans-serif;
|
||||
color: #757575;
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button:hover,
|
||||
.leaflet-container a.leaflet-popup-close-button:focus {
|
||||
color: #585858;
|
||||
}
|
||||
.leaflet-popup-scrolled {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||
-ms-zoom: 1;
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
width: 24px;
|
||||
margin: 0 auto;
|
||||
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-control-zoom,
|
||||
.leaflet-oldie .leaflet-control-layers,
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
|
||||
/* div icon */
|
||||
|
||||
.leaflet-div-icon {
|
||||
background: #fff;
|
||||
border: 1px solid #666;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip */
|
||||
/* Base styles for the element that has a tooltip */
|
||||
.leaflet-tooltip {
|
||||
position: absolute;
|
||||
padding: 6px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 3px;
|
||||
color: #222;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-tooltip.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-tooltip-top:before,
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border: 6px solid transparent;
|
||||
background: transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* Directions */
|
||||
|
||||
.leaflet-tooltip-bottom {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.leaflet-tooltip-top {
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-top:before {
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-top:before {
|
||||
bottom: 0;
|
||||
margin-bottom: -12px;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before {
|
||||
top: 0;
|
||||
margin-top: -12px;
|
||||
margin-left: -6px;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-left {
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-right {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
top: 50%;
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before {
|
||||
right: 0;
|
||||
margin-right: -12px;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-right:before {
|
||||
left: 0;
|
||||
margin-left: -12px;
|
||||
border-right-color: #fff;
|
||||
}
|
||||
|
||||
/* Printing */
|
||||
|
||||
@media print {
|
||||
/* Prevent printers from removing background-images of controls. */
|
||||
.leaflet-control {
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
}
|
||||
1
searx/static/themes/simple/css/ol.min.css
vendored
Normal file
1
searx/static/themes/simple/css/ol.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
searx/static/themes/simple/css/rss.min.css
vendored
3
searx/static/themes/simple/css/rss.min.css
vendored
File diff suppressed because one or more lines are too long
2
searx/static/themes/simple/css/searxng-ltr.min.css
vendored
Normal file
2
searx/static/themes/simple/css/searxng-ltr.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
searx/static/themes/simple/js/autocomplete.min.js
vendored
Normal file
2
searx/static/themes/simple/js/autocomplete.min.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import{c as e,d as t,e as n,g as r}from"./searxng.core.min.js";const i=async(i,a)=>{try{let o;o=r.method===`GET`?await t(`GET`,`./autocompleter?q=${a}`):await t(`POST`,`./autocompleter`,{body:new URLSearchParams({q:a})});let s=await o.json(),c=document.querySelector(`.autocomplete`);e(c);let l=document.querySelector(`.autocomplete ul`);if(e(l),c.classList.add(`open`),l.replaceChildren(),s?.[1]?.length===0){let e=Object.assign(document.createElement(`li`),{className:`no-item-found`,textContent:r.translations?.no_item_found??`No results found`});l.append(e);return}let u=new DocumentFragment;for(let e of s[1]){let t=Object.assign(document.createElement(`li`),{textContent:e});n(`mousedown`,t,()=>{i.value=e;let t=document.querySelector(`#search`);t?.submit(),c.classList.remove(`open`)}),u.append(t)}l.append(u)}catch(e){console.error(`Error fetching autocomplete results:`,e)}},a=document.getElementById(`q`);e(a);let o;n(`input`,a,()=>{clearTimeout(o);let e=a.value,t=r.autocomplete_min??2;e.length<t||(o=window.setTimeout(async()=>{e===a.value&&await i(a,e)},300))});const s=document.querySelector(`.autocomplete`),c=document.querySelector(`.autocomplete ul`);c&&n(`keyup`,a,e=>{let t=[...c.children],n=t.findIndex(e=>e.classList.contains(`active`)),r=-1;switch(e.key){case`ArrowUp`:{let e=t[n];e&&n>=0&&e.classList.remove(`active`),r=(n-1+t.length)%t.length;break}case`ArrowDown`:{let e=t[n];e&&n>=0&&e.classList.remove(`active`),r=(n+1)%t.length;break}case`Tab`:case`Enter`:s&&s.classList.remove(`open`);break;default:break}if(r!==-1){let e=t[r];if(e&&(e.classList.add(`active`),!e.classList.contains(`no-item-found`))){let t=document.getElementById(`q`);t&&(t.value=e.textContent??``)}}});
|
||||
//# sourceMappingURL=autocomplete.min.js.map
|
||||
1
searx/static/themes/simple/js/autocomplete.min.js.map
Normal file
1
searx/static/themes/simple/js/autocomplete.min.js.map
Normal file
File diff suppressed because one or more lines are too long
2
searx/static/themes/simple/js/infinite_scroll.min.js
vendored
Normal file
2
searx/static/themes/simple/js/infinite_scroll.min.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import{c as e,d as t,g as n}from"./searxng.core.min.js";const r=()=>Object.assign(document.createElement(`div`),{className:`loader`}),i=async(i,a)=>{let o=document.querySelector(`#search`);e(o);let s=document.querySelector(`#pagination form.next_page`);e(s);let c=o.getAttribute(`action`);if(!c)throw Error(`Form action not defined`);let l=document.querySelector(`#pagination`);e(l),l.replaceChildren(r());try{let e=await t(`POST`,c,{body:new FormData(s)}),n=await e.text();if(!n)return;let r=new DOMParser().parseFromString(n,`text/html`),o=r.querySelectorAll(`#urls article`),l=r.querySelector(`#pagination`);document.querySelector(`#pagination`)?.remove();let u=document.querySelector(`#urls`);if(!u)throw Error(`URLs element not found`);if(o.length>0&&!i&&u.appendChild(document.createElement(`hr`)),u.append(...Array.from(o)),l){let e=document.querySelector(`#results`);e?.appendChild(l),a()}}catch(e){console.error(`Error loading next page:`,e);let t=Object.assign(document.createElement(`div`),{textContent:n.translations?.error_loading_next_page??`Error loading next page`,className:`dialog-error`});t.setAttribute(`role`,`alert`),document.querySelector(`#pagination`)?.replaceChildren(t)}},a=document.getElementById(`results`);if(!a)throw Error(`Results element not found`);const o=a.classList.contains(`only_template_images`),s=`article.result:last-child`,c={rootMargin:`320px`},l=new IntersectionObserver(e=>{let[t]=e;t?.isIntersecting&&(l.unobserve(t.target),i(o,()=>{let e=document.querySelector(s);e&&l.observe(e)}).then(()=>{}))},c),u=document.querySelector(s);u&&l.observe(u);
|
||||
//# sourceMappingURL=infinite_scroll.min.js.map
|
||||
1
searx/static/themes/simple/js/infinite_scroll.min.js.map
Normal file
1
searx/static/themes/simple/js/infinite_scroll.min.js.map
Normal file
File diff suppressed because one or more lines are too long
2
searx/static/themes/simple/js/keyboard.min.js
vendored
Normal file
2
searx/static/themes/simple/js/keyboard.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
searx/static/themes/simple/js/keyboard.min.js.map
Normal file
1
searx/static/themes/simple/js/keyboard.min.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
3
searx/static/themes/simple/js/mapresult.min.js
vendored
Normal file
3
searx/static/themes/simple/js/mapresult.min.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["css/ol.min.css"])))=>i.map(i=>d[i]);
|
||||
import{b as e,e as t}from"./searxng.core.min.js";t(`click`,`.searxng_init_map`,async function(t){t.preventDefault(),this.classList.remove(`searxng_init_map`);let{View:n,OlMap:r,TileLayer:i,VectorLayer:a,OSM:o,VectorSource:s,Style:c,Stroke:l,Fill:u,Circle:d,fromLonLat:f,GeoJSON:p,Feature:m,Point:h}=await e(async()=>{let{View:e,OlMap:t,TileLayer:n,VectorLayer:r,OSM:i,VectorSource:a,Style:o,Stroke:s,Fill:c,Circle:l,fromLonLat:u,GeoJSON:d,Feature:f,Point:p}=await import(`./ol.min.js`);return{View:e,OlMap:t,TileLayer:n,VectorLayer:r,OSM:i,VectorSource:a,Style:o,Stroke:s,Fill:c,Circle:l,fromLonLat:u,GeoJSON:d,Feature:f,Point:p}},[]);e(()=>Promise.resolve({}),__vite__mapDeps([0]));let{leafletTarget:g,mapLon:_,mapLat:v,mapGeojson:y}=this.dataset,b=Number.parseFloat(_||`0`),x=Number.parseFloat(v||`0`),S=new n({maxZoom:16,enableRotation:!1}),C=new r({target:g,layers:[new i({source:new o({maxZoom:16})})],view:S});try{let e=new s({features:[new m({geometry:new h(f([b,x]))})]}),t=new a({source:e,style:new c({image:new d({radius:6,fill:new u({color:`#3050ff`})})})});C.addLayer(t)}catch(e){console.error(`Failed to create marker layer:`,e)}if(y)try{let e=new s({features:new p().readFeatures(JSON.parse(y),{dataProjection:`EPSG:4326`,featureProjection:`EPSG:3857`})}),t=new a({source:e,style:new c({stroke:new l({color:`#3050ff`,width:2}),fill:new u({color:`#3050ff33`})})});C.addLayer(t),S.fit(e.getExtent(),{padding:[20,20,20,20]})}catch(e){console.error(`Failed to create GeoJSON layer:`,e)}});
|
||||
//# sourceMappingURL=mapresult.min.js.map
|
||||
1
searx/static/themes/simple/js/mapresult.min.js.map
Normal file
1
searx/static/themes/simple/js/mapresult.min.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"mappings":";iDAIA,EAAO,QAAS,oBAAqB,eAAmC,EAAc,CACpF,EAAM,iBACN,KAAK,UAAU,OAAO,oBAEtB,GAAM,CACJ,OACA,QACA,YACA,cACA,MACA,eACA,QACA,SACA,OACA,SACA,aACA,UACA,UACA,QACD,sBAfK,CACJ,OACA,QACA,YACA,cACA,MACA,eACA,QACA,SACA,OACA,SACA,aACA,UACA,UACA,QACD,CAAG,MAAM,OAAO,sBAdf,OACA,QACA,YACA,cACA,MACA,eACA,QACA,SACA,OACA,SACA,aACA,UACA,UACA,cAEF,yBAAO,uBAEP,GAAM,CAAE,cAAe,EAAQ,SAAQ,SAAQ,aAAY,CAAG,KAAK,QAE7D,EAAM,OAAO,WAAW,GAAU,KAClC,EAAM,OAAO,WAAW,GAAU,KAClC,EAAO,IAAI,EAAK,CAAE,QAAS,GAAI,eAAgB,GAAO,EACtD,EAAM,IAAI,EAAM,CACZ,SACR,OAAQ,CAAC,IAAI,EAAU,CAAE,OAAQ,IAAI,EAAI,CAAE,QAAS,GAAI,EAAG,EAAE,CACvD,OACP,EAED,GAAI,CACF,IAAM,EAAe,IAAI,EAAa,CACpC,SAAU,CACR,IAAI,EAAQ,CACV,SAAU,IAAI,EAAM,EAAW,CAAC,EAAK,EAAI,GAC1C,EACF,CACF,EAEK,EAAc,IAAI,EAAY,CAClC,OAAQ,EACR,MAAO,IAAI,EAAM,CACf,MAAO,IAAI,EAAO,CAChB,OAAQ,EACR,KAAM,IAAI,EAAK,CAAE,MAAO,UAAW,EACpC,EACF,EACF,EAED,EAAI,SAAS,EACd,OAAQ,EAAO,CACd,QAAQ,MAAM,iCAAkC,EACjD,CAED,GAAI,EACF,GAAI,CACF,IAAM,EAAY,IAAI,EAAa,CACjC,SAAU,IAAI,IAAU,aAAa,KAAK,MAAM,GAAa,CAC3D,eAAgB,YAChB,kBAAmB,YACpB,EACF,EAEK,EAAW,IAAI,EAAY,CAC/B,OAAQ,EACR,MAAO,IAAI,EAAM,CACf,OAAQ,IAAI,EAAO,CAAE,MAAO,UAAW,MAAO,EAAG,EACjD,KAAM,IAAI,EAAK,CAAE,MAAO,YAAa,EACtC,EACF,EAED,EAAI,SAAS,GAEb,EAAK,IAAI,EAAU,YAAa,CAAE,QAAS,CAAC,GAAI,GAAI,GAAI,GAAG,CAAE,CAC9D,OAAQ,EAAO,CACd,QAAQ,MAAM,kCAAmC,EAClD,CAEJ","names":[],"ignoreList":[],"sources":["../../../../../client/simple/src/js/main/mapresult.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\nimport { listen } from \"../core/toolkit.ts\";\n\nlisten(\"click\", \".searxng_init_map\", async function (this: HTMLElement, event: Event) {\n event.preventDefault();\n this.classList.remove(\"searxng_init_map\");\n\n const {\n View,\n OlMap,\n TileLayer,\n VectorLayer,\n OSM,\n VectorSource,\n Style,\n Stroke,\n Fill,\n Circle,\n fromLonLat,\n GeoJSON,\n Feature,\n Point\n } = await import(\"../pkg/ol.ts\");\n import(\"ol/ol.css\");\n\n const { leafletTarget: target, mapLon, mapLat, mapGeojson } = this.dataset;\n\n const lon = Number.parseFloat(mapLon || \"0\");\n const lat = Number.parseFloat(mapLat || \"0\");\n const view = new View({ maxZoom: 16, enableRotation: false });\n const map = new OlMap({\n target: target,\n layers: [new TileLayer({ source: new OSM({ maxZoom: 16 }) })],\n view: view\n });\n\n try {\n const markerSource = new VectorSource({\n features: [\n new Feature({\n geometry: new Point(fromLonLat([lon, lat]))\n })\n ]\n });\n\n const markerLayer = new VectorLayer({\n source: markerSource,\n style: new Style({\n image: new Circle({\n radius: 6,\n fill: new Fill({ color: \"#3050ff\" })\n })\n })\n });\n\n map.addLayer(markerLayer);\n } catch (error) {\n console.error(\"Failed to create marker layer:\", error);\n }\n\n if (mapGeojson) {\n try {\n const geoSource = new VectorSource({\n features: new GeoJSON().readFeatures(JSON.parse(mapGeojson), {\n dataProjection: \"EPSG:4326\",\n featureProjection: \"EPSG:3857\"\n })\n });\n\n const geoLayer = new VectorLayer({\n source: geoSource,\n style: new Style({\n stroke: new Stroke({ color: \"#3050ff\", width: 2 }),\n fill: new Fill({ color: \"#3050ff33\" })\n })\n });\n\n map.addLayer(geoLayer);\n\n view.fit(geoSource.getExtent(), { padding: [20, 20, 20, 20] });\n } catch (error) {\n console.error(\"Failed to create GeoJSON layer:\", error);\n }\n }\n});\n"],"file":"js/mapresult.min.js"}
|
||||
8
searx/static/themes/simple/js/ol.min.js
vendored
Normal file
8
searx/static/themes/simple/js/ol.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
searx/static/themes/simple/js/ol.min.js.map
Normal file
1
searx/static/themes/simple/js/ol.min.js.map
Normal file
File diff suppressed because one or more lines are too long
2
searx/static/themes/simple/js/preferences.min.js
vendored
Normal file
2
searx/static/themes/simple/js/preferences.min.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import{d as e,e as t,g as n}from"./searxng.core.min.js";let r;const i=async()=>{if(!r){try{let t=await e(`GET`,`engine_descriptions.json`);r=await t.json()}catch(e){console.error(`Error fetching engineDescriptions:`,e)}if(r)for(let[e,[t,i]]of Object.entries(r)){let r=document.querySelectorAll(`[data-engine-name="${e}"] .engine-description`),a=` (<i>${n.translations?.Source}: ${i}</i>)`;for(let e of r)e.innerHTML=t+a}}},a=(e,t)=>{for(let n of t)n.offsetParent&&(n.checked=!e)},o=document.querySelectorAll(`[data-engine-name]`);for(let e of o)t(`mouseenter`,e,i);const s=document.querySelectorAll(`tbody input[type=checkbox][class~=checkbox-onoff]`),c=document.querySelectorAll(`.enable-all-engines`);for(let e of c)t(`click`,e,()=>a(!0,s));const l=document.querySelectorAll(`.disable-all-engines`);for(let e of l)t(`click`,e,()=>a(!1,s));const u=document.querySelector(`#copy-hash`);u&&t(`click`,u,async e=>{e.preventDefault();let{copiedText:t,hash:n}=u.dataset;if(t&&n)try{await navigator.clipboard.writeText(n),u.innerText=t}catch(e){console.error(`Failed to copy hash:`,e)}});
|
||||
//# sourceMappingURL=preferences.min.js.map
|
||||
1
searx/static/themes/simple/js/preferences.min.js.map
Normal file
1
searx/static/themes/simple/js/preferences.min.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"preferences.min.js","names":["engineDescriptions: Record<string, [string, string]> | undefined","engineElements: NodeListOf<HTMLElement>","engineToggles: NodeListOf<HTMLInputElement>","enableAllEngines: NodeListOf<HTMLElement>","disableAllEngines: NodeListOf<HTMLElement>","copyHashButton: HTMLElement | null"],"sources":["../../../../../client/simple/src/js/main/preferences.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\nimport { http, listen, settings } from \"../core/toolkit.ts\";\n\nlet engineDescriptions: Record<string, [string, string]> | undefined;\n\nconst loadEngineDescriptions = async (): Promise<void> => {\n if (engineDescriptions) return;\n try {\n const res = await http(\"GET\", \"engine_descriptions.json\");\n engineDescriptions = await res.json();\n } catch (error) {\n console.error(\"Error fetching engineDescriptions:\", error);\n }\n if (!engineDescriptions) return;\n\n for (const [engine_name, [description, source]] of Object.entries(engineDescriptions)) {\n const elements = document.querySelectorAll<HTMLElement>(`[data-engine-name=\"${engine_name}\"] .engine-description`);\n const sourceText = ` (<i>${settings.translations?.Source}: ${source}</i>)`;\n\n for (const element of elements) {\n element.innerHTML = description + sourceText;\n }\n }\n};\n\nconst toggleEngines = (enable: boolean, engineToggles: NodeListOf<HTMLInputElement>): void => {\n for (const engineToggle of engineToggles) {\n // check if element visible, so that only engines of the current category are modified\n if (engineToggle.offsetParent) {\n engineToggle.checked = !enable;\n }\n }\n};\n\nconst engineElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\"[data-engine-name]\");\nfor (const engineElement of engineElements) {\n listen(\"mouseenter\", engineElement, loadEngineDescriptions);\n}\n\nconst engineToggles: NodeListOf<HTMLInputElement> = document.querySelectorAll<HTMLInputElement>(\n \"tbody input[type=checkbox][class~=checkbox-onoff]\"\n);\n\nconst enableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\".enable-all-engines\");\nfor (const engine of enableAllEngines) {\n listen(\"click\", engine, () => toggleEngines(true, engineToggles));\n}\n\nconst disableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(\".disable-all-engines\");\nfor (const engine of disableAllEngines) {\n listen(\"click\", engine, () => toggleEngines(false, engineToggles));\n}\n\nconst copyHashButton: HTMLElement | null = document.querySelector<HTMLElement>(\"#copy-hash\");\nif (copyHashButton) {\n listen(\"click\", copyHashButton, async (event: Event) => {\n event.preventDefault();\n\n const { copiedText, hash } = copyHashButton.dataset;\n if (!(copiedText && hash)) return;\n\n try {\n await navigator.clipboard.writeText(hash);\n copyHashButton.innerText = copiedText;\n } catch (error) {\n console.error(\"Failed to copy hash:\", error);\n }\n });\n}\n"],"mappings":"wDAIA,IAAIA,EAEJ,MAAM,EAAyB,SAA2B,CACpD,MACJ,IAAI,CACF,IAAM,EAAM,MAAM,EAAK,MAAO,4BAC9B,EAAqB,MAAM,EAAI,MAChC,OAAQ,EAAO,CACd,QAAQ,MAAM,qCAAsC,EACrD,CACI,KAEL,IAAK,GAAM,CAAC,EAAa,CAAC,EAAa,EAAO,CAAC,GAAI,OAAO,QAAQ,GAAqB,CACrF,IAAM,EAAW,SAAS,iBAA8B,sBAAsB,EAAY,yBACpF,EAAa,QAAQ,EAAS,cAAc,OAAO,SAAS,EAAO,OAEzE,IAAK,IAAM,KAAW,EACpB,EAAQ,UAAY,EAAc,CAErC,CAVA,CAWF,EAEK,GAAiB,EAAiB,IAAsD,CAC5F,IAAK,IAAM,KAAgB,EAErB,EAAa,eACf,EAAa,QAAU,CAAC,EAG7B,EAEKC,EAA0C,SAAS,iBAA8B,sBACvF,IAAK,IAAM,KAAiB,EAC1B,EAAO,aAAc,EAAe,GAGtC,MAAMC,EAA8C,SAAS,iBAC3D,qDAGIC,EAA4C,SAAS,iBAA8B,uBACzF,IAAK,IAAM,KAAU,EACnB,EAAO,QAAS,MAAc,EAAc,GAAM,IAGpD,MAAMC,EAA6C,SAAS,iBAA8B,wBAC1F,IAAK,IAAM,KAAU,EACnB,EAAO,QAAS,MAAc,EAAc,GAAO,IAGrD,MAAMC,EAAqC,SAAS,cAA2B,cAC3E,GACF,EAAO,QAAS,EAAgB,KAAO,IAAiB,CACtD,EAAM,iBAEN,GAAM,CAAE,aAAY,OAAM,CAAG,EAAe,QACtC,MAAc,EAEpB,GAAI,CACF,MAAM,UAAU,UAAU,UAAU,GACpC,EAAe,UAAY,CAC5B,OAAQ,EAAO,CACd,QAAQ,MAAM,uBAAwB,EACvC,CACF"}
|
||||
11
searx/static/themes/simple/js/results.min.js
vendored
Normal file
11
searx/static/themes/simple/js/results.min.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import{c as e,e as t,f as n,g as r}from"./searxng.core.min.js";
|
||||
/*!
|
||||
* swiped-events.js - v@version@
|
||||
* Pure JavaScript swipe events
|
||||
* https://github.com/john-doherty/swiped-events
|
||||
* @inspiration https://stackoverflow.com/questions/16348031/disable-scrolling-when-touch-moving-certain-element
|
||||
* @author John Doherty <www.johndoherty.info>
|
||||
* @license MIT
|
||||
*/
|
||||
(function(e,t){typeof e.CustomEvent!=`function`&&(e.CustomEvent=function(e,n){n||={bubbles:!1,cancelable:!1,detail:void 0};var r=t.createEvent(`CustomEvent`);return r.initCustomEvent(e,n.bubbles,n.cancelable,n.detail),r},e.CustomEvent.prototype=e.Event.prototype),t.addEventListener(`touchstart`,u,!1),t.addEventListener(`touchmove`,d,!1),t.addEventListener(`touchend`,l,!1);var n=null,r=null,i=null,a=null,o=null,s=null,c=0;function l(e){if(s===e.target){var l=parseInt(f(s,`data-swipe-threshold`,`20`),10),u=f(s,`data-swipe-unit`,`px`),d=parseInt(f(s,`data-swipe-timeout`,`500`),10),p=Date.now()-o,m=``,h=e.changedTouches||e.touches||[];if(u===`vh`&&(l=Math.round(l/100*t.documentElement.clientHeight)),u===`vw`&&(l=Math.round(l/100*t.documentElement.clientWidth)),Math.abs(i)>Math.abs(a)?Math.abs(i)>l&&p<d&&(m=i>0?`swiped-left`:`swiped-right`):Math.abs(a)>l&&p<d&&(m=a>0?`swiped-up`:`swiped-down`),m!==``){var g={dir:m.replace(/swiped-/,``),touchType:(h[0]||{}).touchType||`direct`,fingers:c,xStart:parseInt(n,10),xEnd:parseInt((h[0]||{}).clientX||-1,10),yStart:parseInt(r,10),yEnd:parseInt((h[0]||{}).clientY||-1,10)};s.dispatchEvent(new CustomEvent(`swiped`,{bubbles:!0,cancelable:!0,detail:g})),s.dispatchEvent(new CustomEvent(m,{bubbles:!0,cancelable:!0,detail:g}))}n=null,r=null,o=null}}function u(e){e.target.getAttribute(`data-swipe-ignore`)!==`true`&&(s=e.target,o=Date.now(),n=e.touches[0].clientX,r=e.touches[0].clientY,i=0,a=0,c=e.touches.length)}function d(e){if(!(!n||!r)){var t=e.touches[0].clientX,o=e.touches[0].clientY;i=n-t,a=r-o}}function f(e,n,r){for(;e&&e!==t.documentElement;){var i=e.getAttribute(n);if(i)return i;e=e.parentNode}return r}})(window,document);let i;const a=e=>{i&&clearTimeout(i);let t=e.querySelector(`.result-images-source img`);if(!t)return;let n=e.querySelector(`.image_thumbnail`);if(n){if(n.src===`${r.theme_static_path}/img/img_load_error.svg`)return;t.onerror=()=>{t.src=n.src},t.src=n.src}let a=t.getAttribute(`data-src`);a&&(i=setTimeout(()=>{t.src=a,t.removeAttribute(`data-src`)},1e3))},o=document.querySelectorAll(`#urls img.image_thumbnail`);for(let e of o)e.complete&&e.naturalWidth===0&&(e.src=`${r.theme_static_path}/img/img_load_error.svg`),e.onerror=()=>{e.src=`${r.theme_static_path}/img/img_load_error.svg`};const s=document.querySelector(`#search_url button#copy_url`);s?.style.setProperty(`display`,`block`),n.selectImage=e=>{let t=document.getElementById(`results`);t?.classList.add(`image-detail-open`),window.location.hash=`#image-viewer`,n.scrollPageToSelected?.(),e&&a(e)},n.closeDetail=()=>{let e=document.getElementById(`results`);e?.classList.remove(`image-detail-open`),window.location.hash===`#image-viewer`&&window.history.back(),n.scrollPageToSelected?.()},t(`click`,`.btn-collapse`,function(){let t=this.getAttribute(`data-btn-text-collapsed`),n=this.getAttribute(`data-btn-text-not-collapsed`),r=this.getAttribute(`data-target`);if(!(r&&t&&n))return;let i=document.querySelector(r);e(i);let a=this.classList.contains(`collapsed`),o=a?n:t,s=a?t:n;this.innerHTML=this.innerHTML.replace(s,o),this.classList.toggle(`collapsed`),i.classList.toggle(`invisible`)}),t(`click`,`.media-loader`,function(){let t=this.getAttribute(`data-target`);if(!t)return;let n=document.querySelector(`${t} > iframe`);e(n);let r=n.getAttribute(`src`);if(!r){let e=n.getAttribute(`data-src`);e&&n.setAttribute(`src`,e)}}),t(`click`,`#copy_url`,async function(){let t=this.parentElement?.querySelector(`pre`);e(t),await navigator.clipboard.writeText(t.innerText);let n=this.dataset.copiedText;n&&(this.innerText=n)}),t(`click`,`.result-detail-close`,e=>{e.preventDefault(),n.closeDetail?.()}),t(`click`,`.result-detail-previous`,e=>{e.preventDefault(),n.selectPrevious?.(!1)}),t(`click`,`.result-detail-next`,e=>{e.preventDefault(),n.selectNext?.(!1)}),window.addEventListener(`hashchange`,()=>{window.location.hash!==`#image-viewer`&&n.closeDetail?.()});const c=document.querySelectorAll(`.swipe-horizontal`);for(let e of c)t(`swiped-left`,e,()=>{n.selectNext?.(!1)}),t(`swiped-right`,e,()=>{n.selectPrevious?.(!1)});window.addEventListener(`scroll`,()=>{let e=document.getElementById(`backToTop`),t=document.getElementById(`results`);if(e&&t){let e=document.documentElement.scrollTop||document.body.scrollTop,n=e>=100;t.classList.toggle(`scrolling`,n)}},!0);
|
||||
//# sourceMappingURL=results.min.js.map
|
||||
1
searx/static/themes/simple/js/results.min.js.map
Normal file
1
searx/static/themes/simple/js/results.min.js.map
Normal file
File diff suppressed because one or more lines are too long
2
searx/static/themes/simple/js/search.min.js
vendored
Normal file
2
searx/static/themes/simple/js/search.min.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import{c as e,e as t,g as n}from"./searxng.core.min.js";const r=e=>{if(e.value.length>0){let e=document.getElementById(`search`);e?.submit()}},i=(e,t)=>{t.classList.toggle(`empty`,e.value.length===0)},a=n=>{let r=document.getElementById(`clear_search`);e(r),i(n,r),t(`click`,r,e=>{e.preventDefault(),n.value=``,n.focus(),i(n,r)}),t(`input`,n,()=>i(n,r),{passive:!0})},o=document.getElementById(`q`);e(o);const s=window.matchMedia(`(max-width: 50em)`).matches,c=document.querySelector(`main`)?.id===`main_results`;if(s||c||o.focus(),a(o),n.search_on_category_select&&document.querySelector(`.search_filters`)){let e=document.getElementById(`safesearch`);e&&t(`change`,e,()=>r(o));let n=document.getElementById(`time_range`);n&&t(`change`,n,()=>r(o));let i=document.getElementById(`language`);i&&t(`change`,i,()=>r(o))}const l=[...document.querySelectorAll(`button.category_button`)];for(let e of l)t(`click`,e,t=>{if(t.shiftKey){t.preventDefault(),e.classList.toggle(`selected`);return}for(let t of l)t.classList.toggle(`selected`,t===e)});const u=document.querySelector(`#search`);e(u),t(`submit`,u,e=>{e.preventDefault();let t=document.querySelector(`#selected-categories`);if(t){let e=l.filter(e=>e.classList.contains(`selected`)).map(e=>e.name.replace(`category_`,``));t.value=e.join(`,`)}u.submit()});
|
||||
//# sourceMappingURL=search.min.js.map
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user