Skip to content

Commit 5935654

Browse files
committed
fix(frontend) :: import the browser libraries and types
Before Tabler, ApexCharts and Tom Select were pulled from CDN so their code nor their types could be imported into our frontend code. This is no longer a blocker as the frontend is bundled with npm. Now 3rd party code and types are imported directly in SQLPage's frontend.
1 parent e86d210 commit 5935654

7 files changed

Lines changed: 35 additions & 50 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Frontend asset urls are now idempotent between builds if nothing changed in the frontend. Now browsers will only re-download frontend assets when there has been a code change in them.
77
- `cargo install sqlpage` or any build from the crates.io tarball only needs Rust and no longer needs internet access. Pre-built binaries and the Docker image are unaffected.
88
- The browser assets are now bundled as a whole, so SQLPage's own scripts are minified along with their dependencies and are no longer readable in browser devtools.
9+
- SQLPage no longer exposes `window.ApexCharts`, `window.TomSelect` and `window.tabler` as globals. ApexCharts still reads its `window.Apex` global configuration, and a page that loads its own Bootstrap into `window.bootstrap` still overrides the bundled one.
910

1011
## v0.46.2
1112

frontend/src/apexcharts.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ApexCharts from "apexcharts";
12
import { align_series_for, xaxis_type_for } from "./chart_series.js";
23
import { add_init_fn } from "./init.js";
34

@@ -295,7 +296,10 @@ function build_sqlpage_chart(c) {
295296
// Numeric axes count intervals; category and time axes use tickAmount as a
296297
// target for label density.
297298
if (data.xticks) options.xaxis.tickAmount = data.xticks;
298-
const chart = new ApexCharts(chartContainer, options);
299+
const chart = new ApexCharts(
300+
chartContainer,
301+
/** @type {import("apexcharts").ApexOptions} */ (options),
302+
);
299303
chart.render();
300304
if (window.charts) window.charts.push(chart);
301305
else window.charts = [chart];

frontend/src/globals.d.ts

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Names the browser bundle relies on at runtime rather than through an import:
2-
// libraries build.rs inlines ahead of our own code, libraries loaded on demand,
3-
// and the objects SQLPage's scripts hang off the window for each other.
2+
// libraries loaded on demand, and the objects SQLPage's scripts hang off the
3+
// window for each other.
44

55
/**
66
* A library this project ships no type definitions for. Saying `unknown`
@@ -12,24 +12,9 @@ type Untyped = any;
1212
/** Leaflet, loaded from a CDN by sqlpage_map when a page holds a map. */
1313
declare const L: Untyped;
1414

15-
/** ApexCharts, inlined ahead of apexcharts.js by build.rs. */
16-
declare const ApexCharts: Untyped;
17-
18-
/** Tom Select, inlined ahead of tomselect.js by build.rs. */
19-
declare const TomSelect: Untyped;
20-
21-
/**
22-
* Tabler's bundled Bootstrap, inlined ahead of sqlpage.js by build.rs. Its
23-
* widgets are untyped: naming a few of them here would only claim more than
24-
* this file knows.
25-
*/
26-
interface TablerBootstrap {
27-
[widget: string]: Untyped;
28-
}
29-
3015
interface Window {
3116
/** Every chart rendered on the page, in the order they were built. */
3217
charts?: unknown[];
33-
tabler: { bootstrap: TablerBootstrap };
34-
bootstrap?: TablerBootstrap;
18+
/** A Bootstrap the page loaded itself, used in place of the bundled one. */
19+
bootstrap?: typeof import("@tabler/core").bootstrap;
3520
}

frontend/src/sqlpage.js

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1+
import { bootstrap as bundled_bootstrap } from "@tabler/core";
12
import { add_init_fn } from "./init.js";
23

4+
const page_bootstrap = () => window.bootstrap ?? bundled_bootstrap;
5+
6+
/**
7+
* Tabler declares getOrCreateInstance on the base class, which drops show().
8+
* @typedef {InstanceType<typeof bundled_bootstrap.Toast>} ToastWidget
9+
* @typedef {InstanceType<typeof bundled_bootstrap.Modal>} ModalWidget
10+
*/
11+
312
const nonce = /** @type {HTMLScriptElement} */ (document.currentScript).nonce;
413

514
function sqlpage_card() {
@@ -359,13 +368,12 @@ function normalize_hash(hash) {
359368
}
360369

361370
function open_toasts_for_hash(toasts) {
362-
const Toast = (window.bootstrap || window.tabler?.bootstrap)?.Toast;
363-
if (!Toast) return;
371+
const Toast = page_bootstrap().Toast;
364372
const hash = normalize_hash(window.location.hash);
365373
if (!hash) return;
366374
for (const toast of toasts) {
367375
if (normalize_hash(toast.dataset.toastTrigger) === hash) {
368-
Toast.getOrCreateInstance(toast).show();
376+
/** @type {ToastWidget} */ (Toast.getOrCreateInstance(toast)).show();
369377
}
370378
}
371379
}
@@ -386,8 +394,7 @@ function restore_focus_after_toast(toast, container) {
386394
}
387395

388396
function sqlpage_toast() {
389-
const Toast = (window.bootstrap || window.tabler?.bootstrap)?.Toast;
390-
if (!Toast) return;
397+
const Toast = page_bootstrap().Toast;
391398

392399
const initialized_toasts = [];
393400
/** @type {NodeListOf<HTMLElement>} */
@@ -411,7 +418,9 @@ function sqlpage_toast() {
411418
}
412419

413420
toast.removeAttribute("data-pre-init");
414-
const instance = Toast.getOrCreateInstance(toast);
421+
const instance = /** @type {ToastWidget} */ (
422+
Toast.getOrCreateInstance(toast)
423+
);
415424
initialized_toasts.push(toast);
416425
toast.addEventListener("hidden.bs.toast", () => {
417426
restore_focus_after_toast(toast, container);
@@ -463,7 +472,7 @@ window.addEventListener("hashchange", () =>
463472
);
464473

465474
function init_bootstrap_components(event) {
466-
const bootstrap = window.bootstrap || window.tabler.bootstrap;
475+
const bootstrap = page_bootstrap();
467476
const fragment = event.target;
468477
for (const el of fragment.querySelectorAll('[data-bs-toggle="tooltip"]')) {
469478
new bootstrap.Tooltip(el);
@@ -486,8 +495,9 @@ function open_modal_for_hash() {
486495
if (!hash) return;
487496
const modal = document.getElementById(hash);
488497
if (!modal?.classList.contains("modal")) return;
489-
const bootstrap_modal =
490-
window.tabler.bootstrap.Modal.getOrCreateInstance(modal);
498+
const bootstrap_modal = /** @type {ModalWidget} */ (
499+
page_bootstrap().Modal.getOrCreateInstance(modal)
500+
);
491501
bootstrap_modal.show();
492502
modal.addEventListener(
493503
"hidden.bs.modal",

frontend/src/tomselect.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import TomSelect from "tom-select/popular";
12
import { add_init_fn } from "./init.js";
23

34
function sqlpage_select_dropdown() {
@@ -27,8 +28,8 @@ function sqlpage_select_dropdown_individual(s) {
2728
load: sqlpage_load_options_source(s.dataset.options_source),
2829
valueField: "value",
2930
labelField: "label",
30-
searchField: "label",
31-
create: s.dataset.create_new,
31+
searchField: ["label"],
32+
create: !!s.dataset.create_new,
3233
maxOptions: null,
3334
closeAfterSelect: !s.multiple,
3435
clearAfterSelect: true,

tests/end-to-end/globals.d.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// What the browser tests reach for on the page: widgets that the scripts under
2-
// test attach to elements at runtime, and libraries the page loads itself.
2+
// test attach to elements at runtime.
33

44
interface TomSelectInstance {
55
getValue(): string | string[];
@@ -13,10 +13,3 @@ interface HTMLElement {
1313
/** Attached by sqlpage_select_dropdown to every select it takes over. */
1414
tomselect?: TomSelectInstance;
1515
}
16-
17-
interface Window {
18-
/** Tabler's bundled Bootstrap, inlined ahead of sqlpage.js by build.rs. */
19-
tabler?: {
20-
bootstrap: { Toast: { getInstance(element: Element): unknown } };
21-
};
22-
}

tests/end-to-end/official-site.spec.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -220,17 +220,8 @@ test("toast notifications initialize, stack, dismiss, and render safely", async
220220
await expect(stackOne).toBeVisible();
221221
await expect(automatic).toBeHidden({ timeout: 7000 });
222222
expect(
223-
await automaticHandle?.evaluate((element) => {
224-
const testWindow = window as Window & {
225-
tabler: {
226-
bootstrap: {
227-
Toast: { getInstance(element: Element): unknown };
228-
};
229-
};
230-
};
231-
return testWindow.tabler.bootstrap.Toast.getInstance(element) === null;
232-
}),
233-
).toBe(true);
223+
await automaticHandle?.evaluate((element) => element.isConnected),
224+
).toBe(false);
234225

235226
const dismissible = page.locator("#toast-dismissible");
236227
await expect(dismissible).toHaveAttribute("data-bs-delay", "0");

0 commit comments

Comments
 (0)